Core: move option results to the World class instead of MultiWorld (#993)

🤞 

* map option objects to a `World.options` dict

* convert RoR2 to options dict system for testing

* add temp behavior for lttp with notes

* copy/paste bad

* convert `set_default_common_options` to a namespace property

* reorganize test call order

* have fill_restrictive use the new options system

* update world api

* update soe tests

* fix world api

* core: auto initialize a dataclass on the World class with the option results

* core: auto initialize a dataclass on the World class with the option results: small tying improvement

* add `as_dict` method to the options dataclass

* fix namespace issues with tests

* have current option updates use `.value` instead of changing the option

* update ror2 to use the new options system again

* revert the junk pool dict since it's cased differently

* fix begin_with_loop typo

* write new and old options to spoiler

* change factorio option behavior back

* fix comparisons

* move common and per_game_common options to new system

* core: automatically create missing options_dataclass from legacy option_definitions

* remove spoiler special casing and add back the Factorio option changing but in new system

* give ArchipIDLE the default options_dataclass so its options get generated and spoilered properly

* reimplement `inspect.get_annotations`

* move option info generation for webhost to new system

* need to include Common and PerGame common since __annotations__ doesn't include super

* use get_type_hints for the options dictionary

* typing.get_type_hints returns the bases too.

* forgot to sweep through generate

* sweep through all the tests

* swap to a metaclass property

* move remaining usages from get_type_hints to metaclass property

* move remaining usages from __annotations__ to metaclass property

* move remaining usages from legacy dictionaries to metaclass property

* remove legacy dictionaries

* cache the metaclass property

* clarify inheritance in world api

* move the messenger to new options system

* add an assert for my dumb

* update the doc

* rename o to options

* missed a spot

* update new messenger options

* comment spacing

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* fix tests

* fix missing import

* make the documentation definition more accurate

* use options system for loc creation

* type cast MessengerWorld

* fix typo and use quotes for cast

* LTTP: set random seed in tests

* ArchipIdle: remove change here as it's default on AutoWorld

* Stardew: Need to set state because `set_default_common_options` used to

* The Messenger: update shop rando and helpers to new system; optimize imports

* Add a kwarg to `as_dict` to do the casing for you

* RoR2: use new kwarg for less code

* RoR2: revert some accidental reverts

* The Messenger: remove an unnecessary variable

* remove TypeVar that isn't used

* CommonOptions not abstract

* Docs: fix mistake in options api.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* create options for item link worlds

* revert accidental doc removals

* Item Links: set default options on group

* change Zillion to new options dataclass

* remove unused parameter to function

* use TypeGuard for Literal narrowing

* move dlc quest to new api

* move overcooked 2 to new api

* fixed some missed code in oc2

* - Tried to be compliant with 993 (WIP?)

* - I think it all works now

* - Removed last trace of me touching core

* typo

* It now passes all tests!

* Improve options, fix all issues I hope

* - Fixed init options

* dlcquest: fix bad imports

* missed a file

* - Reduce code duplication

* add as_dict documentation

* - Use .items(), get option name more directly, fix slot data content

* - Remove generic options from the slot data

* improve slot data documentation

* remove `CommonOptions.get_value` (#21)

* better slot data description

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: el-u <109771707+el-u@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
Aaron Wagener 2023-10-10 15:30:20 -05:00 committed by GitHub
parent a7b4914bb7
commit 7193182294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1587 additions and 1603 deletions

View File

@ -226,25 +226,24 @@ class MultiWorld():
range(1, self.players + 1)} range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None: 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: for player in self.player_ids:
self.custom_data[player] = {} self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[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] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[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): def set_item_links(self):
item_links = {} item_links = {}
replacement_prio = [False, True, None] replacement_prio = [False, True, None]
for player in self.player_ids: 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_link["name"] in item_links:
if item_links[item_link["name"]]["game"] != self.game[player]: if item_links[item_link["name"]]["game"] != self.game[player]:
raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}") raise Exception(f"Cannot ItemLink across games. Link: {item_link['name']}")
@ -299,14 +298,6 @@ class MultiWorld():
group["non_local_items"] = item_link["non_local_items"] group["non_local_items"] = item_link["non_local_items"]
group["link_replacement"] = replacement_prio[item_link["link_replacement"]] 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): def secure(self):
self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.random = ThreadBarrierProxy(secrets.SystemRandom())
self.is_race = True self.is_race = True
@ -1257,7 +1248,7 @@ class Spoiler:
def to_file(self, filename: str) -> None: def to_file(self, filename: str) -> None:
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> 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) display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n") outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
@ -1275,8 +1266,7 @@ class Spoiler:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[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 self.multiworld.worlds[player].options_dataclass.type_hints.items():
for f_option, option in options.items():
write_option(f_option, option) write_option(f_option, option)
AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile)

12
Fill.py
View File

@ -5,6 +5,8 @@ import typing
from collections import Counter, deque from collections import Counter, deque
from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld
from Options import Accessibility
from worlds.AutoWorld import call_all from worlds.AutoWorld import call_all
from worlds.generic.Rules import add_item_rule 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 spot_to_fill: typing.Optional[Location] = None
# if minimal accessibility, only check whether location is reachable if game not beatable # 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, perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) \ item_to_place.player) \
if single_player_placement else not has_beaten_game 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=[]): def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
maximum_exploration_state = sweep_from_pool(state, 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 unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
not location.can_reach(maximum_exploration_state)] not location.can_reach(maximum_exploration_state)]
for location in unreachable_locations: 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)] unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
if unreachable_locations: if unreachable_locations:
def forbid_important_item_rule(item: Item): 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: for location in unreachable_locations:
add_item_rule(location, forbid_important_item_rule) 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, # If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere. # which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = { 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 for player in world.player_ids
if world.progression_balancing[player] > 0 if world.worlds[player].options.progression_balancing > 0
} }
if not balanceable_players: if not balanceable_players:
logging.info('Skipping multiworld progression balancing.') logging.info('Skipping multiworld progression balancing.')

View File

@ -157,7 +157,8 @@ def main(args=None, callback=ERmain):
for yaml in weights_cache[path]: for yaml in weights_cache[path]:
if category_name is None: if category_name is None:
for category in yaml: 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 yaml[category][key] = option
elif category_name not in yaml: elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.") 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) return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types: if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game] 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 option_key in options:
if options[option_key].supports_weighting: if options[option_key].supports_weighting:
return get_choice(option_key, category_dict) 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.") f"which is not enabled.")
ret = argparse.Namespace() ret = argparse.Namespace()
for option_key in Options.per_game_common_options: for option_key in Options.PerGameCommonOptions.type_hints:
if option_key in weights and option_key not in Options.common_options: 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.") raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights) 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] game_weights = weights[ret.game]
ret.name = get_choice('name', weights) 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))) 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) 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: if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", []) ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time": if ret.game == "Minecraft" or ret.game == "Ocarina of Time":

22
Main.py
View File

@ -108,7 +108,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.info('') logger.info('')
for player in world.player_ids: 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): for _ in range(count):
world.push_precollected(world.create_item(item_name, player)) 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: for player in world.player_ids:
# items can't be both local and non-local, prefer local # items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
world.non_local_items[player].value -= set(world.local_early_items[player]) world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
AutoWorld.call_all(world, "set_rules") AutoWorld.call_all(world, "set_rules")
for player in world.player_ids: for player in world.player_ids:
exclusion_rules(world, player, world.exclude_locations[player].value) exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
world.priority_locations[player].value -= world.exclude_locations[player].value world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
for location_name in world.priority_locations[player].value: for location_name in world.worlds[player].options.priority_locations.value:
try: try:
location = world.get_location(location_name, player) location = world.get_location(location_name, player)
except KeyError as e: # failed to find the given location. Check if it's a legitimate location 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: if world.players > 1:
locality_rules(world) locality_rules(world)
else: else:
world.non_local_items[1].value = set() world.worlds[1].options.non_local_items.value = set()
world.local_items[1].value = set() world.worlds[1].options.local_items.value = set()
AutoWorld.call_all(world, "generate_basic") AutoWorld.call_all(world, "generate_basic")
@ -360,11 +360,11 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
f" {location}" f" {location}"
locations_data[location.player][location.address] = \ locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags 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) 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) 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", [])]): for player in world.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location) precollect_hint(location)

View File

@ -2,6 +2,9 @@ from __future__ import annotations
import abc import abc
import logging import logging
from copy import deepcopy
from dataclasses import dataclass
import functools
import math import math
import numbers import numbers
import random import random
@ -211,6 +214,12 @@ class NumericOption(Option[int], numbers.Integral, abc.ABC):
else: else:
return self.value > other 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: def __bool__(self) -> bool:
return bool(self.value) return bool(self.value)
@ -896,10 +905,55 @@ class ProgressionBalancing(SpecialRange):
} }
common_options = { class OptionsMetaProperty(type):
"progression_balancing": ProgressionBalancing, def __new__(mcs,
"accessibility": Accessibility 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): class LocalItems(ItemSet):
@ -1020,17 +1074,16 @@ class ItemLinks(OptionList):
link.setdefault("link_replacement", None) link.setdefault("link_replacement", None)
per_game_common_options = { @dataclass
**common_options, # can be overwritten per-game class PerGameCommonOptions(CommonOptions):
"local_items": LocalItems, local_items: LocalItems
"non_local_items": NonLocalItems, non_local_items: NonLocalItems
"start_inventory": StartInventory, start_inventory: StartInventory
"start_hints": StartHints, start_hints: StartHints
"start_location_hints": StartLocationHints, start_location_hints: StartLocationHints
"exclude_locations": ExcludeLocations, exclude_locations: ExcludeLocations
"priority_locations": PriorityLocations, priority_locations: PriorityLocations
"item_links": ItemLinks item_links: ItemLinks
}
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
@ -1071,10 +1124,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items(): for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden: if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = { all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints
**per_game_common_options,
**world.option_definitions
}
with open(local_path("data", "options.yaml")) as f: with open(local_path("data", "options.yaml")) as f:
file_data = f.read() file_data = f.read()

View File

@ -36,10 +36,7 @@ def create():
for game_name, world in AutoWorldRegister.world_types.items(): for game_name, world in AutoWorldRegister.world_types.items():
all_options: typing.Dict[str, Options.AssembleOptions] = { all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
**Options.per_game_common_options,
**world.option_definitions
}
# Generate JSON files for player-settings pages # Generate JSON files for player-settings pages
player_settings = { player_settings = {

View File

@ -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`. 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 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 create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
options:
```python ```python
# Options.py # Options.py
from dataclasses import dataclass
from Options import Toggle, PerGameCommonOptions
class StartingSword(Toggle): class StartingSword(Toggle):
"""Adds a sword to your starting inventory.""" """Adds a sword to your starting inventory."""
display_name = "Start With Sword" display_name = "Start With Sword"
example_options = { @dataclass
"starting_sword": StartingSword 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 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 ```python
from worlds.AutoWorld import World from worlds.AutoWorld import World
from .Options import options from .Options import ExampleGameOptions
class ExampleWorld(World): 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 ### Option Checking
Options are parsed by `Generate.py` before the worlds are created, and then the option classes are created shortly after 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 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 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: the option class's `value` attribute. For our example above we can do a simple check:
```python ```python
if self.multiworld.starting_sword[self.player]: if self.options.starting_sword:
do_some_things() do_some_things()
``` ```
or if I need a boolean object, such as in my slot_data I can access it as: or if I need a boolean object, such as in my slot_data I can access it as:
```python ```python
start_with_sword = bool(self.multiworld.starting_sword[self.player].value) start_with_sword = bool(self.options.starting_sword.value)
``` ```
## Generic Option Classes ## 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 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: comparison methods with the class and strings, so if you have an `option_early_sword`, this can be compared with:
```python ```python
if self.multiworld.sword_availability[self.player] == "early_sword": if self.options.sword_availability == "early_sword":
do_early_sword_things() do_early_sword_things()
``` ```
@ -128,7 +135,7 @@ or:
```python ```python
from .Options import SwordAvailability 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() 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 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 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 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 ### PlandoBosses
An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports An option specifically built for handling boss rando, if your game can use it. Is a subclass of TextChoice so supports

View File

@ -86,9 +86,11 @@ inside a `World` object.
### Player Options ### Player Options
Players provide customized settings for their World in the form of yamls. Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.multiworld.<option_name>[self.player]`. A dict A `dataclass` of valid options definitions has to be provided in `self.options_dataclass`.
of valid options has to be provided in `self.option_definitions`. Options are automatically (It must be a subclass of `PerGameCommonOptions`.)
added to the `World` object for easy access. Option results are automatically added to the `World` object for easy access.
Those are accessible through `self.options.<option_name>`, and you can get a dictionary of the option values via
`self.options.as_dict(<option_names>)`, passing the desired options as strings.
### World Settings ### 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 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. use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load e.g. `from .Options import MyGameOptions` from your `__init__.py` will load
`worlds/<world_name>/Options.py` and make its `mygame_options` accessible. `world/[world_name]/Options.py` and make its `MyGameOptions` accessible.
When imported names pile up it may be easier to use `from . import Options` 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. Imports from directories outside your world should use absolute imports.
Correct use of relative / absolute imports is required for zipped worlds to 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 to describe it and a `display_name` property for display on the website and in
spoiler logs. spoiler logs.
The actual name as used in the yaml is defined in a `Dict[str, AssembleOptions]`, that is 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.option_definitions`. 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`. Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory. For more see `Options.py` in AP's base directory.
@ -309,8 +312,8 @@ default = 0
```python ```python
# Options.py # Options.py
from Options import Toggle, Range, Choice, Option from dataclasses import dataclass
import typing from Options import Toggle, Range, Choice, PerGameCommonOptions
class Difficulty(Choice): class Difficulty(Choice):
"""Sets overall game difficulty.""" """Sets overall game difficulty."""
@ -333,23 +336,27 @@ class FixXYZGlitch(Toggle):
"""Fixes ABC when you do XYZ""" """Fixes ABC when you do XYZ"""
display_name = "Fix XYZ Glitch" display_name = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`. # By convention, we call the options dataclass `<world>Options`.
mygame_options: typing.Dict[str, AssembleOptions] = { # It has to be derived from 'PerGameCommonOptions'.
"difficulty": Difficulty, @dataclass
"final_boss_hp": FinalBossHP, class MyGameOptions(PerGameCommonOptions):
"fix_xyz_glitch": FixXYZGlitch, difficulty: Difficulty
} final_boss_hp: FinalBossHP
fix_xyz_glitch: FixXYZGlitch
``` ```
```python ```python
# __init__.py # __init__.py
from worlds.AutoWorld import World 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): 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 ### A World Class Skeleton
@ -359,13 +366,14 @@ class MyGameWorld(World):
import settings import settings
import typing 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 .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above from .Locations import mygame_locations # same as above
from worlds.AutoWorld import World from worlds.AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
class MyGameItem(Item): # or from Items import MyGameItem class MyGameItem(Item): # or from Items import MyGameItem
game = "My Game" # name of the game/world this item is from 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 game = "My Game" # name of the game/world this location is in
class MyGameSettings(settings.Group): class MyGameSettings(settings.Group):
class RomFile(settings.SNESRomPath): class RomFile(settings.SNESRomPath):
"""Insert help text for host.yaml here.""" """Insert help text for host.yaml here."""
@ -384,7 +393,8 @@ class MyGameSettings(settings.Group):
class MyGameWorld(World): class MyGameWorld(World):
"""Insert description of the world/game here.""" """Insert description of the world/game here."""
game = "My Game" # name of the game/world 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 settings: typing.ClassVar[MyGameSettings] # will be automatically assigned from type hint
topology_present = True # show path to required location checks in spoiler 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 ```python
def generate_early(self) -> None: def generate_early(self) -> None:
# read player settings to world instance # 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 #### create_item
@ -687,9 +697,9 @@ def generate_output(self, output_directory: str):
in self.multiworld.precollected_items[self.player]], in self.multiworld.precollected_items[self.player]],
"final_boss_hp": self.final_boss_hp, "final_boss_hp": self.final_boss_hp,
# store option name "easy", "normal" or "hard" for difficuly # 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 # 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 # point to a ROM specified by the installation
src = self.settings.rom_file src = self.settings.rom_file
@ -702,6 +712,26 @@ def generate_output(self, output_directory: str):
generate_mod(src, out_file, data) 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 ### Documentation
Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading Each world implementation should have a tutorial and a game info page. These are both rendered on the website by reading

View File

@ -125,13 +125,13 @@ class WorldTestBase(unittest.TestCase):
self.multiworld.game[1] = self.game self.multiworld.game[1] = self.game
self.multiworld.player_name = {1: "Tester"} self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed(seed) self.multiworld.set_seed(seed)
self.multiworld.state = CollectionState(self.multiworld)
args = Namespace() 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, { setattr(args, name, {
1: option.from_any(self.options.get(name, getattr(option, "default"))) 1: option.from_any(self.options.get(name, getattr(option, "default")))
}) })
self.multiworld.set_options(args) self.multiworld.set_options(args)
self.multiworld.set_default_common_options()
for step in gen_steps: for step in gen_steps:
call_all(self.multiworld, step) call_all(self.multiworld, step)

View File

@ -1,16 +1,20 @@
from typing import List, Iterable from typing import List, Iterable
import unittest import unittest
import Options
from Options import Accessibility
from worlds.AutoWorld import World from worlds.AutoWorld import World
from Fill import FillError, balance_multiworld_progression, fill_restrictive, \ from Fill import FillError, balance_multiworld_progression, fill_restrictive, \
distribute_early_items, distribute_items_restrictive distribute_early_items, distribute_items_restrictive
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item, Location, \ 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 from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
def generate_multi_world(players: int = 1) -> MultiWorld: def generate_multi_world(players: int = 1) -> MultiWorld:
multi_world = MultiWorld(players) multi_world = MultiWorld(players)
multi_world.player_name = {} multi_world.player_name = {}
multi_world.state = CollectionState(multi_world)
for i in range(players): for i in range(players):
player_id = i+1 player_id = i+1
world = World(multi_world, player_id) 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) multi_world.player_name[player_id] = "Test Player " + str(player_id)
region = Region("Menu", player_id, multi_world, "Menu Region Hint") region = Region("Menu", player_id, multi_world, "Menu Region Hint")
multi_world.regions.append(region) 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_seed(0)
multi_world.set_default_common_options()
return multi_world return multi_world
@ -186,7 +197,7 @@ class TestFillRestrictive(unittest.TestCase):
items = player1.prog_items items = player1.prog_items
locations = player1.locations 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( multi_world.completion_condition[player1.id] = lambda state: state.has(
items[1].name, player1.id) items[1].name, player1.id)
set_rule(locations[1], lambda state: state.has( set_rule(locations[1], lambda state: state.has(

View File

@ -1,3 +1,4 @@
from argparse import Namespace
from typing import Dict, Optional, Callable from typing import Dict, Optional, Callable
from BaseClasses import MultiWorld, CollectionState, Region from BaseClasses import MultiWorld, CollectionState, Region
@ -13,7 +14,6 @@ class TestHelpers(unittest.TestCase):
self.multiworld.game[self.player] = "helper_test_game" self.multiworld.game[self.player] = "helper_test_game"
self.multiworld.player_name = {1: "Tester"} self.multiworld.player_name = {1: "Tester"}
self.multiworld.set_seed() self.multiworld.set_seed()
self.multiworld.set_default_common_options()
def testRegionHelpers(self) -> None: def testRegionHelpers(self) -> None:
regions: Dict[str, str] = { regions: Dict[str, str] = {

View File

@ -6,6 +6,6 @@ class TestOptions(unittest.TestCase):
def testOptionsHaveDocString(self): def testOptionsHaveDocString(self):
for gamename, world_type in AutoWorldRegister.world_types.items(): for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden: 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): with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__) self.assertTrue(option.__doc__)

View File

@ -1,7 +1,7 @@
from argparse import Namespace from argparse import Namespace
from typing import Type, Tuple from typing import Type, Tuple
from BaseClasses import MultiWorld from BaseClasses import MultiWorld, CollectionState
from worlds.AutoWorld import call_all, World from worlds.AutoWorld import call_all, World
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") 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.game[1] = world_type.game
multiworld.player_name = {1: "Tester"} multiworld.player_name = {1: "Tester"}
multiworld.set_seed() multiworld.set_seed()
multiworld.state = CollectionState(multiworld)
args = Namespace() 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)}) setattr(args, name, {1: option.from_any(option.default)})
multiworld.set_options(args) multiworld.set_options(args)
multiworld.set_default_common_options()
for step in steps: for step in steps:
call_all(multiworld, step) call_all(multiworld, step)
return multiworld return multiworld

View File

@ -4,11 +4,12 @@ import hashlib
import logging import logging
import pathlib import pathlib
import sys 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 Union
from Options import PerGameCommonOptions
from BaseClasses import CollectionState from BaseClasses import CollectionState
from Options import AssembleOptions
if TYPE_CHECKING: if TYPE_CHECKING:
import random import random
@ -63,6 +64,12 @@ class AutoWorldRegister(type):
dct["required_client_version"] = max(dct["required_client_version"], dct["required_client_version"] = max(dct["required_client_version"],
base.__dict__["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 # construct class
new_class = super().__new__(mcs, name, bases, dct) new_class = super().__new__(mcs, name, bases, dct)
if "game" in 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 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.""" 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""" """link your Options mapping"""
options: PerGameCommonOptions
"""resulting options for the player of this world"""
game: ClassVar[str] game: ClassVar[str]
"""name the game""" """name the game"""
topology_present: ClassVar[bool] = False topology_present: ClassVar[bool] = False
@ -362,16 +372,14 @@ class World(metaclass=AutoWorldRegister):
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: 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. """Creates a group, which is an instance of World that is responsible for multiple others.
An example case is ItemLinks creating these.""" An example case is ItemLinks creating these."""
import Options # 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()})
for option_key, option in cls.option_definitions.items(): return group
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():
getattr(multiworld, option_key)[new_player_id] = option(option.default)
return cls(multiworld, new_player_id)
# decent place to implement progressive items, in most cases can stay as-is # 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]: def collect_item(self, state: "CollectionState", item: "Item", remove: bool = False) -> Optional[str]:

View File

@ -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)

View File

@ -1,25 +1,16 @@
import unittest from BaseClasses import CollectionState, ItemClassification
from argparse import Namespace from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from BaseClasses import MultiWorld, CollectionState, ItemClassification
from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import create_regions from worlds.alttp.Regions import create_regions
from worlds.alttp.Shops import create_shops 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.starting_regions = [] # Where to start exploring self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits self.remove_exits = [] # Block dungeon exits
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -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.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.InvertedRegions import create_inverted_regions
@ -10,17 +7,12 @@ from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from test.TestBase import TestBase 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.mode[1] = "inverted" self.multiworld.mode[1] = "inverted"
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)

View File

@ -1,27 +1,17 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons 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, \ 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 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.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
self.multiworld.set_seed(None)
self.multiworld.mode[1] = "inverted" 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'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.multiworld, 1) create_inverted_regions(self.multiworld, 1)
self.multiworld.worlds[1].create_dungeons() self.multiworld.worlds[1].create_dungeons()

View File

@ -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.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions 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.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.mode[1] = "inverted" self.multiworld.mode[1] = "inverted"
self.multiworld.logic[1] = "minorglitches" self.multiworld.logic[1] = "minorglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -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.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions 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.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.logic[1] = "owglitches" self.multiworld.logic[1] = "owglitches"
self.multiworld.mode[1] = "inverted" self.multiworld.mode[1] = "inverted"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']

View File

@ -1,27 +1,15 @@
from argparse import Namespace from worlds.alttp.Dungeons import get_dungeon_item_pool
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.InvertedRegions import mark_dark_world_regions from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory 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 test.TestBase import TestBase
from worlds import AutoWorld from worlds.alttp.test import LTTPTestBase
class TestMinor(TestBase): class TestMinor(TestBase, LTTPTestBase):
def setUp(self): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.logic[1] = "minorglitches" self.multiworld.logic[1] = "minorglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].er_seed = 0

View File

@ -1,24 +1,15 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from test.TestBase import TestBase 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.logic[1] = "owglitches" self.multiworld.logic[1] = "owglitches"
self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].er_seed = 0

View File

@ -1,22 +1,14 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.Dungeons import get_dungeon_item_pool
from worlds.alttp.InvertedRegions import mark_dark_world_regions from worlds.alttp.InvertedRegions import mark_dark_world_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from test.TestBase import TestBase 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): def setUp(self):
self.multiworld = MultiWorld(1) self.world_setup()
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.multiworld.logic[1] = "noglitches" self.multiworld.logic[1] = "noglitches"
self.multiworld.difficulty_requirements[1] = difficulties['normal'] self.multiworld.difficulty_requirements[1] = difficulties['normal']
self.multiworld.worlds[1].er_seed = 0 self.multiworld.worlds[1].er_seed = 0

View File

@ -1,11 +1,12 @@
import csv import csv
import enum import enum
import math 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 dataclasses import dataclass, field
from random import Random from random import Random
from typing import Dict, List, Set
from BaseClasses import Item, ItemClassification
from . import Options, data
class DLCQuestItem(Item): 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): def create_items(world, World_Options: Options.DLCQuestOptions, locations_count: int, random: Random):
created_items = [] created_items = []
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ if World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign == Options.Campaign.option_both:
Options.Campaign] == Options.Campaign.option_both:
for item in items_by_group[Group.DLCQuest]: for item in items_by_group[Group.DLCQuest]:
if item.has_any_group(Group.DLC): if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item)) created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and World_Options[ if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item)) created_items.append(world.create_item(item))
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
for item in items_by_group[Group.DLCQuest]: for item in items_by_group[Group.DLCQuest]:
if item.has_any_group(Group.Coin): if item.has_any_group(Group.Coin):
for i in range(coin_bundle_needed): for i in range(coin_bundle_needed):
created_items.append(world.create_item(item)) 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)) created_items.append(world.create_item(item))
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or
Options.Campaign] == Options.Campaign.option_both: World_Options.campaign == Options.Campaign.option_both):
for item in items_by_group[Group.Freemium]: for item in items_by_group[Group.Freemium]:
if item.has_any_group(Group.DLC): if item.has_any_group(Group.DLC):
created_items.append(world.create_item(item)) created_items.append(world.create_item(item))
if item.has_any_group(Group.Item) and World_Options[ if item.has_any_group(Group.Item) and World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Options.ItemShuffle] == Options.ItemShuffle.option_shuffled:
created_items.append(world.create_item(item)) created_items.append(world.create_item(item))
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
for item in items_by_group[Group.Freemium]: for item in items_by_group[Group.Freemium]:
if item.has_any_group(Group.Coin): if item.has_any_group(Group.Coin):
for i in range(coin_bundle_needed): for i in range(coin_bundle_needed):
created_items.append(world.create_item(item)) 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)) created_items.append(world.create_item(item))
trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random) trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random)

View File

@ -1,5 +1,4 @@
from BaseClasses import Location, MultiWorld from BaseClasses import Location
from . import Options
class DLCQuestLocation(Location): class DLCQuestLocation(Location):

View File

@ -1,22 +1,6 @@
from typing import Union, Dict, runtime_checkable, Protocol
from Options import Option, DeathLink, Choice, Toggle, SpecialRange
from dataclasses import dataclass from dataclasses import dataclass
from Options import Choice, DeathLink, PerGameCommonOptions, SpecialRange
@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)
class DoubleJumpGlitch(Choice): class DoubleJumpGlitch(Choice):
@ -94,31 +78,13 @@ class ItemShuffle(Choice):
default = 0 default = 0
DLCQuest_options: Dict[str, type(Option)] = { @dataclass
option.internal_name: option class DLCQuestOptions(PerGameCommonOptions):
for option in [ double_jump_glitch: DoubleJumpGlitch
DoubleJumpGlitch, coinsanity: CoinSanity
CoinSanity, coinbundlequantity: CoinSanityRange
CoinSanityRange, time_is_money: TimeIsMoney
TimeIsMoney, ending_choice: EndingChoice
EndingChoice, campaign: Campaign
Campaign, item_shuffle: ItemShuffle
ItemShuffle, death_link: DeathLink
]
}
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

View File

@ -1,8 +1,9 @@
import math 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 .Locations import DLCQuestLocation, location_table
from .Rules import create_event from .Rules import create_event
from . import Options
DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left", DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left",
"Double Jump Behind the Tree", "The Forest", "Final Room"] "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): def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
Regmenu = Region("Menu", player, world) Regmenu = Region("Menu", player, world)
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
Options.Campaign] == Options.Campaign.option_both: == Options.Campaign.option_both):
Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)] Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
Options.Campaign] == Options.Campaign.option_both: == Options.Campaign.option_both):
Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)] Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
world.regions.append(Regmenu) world.regions.append(Regmenu)
if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
Options.Campaign] == Options.Campaign.option_both: == Options.Campaign.option_both):
Regmoveright = Region("Move Right", player, world, "Start of the basic game") Regmoveright = Region("Move Right", player, world, "Start of the basic game")
Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"] 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 Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
loc_name in Locmoveright_name] loc_name in Locmoveright_name]
add_coin_dlcquest(Regmoveright, 4, player) add_coin_dlcquest(Regmoveright, 4, player)
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
for i in range(coin_bundle_needed): 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 += [ Regmoveright.locations += [
DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)] DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
if 825 % World_Options[Options.CoinSanityRange] != 0: if 825 % World_Options.coinbundlequantity != 0:
Regmoveright.locations += [ Regmoveright.locations += [
DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"], DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
Regmoveright)] Regmoveright)]
@ -58,7 +59,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue
Regmovpack = Region("Movement Pack", player, world) Regmovpack = Region("Movement Pack", player, world)
Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
"Shepherd Sheep"] "Shepherd Sheep"]
if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
Locmovpack_name += ["Sword"] Locmovpack_name += ["Sword"]
Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)] Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name 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) Regbtree = Region("Behind Tree", player, world)
Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] 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"] Locbtree_name += ["Gun"]
Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree), Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
Entrance(player, "Forest Entrance", 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)) 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[ if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
Options.Campaign] == Options.Campaign.option_both: == Options.Campaign.option_both):
Regfreemiumstart = Region("Freemium Start", player, world) Regfreemiumstart = Region("Freemium Start", player, world)
Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack", Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
"Nice Try", "Story is Important", "I Get That Reference!"] "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"] Locfreemiumstart_name += ["Wooden Sword"]
Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)] Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart) Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
for loc_name in for loc_name in
Locfreemiumstart_name] Locfreemiumstart_name]
add_coin_freemium(Regfreemiumstart, 50, player) add_coin_freemium(Regfreemiumstart, 50, player)
if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: if World_Options.coinsanity == Options.CoinSanity.option_coin:
coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
for i in range(coin_bundle_needed): 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 += [ Regfreemiumstart.locations += [
DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium], DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
Regfreemiumstart)] Regfreemiumstart)]
if 889 % World_Options[Options.CoinSanityRange] != 0: if 889 % World_Options.coinbundlequantity != 0:
Regfreemiumstart.locations += [ Regfreemiumstart.locations += [
DLCQuestLocation(player, "Live Freemium or Die: 889 Coin", DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
location_table["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) Regbehindvine = Region("Behind the Vines", player, world)
Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] 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"] Locbehindvine_name += ["Pickaxe"]
Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)] Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for 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) Regcutcontent = Region("Cut Content", player, world)
Loccutcontent_name = [] 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"] Loccutcontent_name += ["Humble Indie Bindle"]
Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
loc_name in Loccutcontent_name] 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) Regnamechange = Region("Name Change", player, world)
Locnamechange_name = [] 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"] Locnamechange_name += ["Box of Various Supplies"]
Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)] Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for

View File

@ -1,10 +1,10 @@
import math import math
import re 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 BaseClasses import ItemClassification
from worlds.generic.Rules import add_rule, item_name_in_locations, set_rule
from . import Options from . import Options
from .Items import DLCQuestItem
def create_event(player, event: str): def create_event(player, event: str):
@ -42,7 +42,7 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions):
def set_basic_rules(World_Options, has_enough_coin, player, world): 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 return
set_basic_entrance_rules(player, world) set_basic_entrance_rules(player, world)
set_basic_self_obtained_items_rules(World_Options, 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): 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 return
set_rule(world.get_entrance("Behind Ogre", player), set_rule(world.get_entrance("Behind Ogre", player),
lambda state: state.has("Gun Pack", 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), set_rule(world.get_entrance("Tree", player),
lambda state: state.has("Time is Money Pack", player)) lambda state: state.has("Time is Money Pack", player))
set_rule(world.get_entrance("Cave Tree", 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): 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 return
set_rule(world.get_entrance("Behind Ogre", player), set_rule(world.get_entrance("Behind Ogre", player),
lambda state: state.has("Gun", 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), set_rule(world.get_location("Gun", player),
lambda state: state.has("Gun Pack", 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), set_rule(world.get_location("Sword", player),
lambda state: state.has("Time is Money Pack", player)) lambda state: state.has("Time is Money Pack", player))
def set_double_jump_glitchless_rules(World_Options, player, world): 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 return
set_rule(world.get_entrance("Cloud Double Jump", player), set_rule(world.get_entrance("Cloud Double Jump", player),
lambda state: state.has("Double Jump Pack", 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): 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 return
set_rule(world.get_entrance("Behind Tree Double Jump", player), set_rule(world.get_entrance("Behind Tree Double Jump", player),
lambda state: state.has("Double Jump Pack", 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): 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 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): 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), set_rule(world.get_location(item_coin, player),
has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) has_enough_coin(player, World_Options.coinbundlequantity * (i + 1)))
if 825 % World_Options[Options.CoinSanityRange] != 0: if 825 % World_Options.coinbundlequantity != 0:
set_rule(world.get_location("DLC Quest: 825 Coin", player), set_rule(world.get_location("DLC Quest: 825 Coin", player),
has_enough_coin(player, 825)) has_enough_coin(player, 825))
set_rule(world.get_location("Movement Pack", player), set_rule(world.get_location("Movement Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Animation Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Audio Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Pause Menu Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Time is Money Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Double Jump Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Pet Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Sexy Outfits Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Top Hat Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Gun Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("The Zombie Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Night Map Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Psychological Warfare Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Armor for your Horse Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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), set_rule(world.get_location("Finish the Fight Pack", player),
lambda state: state.has("DLC Quest: Coin Bundle", 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): 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 return
set_rule(world.get_location("Movement Pack", player), set_rule(world.get_location("Movement Pack", player),
has_enough_coin(player, 4)) 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): 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), set_rule(world.get_location("Winning Basic", player),
lambda state: state.has("Finish the Fight Pack", 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), 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", lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack",
player)) player))
def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): 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 return
set_lfod_entrance_rules(player, world) set_lfod_entrance_rules(player, world)
set_boss_door_requirements_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): 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 return
set_rule(world.get_entrance("Vines", player), set_rule(world.get_entrance("Vines", player),
lambda state: state.has("Incredibly Important Pack", 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): 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 return
set_rule(world.get_entrance("Vines", player), set_rule(world.get_entrance("Vines", player),
lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", 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): 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 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): for i in range(number_of_bundle):
item_coin_freemium = "Live Freemium or Die: number Coin" 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) item_coin_freemium)
set_rule(world.get_location(item_coin_loc_freemium, player), set_rule(world.get_location(item_coin_loc_freemium, player),
has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) has_enough_coin_freemium(player, World_Options.coinbundlequantity * (i + 1)))
if 889 % World_Options[Options.CoinSanityRange] != 0: if 889 % World_Options.coinbundlequantity != 0:
set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), set_rule(world.get_location("Live Freemium or Die: 889 Coin", player),
has_enough_coin_freemium(player, 889)) has_enough_coin_freemium(player, 889))
add_rule(world.get_entrance("Boss Door", player), add_rule(world.get_entrance("Boss Door", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Particles Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Day One Patch Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Checkpoint Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Incredibly Important Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Wall Jump Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Health Bar Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Parallax Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Harmless Plants Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Death of Comedy Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Canadian Dialog Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("DLC NPC Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Cut Content Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Name Change Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Season Pass", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("High Definition Next Gen Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Increased HP Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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), set_rule(world.get_location("Remove Ads Pack", player),
lambda state: state.has("Live Freemium or Die: Coin Bundle", 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): 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 return
add_rule(world.get_entrance("Boss Door", player), add_rule(world.get_entrance("Boss Door", player),
has_enough_coin_freemium(player, 889)) 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): 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) 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) 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( world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has(
"Victory Freemium", player) "Victory Freemium", player)

View File

@ -1,12 +1,13 @@
from typing import Dict, Any, Iterable, Optional, Union from typing import Union
from BaseClasses import Tutorial from BaseClasses import Tutorial
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import WebWorld, World
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 . import Options 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 client_version = 0
@ -35,10 +36,8 @@ class DLCqworld(World):
data_version = 1 data_version = 1
option_definitions = DLCQuest_options options_dataclass = DLCQuestOptions
options: DLCQuestOptions
def generate_early(self):
self.options = fetch_options(self.multiworld, self.player)
def create_regions(self): def create_regions(self):
create_regions(self.multiworld, self.player, self.options) create_regions(self.multiworld, self.player, self.options)
@ -68,8 +67,8 @@ class DLCqworld(World):
self.multiworld.itempool.remove(item) self.multiworld.itempool.remove(item)
def precollect_coinsanity(self): def precollect_coinsanity(self):
if self.options[Options.Campaign] == Options.Campaign.option_basic: if self.options.campaign == Options.Campaign.option_basic:
if self.options[Options.CoinSanity] == Options.CoinSanity.option_coin and self.options[Options.CoinSanityRange] >= 5: if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
self.multiworld.push_precollected(self.create_item("Movement Pack")) self.multiworld.push_precollected(self.create_item("Movement Pack"))
@ -80,12 +79,11 @@ class DLCqworld(World):
return DLCQuestItem(item.name, item.classification, item.code, self.player) return DLCQuestItem(item.name, item.classification, item.code, self.player)
def fill_slot_data(self): def fill_slot_data(self):
return { options_dict = self.options.as_dict(
"death_link": self.multiworld.death_link[self.player].value, "death_link", "ending_choice", "campaign", "coinsanity", "item_shuffle"
"ending_choice": self.multiworld.ending_choice[self.player].value, )
"campaign": self.multiworld.campaign[self.player].value, options_dict.update({
"coinsanity": self.multiworld.coinsanity[self.player].value, "coinbundlerange": self.options.coinbundlequantity.value,
"coinbundlerange": self.multiworld.coinbundlequantity[self.player].value, "seed": self.random.randrange(99999999)
"item_shuffle": self.multiworld.item_shuffle[self.player].value, })
"seed": self.multiworld.per_slot_randoms[self.player].randrange(99999999) return options_dict
}

View File

@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS 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 .regions import MEGA_SHARDS, REGIONS, REGION_CONNECTIONS, SEALS
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
from .shop import FIGURINES, SHOP_ITEMS, shuffle_shop_prices from .shop import FIGURINES, SHOP_ITEMS, shuffle_shop_prices
@ -44,7 +44,8 @@ class MessengerWorld(World):
"Phobekin": set(PHOBEKINS), "Phobekin": set(PHOBEKINS),
} }
option_definitions = messenger_options options_dataclass = MessengerOptions
options: MessengerOptions
base_offset = 0xADD_000 base_offset = 0xADD_000
item_name_to_id = {item: item_id item_name_to_id = {item: item_id
@ -74,9 +75,9 @@ class MessengerWorld(World):
_filler_items: List[str] _filler_items: List[str]
def generate_early(self) -> None: def generate_early(self) -> None:
if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: if self.options.goal == Goal.option_power_seal_hunt:
self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true self.options.shuffle_seals.value = PowerSeals.option_true
self.total_seals = self.multiworld.total_seals[self.player].value self.total_seals = self.options.total_seals.value
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self) self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
@ -87,7 +88,7 @@ class MessengerWorld(World):
def create_items(self) -> None: def create_items(self) -> None:
# create items that are always in the item pool # create items that are always in the item pool
itempool = [ itempool: List[MessengerItem] = [
self.create_item(item) self.create_item(item)
for item in self.item_name_to_id for item in self.item_name_to_id
if item not in if item not in
@ -97,13 +98,13 @@ class MessengerWorld(World):
} and "Time Shard" not in item } 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 # 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 # 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]] notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
self.random.shuffle(notes) self.random.shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \ precollected_notes_amount = NotesNeeded.range_end - \
self.multiworld.notes_needed[self.player] - \ self.options.notes_needed - \
(len(NOTES) - len(notes)) (len(NOTES) - len(notes))
if precollected_notes_amount: if precollected_notes_amount:
for note in notes[:precollected_notes_amount]: for note in notes[:precollected_notes_amount]:
@ -111,15 +112,14 @@ class MessengerWorld(World):
notes = notes[precollected_notes_amount:] notes = notes[precollected_notes_amount:]
itempool += [self.create_item(note) for note in notes] 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), 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: if total_seals < self.total_seals:
logging.warning(f"Not enough locations for total seals setting " 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.total_seals = total_seals
self.required_seals =\ self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals)
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
for i in range(self.required_seals): for i in range(self.required_seals):
@ -138,7 +138,7 @@ class MessengerWorld(World):
self.multiworld.itempool += itempool self.multiworld.itempool += itempool
def set_rules(self) -> None: def set_rules(self) -> None:
logic = self.multiworld.logic_level[self.player] logic = self.options.logic_level
if logic == Logic.option_normal: if logic == Logic.option_normal:
MessengerRules(self).set_messenger_rules() MessengerRules(self).set_messenger_rules()
elif logic == Logic.option_hard: elif logic == Logic.option_hard:
@ -151,12 +151,12 @@ class MessengerWorld(World):
figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()} figure_prices = {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}
return { return {
"deathlink": self.multiworld.death_link[self.player].value, "deathlink": self.options.death_link.value,
"goal": self.multiworld.goal[self.player].current_key, "goal": self.options.goal.current_key,
"music_box": self.multiworld.music_box[self.player].value, "music_box": self.options.music_box.value,
"required_seals": self.required_seals, "required_seals": self.required_seals,
"mega_shards": self.multiworld.shuffle_shards[self.player].value, "mega_shards": self.options.shuffle_shards.value,
"logic": self.multiworld.logic_level[self.player].current_key, "logic": self.options.logic_level.current_key,
"shop": shop_prices, "shop": shop_prices,
"figures": figure_prices, "figures": figure_prices,
"max_price": self.total_shards, "max_price": self.total_shards,
@ -175,7 +175,7 @@ class MessengerWorld(World):
item_id: Optional[int] = self.item_name_to_id.get(name, None) item_id: Optional[int] = self.item_name_to_id.get(name, None)
override_prog = getattr(self, "multiworld") is not None and \ override_prog = getattr(self, "multiworld") is not None and \
name in {"Windmill Shuriken"} and \ name in {"Windmill Shuriken"} and \
self.multiworld.logic_level[self.player] > Logic.option_normal self.options.logic_level > Logic.option_normal
count = 0 count = 0
if "Time Shard " in name: if "Time Shard " in name:
count = int(name.strip("Time Shard ()")) count = int(name.strip("Time Shard ()"))

View File

@ -1,7 +1,7 @@
from .shop import FIGURINES, SHOP_ITEMS
# items # items
# listing individual groups first for easy lookup # listing individual groups first for easy lookup
from .shop import SHOP_ITEMS, FIGURINES
NOTES = [ NOTES = [
"Key of Hope", "Key of Hope",
"Key of Chaos", "Key of Chaos",

View File

@ -1,7 +1,10 @@
from dataclasses import dataclass
from typing import Dict 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): class MessengerAccessibility(Accessibility):
@ -129,18 +132,19 @@ class PlannedShopPrices(OptionDict):
}) })
messenger_options = { @dataclass
"accessibility": MessengerAccessibility, class MessengerOptions(PerGameCommonOptions):
"start_inventory": StartInventoryPool, accessibility: MessengerAccessibility
"logic_level": Logic, start_inventory: StartInventoryPool
"shuffle_seals": PowerSeals, logic_level: Logic
"shuffle_shards": MegaShards, shuffle_seals: PowerSeals
"goal": Goal, shuffle_shards: MegaShards
"music_box": MusicBox, goal: Goal
"notes_needed": NotesNeeded, music_box: MusicBox
"total_seals": AmountSeals, notes_needed: NotesNeeded
"percent_seals_required": RequiredSeals, total_seals: AmountSeals
"shop_price": ShopPrices, percent_seals_required: RequiredSeals
"shop_price_plan": PlannedShopPrices, shop_price: ShopPrices
"death_link": DeathLink, shop_price_plan: PlannedShopPrices
} death_link: DeathLink

View File

@ -1,4 +1,4 @@
from typing import Dict, Set, List from typing import Dict, List, Set
REGIONS: Dict[str, List[str]] = { REGIONS: Dict[str, List[str]] = {
"Menu": [], "Menu": [],

View File

@ -1,9 +1,9 @@
from typing import Dict, Callable, TYPE_CHECKING from typing import Callable, Dict, TYPE_CHECKING
from BaseClasses import CollectionState, MultiWorld from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule, allow_self_locking_items, add_rule from worlds.generic.Rules import add_rule, allow_self_locking_items, set_rule
from .options import MessengerAccessibility, Goal
from .constants import NOTES, PHOBEKINS from .constants import NOTES, PHOBEKINS
from .options import Goal, MessengerAccessibility
from .subclasses import MessengerShopLocation from .subclasses import MessengerShopLocation
if TYPE_CHECKING: if TYPE_CHECKING:
@ -145,13 +145,13 @@ class MessengerRules:
if region.name == "The Shop": if region.name == "The Shop":
for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]: for loc in [location for location in region.locations if isinstance(location, MessengerShopLocation)]:
loc.access_rule = loc.can_afford 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), set_rule(multiworld.get_entrance("Tower HQ -> Music Box", self.player),
lambda state: state.has("Shop Chest", self.player)) lambda state: state.has("Shop Chest", self.player))
multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player) multiworld.completion_condition[self.player] = lambda state: state.has("Rescue Phantom", self.player)
if multiworld.accessibility[self.player] > MessengerAccessibility.option_locations: 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): class MessengerHardRules(MessengerRules):
@ -212,9 +212,9 @@ class MessengerHardRules(MessengerRules):
def set_messenger_rules(self) -> None: def set_messenger_rules(self) -> None:
super().set_messenger_rules() super().set_messenger_rules()
for loc, rule in self.extra_rules.items(): 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 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 continue
add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or")
@ -249,20 +249,22 @@ class MessengerOOBRules(MessengerRules):
def set_messenger_rules(self) -> None: def set_messenger_rules(self) -> None:
super().set_messenger_rules() super().set_messenger_rules()
self.world.multiworld.completion_condition[self.player] = lambda state: True 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 # 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("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("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") allow_self_locking_items(multiworld.get_location("Corrupted Future - Key of Courage", player), "Demon King Crown")
# add these locations when seals are shuffled # 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") allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master")
# add these locations when seals and shards aren't shuffled # 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: 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) 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) allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS)

View File

@ -74,8 +74,8 @@ FIGURINES: Dict[str, ShopData] = {
def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: 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_mod = world.options.shop_price.value
shop_price_planned = world.multiworld.shop_price_plan[world.player] shop_price_planned = world.options.shop_price_plan
shop_prices: Dict[str, int] = {} shop_prices: Dict[str, int] = {}
figurine_prices: Dict[str, int] = {} figurine_prices: Dict[str, int] = {}

View File

@ -9,16 +9,15 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS
if TYPE_CHECKING: if TYPE_CHECKING:
from . import MessengerWorld from . import MessengerWorld
else:
MessengerWorld = object
class MessengerRegion(Region): 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) super().__init__(name, world.player, world.multiworld)
locations = [loc for loc in REGIONS[self.name]] locations = [loc for loc in REGIONS[self.name]]
if self.name == "The Shop": 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") locations.append("Shop Chest")
shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"] shop_locations = {f"The Shop - {shop_loc}": world.location_name_to_id[f"The Shop - {shop_loc}"]
for shop_loc in SHOP_ITEMS} for shop_loc in SHOP_ITEMS}
@ -26,9 +25,9 @@ class MessengerRegion(Region):
self.add_locations(shop_locations, MessengerShopLocation) self.add_locations(shop_locations, MessengerShopLocation)
elif self.name == "Tower HQ": elif self.name == "Tower HQ":
locations.append("Money Wrench") 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]] 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]] 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 loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
for loc in locations} for loc in locations}
@ -49,7 +48,7 @@ class MessengerShopLocation(MessengerLocation):
@cached_property @cached_property
def cost(self) -> int: def cost(self) -> int:
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped 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 # short circuit figurines which all require demon's bane be purchased, but nothing else
if "Figurine" in name: if "Figurine" in name:
return world.figurine_prices[name] +\ return world.figurine_prices[name] +\
@ -70,9 +69,8 @@ class MessengerShopLocation(MessengerLocation):
return world.shop_prices[name] return world.shop_prices[name]
def can_afford(self, state: CollectionState) -> bool: def can_afford(self, state: CollectionState) -> bool:
world: MessengerWorld = state.multiworld.worlds[self.player] world = cast("MessengerWorld", state.multiworld.worlds[self.player])
cost = self.cost can_afford = state.has("Shards", self.player, min(self.cost, world.total_shards))
can_afford = state.has("Shards", self.player, min(cost, world.total_shards))
if "Figurine" in self.name: if "Figurine" in self.name:
can_afford = state.has("Money Wrench", self.player) and can_afford\ can_afford = state.has("Money Wrench", self.player) and can_afford\
and state.can_reach("Money Wrench", "Location", self.player) and state.can_reach("Money Wrench", "Location", self.player)

View File

@ -32,7 +32,7 @@ from .Cosmetics import patch_cosmetics
from Utils import get_options from Utils import get_options
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType 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 Fill import fill_restrictive, fast_fill, FillError
from worlds.generic.Rules import exclusion_rules, add_item_rule from worlds.generic.Rules import exclusion_rules, add_item_rule
from ..AutoWorld import World, AutoLogicRegister, WebWorld from ..AutoWorld import World, AutoLogicRegister, WebWorld
@ -286,7 +286,7 @@ class OOTWorld(World):
# No Logic forces all tricks on, prog balancing off and beatable-only # No Logic forces all tricks on, prog balancing off and beatable-only
elif self.logic_rules == 'no_logic': elif self.logic_rules == 'no_logic':
self.multiworld.progression_balancing[self.player].value = False 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(): for trick in normalized_name_tricks.values():
setattr(self, trick['name'], True) setattr(self, trick['name'], True)

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum
from typing import TypedDict 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 from .Overcooked2Levels import Overcooked2Dlc
class LocationBalancingMode(IntEnum): class LocationBalancingMode(IntEnum):
@ -167,32 +168,30 @@ class StarThresholdScale(Range):
default = 35 default = 35
overcooked_options = { @dataclass
class OC2Options(PerGameCommonOptions):
# generator options # generator options
"location_balancing": LocationBalancing, location_balancing: LocationBalancing
"ramp_tricks": RampTricks, ramp_tricks: RampTricks
# deathlink # deathlink
"deathlink": DeathLink, deathlink: DeathLink
# randomization options # randomization options
"shuffle_level_order": ShuffleLevelOrder, shuffle_level_order: ShuffleLevelOrder
"include_dlcs": DLCOptionSet, include_dlcs: DLCOptionSet
"include_horde_levels": IncludeHordeLevels, include_horde_levels: IncludeHordeLevels
"prep_levels": PrepLevels, prep_levels: PrepLevels
"kevin_levels": KevinLevels, kevin_levels: KevinLevels
# quality of life options # quality of life options
"fix_bugs": FixBugs, fix_bugs: FixBugs
"shorter_level_duration": ShorterLevelDuration, shorter_level_duration: ShorterLevelDuration
"short_horde_levels": ShortHordeLevels, short_horde_levels: ShortHordeLevels
"always_preserve_cooking_progress": AlwaysPreserveCookingProgress, always_preserve_cooking_progress: AlwaysPreserveCookingProgress
"always_serve_oldest_order": AlwaysServeOldestOrder, always_serve_oldest_order: AlwaysServeOldestOrder
"display_leaderboard_scores": DisplayLeaderboardScores, display_leaderboard_scores: DisplayLeaderboardScores
# difficulty settings # difficulty settings
"stars_to_win": StarsToWin, stars_to_win: StarsToWin
"star_threshold_scale": StarThresholdScale, star_threshold_scale: StarThresholdScale
}
OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()})

View File

@ -6,7 +6,7 @@ from worlds.AutoWorld import World, WebWorld
from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name 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 .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 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" game = "Overcooked! 2"
web = Overcooked2Web() web = Overcooked2Web()
required_client_version = (0, 3, 8) required_client_version = (0, 3, 8)
option_definitions = overcooked_options
topology_present: bool = False topology_present: bool = False
data_version = 3 data_version = 3
@ -57,13 +56,14 @@ class Overcooked2World(World):
location_id_to_name = oc2_location_id_to_name location_id_to_name = oc2_location_id_to_name
location_name_to_id = oc2_location_name_to_id location_name_to_id = oc2_location_name_to_id
options: Dict[str, Any] options_dataclass = OC2Options
options: OC2Options
itempool: List[Overcooked2Item] itempool: List[Overcooked2Item]
# Helper Functions # Helper Functions
def is_level_horde(self, level_id: int) -> bool: 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 \ (self.level_mapping is not None) and \
level_id in self.level_mapping.keys() and \ level_id in self.level_mapping.keys() and \
self.level_mapping[level_id].is_horde self.level_mapping[level_id].is_horde
@ -145,11 +145,6 @@ class Overcooked2World(World):
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]: def get_n_random_locations(self, n: int) -> List[int]:
"""Return a list of n random non-repeating level locations""" """Return a list of n random non-repeating level locations"""
levels = list() levels = list()
@ -160,7 +155,7 @@ class Overcooked2World(World):
for level in Overcooked2Level(): for level in Overcooked2Level():
if level.level_id == 36: if level.level_id == 36:
continue continue
elif not self.options["KevinLevels"] and level.level_id > 36: elif not self.options.kevin_levels and level.level_id > 36:
break break
levels.append(level.level_id) levels.append(level.level_id)
@ -231,26 +226,25 @@ class Overcooked2World(World):
def generate_early(self): def generate_early(self):
self.player_name = self.multiworld.player_name[self.player] 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 # 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 # 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 # 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 # 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 # 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 = \ self.level_mapping = \
level_shuffle_factory( level_shuffle_factory(
self.multiworld.random, self.multiworld.random,
self.options["PrepLevels"] != PrepLevelMode.excluded, self.options.prep_levels != PrepLevelMode.excluded,
self.options["IncludeHordeLevels"], self.options.include_horde_levels.result,
self.options["KevinLevels"], self.options.kevin_levels.result,
self.enabled_dlc, self.enabled_dlc,
self.player_name, self.player_name,
) )
@ -277,7 +271,7 @@ class Overcooked2World(World):
# Create and populate "regions" (a.k.a. levels) # Create and populate "regions" (a.k.a. levels)
for level in Overcooked2Level(): 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 break
# Create Region (e.g. "1-1") # Create Region (e.g. "1-1")
@ -336,7 +330,7 @@ class Overcooked2World(World):
level_access_rule: Callable[[CollectionState], bool] = \ 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: \ 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) self.connect_regions("Overworld", level.level_name, level_access_rule)
# Level --> Overworld # Level --> Overworld
@ -369,11 +363,11 @@ class Overcooked2World(World):
# Item is always useless with these settings # Item is always useless with these settings
continue 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 # skip horde-specific items if no horde levels
continue continue
if not self.options["KevinLevels"]: if not self.options.kevin_levels:
if item_name.startswith("Kevin"): if item_name.startswith("Kevin"):
# skip kevin items if no kevin levels # skip kevin items if no kevin levels
continue continue
@ -382,7 +376,7 @@ class Overcooked2World(World):
# skip dark green ramp if there's no Kevin-1 to reveal it # skip dark green ramp if there's no Kevin-1 to reveal it
continue 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) # progression.append(item_name)
classification = ItemClassification.progression classification = ItemClassification.progression
else: else:
@ -404,7 +398,7 @@ class Overcooked2World(World):
# Fill any free space with filler # Fill any free space with filler
pool_count = len(oc2_location_name_to_id) pool_count = len(oc2_location_name_to_id)
if not self.options["KevinLevels"]: if not self.options.kevin_levels:
pool_count -= 8 pool_count -= 8
while len(self.itempool) < pool_count: while len(self.itempool) < pool_count:
@ -416,7 +410,7 @@ class Overcooked2World(World):
def place_events(self): def place_events(self):
# Add Events (Star Acquisition) # Add Events (Star Acquisition)
for level in Overcooked2Level(): 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 break
if level.level_id != 36: if level.level_id != 36:
@ -449,7 +443,7 @@ class Overcooked2World(World):
# Serialize Level Order # Serialize Level Order
story_level_order = dict() story_level_order = dict()
if self.options["ShuffleLevelOrder"]: if self.options.shuffle_level_order:
for level_id in self.level_mapping: for level_id in self.level_mapping:
level: Overcooked2GenericLevel = self.level_mapping[level_id] level: Overcooked2GenericLevel = self.level_mapping[level_id]
story_level_order[str(level_id)] = { story_level_order[str(level_id)] = {
@ -481,7 +475,7 @@ class Overcooked2World(World):
level_unlock_requirements[str(level_id)] = level_id - 1 level_unlock_requirements[str(level_id)] = level_id - 1
# Set Kevin Unlock Requirements # 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]: def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
location = self.multiworld.find_item(f"Kevin-{level_id-36}", self.player) location = self.multiworld.find_item(f"Kevin-{level_id-36}", self.player)
if location.player != self.player: if location.player != self.player:
@ -506,7 +500,7 @@ class Overcooked2World(World):
on_level_completed[level_id] = [item_to_unlock_event(location.item.name)] on_level_completed[level_id] = [item_to_unlock_event(location.item.name)]
# Put it all together # Put it all together
star_threshold_scale = self.options["StarThresholdScale"] / 100 star_threshold_scale = self.options.star_threshold_scale / 100
base_data = { base_data = {
# Changes Inherent to rando # Changes Inherent to rando
@ -528,13 +522,13 @@ class Overcooked2World(World):
"SaveFolderName": mod_name, "SaveFolderName": mod_name,
"CustomOrderTimeoutPenalty": 10, "CustomOrderTimeoutPenalty": 10,
"LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44], "LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44],
"LocalDeathLink": self.options["DeathLink"] != DeathLinkMode.disabled, "LocalDeathLink": self.options.deathlink != DeathLinkMode.disabled,
"BurnTriggersDeath": self.options["DeathLink"] == DeathLinkMode.death_and_overcook, "BurnTriggersDeath": self.options.deathlink == DeathLinkMode.death_and_overcook,
# Game Modifications # Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements, "LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6), "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, "CustomLevelOrder": custom_level_order,
# Items (Starting Inventory) # Items (Starting Inventory)
@ -580,28 +574,27 @@ class Overcooked2World(World):
# Set remaining data in the options dict # Set remaining data in the options dict
bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"] bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"]
for bug in bugs: for bug in bugs:
self.options[bug] = self.options["FixBugs"] base_data[bug] = self.options.fix_bugs.result
self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"] base_data["PreserveCookingProgress"] = self.options.always_preserve_cooking_progress.result
self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce base_data["TimerAlwaysStarts"] = self.options.prep_levels == PrepLevelMode.ayce
self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0 base_data["LevelTimerScale"] = 0.666 if self.options.shorter_level_duration else 1.0
self.options["LeaderboardScoreScale"] = { base_data["LeaderboardScoreScale"] = {
"FourStars": 1.0, "FourStars": 1.0,
"ThreeStars": star_threshold_scale, "ThreeStars": star_threshold_scale,
"TwoStars": star_threshold_scale * 0.75, "TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35, "OneStar": star_threshold_scale * 0.35,
} }
base_data.update(self.options)
return base_data return base_data
def fill_slot_data(self) -> Dict[str, Any]: def fill_slot_data(self) -> Dict[str, Any]:
return self.fill_json_data() return self.fill_json_data()
def write_spoiler(self, spoiler_handle: TextIO) -> None: def write_spoiler(self, spoiler_handle: TextIO) -> None:
if not self.options["ShuffleLevelOrder"]: if not self.options.shuffle_level_order:
return 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") spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n")
for overworld_id in world.level_mapping: for overworld_id in world.level_mapping:
overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1] overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1]

View File

@ -1,5 +1,5 @@
from typing import Dict from dataclasses import dataclass
from Options import Option, Toggle, DefaultOnToggle, DeathLink, Range, Choice 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 # 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 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, # define a class for the weights of the generated item pool.
"total_locations": TotalLocations, @dataclass
"chests_per_stage": ChestsPerEnvironment, class ROR2Weights:
"shrines_per_stage": ShrinesPerEnvironment, green_scrap: GreenScrap
"scavengers_per_stage": ScavengersPerEnvironment, red_scrap: RedScrap
"scanner_per_stage": ScannersPerEnvironment, yellow_scrap: YellowScrap
"altars_per_stage": AltarsPerEnvironment, white_scrap: WhiteScrap
"total_revivals": TotalRevivals, common_item: CommonItem
"start_with_revive": StartWithRevive, uncommon_item: UncommonItem
"final_stage_death": FinalStageDeath, legendary_item: LegendaryItem
"begin_with_loop": BeginWithLoop, boss_item: BossItem
"dlc_sotv": DLC_SOTV, lunar_item: LunarItem
"death_link": DeathLink, void_item: VoidItem
"item_pickup_step": ItemPickupStep, equipment: Equipment
"shrine_use_step": ShrineUseStep,
"enable_lunar": AllowLunarItems, @dataclass
"item_weights": ItemWeights, class ROR2Options(PerGameCommonOptions, ROR2Weights):
"item_pool_presets": ItemPoolPresetToggle, goal: Goal
**ror2_weights 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

View File

@ -6,7 +6,7 @@ from .Rules import set_rules
from .RoR2Environments import * from .RoR2Environments import *
from BaseClasses import Region, Entrance, Item, ItemClassification, MultiWorld, Tutorial 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 worlds.AutoWorld import World, WebWorld
from .Regions import create_regions 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 Combine loot in surprising ways and master each character until you become the havoc you feared upon your
first crash landing. first crash landing.
""" """
game: str = "Risk of Rain 2" game = "Risk of Rain 2"
option_definitions = ror2_options options_dataclass = ROR2Options
options: ROR2Options
topology_present = False topology_present = False
item_name_to_id = item_table item_name_to_id = item_table
@ -46,45 +47,44 @@ class RiskOfRainWorld(World):
def generate_early(self) -> None: def generate_early(self) -> None:
# figure out how many revivals should exist in the pool # figure out how many revivals should exist in the pool
if self.multiworld.goal[self.player] == "classic": if self.options.goal == "classic":
total_locations = self.multiworld.total_locations[self.player].value total_locations = self.options.total_locations.value
else: else:
total_locations = len( total_locations = len(
orderedstage_location.get_locations( orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value, chests=self.options.chests_per_stage.value,
shrines=self.multiworld.shrines_per_stage[self.player].value, shrines=self.options.shrines_per_stage.value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value, scavengers=self.options.scavengers_per_stage.value,
scanners=self.multiworld.scanner_per_stage[self.player].value, scanners=self.options.scanner_per_stage.value,
altars=self.multiworld.altars_per_stage[self.player].value, altars=self.options.altars_per_stage.value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].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) total_locations)
# self.total_revivals = self.multiworld.total_revivals[self.player].value if self.options.start_with_revive:
if self.multiworld.start_with_revive[self.player].value:
self.total_revivals -= 1 self.total_revivals -= 1
def create_items(self) -> None: def create_items(self) -> None:
# shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend # 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)) self.multiworld.push_precollected(self.multiworld.create_item("Dio's Best Friend", self.player))
environments_pool = {} environments_pool = {}
# only mess with the environments if they are set as items # 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 # figure out all available ordered stages for each tier
environment_available_orderedstages_table = environment_vanilla_orderedstages_table 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) 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) 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) environment_offset_table = shift_by_offset(environment_sotv_table, environment_offest)
environments_pool = {**environments_pool, **environment_offset_table} 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) # percollect environments for each stage (or just stage 1)
for i in range(environments_to_precollect): for i in range(environments_to_precollect):
unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1) unlock = self.multiworld.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
@ -100,19 +100,19 @@ class RiskOfRainWorld(World):
for env_name, _ in environments_pool.items(): for env_name, _ in environments_pool.items():
itempool += [env_name] itempool += [env_name]
if self.multiworld.goal[self.player] == "classic": if self.options.goal == "classic":
# classic mode # classic mode
total_locations = self.multiworld.total_locations[self.player].value total_locations = self.options.total_locations.value
else: else:
# explore mode # explore mode
total_locations = len( total_locations = len(
orderedstage_location.get_locations( orderedstage_location.get_locations(
chests=self.multiworld.chests_per_stage[self.player].value, chests=self.options.chests_per_stage.value,
shrines=self.multiworld.shrines_per_stage[self.player].value, shrines=self.options.shrines_per_stage.value,
scavengers=self.multiworld.scavengers_per_stage[self.player].value, scavengers=self.options.scavengers_per_stage.value,
scanners=self.multiworld.scanner_per_stage[self.player].value, scanners=self.options.scanner_per_stage.value,
altars=self.multiworld.altars_per_stage[self.player].value, altars=self.options.altars_per_stage.value,
dlc_sotv=self.multiworld.dlc_sotv[self.player].value dlc_sotv=self.options.dlc_sotv.value
) )
) )
# Create junk items # Create junk items
@ -138,9 +138,9 @@ class RiskOfRainWorld(World):
def create_junk_pool(self) -> Dict: def create_junk_pool(self) -> Dict:
# if presets are enabled generate junk_pool from the selected preset # 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] = {} 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 # generate chaos weights if the preset is chosen
if pool_option == ItemWeights.option_chaos: if pool_option == ItemWeights.option_chaos:
for name, max_value in item_pool_weights[pool_option].items(): for name, max_value in item_pool_weights[pool_option].items():
@ -149,31 +149,31 @@ class RiskOfRainWorld(World):
junk_pool = item_pool_weights[pool_option].copy() junk_pool = item_pool_weights[pool_option].copy()
else: # generate junk pool from user created presets else: # generate junk pool from user created presets
junk_pool = { junk_pool = {
"Item Scrap, Green": self.multiworld.green_scrap[self.player].value, "Item Scrap, Green": self.options.green_scrap.value,
"Item Scrap, Red": self.multiworld.red_scrap[self.player].value, "Item Scrap, Red": self.options.red_scrap.value,
"Item Scrap, Yellow": self.multiworld.yellow_scrap[self.player].value, "Item Scrap, Yellow": self.options.yellow_scrap.value,
"Item Scrap, White": self.multiworld.white_scrap[self.player].value, "Item Scrap, White": self.options.white_scrap.value,
"Common Item": self.multiworld.common_item[self.player].value, "Common Item": self.options.common_item.value,
"Uncommon Item": self.multiworld.uncommon_item[self.player].value, "Uncommon Item": self.options.uncommon_item.value,
"Legendary Item": self.multiworld.legendary_item[self.player].value, "Legendary Item": self.options.legendary_item.value,
"Boss Item": self.multiworld.boss_item[self.player].value, "Boss Item": self.options.boss_item.value,
"Lunar Item": self.multiworld.lunar_item[self.player].value, "Lunar Item": self.options.lunar_item.value,
"Void Item": self.multiworld.void_item[self.player].value, "Void Item": self.options.void_item.value,
"Equipment": self.multiworld.equipment[self.player].value "Equipment": self.options.equipment.value
} }
# remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled # 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") junk_pool.pop("Lunar Item")
# remove void items from the pool # 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") junk_pool.pop("Void Item")
return junk_pool return junk_pool
def create_regions(self) -> None: def create_regions(self) -> None:
if self.multiworld.goal[self.player] == "classic": if self.options.goal == "classic":
# classic mode # classic mode
menu = create_region(self.multiworld, self.player, "Menu") menu = create_region(self.multiworld, self.player, "Menu")
self.multiworld.regions.append(menu) self.multiworld.regions.append(menu)
@ -182,7 +182,7 @@ class RiskOfRainWorld(World):
victory_region = create_region(self.multiworld, self.player, "Victory") victory_region = create_region(self.multiworld, self.player, "Victory")
self.multiworld.regions.append(victory_region) self.multiworld.regions.append(victory_region)
petrichor = create_region(self.multiworld, self.player, "Petrichor V", 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) self.multiworld.regions.append(petrichor)
# classic mode can get to victory from the beginning of the game # classic mode can get to victory from the beginning of the game
@ -200,21 +200,13 @@ class RiskOfRainWorld(World):
create_events(self.multiworld, self.player) create_events(self.multiworld, self.player)
def fill_slot_data(self): 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 { return {
"itemPickupStep": self.multiworld.item_pickup_step[self.player].value, **options_dict,
"shrineUseStep": self.multiworld.shrine_use_step[self.player].value,
"goal": self.multiworld.goal[self.player].value,
"seed": "".join(self.multiworld.per_slot_randoms[self.player].choice(string.digits) for _ in range(16)), "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: def create_item(self, name: str) -> Item:
@ -241,12 +233,12 @@ class RiskOfRainWorld(World):
def create_events(world: MultiWorld, player: int) -> None: 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 num_of_events = total_locations // 25
if total_locations / 25 == num_of_events: if total_locations / 25 == num_of_events:
num_of_events -= 1 num_of_events -= 1
world_region = world.get_region("Petrichor V", player) 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 # only setup Pickups when using classic_mode
for i in range(num_of_events): for i in range(num_of_events):
event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) 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 = \ event_loc.access_rule = \
lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player) lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", "Location", player)
world_region.locations.append(event_loc) world_region.locations.append(event_loc)
elif world.goal[player] == "explore": elif world.worlds[player].options.goal == "explore":
for n in range(1, 6): for n in range(1, 6):
event_region = world.get_region(f"OrderedStage_{n}", player) event_region = world.get_region(f"OrderedStage_{n}", player)

View File

@ -148,7 +148,7 @@ class SMWorld(World):
self.remote_items = self.multiworld.remote_items[self.player] self.remote_items = self.multiworld.remote_items[self.player]
if (len(self.variaRando.randoExec.setup.restrictedLocs) > 0): 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") logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings")
def create_items(self): def create_items(self):

View File

@ -1,18 +1,20 @@
import logging 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 BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState, ItemClassification, MultiWorld
from Options import PerGameCommonOptions
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from . import rules, logic, options from . import rules
from .bundles import get_all_bundles, Bundle from .bundles import get_all_bundles, Bundle
from .items import item_table, create_items, ItemData, Group, items_by_group from .items import item_table, create_items, ItemData, Group, items_by_group
from .locations import location_table, create_locations, LocationData from .locations import location_table, create_locations, LocationData
from .logic import StardewLogic, StardewRule, True_, MAX_MONTHS 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 .regions import create_regions
from .rules import set_rules from .rules import set_rules
from worlds.generic.Rules import set_rule from worlds.generic.Rules import set_rule
from .strings.goal_names import Goal from .strings.goal_names import Goal as GoalName
client_version = 0 client_version = 0
@ -50,7 +52,6 @@ class StardewValleyWorld(World):
befriend villagers, and uncover dark secrets. befriend villagers, and uncover dark secrets.
""" """
game = "Stardew Valley" game = "Stardew Valley"
option_definitions = stardew_valley_options
topology_present = False topology_present = False
item_name_to_id = {name: data.code for name, data in item_table.items()} item_name_to_id = {name: data.code for name, data in item_table.items()}
@ -59,7 +60,8 @@ class StardewValleyWorld(World):
data_version = 3 data_version = 3
required_client_version = (0, 4, 0) required_client_version = (0, 4, 0)
options: StardewOptions options_dataclass = StardewValleyOptions
options: StardewValleyOptions
logic: StardewLogic logic: StardewLogic
web = StardewWebWorld() web = StardewWebWorld()
@ -72,25 +74,24 @@ class StardewValleyWorld(World):
self.all_progression_items = set() self.all_progression_items = set()
def generate_early(self): def generate_early(self):
self.options = fetch_options(self.multiworld, self.player)
self.force_change_options_if_incompatible() self.force_change_options_if_incompatible()
self.logic = StardewLogic(self.player, self.options) self.logic = StardewLogic(self.player, self.options)
self.modified_bundles = get_all_bundles(self.multiworld.random, self.modified_bundles = get_all_bundles(self.multiworld.random,
self.logic, self.logic,
self.options[options.BundleRandomization], self.options.bundle_randomization,
self.options[options.BundlePrice]) self.options.bundle_price)
def force_change_options_if_incompatible(self): def force_change_options_if_incompatible(self):
goal_is_walnut_hunter = self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter
goal_is_perfection = self.options[options.Goal] == options.Goal.option_perfection goal_is_perfection = self.options.goal == Goal.option_perfection
goal_is_island_related = goal_is_walnut_hunter or goal_is_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: if goal_is_island_related and exclude_ginger_island:
self.options[options.ExcludeGingerIsland] = options.ExcludeGingerIsland.option_false self.options.exclude_ginger_island.value = ExcludeGingerIsland.option_false
goal = options.Goal.name_lookup[self.options[options.Goal]] goal_name = self.options.goal.current_key
player_name = self.multiworld.player_name[self.player] 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_regions(self):
def create_region(name: str, exits: Iterable[str]) -> Region: def create_region(name: str, exits: Iterable[str]) -> Region:
@ -116,7 +117,7 @@ class StardewValleyWorld(World):
if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK,
Group.FRIENDSHIP_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 items_to_exclude = [item for item in items_to_exclude
if item_table[item.name] not in items_by_group[Group.SEASON]] if item_table[item.name] not in items_by_group[Group.SEASON]]
@ -134,12 +135,12 @@ class StardewValleyWorld(World):
self.setup_victory() self.setup_victory()
def precollect_starting_season(self) -> Optional[StardewItem]: 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 return
season_pool = items_by_group[Group.SEASON] 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: for season in season_pool:
self.multiworld.push_precollected(self.create_item(season)) self.multiworld.push_precollected(self.create_item(season))
return return
@ -148,18 +149,18 @@ class StardewValleyWorld(World):
if item.name in {season.name for season in items_by_group[Group.SEASON]}]: if item.name in {season.name for season in items_by_group[Group.SEASON]}]:
return 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"] season_pool = [season for season in season_pool if season.name != "Winter"]
starting_season = self.create_item(self.multiworld.random.choice(season_pool)) starting_season = self.create_item(self.multiworld.random.choice(season_pool))
self.multiworld.push_precollected(starting_season) self.multiworld.push_precollected(starting_season)
def setup_early_items(self): def setup_early_items(self):
if (self.options[options.BuildingProgression] == if (self.options.building_progression ==
options.BuildingProgression.option_progressive_early_shipping_bin): BuildingProgression.option_progressive_early_shipping_bin):
self.multiworld.early_items[self.player]["Shipping Bin"] = 1 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 self.multiworld.early_items[self.player]["Progressive Backpack"] = 1
def setup_month_events(self): def setup_month_events(self):
@ -172,40 +173,40 @@ class StardewValleyWorld(World):
self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End") self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End")
def setup_victory(self): def setup_victory(self):
if self.options[options.Goal] == options.Goal.option_community_center: if self.options.goal == Goal.option_community_center:
self.create_event_location(location_table[Goal.community_center], self.create_event_location(location_table[GoalName.community_center],
self.logic.can_complete_community_center().simplify(), self.logic.can_complete_community_center().simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation: elif self.options.goal == Goal.option_grandpa_evaluation:
self.create_event_location(location_table[Goal.grandpa_evaluation], self.create_event_location(location_table[GoalName.grandpa_evaluation],
self.logic.can_finish_grandpa_evaluation().simplify(), self.logic.can_finish_grandpa_evaluation().simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines: elif self.options.goal == Goal.option_bottom_of_the_mines:
self.create_event_location(location_table[Goal.bottom_of_the_mines], self.create_event_location(location_table[GoalName.bottom_of_the_mines],
self.logic.can_mine_to_floor(120).simplify(), self.logic.can_mine_to_floor(120).simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_cryptic_note: elif self.options.goal == Goal.option_cryptic_note:
self.create_event_location(location_table[Goal.cryptic_note], self.create_event_location(location_table[GoalName.cryptic_note],
self.logic.can_complete_quest("Cryptic Note").simplify(), self.logic.can_complete_quest("Cryptic Note").simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_master_angler: elif self.options.goal == Goal.option_master_angler:
self.create_event_location(location_table[Goal.master_angler], self.create_event_location(location_table[GoalName.master_angler],
self.logic.can_catch_every_fish().simplify(), self.logic.can_catch_every_fish().simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_complete_collection: elif self.options.goal == Goal.option_complete_collection:
self.create_event_location(location_table[Goal.complete_museum], self.create_event_location(location_table[GoalName.complete_museum],
self.logic.can_complete_museum().simplify(), self.logic.can_complete_museum().simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_full_house: elif self.options.goal == Goal.option_full_house:
self.create_event_location(location_table[Goal.full_house], self.create_event_location(location_table[GoalName.full_house],
(self.logic.has_children(2) & self.logic.can_reproduce()).simplify(), (self.logic.has_children(2) & self.logic.can_reproduce()).simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_greatest_walnut_hunter: elif self.options.goal == Goal.option_greatest_walnut_hunter:
self.create_event_location(location_table[Goal.greatest_walnut_hunter], self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
self.logic.has_walnut(130).simplify(), self.logic.has_walnut(130).simplify(),
"Victory") "Victory")
elif self.options[options.Goal] == options.Goal.option_perfection: elif self.options.goal == Goal.option_perfection:
self.create_event_location(location_table[Goal.perfection], self.create_event_location(location_table[GoalName.perfection],
self.logic.has_everything(self.all_progression_items).simplify(), self.logic.has_everything(self.all_progression_items).simplify(),
"Victory") "Victory")
@ -230,7 +231,7 @@ class StardewValleyWorld(World):
location.place_locked_item(self.create_item(item)) location.place_locked_item(self.create_item(item))
def set_rules(self): 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() self.force_first_month_once_all_early_items_are_found()
def force_first_month_once_all_early_items_are_found(self): def force_first_month_once_all_early_items_are_found(self):
@ -276,11 +277,12 @@ class StardewValleyWorld(World):
key, value = self.modified_bundles[bundle_key].to_pair() key, value = self.modified_bundles[bundle_key].to_pair()
modified_bundles[key] = value modified_bundles[key] = value
excluded_options = [options.BundleRandomization, options.BundlePrice, excluded_options = [BundleRandomization, BundlePrice, NumberOfMovementBuffs, NumberOfLuckBuffs]
options.NumberOfMovementBuffs, options.NumberOfLuckBuffs] excluded_option_names = [option.internal_name for option in excluded_options]
slot_data = dict(self.options.options) generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints]
for option in excluded_options: excluded_option_names.extend(generic_option_names)
slot_data.pop(option.internal_name) 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({ slot_data.update({
"seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits
"randomized_entrances": self.randomized_entrances, "randomized_entrances": self.randomized_entrances,

View File

@ -152,7 +152,7 @@ class Bundle:
# shuffle_vault_amongst_themselves(random, bundles) # 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 = {} bundles = {}
for bundle_key in vanilla_bundles: for bundle_key in vanilla_bundles:
bundle_value = vanilla_bundles[bundle_key] bundle_value = vanilla_bundles[bundle_key]

View File

@ -7,10 +7,11 @@ from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional from typing import Dict, List, Protocol, Union, Set, Optional
from BaseClasses import Item, ItemClassification from BaseClasses import Item, ItemClassification
from . import options, data from . import data
from .data.villagers_data import all_villagers from .data.villagers_data import all_villagers
from .mods.mod_data import ModNames 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 from .strings.ap_names.buff_names import Buff
ITEM_CODE_OFFSET = 717000 ITEM_CODE_OFFSET = 717000
@ -138,10 +139,9 @@ initialize_groups()
def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item],
world_options: StardewOptions, options: StardewValleyOptions, random: Random) -> List[Item]:
random: Random) -> List[Item]:
items = [] 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: for item in items_to_exclude:
if item in unique_items: if item in unique_items:
@ -151,58 +151,58 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, items_t
items += unique_items items += unique_items
logger.debug(f"Created {len(unique_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 items += unique_filler_items
logger.debug(f"Created {len(unique_filler_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 items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs") logger.debug(f"Created {len(resource_pack_items)} resource packs")
return items 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 = []
items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) 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_mine_rewards(item_factory, items, random)
create_elevators(item_factory, world_options, items) create_elevators(item_factory, options, items)
create_tools(item_factory, world_options, items) create_tools(item_factory, options, items)
create_skills(item_factory, world_options, items) create_skills(item_factory, options, items)
create_wizard_buildings(item_factory, world_options, items) create_wizard_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, world_options, items) create_carpenter_buildings(item_factory, options, items)
items.append(item_factory("Beach Bridge")) items.append(item_factory("Beach Bridge"))
items.append(item_factory("Dark Talisman")) items.append(item_factory("Dark Talisman"))
create_tv_channels(item_factory, items) create_tv_channels(item_factory, items)
create_special_quest_rewards(item_factory, items) create_special_quest_rewards(item_factory, items)
create_stardrops(item_factory, world_options, items) create_stardrops(item_factory, options, items)
create_museum_items(item_factory, world_options, items) create_museum_items(item_factory, options, items)
create_arcade_machine_items(item_factory, world_options, items) create_arcade_machine_items(item_factory, options, items)
items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) 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) create_traveling_merchant_items(item_factory, items)
items.append(item_factory("Return Scepter")) items.append(item_factory("Return Scepter"))
create_seasons(item_factory, world_options, items) create_seasons(item_factory, options, items)
create_seeds(item_factory, world_options, items) create_seeds(item_factory, options, items)
create_friendsanity_items(item_factory, world_options, items) create_friendsanity_items(item_factory, options, items)
create_festival_rewards(item_factory, world_options, items) create_festival_rewards(item_factory, options, items)
create_babies(item_factory, items, random) create_babies(item_factory, items, random)
create_special_order_board_rewards(item_factory, world_options, items) create_special_order_board_rewards(item_factory, options, items)
create_special_order_qi_rewards(item_factory, world_options, items) create_special_order_qi_rewards(item_factory, options, items)
create_walnut_purchase_rewards(item_factory, world_options, items) create_walnut_purchase_rewards(item_factory, options, items)
create_magic_mod_spells(item_factory, world_options, items) create_magic_mod_spells(item_factory, options, items)
return items return items
def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or if (options.backpack_progression == BackpackProgression.option_progressive or
world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive): options.backpack_progression == BackpackProgression.option_early_progressive):
items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2) 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")) 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")) items.append(item_factory("Skull Key"))
def create_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla: if options.elevator_progression == ElevatorProgression.option_vanilla:
return return
items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24]) 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]) 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]) 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]): def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.ToolProgression] == options.ToolProgression.option_progressive: if options.tool_progression == ToolProgression.option_progressive:
items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4) items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4)
items.append(item_factory("Golden Scythe")) items.append(item_factory("Golden Scythe"))
def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.SkillProgression] == options.SkillProgression.option_progressive: if options.skill_progression == SkillProgression.option_progressive:
for item in items_by_group[Group.SKILL_LEVEL_UP]: 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 continue
items.extend(item_factory(item) for item in [item.name] * 10) 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("Earth Obelisk"))
items.append(item_factory("Water Obelisk")) items.append(item_factory("Water Obelisk"))
items.append(item_factory("Desert Obelisk")) items.append(item_factory("Desert Obelisk"))
items.append(item_factory("Junimo Hut")) items.append(item_factory("Junimo Hut"))
items.append(item_factory("Gold Clock")) 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")) 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")) items.append(item_factory("Woods Obelisk"))
def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, if options.building_progression in {BuildingProgression.option_progressive,
options.BuildingProgression.option_progressive_early_shipping_bin}: BuildingProgression.option_progressive_early_shipping_bin}:
items.append(item_factory("Progressive Coop")) items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop")) 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")) 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")) 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")) 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")) # The Mines level 100
items.append(item_factory("Stardrop")) # Old Master Cannoli 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 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 items.append(item_factory("Stardrop")) # Petting the Unicorn
def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.Museumsanity] == options.Museumsanity.option_none: if options.museumsanity == Museumsanity.option_none:
return return
items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5)
items.extend(item_factory(item) for item in ["Ancient Seeds"] * 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")) items.append(item_factory("Dwarvish Translation Guide"))
def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.Friendsanity] == options.Friendsanity.option_none: if options.friendsanity == Friendsanity.option_none:
return return
exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors
exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \
world_options[options.Friendsanity] == options.Friendsanity.option_bachelors options.friendsanity == Friendsanity.option_bachelors
include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage
exclude_ginger_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
heart_size = world_options[options.FriendsanityHeartSize] heart_size = options.friendsanity_heart_size
for villager in all_villagers: 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 continue
if not villager.available and exclude_locked_villagers: if not villager.available and exclude_locked_villagers:
continue continue
@ -350,8 +350,8 @@ def create_babies(item_factory: StardewItemFactory, items: List[Item], random: R
items.append(item_factory(chosen_baby)) items.append(item_factory(chosen_baby))
def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: 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 Boots")) items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Gun")) 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) 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]): def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
number_of_movement_buffs: int = world_options[options.NumberOfMovementBuffs] items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value)
number_of_luck_buffs: int = world_options[options.NumberOfLuckBuffs] items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value)
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_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): 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)]) *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)])
def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: if options.season_randomization == SeasonRandomization.option_disabled:
return 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]) items.extend([item_factory(item) for item in ["Progressive Season"] * 3])
return return
items.extend([item_factory(item) for item in items_by_group[Group.SEASON]]) 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]): def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: if options.cropsanity == Cropsanity.option_disabled:
return 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] 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) items.extend(seed_items)
def create_festival_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled: if options.festival_locations == FestivalLocations.option_disabled:
return return
items.extend([*[item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler], items.extend([*[item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler],
item_factory("Stardrop")]) item_factory("Stardrop")])
def create_walnut_purchase_rewards(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return return
items.extend([item_factory("Boat Repair"), 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]): def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: if options.special_order_locations == SpecialOrderLocations.option_disabled:
return return
items.extend([item_factory(item) for item in items_by_group[Group.SPECIAL_ORDER_BOARD]]) 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]): def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if (world_options[options.SpecialOrderLocations] != options.SpecialOrderLocations.option_board_qi or if (options.special_order_locations != SpecialOrderLocations.option_board_qi or
world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true): options.exclude_ginger_island == ExcludeGingerIsland.option_true):
return return
qi_gem_rewards = ["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems", 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"] "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]]) 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]: def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]:
if world_options[options.FestivalLocations] == options.FestivalLocations.option_disabled: if options.festival_locations == FestivalLocations.option_disabled:
return [] return []
return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if
item.classification == ItemClassification.filler] item.classification == ItemClassification.filler]
def create_magic_mod_spells(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if ModNames.magic not in world_options[options.Mods]: if ModNames.magic not in options.mods:
return [] return []
items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]]) 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]: available_item_slots: int) -> List[Item]:
items = [] 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: if len(items) > available_item_slots:
items = random.sample(items, available_item_slots) items = random.sample(items, available_item_slots)
return items 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], items_already_added: List[Item],
number_locations: int) -> 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 = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
all_filler_packs.extend(items_by_group[Group.TRASH]) all_filler_packs.extend(items_by_group[Group.TRASH])
if include_traps: 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] if pack.name not in items_already_added_names]
trap_items = [pack for pack in items_by_group[Group.TRAP] trap_items = [pack for pack in items_by_group[Group.TRAP]
if pack.name not in items_already_added_names and 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 = []
priority_filler_items.extend(useful_resource_packs) priority_filler_items.extend(useful_resource_packs)
if include_traps: if include_traps:
priority_filler_items.extend(trap_items) priority_filler_items.extend(trap_items)
all_filler_packs = remove_excluded_packs(all_filler_packs, world_options) all_filler_packs = remove_excluded_packs(all_filler_packs, options)
priority_filler_items = remove_excluded_packs(priority_filler_items, world_options) priority_filler_items = remove_excluded_packs(priority_filler_items, options)
number_priority_items = len(priority_filler_items) number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added) 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 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] 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] included_packs = [pack for pack in included_packs if Group.GINGER_ISLAND not in pack.groups]
return included_packs return included_packs

View File

@ -4,10 +4,12 @@ from dataclasses import dataclass
from random import Random from random import Random
from typing import Optional, Dict, Protocol, List, FrozenSet 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.fish_data import legendary_fish, special_fish, all_fish
from .data.museum_data import all_museum_items from .data.museum_data import all_museum_items
from .data.villagers_data import all_villagers 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.goal_names import Goal
from .strings.region_names import Region from .strings.region_names import Region
@ -133,12 +135,12 @@ def initialize_groups():
initialize_groups() initialize_groups()
def extend_cropsanity_locations(randomized_locations: List[LocationData], world_options): def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: if options.cropsanity == Cropsanity.option_disabled:
return return
cropsanity_locations = locations_by_tag[LocationTags.CROPSANITY] 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) 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}"]) 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: " prefix = "Fishsanity: "
if world_options[options.Fishsanity] == options.Fishsanity.option_none: if options.fishsanity == Fishsanity.option_none:
return 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) 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) 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] 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)) randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_all: elif options.fishsanity == Fishsanity.option_all:
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish] fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish]
randomized_locations.extend(filter_ginger_island(world_options, fish_locations)) randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_legendaries: 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] 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)) randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_hard_fish: 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] 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)) randomized_locations.extend(filter_ginger_island(options, fish_locations))
elif world_options[options.Fishsanity] == options.Fishsanity.option_only_easy_fish: 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] 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: " prefix = "Museumsanity: "
if museumsanity == options.Museumsanity.option_none: if options.museumsanity == Museumsanity.option_none:
return return
elif museumsanity == options.Museumsanity.option_milestones: elif options.museumsanity == Museumsanity.option_milestones:
randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_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}"] randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"]
for museum_item in all_museum_items if random.random() < 0.4) 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) 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): def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.Friendsanity] == options.Friendsanity.option_none: if options.friendsanity == Friendsanity.option_none:
return return
exclude_leo = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true exclude_leo = options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors
exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \
world_options[options.Friendsanity] == options.Friendsanity.option_bachelors options.friendsanity == Friendsanity.option_bachelors
include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage
heart_size = world_options[options.FriendsanityHeartSize] heart_size = options.friendsanity_heart_size
for villager in all_villagers: 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 continue
if not villager.available and exclude_locked_villagers: if not villager.available and exclude_locked_villagers:
continue continue
@ -228,38 +230,38 @@ def extend_friendsanity_locations(randomized_locations: List[LocationData], worl
randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"])
def extend_festival_locations(randomized_locations: List[LocationData], festival_option: int): def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if festival_option == options.FestivalLocations.option_disabled: if options.festival_locations == FestivalLocations.option_disabled:
return return
festival_locations = locations_by_tag[LocationTags.FESTIVAL] festival_locations = locations_by_tag[LocationTags.FESTIVAL]
randomized_locations.extend(festival_locations) 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): def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions):
if festival_option != options.FestivalLocations.option_hard: if options.festival_locations != FestivalLocations.option_hard:
return return
hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD] hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD]
randomized_locations.extend(hard_festival_locations) randomized_locations.extend(hard_festival_locations)
def extend_special_order_locations(randomized_locations: List[LocationData], world_options): def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: if options.special_order_locations == SpecialOrderLocations.option_disabled:
return return
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
board_locations = filter_disabled_locations(world_options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
randomized_locations.extend(board_locations) randomized_locations.extend(board_locations)
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_qi and include_island: if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island:
include_arcade = world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled 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] 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) randomized_locations.extend(qi_orders)
def extend_walnut_purchase_locations(randomized_locations: List[LocationData], world_options): def extend_walnut_purchase_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return return
randomized_locations.append(location_table["Repair Ticket Machine"]) randomized_locations.append(location_table["Repair Ticket Machine"])
randomized_locations.append(location_table["Repair Boat Hull"]) 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]) 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]] 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) randomized_locations.extend(filtered_mandatory_locations)
def extend_backpack_locations(randomized_locations: List[LocationData], world_options): def extend_backpack_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla: if options.backpack_progression == BackpackProgression.option_vanilla:
return return
backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK]] 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) randomized_locations.extend(filtered_backpack_locations)
def extend_elevator_locations(randomized_locations: List[LocationData], world_options): def extend_elevator_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla: if options.elevator_progression == ElevatorProgression.option_vanilla:
return return
elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]] 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) randomized_locations.extend(filtered_elevator_locations)
def create_locations(location_collector: StardewLocationCollector, def create_locations(location_collector: StardewLocationCollector,
world_options: options.StardewOptions, options: StardewValleyOptions,
random: Random): random: Random):
randomized_locations = [] randomized_locations = []
extend_mandatory_locations(randomized_locations, world_options) extend_mandatory_locations(randomized_locations, options)
extend_backpack_locations(randomized_locations, world_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]) 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]: 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]) 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]: 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]) 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]) 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]) randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE])
extend_cropsanity_locations(randomized_locations, world_options) extend_cropsanity_locations(randomized_locations, options)
extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) extend_help_wanted_quests(randomized_locations, options.help_wanted_locations.value)
extend_fishsanity_locations(randomized_locations, world_options, random) extend_fishsanity_locations(randomized_locations, options, random)
extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random) extend_museumsanity_locations(randomized_locations, options, random)
extend_friendsanity_locations(randomized_locations, world_options) extend_friendsanity_locations(randomized_locations, options)
extend_festival_locations(randomized_locations, world_options[options.FestivalLocations]) extend_festival_locations(randomized_locations, options)
extend_special_order_locations(randomized_locations, world_options) extend_special_order_locations(randomized_locations, options)
extend_walnut_purchase_locations(randomized_locations, world_options) extend_walnut_purchase_locations(randomized_locations, options)
for location_data in randomized_locations: for location_data in randomized_locations:
location_collector(location_data.name, location_data.code, location_data.region) location_collector(location_data.name, location_data.code, location_data.region)
def filter_ginger_island(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]: def filter_ginger_island(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false 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] 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]: def filter_modded_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
current_mod_names = world_options[options.Mods] 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] 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]: def filter_disabled_locations(options: StardewValleyOptions, locations: List[LocationData]) -> List[LocationData]:
locations_first_pass = filter_ginger_island(world_options, locations) locations_first_pass = filter_ginger_island(options, locations)
locations_second_pass = filter_modded_locations(world_options, locations_first_pass) locations_second_pass = filter_modded_locations(options, locations_first_pass)
return locations_second_pass return locations_second_pass

View File

@ -4,7 +4,6 @@ import math
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Union, Optional, Iterable, Sized, List, Set 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 import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem
from .data.bundle_data import BundleItem from .data.bundle_data import BundleItem
from .data.crops_data import crops_by_name from .data.crops_data import crops_by_name
@ -20,7 +19,8 @@ from .mods.logic.special_orders import get_modded_special_orders_rules
from .mods.logic.skullcavernelevator import has_skull_cavern_elevator_to_floor from .mods.logic.skullcavernelevator import has_skull_cavern_elevator_to_floor
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .mods.logic import magic, skills 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 .regions import vanilla_regions
from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule
from .strings.animal_names import Animal, coop_animals, barn_animals from .strings.animal_names import Animal, coop_animals, barn_animals
@ -81,10 +81,11 @@ tool_upgrade_prices = {
fishing_regions = [Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west] fishing_regions = [Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west]
@dataclass(frozen=True, repr=False) @dataclass(frozen=True, repr=False)
class StardewLogic: class StardewLogic:
player: int player: int
options: StardewOptions options: StardewValleyOptions
item_rules: Dict[str, StardewRule] = field(default_factory=dict) item_rules: Dict[str, StardewRule] = field(default_factory=dict)
sapling_rules: Dict[str, StardewRule] = field(default_factory=dict) sapling_rules: Dict[str, StardewRule] = field(default_factory=dict)
@ -398,7 +399,7 @@ class StardewLogic:
Building.cellar: self.can_spend_money_at(Region.carpenter, 100000) & self.has_house(2), 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({ self.quest_rules.update({
Quest.introductions: self.can_reach_region(Region.town), Quest.introductions: self.can_reach_region(Region.town),
@ -455,7 +456,7 @@ class StardewLogic:
self.can_meet(NPC.wizard) & self.can_meet(NPC.willy), 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({ self.festival_rules.update({
FestivalCheck.egg_hunt: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_win_egg_hunt(), FestivalCheck.egg_hunt: self.has_season(Season.spring) & self.can_reach_region(Region.town) & self.can_win_egg_hunt(),
@ -539,7 +540,7 @@ class StardewLogic:
self.can_spend_money(80000), # I need this extra rule because money rules aren't additive... 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: def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule:
if isinstance(items, str): if isinstance(items, str):
@ -596,7 +597,7 @@ class StardewLogic:
return self.has_lived_months(min(8, amount // MONEY_PER_MONTH)) return self.has_lived_months(min(8, amount // MONEY_PER_MONTH))
def can_spend_money(self, amount: int) -> StardewRule: def can_spend_money(self, amount: int) -> StardewRule:
if self.options[options.StartingMoney] == -1: if self.options.starting_money == -1:
return True_() return True_()
return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5))) return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5)))
@ -607,7 +608,7 @@ class StardewLogic:
if material == ToolMaterial.basic or tool == Tool.scythe: if material == ToolMaterial.basic or tool == Tool.scythe:
return True_() 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.received(f"Progressive {tool}", count=tool_materials[material])
return self.has(f"{material} Bar") & self.can_spend_money(tool_upgrade_prices[material]) return self.has(f"{material} Bar") & self.can_spend_money(tool_upgrade_prices[material])
@ -644,7 +645,7 @@ class StardewLogic:
if level <= 0: if level <= 0:
return True_() 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.received(f"{skill} Level", count=level)
return self.can_earn_skill_level(skill, level) return self.can_earn_skill_level(skill, level)
@ -656,7 +657,7 @@ class StardewLogic:
if level <= 0: if level <= 0:
return True_() 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", skills_items = ["Farming Level", "Mining Level", "Foraging Level",
"Fishing Level", "Combat Level"] "Fishing Level", "Combat Level"]
if allow_modded_skills: if allow_modded_skills:
@ -672,7 +673,7 @@ class StardewLogic:
def has_building(self, building: str) -> StardewRule: def has_building(self, building: str) -> StardewRule:
carpenter_rule = self.can_reach_region(Region.carpenter) 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 count = 1
if building in [Building.coop, Building.barn, Building.shed]: if building in [Building.coop, Building.barn, Building.shed]:
building = f"Progressive {building}" building = f"Progressive {building}"
@ -693,7 +694,7 @@ class StardewLogic:
if upgrade_level > 3: if upgrade_level > 3:
return False_() 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) return self.received(f"Progressive House", upgrade_level) & self.can_reach_region(Region.carpenter)
if upgrade_level == 1: if upgrade_level == 1:
@ -734,7 +735,7 @@ class StardewLogic:
return tool_rule & enemy_rule return tool_rule & enemy_rule
def can_get_fishing_xp(self) -> StardewRule: 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() | self.can_crab_pot()
return self.can_fish() return self.can_fish()
@ -746,7 +747,7 @@ class StardewLogic:
skill_rule = self.has_skill_level(Skill.fishing, skill_required) skill_rule = self.has_skill_level(Skill.fishing, skill_required)
region_rule = self.can_reach_any_region(fishing_regions) region_rule = self.can_reach_any_region(fishing_regions)
number_fishing_rod_required = 1 if difficulty < 50 else 2 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 self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule & region_rule
return skill_rule & region_rule return skill_rule & region_rule
@ -763,7 +764,7 @@ class StardewLogic:
return self.has_max_fishing_rod() & skill_rule return self.has_max_fishing_rod() & skill_rule
def can_buy_seed(self, seed: SeedItem) -> StardewRule: 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_() item_rule = True_()
else: else:
item_rule = self.received(seed.name) item_rule = self.received(seed.name)
@ -781,7 +782,7 @@ class StardewLogic:
Fruit.peach: 6000, Fruit.peach: 6000,
Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0} Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0}
received_sapling = self.received(f"{fruit} Sapling") 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_() allowed_buy_sapling = True_()
else: else:
allowed_buy_sapling = received_sapling allowed_buy_sapling = received_sapling
@ -824,14 +825,14 @@ class StardewLogic:
def can_catch_every_fish(self) -> StardewRule: def can_catch_every_fish(self) -> StardewRule:
rules = [self.has_skill_level(Skill.fishing, 10), self.has_max_fishing_rod()] rules = [self.has_skill_level(Skill.fishing, 10), self.has_max_fishing_rod()]
for fish in all_fish: 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: fish in island_fish:
continue continue
rules.append(self.can_catch_fish(fish)) rules.append(self.can_catch_fish(fish))
return And(rules) return And(rules)
def has_max_fishing_rod(self) -> StardewRule: 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.received(APTool.fishing_rod, 4)
return self.can_get_fishing_xp() return self.can_get_fishing_xp()
@ -875,7 +876,7 @@ class StardewLogic:
def can_crab_pot(self, region: str = Generic.any) -> StardewRule: def can_crab_pot(self, region: str = Generic.any) -> StardewRule:
crab_pot_rule = self.has(Craftable.bait) 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) crab_pot_rule = crab_pot_rule & self.has(Machine.crab_pot)
else: else:
crab_pot_rule = crab_pot_rule & self.can_get_fishing_xp() crab_pot_rule = crab_pot_rule & self.can_get_fishing_xp()
@ -926,9 +927,7 @@ class StardewLogic:
return region_rule & ((tool_rule & foraging_rule) | magic_rule) return region_rule & ((tool_rule & foraging_rule) | magic_rule)
def has_max_buffs(self) -> StardewRule: def has_max_buffs(self) -> StardewRule:
number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs] return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value)
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)
def get_weapon_rule_for_floor_tier(self, tier: int): def get_weapon_rule_for_floor_tier(self, tier: int):
if tier >= 4: if tier >= 4:
@ -946,9 +945,9 @@ class StardewLogic:
rules = [] rules = []
weapon_rule = self.get_weapon_rule_for_floor_tier(tier) weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule) 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])) 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)) combat_tier = min(10, max(0, tier * 2))
rules.append(self.has_skill_level(Skill.combat, combat_tier)) rules.append(self.has_skill_level(Skill.combat, combat_tier))
return And(rules) return And(rules)
@ -958,15 +957,15 @@ class StardewLogic:
rules = [] rules = []
weapon_rule = self.get_weapon_rule_for_floor_tier(tier) weapon_rule = self.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule) 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])) 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)) combat_tier = min(10, max(0, tier * 2))
rules.append(self.has_skill_level(Skill.combat, combat_tier)) rules.append(self.has_skill_level(Skill.combat, combat_tier))
return And(rules) return And(rules)
def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: 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 self.received("Progressive Mine Elevator", count=int(floor / 5))
return True_() return True_()
@ -984,9 +983,9 @@ class StardewLogic:
weapon_rule = self.has_great_weapon() weapon_rule = self.has_great_weapon()
rules.append(weapon_rule) rules.append(weapon_rule)
rules.append(self.can_cook()) 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)))) 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)) skill_tier = min(10, max(0, tier * 2 + 6))
rules.extend({self.has_skill_level(Skill.combat, skill_tier), rules.extend({self.has_skill_level(Skill.combat, skill_tier),
self.has_skill_level(Skill.mining, skill_tier)}) self.has_skill_level(Skill.mining, skill_tier)})
@ -1005,20 +1004,20 @@ class StardewLogic:
self.can_progress_easily_in_the_skull_cavern_from_floor(previous_previous_elevator))) & has_mine_elevator 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: 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_() return True_()
jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun",
"JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"]
return self.received(jotpk_buffs, power_level) return self.received(jotpk_buffs, power_level)
def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: 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 True_()
return self.received("Junimo Kart: Extra Life", power_level) return self.received("Junimo Kart: Extra Life", power_level)
def has_junimo_kart_max_level(self) -> StardewRule: def has_junimo_kart_max_level(self) -> StardewRule:
play_rule = self.can_reach_region(Region.junimo_kart_3) 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 play_rule
return self.has_junimo_kart_power_level(8) return self.has_junimo_kart_power_level(8)
@ -1043,12 +1042,12 @@ class StardewLogic:
def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule:
if hearts <= 0: if hearts <= 0:
return True_() return True_()
friendsanity = self.options[options.Friendsanity] friendsanity = self.options.friendsanity
if friendsanity == options.Friendsanity.option_none: if friendsanity == Friendsanity.option_none:
return self.can_earn_relationship(npc, hearts) return self.can_earn_relationship(npc, hearts)
if npc not in all_villagers_by_name: if npc not in all_villagers_by_name:
if npc == NPC.pet: if npc == NPC.pet:
if friendsanity == options.Friendsanity.option_bachelors: if friendsanity == Friendsanity.option_bachelors:
return self.can_befriend_pet(hearts) return self.can_befriend_pet(hearts)
return self.received_hearts(NPC.pet, hearts) return self.received_hearts(NPC.pet, hearts)
if npc == Generic.any or npc == Generic.bachelor: if npc == Generic.any or npc == Generic.bachelor:
@ -1078,11 +1077,11 @@ class StardewLogic:
if not self.npc_is_in_current_slot(npc): if not self.npc_is_in_current_slot(npc):
return True_() return True_()
villager = all_villagers_by_name[npc] 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) 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) 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: 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, 8) & self.can_earn_relationship(npc, hearts)
return self.received_hearts(villager, hearts) return self.received_hearts(villager, hearts)
@ -1090,7 +1089,7 @@ class StardewLogic:
def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule: def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule:
if isinstance(npc, Villager): if isinstance(npc, Villager):
return self.received_hearts(npc.name, hearts) 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)) return self.received(self.heart(npc), math.ceil(hearts / heart_size))
def can_meet(self, npc: str) -> StardewRule: def can_meet(self, npc: str) -> StardewRule:
@ -1122,13 +1121,13 @@ class StardewLogic:
if hearts <= 0: if hearts <= 0:
return True_() return True_()
heart_size: int = self.options[options.FriendsanityHeartSize] heart_size = self.options.friendsanity_heart_size.value
previous_heart = hearts - heart_size previous_heart = hearts - heart_size
previous_heart_rule = self.has_relationship(npc, previous_heart) previous_heart_rule = self.has_relationship(npc, previous_heart)
if npc == NPC.pet: if npc == NPC.pet:
earn_rule = self.can_befriend_pet(hearts) 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) earn_rule = self.can_meet(npc) & self.has_lived_months(hearts)
elif npc in all_villagers_by_name: elif npc in all_villagers_by_name:
if not self.npc_is_in_current_slot(npc): if not self.npc_is_in_current_slot(npc):
@ -1284,7 +1283,7 @@ class StardewLogic:
return self.has_lived_months(8) return self.has_lived_months(8)
def can_speak_dwarf(self) -> StardewRule: 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 And([self.can_donate_museum_item(item) for item in dwarf_scrolls])
return self.received("Dwarvish Translation Guide") return self.received("Dwarvish Translation Guide")
@ -1334,7 +1333,7 @@ class StardewLogic:
def can_complete_museum(self) -> StardewRule: def can_complete_museum(self) -> StardewRule:
rules = [self.can_reach_region(Region.museum), self.can_mine_perfectly()] 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)) rules.append(self.received("Traveling Merchant Metal Detector", 4))
for donation in all_museum_items: for donation in all_museum_items:
@ -1345,9 +1344,9 @@ class StardewLogic:
if season == Generic.any: if season == Generic.any:
return True_() return True_()
seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter] 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)) 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: if season == Season.spring:
return True_() return True_()
return self.has_lived_months(1) return self.has_lived_months(1)
@ -1371,19 +1370,19 @@ class StardewLogic:
return self.received("Month End", number) return self.received("Month End", number)
def has_rusty_key(self) -> StardewRule: 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 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.has([item.name for item in all_museum_items], required_donations) & self.can_reach_region(Region.museum)
return self.received(Wallet.rusty_key) return self.received(Wallet.rusty_key)
def can_win_egg_hunt(self) -> StardewRule: def can_win_egg_hunt(self) -> StardewRule:
number_of_movement_buffs: int = self.options[options.NumberOfMovementBuffs] number_of_movement_buffs = self.options.number_of_movement_buffs.value
if self.options[options.FestivalLocations] == options.FestivalLocations.option_hard or number_of_movement_buffs < 2: if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
return True_() return True_()
return self.received(Buff.movement, number_of_movement_buffs // 2) return self.received(Buff.movement, number_of_movement_buffs // 2)
def can_succeed_luau_soup(self) -> StardewRule: 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_() return True_()
eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, 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"] Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"]
@ -1398,7 +1397,7 @@ class StardewLogic:
return Or(fish_rule) | Or(aged_rule) return Or(fish_rule) | Or(aged_rule)
def can_succeed_grange_display(self) -> StardewRule: 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_() return True_()
animal_rule = self.has_animal(Generic.any) animal_rule = self.has_animal(Generic.any)
artisan_rule = self.can_keg(Generic.any) | self.can_preserves_jar(Generic.any) artisan_rule = self.can_keg(Generic.any) | self.can_preserves_jar(Generic.any)
@ -1527,12 +1526,12 @@ class StardewLogic:
return blacksmith_access & self.has(geode) return blacksmith_access & self.has(geode)
def has_island_trader(self) -> StardewRule: 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 False_()
return self.can_reach_region(Region.island_trader) return self.can_reach_region(Region.island_trader)
def has_walnut(self, number: int) -> StardewRule: 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_() return False_()
if number <= 0: if number <= 0:
return True_() return True_()
@ -1592,7 +1591,7 @@ class StardewLogic:
def npc_is_in_current_slot(self, name: str) -> bool: def npc_is_in_current_slot(self, name: str) -> bool:
npc = all_villagers_by_name[name] npc = all_villagers_by_name[name]
mod = npc.mod_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: def can_do_combat_at_level(self, level: str) -> StardewRule:
if level == Performance.basic: if level == Performance.basic:
@ -1612,7 +1611,7 @@ class StardewLogic:
return tool_rule | spell_rule return tool_rule | spell_rule
def has_prismatic_jelly_reward_access(self) -> StardewRule: 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.can_complete_special_order("Prismatic Jelly")
return self.received("Monster Musk Recipe") return self.received("Monster Musk Recipe")

View File

@ -17,14 +17,14 @@ def can_reach_woods_depth(vanilla_logic, depth: int) -> StardewRule:
if depth > 50: if depth > 50:
rules.append(vanilla_logic.can_do_combat_at_level(Performance.great) & vanilla_logic.can_cook() & rules.append(vanilla_logic.can_do_combat_at_level(Performance.great) & vanilla_logic.can_cook() &
vanilla_logic.received(ModTransportation.woods_obelisk)) 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)) combat_tier = min(10, max(0, tier + 5))
rules.append(vanilla_logic.has_skill_level(Skill.combat, combat_tier)) rules.append(vanilla_logic.has_skill_level(Skill.combat, combat_tier))
return And(rules) return And(rules)
def has_woods_rune_to_depth(vanilla_logic, floor: int) -> StardewRule: 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 True_()
return vanilla_logic.received("Progressive Woods Obelisk Sigils", count=int(floor / 10)) return vanilla_logic.received("Progressive Woods Obelisk Sigils", count=int(floor / 10))

View File

@ -7,19 +7,19 @@ from ... import options
def can_use_clear_debris_instead_of_tool_level(vanilla_logic, level: int) -> StardewRule: 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 False_()
return vanilla_logic.received(MagicSpell.clear_debris) & can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, level) 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: 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 False_()
return vanilla_logic.can_reach_region(MagicRegion.altar) return vanilla_logic.can_reach_region(MagicRegion.altar)
def has_any_spell(vanilla_logic) -> StardewRule: 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 False_()
return can_use_altar(vanilla_logic) 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: 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_() return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 2) 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) 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: 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_() return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 4) 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) 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: 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_() return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 6) 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) 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: 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_() return False_()
magic_resource_rule = can_use_altar(vanilla_logic) & vanilla_logic.received(ModSkillLevel.magic_level, 8) 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) 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: 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 False_()
return vanilla_logic.received(MagicSpell.blink) & can_use_altar(vanilla_logic) return vanilla_logic.received(MagicSpell.blink) & can_use_altar(vanilla_logic)

View File

@ -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: 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) 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) 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) 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) 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) 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 can_earn_binning_skill_level(logic, level)
return False_() 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: def can_earn_socializing_skill_level(vanilla_logic, level: int) -> StardewRule:
villager_count = [] villager_count = []
for villager in all_villagers: 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)) villager_count.append(vanilla_logic.can_earn_relationship(villager.name, level))
return Count(level * 2, villager_count) return Count(level * 2, villager_count)

View File

@ -4,7 +4,7 @@ from ... import options
def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule: def has_skull_cavern_elevator_to_floor(self, floor: int) -> StardewRule:
if self.options[options.ElevatorProgression] != options.ElevatorProgression.option_vanilla and \ if self.options.elevator_progression != options.ElevatorProgression.option_vanilla and \
ModNames.skull_cavern_elevator in self.options[options.Mods]: ModNames.skull_cavern_elevator in self.options.mods:
return self.received("Progressive Skull Cavern Elevator", floor // 25) return self.received("Progressive Skull Cavern Elevator", floor // 25)
return True_() return True_()

View File

@ -1,29 +1,9 @@
from dataclasses import dataclass 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 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): class Goal(Choice):
"""What's your goal with this play-through? """What's your goal with this play-through?
@ -553,56 +533,39 @@ class Mods(OptionSet):
} }
stardew_valley_option_classes = [ @dataclass
Goal, class StardewValleyOptions(PerGameCommonOptions):
StartingMoney, goal: Goal
ProfitMargin, starting_money: StartingMoney
BundleRandomization, profit_margin: ProfitMargin
BundlePrice, bundle_randomization: BundleRandomization
EntranceRandomization, bundle_price: BundlePrice
SeasonRandomization, entrance_randomization: EntranceRandomization
Cropsanity, season_randomization: SeasonRandomization
BackpackProgression, cropsanity: Cropsanity
ToolProgression, backpack_progression: BackpackProgression
SkillProgression, tool_progression: ToolProgression
BuildingProgression, skill_progression: SkillProgression
FestivalLocations, building_progression: BuildingProgression
ElevatorProgression, festival_locations: FestivalLocations
ArcadeMachineLocations, elevator_progression: ElevatorProgression
SpecialOrderLocations, arcade_machine_locations: ArcadeMachineLocations
HelpWantedLocations, special_order_locations: SpecialOrderLocations
Fishsanity, help_wanted_locations: HelpWantedLocations
Museumsanity, fishsanity: Fishsanity
Friendsanity, museumsanity: Museumsanity
FriendsanityHeartSize, friendsanity: Friendsanity
NumberOfMovementBuffs, friendsanity_heart_size: FriendsanityHeartSize
NumberOfLuckBuffs, number_of_movement_buffs: NumberOfMovementBuffs
ExcludeGingerIsland, number_of_luck_buffs: NumberOfLuckBuffs
TrapItems, exclude_ginger_island: ExcludeGingerIsland
MultipleDaySleepEnabled, trap_items: TrapItems
MultipleDaySleepCost, multiple_day_sleep_enabled: MultipleDaySleepEnabled
ExperienceMultiplier, multiple_day_sleep_cost: MultipleDaySleepCost
FriendshipMultiplier, experience_multiplier: ExperienceMultiplier
DebrisMultiplier, friendship_multiplier: FriendshipMultiplier
QuickStart, debris_multiplier: DebrisMultiplier
Gifting, quick_start: QuickStart
Mods, gifting: Gifting
] mods: Mods
stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in death_link: DeathLink
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

View File

@ -2,11 +2,10 @@ from random import Random
from typing import Iterable, Dict, Protocol, List, Tuple, Set from typing import Iterable, Dict, Protocol, List, Tuple, Set
from BaseClasses import Region, Entrance from BaseClasses import Region, Entrance
from . import options from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity
from .strings.entrance_names import Entrance from .strings.entrance_names import Entrance
from .strings.region_names import Region from .strings.region_names import Region
from .region_classes import RegionData, ConnectionData, RandomizationFlag from .region_classes import RegionData, ConnectionData, RandomizationFlag
from .options import StardewOptions
from .mods.mod_regions import ModDataList from .mods.mod_regions import ModDataList
@ -397,12 +396,12 @@ vanilla_connections = [
] ]
def create_final_regions(world_options: StardewOptions) -> List[RegionData]: def create_final_regions(world_options) -> List[RegionData]:
final_regions = [] final_regions = []
final_regions.extend(vanilla_regions) final_regions.extend(vanilla_regions)
if world_options[options.Mods] is None: if world_options.mods is None:
return final_regions return final_regions
for mod in world_options[options.Mods]: for mod in world_options.mods.value:
if mod not in ModDataList: if mod not in ModDataList:
continue continue
for mod_region in ModDataList[mod].regions: for mod_region in ModDataList[mod].regions:
@ -417,19 +416,19 @@ def create_final_regions(world_options: StardewOptions) -> List[RegionData]:
return final_regions return final_regions
def create_final_connections(world_options: StardewOptions) -> List[ConnectionData]: def create_final_connections(world_options) -> List[ConnectionData]:
final_connections = [] final_connections = []
final_connections.extend(vanilla_connections) final_connections.extend(vanilla_connections)
if world_options[options.Mods] is None: if world_options.mods is None:
return final_connections return final_connections
for mod in world_options[options.Mods]: for mod in world_options.mods.value:
if mod not in ModDataList: if mod not in ModDataList:
continue continue
final_connections.extend(ModDataList[mod].connections) final_connections.extend(ModDataList[mod].connections)
return final_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]]: Iterable[Region], Dict[str, str]]:
final_regions = create_final_regions(world_options) final_regions = create_final_regions(world_options)
regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in 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 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]]: List[ConnectionData], Dict[str, str]]:
connections_to_randomize = [] connections_to_randomize = []
final_connections = create_final_connections(world_options) final_connections = create_final_connections(world_options)
connections_by_name: Dict[str, ConnectionData] = {connection.name: connection for connection in final_connections} 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 connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.PELICAN_TOWN in connection.flag] 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 connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.NON_PROGRESSION in connection.flag] 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 connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.BUILDINGS in connection.flag] 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 connections_to_randomize = [connection for connection in final_connections if
RandomizationFlag.BUILDINGS in connection.flag] RandomizationFlag.BUILDINGS in connection.flag]
connections_to_randomize = exclude_island_if_necessary(connections_to_randomize, world_options) 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): def remove_excluded_entrances(connections_to_randomize, world_options):
exclude_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_sewers = world_options[options.Museumsanity] == options.Museumsanity.option_none exclude_sewers = world_options.museumsanity == Museumsanity.option_none
if exclude_island: if exclude_island:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
if exclude_sewers: 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]: 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: if exclude_island:
connections_to_randomize = [connection for connection in connections_to_randomize if connections_to_randomize = [connection for connection in connections_to_randomize if
RandomizationFlag.GINGER_ISLAND not in connection.flag] RandomizationFlag.GINGER_ISLAND not in connection.flag]

View File

@ -1,10 +1,10 @@
import itertools import itertools
from typing import Dict, List from typing import List
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.generic import Rules as MultiWorldRules from worlds.generic import Rules as MultiWorldRules
from . import options, locations from .options import StardewValleyOptions, ToolProgression, BuildingProgression, SkillProgression, ExcludeGingerIsland, Cropsanity, SpecialOrderLocations, Museumsanity, \
from .bundles import Bundle BackpackProgression, ArcadeMachineLocations
from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \ from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, \
DeepWoodsEntrance, AlecEntrance, MagicEntrance DeepWoodsEntrance, AlecEntrance, MagicEntrance
from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \ from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_artifacts, \
@ -13,9 +13,8 @@ from .data.museum_data import all_museum_items, all_museum_minerals, all_museum_
from .strings.region_names import Region from .strings.region_names import Region
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .mods.logic import magic, deepwoods 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 .logic import StardewLogic, And, tool_upgrade_prices
from .options import StardewOptions
from .strings.ap_names.transport_names import Transportation from .strings.ap_names.transport_names import Transportation
from .strings.artisan_good_names import ArtisanGood from .strings.artisan_good_names import ArtisanGood
from .strings.calendar_names import Weekday from .strings.calendar_names import Weekday
@ -28,251 +27,256 @@ from .strings.villager_names import NPC, ModNPC
from .strings.wallet_item_names import Wallet from .strings.wallet_item_names import Wallet
def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic, def set_rules(world):
current_bundles: Dict[str, Bundle]): multiworld = world.multiworld
all_location_names = list(location.name for location in multi_world.get_locations(player)) world_options = world.options
player = world.player
logic = world.logic
current_bundles = world.modified_bundles
set_entrance_rules(logic, multi_world, player, world_options) all_location_names = list(location.name for location in multiworld.get_locations(player))
set_ginger_island_rules(logic, multi_world, player, world_options) set_entrance_rules(logic, multiworld, player, world_options)
set_ginger_island_rules(logic, multiworld, player, world_options)
# Those checks do not exist if ToolProgression is vanilla # Those checks do not exist if ToolProgression is vanilla
if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: if world_options.tool_progression != ToolProgression.option_vanilla:
MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player), MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player),
(logic.has_skill_level(Skill.fishing, 2) & logic.can_spend_money(1800)).simplify()) (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()) (logic.has_skill_level(Skill.fishing, 6) & logic.can_spend_money(7500)).simplify())
materials = [None, "Copper", "Iron", "Gold", "Iridium"] materials = [None, "Copper", "Iron", "Gold", "Iridium"]
tool = [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.watering_can, Tool.trash_can] 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): for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool):
if previous is None: 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.has(f"{material} Ore") &
logic.can_spend_money(tool_upgrade_prices[material])).simplify()) logic.can_spend_money(tool_upgrade_prices[material])).simplify())
else: 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.has(f"{material} Ore") & logic.has_tool(tool, previous) &
logic.can_spend_money(tool_upgrade_prices[material])).simplify()) 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 # Bundles
for bundle in current_bundles.values(): 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) rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required)
simplified_rules = rules.simplify() simplified_rules = rules.simplify()
MultiWorldRules.set_rule(location, simplified_rules) 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) And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) for bundle in locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player), MultiWorldRules.add_rule(multiworld.get_location("Complete Pantry", player),
And(logic.can_reach_location(bundle.name) And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) for bundle in locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player), MultiWorldRules.add_rule(multiworld.get_location("Complete Fish Tank", player),
And(logic.can_reach_location(bundle.name) And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) for bundle in locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player), MultiWorldRules.add_rule(multiworld.get_location("Complete Boiler Room", player),
And(logic.can_reach_location(bundle.name) And(logic.can_reach_location(bundle.name)
for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) for bundle in locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player), MultiWorldRules.add_rule(multiworld.get_location("Complete Bulletin Board", player),
And(logic.can_reach_location(bundle.name) And(logic.can_reach_location(bundle.name)
for bundle for bundle
in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) in locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player), MultiWorldRules.add_rule(multiworld.get_location("Complete Vault", player),
And(logic.can_reach_location(bundle.name) 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 # Buildings
if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: if world_options.building_progression != BuildingProgression.option_vanilla:
for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: for building in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
if building.mod_name is not None and building.mod_name not in world_options[options.Mods]: if building.mod_name is not None and building.mod_name not in world_options.mods:
continue 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()) logic.building_rules[building.name.replace(" Blueprint", "")].simplify())
set_cropsanity_rules(all_location_names, logic, multi_world, player, world_options) set_cropsanity_rules(all_location_names, logic, multiworld, player, world_options)
set_story_quests_rules(all_location_names, logic, multi_world, player, world_options) set_story_quests_rules(all_location_names, logic, multiworld, player, world_options)
set_special_order_rules(all_location_names, logic, multi_world, player, world_options) set_special_order_rules(all_location_names, logic, multiworld, player, world_options)
set_help_wanted_quests_rules(logic, multi_world, player, world_options) set_help_wanted_quests_rules(logic, multiworld, player, world_options)
set_fishsanity_rules(all_location_names, logic, multi_world, player) set_fishsanity_rules(all_location_names, logic, multiworld, player)
set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options) set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options)
set_friendsanity_rules(all_location_names, logic, multi_world, player) set_friendsanity_rules(all_location_names, logic, multiworld, player)
set_backpack_rules(logic, multi_world, player, world_options) set_backpack_rules(logic, multiworld, player, world_options)
set_festival_rules(all_location_names, logic, multi_world, player) set_festival_rules(all_location_names, logic, multiworld, player)
MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), MultiWorldRules.add_rule(multiworld.get_location("Old Master Cannoli", player),
logic.has("Sweet Gem Berry").simplify()) 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()) 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()) 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()) logic.can_reproduce(2).simplify())
set_traveling_merchant_rules(logic, multi_world, player) set_traveling_merchant_rules(logic, multiworld, player)
set_arcade_machine_rules(logic, multi_world, player, world_options) set_arcade_machine_rules(logic, multiworld, player, world_options)
set_deepwoods_rules(logic, multi_world, player, world_options) set_deepwoods_rules(logic, multiworld, player, world_options)
set_magic_spell_rules(logic, multi_world, 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 # Skills
if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla: if world_options.skill_progression != SkillProgression.option_vanilla:
for i in range(1, 11): for i in range(1, 11):
set_skill_rule(logic, multi_world, player, Skill.farming, i) set_skill_rule(logic, multiworld, player, Skill.farming, i)
set_skill_rule(logic, multi_world, player, Skill.fishing, i) set_skill_rule(logic, multiworld, player, Skill.fishing, i)
set_skill_rule(logic, multi_world, player, Skill.foraging, i) set_skill_rule(logic, multiworld, player, Skill.foraging, i)
set_skill_rule(logic, multi_world, player, Skill.mining, i) set_skill_rule(logic, multiworld, player, Skill.mining, i)
set_skill_rule(logic, multi_world, player, Skill.combat, i) set_skill_rule(logic, multiworld, player, Skill.combat, i)
# Modded Skills # Modded Skills
if ModNames.luck_skill in world_options[options.Mods]: if ModNames.luck_skill in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.luck, i) set_skill_rule(logic, multiworld, player, ModSkill.luck, i)
if ModNames.magic in world_options[options.Mods]: if ModNames.magic in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.magic, i) set_skill_rule(logic, multiworld, player, ModSkill.magic, i)
if ModNames.binning_skill in world_options[options.Mods]: if ModNames.binning_skill in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.binning, i) set_skill_rule(logic, multiworld, player, ModSkill.binning, i)
if ModNames.cooking_skill in world_options[options.Mods]: if ModNames.cooking_skill in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.cooking, i) set_skill_rule(logic, multiworld, player, ModSkill.cooking, i)
if ModNames.socializing_skill in world_options[options.Mods]: if ModNames.socializing_skill in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.socializing, i) set_skill_rule(logic, multiworld, player, ModSkill.socializing, i)
if ModNames.archaeology in world_options[options.Mods]: if ModNames.archaeology in world_options.mods:
set_skill_rule(logic, multi_world, player, ModSkill.archaeology, i) set_skill_rule(logic, multiworld, player, ModSkill.archaeology, i)
def set_skill_rule(logic, multi_world, player, skill: str, level: int): def set_skill_rule(logic, multiworld, player, skill: str, level: int):
location_name = f"Level {level} {skill}" 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() rule = logic.can_earn_skill_level(skill, level).simplify()
MultiWorldRules.set_rule(location, rule) 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): 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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()) logic.received(Wallet.skull_key).simplify())
for floor in range(25, 200 + 25, 25): 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()) 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)) 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()) 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()) 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()) 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()) 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")) 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")) 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)) 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()) 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()) (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.has_rusty_key() & logic.can_reach_region(Region.railroad) &
logic.can_meet(NPC.krobus) | magic.can_blink(logic)).simplify())) 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)) 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)) 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()) (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)) 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)) 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)) 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)) 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")) logic.received("Treehouse"))
if ModNames.alec in world_options[options.Mods]: if ModNames.alec in world_options.mods:
MultiWorldRules.set_rule(multi_world.get_entrance(AlecEntrance.petshop_to_bedroom, player), MultiWorldRules.set_rule(multiworld.get_entrance(AlecEntrance.petshop_to_bedroom, player),
(logic.has_relationship(ModNPC.alec, 2) | magic.can_blink(logic)).simplify()) (logic.has_relationship(ModNPC.alec, 2) | magic.can_blink(logic)).simplify())
def set_ginger_island_rules(logic: StardewLogic, multi_world, player, world_options: StardewOptions): def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
set_island_entrances_rules(logic, multi_world, player) set_island_entrances_rules(logic, multiworld, player)
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true: if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return return
set_boat_repair_rules(logic, multi_world, player) set_boat_repair_rules(logic, multiworld, player)
set_island_parrot_rules(logic, multi_world, player) set_island_parrot_rules(logic, multiworld, player)
MultiWorldRules.add_rule(multi_world.get_location("Open Professor Snail Cave", player), MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player),
logic.has(Craftable.cherry_bomb).simplify()) 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()) logic.can_complete_field_office().simplify())
def set_boat_repair_rules(logic: StardewLogic, multi_world, player): def set_boat_repair_rules(logic: StardewLogic, multiworld, player):
MultiWorldRules.add_rule(multi_world.get_location("Repair Boat Hull", player), MultiWorldRules.add_rule(multiworld.get_location("Repair Boat Hull", player),
logic.has(Material.hardwood).simplify()) 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()) 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()) 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() 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) 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) 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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") | (logic.can_water(0) | logic.received("Volcano Bridge") |
magic.can_blink(logic)).simplify()) 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()) 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()) (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()) 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()) (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, 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, 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_volcano_to_jungle, Entrance.parrot_express_jungle_to_docks,
Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_volcano_to_docks] Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_volcano_to_docks]
for parrot in parrots: 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_walnut = logic.has_walnut(1).simplify()
has_5_walnut = logic.has_walnut(5).simplify() has_5_walnut = logic.has_walnut(5).simplify()
has_10_walnut = logic.has_walnut(10).simplify() has_10_walnut = logic.has_walnut(10).simplify()
has_20_walnut = logic.has_walnut(20).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) 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")) 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) 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")) 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")) 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")) 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")) 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") & has_5_walnut & logic.received("Island West Turtle") &
logic.can_reach_region(Region.volcano_floor_10)) 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")) 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")) 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) has_10_walnut)
def set_cropsanity_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions): def set_cropsanity_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions):
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled: if world_options.cropsanity == Cropsanity.option_disabled:
return return
harvest_prefix = "Harvest " harvest_prefix = "Harvest "
harvest_prefix_length = len(harvest_prefix) harvest_prefix_length = len(harvest_prefix)
for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]: 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[options.Mods]): 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:] 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()) logic.has(crop_name).simplify())
def set_story_quests_rules(all_location_names: List[str], logic, multi_world, player, world_options: StardewOptions): def set_story_quests_rules(all_location_names: List[str], logic, multiworld, player, world_options: StardewValleyOptions):
for quest in locations.locations_by_tag[LocationTags.QUEST]: 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[options.Mods]): if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options.mods):
MultiWorldRules.set_rule(multi_world.get_location(quest.name, player), MultiWorldRules.set_rule(multiworld.get_location(quest.name, player),
logic.quest_rules[quest.name].simplify()) logic.quest_rules[quest.name].simplify())
def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multi_world, player, def set_special_order_rules(all_location_names: List[str], logic: StardewLogic, multiworld, player,
world_options: StardewOptions): world_options: StardewValleyOptions):
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled: if world_options.special_order_locations == SpecialOrderLocations.option_disabled:
return return
board_rule = logic.received("Special Order Board") & logic.has_lived_months(4) 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: if board_order.name in all_location_names:
order_rule = board_rule & logic.special_order_rules[board_order.name] 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 return
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_only: if world_options.special_order_locations == SpecialOrderLocations.option_board_only:
return return
qi_rule = logic.can_reach_region(Region.qi_walnut_room) & logic.has_lived_months(8) 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: if qi_order.name in all_location_names:
order_rule = qi_rule & logic.special_order_rules[qi_order.name] 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:" help_wanted_prefix = "Help Wanted:"
@ -362,8 +366,8 @@ fishing = "Fishing"
slay_monsters = "Slay Monsters" slay_monsters = "Slay Monsters"
def set_help_wanted_quests_rules(logic: StardewLogic, multi_world, player, world_options): def set_help_wanted_quests_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
help_wanted_number = world_options[options.HelpWantedLocations] help_wanted_number = world_options.help_wanted_locations
for i in range(0, help_wanted_number): for i in range(0, help_wanted_number):
set_number = i // 7 set_number = i // 7
month_rule = logic.has_lived_months(set_number).simplify() 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 quest_number_in_set = i % 7
if quest_number_in_set < 4: if quest_number_in_set < 4:
quest_number = set_number * 4 + quest_number_in_set + 1 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: 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: 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: 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}" 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}" 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}" location_name = f"{help_wanted_prefix} {fishing} {quest_number}"
fishing_rule = month_rule & logic.can_fish() 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}" location_name = f"{help_wanted_prefix} {slay_monsters} {quest_number}"
slay_rule = month_rule & logic.can_do_combat_at_level("Basic") 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: " 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: if fish_location.name in all_location_names:
fish_name = fish_location.name[len(fish_prefix):] 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()) logic.has(fish_name).simplify())
def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int, def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multiworld: MultiWorld, player: int,
world_options: StardewOptions): world_options: StardewValleyOptions):
museum_prefix = "Museumsanity: " museum_prefix = "Museumsanity: "
if world_options[options.Museumsanity] == options.Museumsanity.option_milestones: if world_options.museumsanity == Museumsanity.option_milestones:
for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]: for museum_milestone in locations_by_tag[LocationTags.MUSEUM_MILESTONES]:
set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player) set_museum_milestone_rule(logic, multiworld, museum_milestone, museum_prefix, player)
elif world_options[options.Museumsanity] != options.Museumsanity.option_none: elif world_options.museumsanity != Museumsanity.option_none:
set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player) 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): def set_museum_individual_donations_rules(all_location_names, logic: StardewLogic, multiworld, museum_prefix, player):
all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS], 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) key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True)
counter = 0 counter = 0
number_donations = len(all_donations) 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: if museum_location.name in all_location_names:
donation_name = museum_location.name[len(museum_prefix):] donation_name = museum_location.name[len(museum_prefix):]
required_detectors = counter * 5 // number_donations 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) rule = logic.can_donate_museum_item(all_museum_items_by_name[donation_name]) & logic.received("Traveling Merchant Metal Detector",
MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player), required_detectors)
MultiWorldRules.set_rule(multiworld.get_location(museum_location.name, player),
rule.simplify()) rule.simplify())
counter += 1 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): player: int):
milestone_name = museum_milestone.name[len(museum_prefix):] milestone_name = museum_milestone.name[len(museum_prefix):]
donations_suffix = " Donations" 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) rule = logic.can_donate_museum_item(Artifact.ancient_seed) & logic.received(metal_detector, 4)
if rule is None: if rule is None:
return 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): 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 return rule
def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): def set_backpack_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: if world_options.backpack_progression != BackpackProgression.option_vanilla:
MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player), MultiWorldRules.set_rule(multiworld.get_location("Large Pack", player),
logic.can_spend_money(2000).simplify()) 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()) (logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify())
if ModNames.big_backpack in world_options[options.Mods]: if ModNames.big_backpack in world_options.mods:
MultiWorldRules.set_rule(multi_world.get_location("Premium Pack", player), MultiWorldRules.set_rule(multiworld.get_location("Premium Pack", player),
(logic.can_spend_money(150000) & (logic.can_spend_money(150000) &
logic.received("Progressive Backpack", 2)).simplify()) 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 = []
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL]) festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL])
festival_locations.extend(locations.locations_by_tag[LocationTags.FESTIVAL_HARD]) festival_locations.extend(locations_by_tag[LocationTags.FESTIVAL_HARD])
for festival in festival_locations: for festival in festival_locations:
if festival.name in all_location_names: 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()) 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: for day in Weekday.all_days:
item_for_day = f"Traveling Merchant: {day}" item_for_day = f"Traveling Merchant: {day}"
for i in range(1, 4): for i in range(1, 4):
location_name = f"Traveling Merchant {day} Item {i}" 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)) logic.received(item_for_day))
def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
MultiWorldRules.add_rule(multi_world.get_entrance(Entrance.play_junimo_kart, player), MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
logic.received(Wallet.skull_key).simplify()) 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 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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()) 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_prefix = "Friendsanity: "
friend_suffix = " <3" friend_suffix = " <3"
for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: for friend_location in locations_by_tag[LocationTags.FRIENDSANITY]:
if not friend_location.name in all_location_names: if friend_location.name not in all_location_names:
continue continue
friend_location_without_prefix = friend_location.name[len(friend_prefix):] friend_location_without_prefix = friend_location.name[len(friend_prefix):]
friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)]
split_index = friend_location_trimmed.rindex(" ") split_index = friend_location_trimmed.rindex(" ")
friend_name = friend_location_trimmed[:split_index] friend_name = friend_location_trimmed[:split_index]
num_hearts = int(friend_location_trimmed[split_index + 1:]) 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()) logic.can_earn_relationship(friend_name, num_hearts).simplify())
def set_deepwoods_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions): def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if ModNames.deepwoods in world_options[options.Mods]: if ModNames.deepwoods in world_options.mods:
MultiWorldRules.add_rule(multi_world.get_location("Breaking Up Deep Woods Gingerbread House", player), 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()) 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()) 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()) logic.received("Woods Obelisk").simplify())
for depth in range(10, 100 + 10, 10): 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()) deepwoods.can_chop_to_depth(logic, depth).simplify())
def set_magic_spell_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options: StardewOptions): def set_magic_spell_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
if ModNames.magic not in world_options[options.Mods]: if ModNames.magic not in world_options.mods:
return return
MultiWorldRules.set_rule(multi_world.get_entrance(MagicEntrance.store_to_altar, player), MultiWorldRules.set_rule(multiworld.get_entrance(MagicEntrance.store_to_altar, player),
(logic.has_relationship(NPC.wizard, 3) & (logic.has_relationship(NPC.wizard, 3) &
logic.can_reach_region(Region.wizard_tower)).simplify()) logic.can_reach_region(Region.wizard_tower)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Clear Debris", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Clear Debris", player),
((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) ((logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify()) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Till", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Till", player),
(logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify()) (logic.has_tool("Hoe", "Basic") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Water", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Water", player),
(logic.has_tool("Watering Can", "Basic") & magic.can_use_altar(logic)).simplify()) (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), 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("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic"))
& magic.can_use_altar(logic)).simplify()) & magic.can_use_altar(logic)).simplify())
# Do I *want* to add boots into logic when you get them even in vanilla without effort? idk # 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), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Evac", player),
(logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) (logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Haste", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Haste", player),
(logic.has("Coffee") & magic.can_use_altar(logic)).simplify()) (logic.has("Coffee") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Heal", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Heal", player),
(logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify()) (logic.has("Life Elixir") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Life School Locations", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze All Life School Locations", player),
(logic.has("Coffee") & logic.has("Life Elixir") (logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify()) & logic.can_mine_perfectly() & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Descend", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Descend", player),
(logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify()) (logic.can_reach_region(Region.mines) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Fireball", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Fireball", player),
(logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify()) (logic.has("Fire Quartz") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Frostbite", player), 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()) (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), 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) & logic.has("Fire Quartz")
& logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) & & logic.can_reach_region(Region.mines_floor_70) & logic.can_fish(85) &
magic.can_use_altar(logic)).simplify()) magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lantern", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lantern", player),
magic.can_use_altar(logic).simplify()) magic.can_use_altar(logic).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Tendrils", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Tendrils", player),
(logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify()) (logic.can_reach_region(Region.farm) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Shockwave", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Shockwave", player),
(logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify()) (logic.has("Earth Crystal") & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze All Nature School Locations", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze All Nature School Locations", player),
(logic.has("Earth Crystal") & logic.can_reach_region("Farm") & (logic.has("Earth Crystal") & logic.can_reach_region("Farm") &
magic.can_use_altar(logic)).simplify()), magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Meteor", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Meteor", player),
(logic.can_reach_region(Region.farm) & logic.has_lived_months(12) (logic.can_reach_region(Region.farm) & logic.has_lived_months(12)
& magic.can_use_altar(logic)).simplify()), & magic.can_use_altar(logic)).simplify()),
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Lucksteal", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Lucksteal", player),
(logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify()) (logic.can_reach_region(Region.witch_hut) & magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze: Bloodmana", player), MultiWorldRules.add_rule(multiworld.get_location("Analyze: Bloodmana", player),
(logic.can_reach_region(Region.mines_floor_100) & magic.can_use_altar(logic)).simplify()) (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), MultiWorldRules.add_rule(multiworld.get_location("Analyze All Eldritch School Locations", player),
(logic.can_reach_region(Region.witch_hut) & (logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) & logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify()) magic.can_use_altar(logic)).simplify())
MultiWorldRules.add_rule(multi_world.get_location("Analyze Every Magic School Location", player), 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("Watering Can", "Basic") & logic.has_tool("Hoe", "Basic")
& (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) & & (logic.has_tool("Axe", "Basic") | logic.has_tool("Pickaxe", "Basic")) &
logic.has("Coffee") & logic.has("Life Elixir") logic.has("Coffee") & logic.has("Life Elixir")
& logic.can_mine_perfectly() & logic.has("Earth Crystal") & & logic.can_mine_perfectly() & logic.has("Earth Crystal") &
logic.can_reach_region(Region.mines) & logic.can_reach_region(Region.mines) &
logic.has("Fire Quartz") & logic.can_fish(85) & logic.has("Fire Quartz") & logic.can_fish(85) &
logic.can_reach_region(Region.witch_hut) & logic.can_reach_region(Region.witch_hut) &
logic.can_reach_region(Region.mines_floor_100) & logic.can_reach_region(Region.mines_floor_100) &
logic.can_reach_region(Region.farm) & logic.has_lived_months(12) & logic.can_reach_region(Region.farm) & logic.has_lived_months(12) &
magic.can_use_altar(logic)).simplify()) magic.can_use_altar(logic)).simplify())

View File

@ -1,6 +1,5 @@
from .. import True_ from .. import True_
from ..logic import Received, Has, False_, And, Or, StardewLogic from ..logic import Received, Has, False_, And, Or
from ..options import default_options, StardewOptions
def test_simplify_true_in_and(): def test_simplify_true_in_and():

View File

@ -1,15 +1,14 @@
import itertools import itertools
import unittest
from random import random from random import random
from typing import Dict from typing import Dict
from BaseClasses import ItemClassification, MultiWorld from BaseClasses import ItemClassification, MultiWorld
from Options import SpecialRange, OptionSet from Options import SpecialRange
from . import setup_solo_multiworld, SVTestBase 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 ..locations import locations_by_tag, LocationTags, location_table
from ..options import StardewOption, stardew_valley_option_classes, Mods from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
from ..strings.goal_names import Goal from ..strings.goal_names import Goal as GoalName
from ..strings.season_names import Season from ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import ToolMaterial, Tool from ..strings.tool_names import ToolMaterial, Tool
@ -51,39 +50,41 @@ def get_option_choices(option) -> Dict[str, int]:
class TestGenerateDynamicOptions(SVTestBase): class TestGenerateDynamicOptions(SVTestBase):
def test_given_special_range_when_generate_then_basic_checks(self): def test_given_special_range_when_generate_then_basic_checks(self):
for option in stardew_valley_option_classes: options = self.world.options_dataclass.type_hints
if not issubclass(option, SpecialRange): for option_name, option in options.items():
if not isinstance(option, SpecialRange):
continue continue
for value in option.special_range_names: for value in option.special_range_names:
with self.subTest(f"{option.internal_name}: {value}"): with self.subTest(f"{option_name}: {value}"):
choices = {option.internal_name: option.special_range_names[value]} choices = {option_name: option.special_range_names[value]}
multiworld = setup_solo_multiworld(choices) multiworld = setup_solo_multiworld(choices)
basic_checks(self, multiworld) basic_checks(self, multiworld)
def test_given_choice_when_generate_then_basic_checks(self): def test_given_choice_when_generate_then_basic_checks(self):
seed = int(random() * pow(10, 18) - 1) 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: if not option.options:
continue continue
for value in option.options: for value in option.options:
with self.subTest(f"{option.internal_name}: {value} [Seed: {seed}]"): with self.subTest(f"{option_name}: {value} [Seed: {seed}]"):
world_options = {option.internal_name: option.options[value]} world_options = {option_name: option.options[value]}
multiworld = setup_solo_multiworld(world_options, seed) multiworld = setup_solo_multiworld(world_options, seed)
basic_checks(self, multiworld) basic_checks(self, multiworld)
class TestGoal(SVTestBase): class TestGoal(SVTestBase):
def test_given_goal_when_generate_then_victory_is_in_correct_location(self): def test_given_goal_when_generate_then_victory_is_in_correct_location(self):
for goal, location in [("community_center", Goal.community_center), for goal, location in [("community_center", GoalName.community_center),
("grandpa_evaluation", Goal.grandpa_evaluation), ("grandpa_evaluation", GoalName.grandpa_evaluation),
("bottom_of_the_mines", Goal.bottom_of_the_mines), ("bottom_of_the_mines", GoalName.bottom_of_the_mines),
("cryptic_note", Goal.cryptic_note), ("cryptic_note", GoalName.cryptic_note),
("master_angler", Goal.master_angler), ("master_angler", GoalName.master_angler),
("complete_collection", Goal.complete_museum), ("complete_collection", GoalName.complete_museum),
("full_house", Goal.full_house), ("full_house", GoalName.full_house),
("perfection", Goal.perfection)]: ("perfection", GoalName.perfection)]:
with self.subTest(msg=f"Goal: {goal}, Location: {location}"): 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) multi_world = setup_solo_multiworld(world_options)
victory = multi_world.find_item("Victory", 1) victory = multi_world.find_item("Victory", 1)
self.assertEqual(victory.name, location) self.assertEqual(victory.name, location)
@ -91,14 +92,14 @@ class TestGoal(SVTestBase):
class TestSeasonRandomization(SVTestBase): class TestSeasonRandomization(SVTestBase):
def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): 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) multi_world = setup_solo_multiworld(world_options)
precollected_items = {item.name for item in multi_world.precollected_items[1]} precollected_items = {item.name for item in multi_world.precollected_items[1]}
self.assertTrue(all([season in precollected_items for season in SEASONS])) 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): 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) multi_world = setup_solo_multiworld(world_options)
precollected_items = {item.name for item in multi_world.precollected_items[1]} precollected_items = {item.name for item in multi_world.precollected_items[1]}
items = {item.name for item in multi_world.get_items()} | precollected_items items = {item.name for item in multi_world.get_items()} | precollected_items
@ -106,7 +107,7 @@ class TestSeasonRandomization(SVTestBase):
self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) self.assertEqual(len(SEASONS.intersection(precollected_items)), 1)
def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): 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) multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()] items = [item.name for item in multi_world.get_items()]
@ -115,7 +116,7 @@ class TestSeasonRandomization(SVTestBase):
class TestToolProgression(SVTestBase): class TestToolProgression(SVTestBase):
def test_given_vanilla_when_generate_then_no_tool_in_pool(self): 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) multi_world = setup_solo_multiworld(world_options)
items = {item.name for item in multi_world.get_items()} items = {item.name for item in multi_world.get_items()}
@ -123,7 +124,7 @@ class TestToolProgression(SVTestBase):
self.assertNotIn(tool, items) self.assertNotIn(tool, items)
def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): 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) multi_world = setup_solo_multiworld(world_options)
items = [item.name for item in multi_world.get_items()] items = [item.name for item in multi_world.get_items()]
@ -131,7 +132,7 @@ class TestToolProgression(SVTestBase):
self.assertEqual(items.count("Progressive " + tool), 4) self.assertEqual(items.count("Progressive " + tool), 4)
def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): 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) multi_world = setup_solo_multiworld(world_options)
locations = {locations.name for locations in multi_world.get_locations(1)} locations = {locations.name for locations in multi_world.get_locations(1)}
@ -148,50 +149,52 @@ class TestToolProgression(SVTestBase):
class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase):
def test_given_special_range_when_generate_exclude_ginger_island(self): def test_given_special_range_when_generate_exclude_ginger_island(self):
for option in stardew_valley_option_classes: options = self.world.options_dataclass.type_hints
if not issubclass(option, for option_name, option in options.items():
SpecialRange) or option.internal_name == options.ExcludeGingerIsland.internal_name: if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name:
continue continue
for value in option.special_range_names: 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( multiworld = setup_solo_multiworld(
{options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, {ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
option.internal_name: option.special_range_names[value]}) option_name: option.special_range_names[value]})
check_no_ginger_island(self, multiworld) check_no_ginger_island(self, multiworld)
def test_given_choice_when_generate_exclude_ginger_island(self): def test_given_choice_when_generate_exclude_ginger_island(self):
seed = int(random() * pow(10, 18) - 1) seed = int(random() * pow(10, 18) - 1)
island_option = options.ExcludeGingerIsland options = self.world.options_dataclass.type_hints
for option in stardew_valley_option_classes: for option_name, option in options.items():
if not option.options or option.internal_name == island_option.internal_name: if not option.options or option_name == ExcludeGingerIsland.internal_name:
continue continue
for value in option.options: 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( multiworld = setup_solo_multiworld(
{island_option.internal_name: island_option.option_true, {ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
option.internal_name: option.options[value]}, seed) option_name: option.options[value]}, seed)
if multiworld.worlds[self.player].options[island_option.internal_name] != island_option.option_true: stardew_world: StardewValleyWorld = multiworld.worlds[self.player]
if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true:
continue continue
basic_checks(self, multiworld) basic_checks(self, multiworld)
check_no_ginger_island(self, multiworld) check_no_ginger_island(self, multiworld)
def test_given_island_related_goal_then_override_exclude_ginger_island(self): 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_goals = [value for value in Goal.options if value in ["walnut_hunter", "perfection"]]
island_option = options.ExcludeGingerIsland island_option = ExcludeGingerIsland
for goal in island_goals: for goal in island_goals:
for value in island_option.options: for value in island_option.options:
with self.subTest(f"Goal: {goal}, {island_option.internal_name}: {value}"): with self.subTest(f"Goal: {goal}, {island_option.internal_name}: {value}"):
multiworld = setup_solo_multiworld( 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]}) 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) basic_checks(self, multiworld)
class TestTraps(SVTestBase): class TestTraps(SVTestBase):
def test_given_no_traps_when_generate_then_no_trap_in_pool(self): def test_given_no_traps_when_generate_then_no_trap_in_pool(self):
world_options = self.allsanity_options_without_mods() 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) multi_world = setup_solo_multiworld(world_options)
trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]]
@ -202,12 +205,12 @@ class TestTraps(SVTestBase):
self.assertNotIn(item, multiworld_items) self.assertNotIn(item, multiworld_items)
def test_given_traps_when_generate_then_all_traps_in_pool(self): 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: for value in trap_option.options:
if value == "no_traps": if value == "no_traps":
continue continue
world_options = self.allsanity_options_with_mods() 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) 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] 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()] multiworld_items = [item.name for item in multi_world.get_items()]
@ -218,7 +221,7 @@ class TestTraps(SVTestBase):
class TestSpecialOrders(SVTestBase): class TestSpecialOrders(SVTestBase):
def test_given_disabled_then_no_order_in_pool(self): 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) 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} locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
@ -228,7 +231,7 @@ class TestSpecialOrders(SVTestBase):
self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags)
def test_given_board_only_then_no_qi_order_in_pool(self): 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) 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} locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table}
@ -242,8 +245,8 @@ class TestSpecialOrders(SVTestBase):
self.assertIn(board_location.name, locations_in_pool) self.assertIn(board_location.name, locations_in_pool)
def test_given_board_and_qi_then_all_orders_in_pool(self): def test_given_board_and_qi_then_all_orders_in_pool(self):
world_options = {options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_victories} ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories}
multi_world = setup_solo_multiworld(world_options) multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()} locations_in_pool = {location.name for location in multi_world.get_locations()}
@ -258,8 +261,8 @@ class TestSpecialOrders(SVTestBase):
self.assertIn(board_location.name, locations_in_pool) 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): 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, world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled} ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled}
multi_world = setup_solo_multiworld(world_options) multi_world = setup_solo_multiworld(world_options)
locations_in_pool = {location.name for location in multi_world.get_locations()} locations_in_pool = {location.name for location in multi_world.get_locations()}

View File

@ -3,7 +3,8 @@ import sys
import unittest import unittest
from . import SVTestBase, setup_solo_multiworld 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 from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag
connections_by_name = {connection.name for connection in vanilla_connections} connections_by_name = {connection.name for connection in vanilla_connections}
@ -37,11 +38,12 @@ class TestEntranceRando(unittest.TestCase):
seed = random.randrange(sys.maxsize) seed = random.randrange(sys.maxsize)
with self.subTest(flag=flag, msg=f"Seed: {seed}"): with self.subTest(flag=flag, msg=f"Seed: {seed}"):
rand = random.Random(seed) rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option, world_options = {EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}) ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}
multiworld = setup_solo_multiworld(world_options)
regions_by_name = {region.name: region for region in vanilla_regions} 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: for connection in vanilla_connections:
if flag in connection.flag: if flag in connection.flag:
@ -62,11 +64,12 @@ class TestEntranceRando(unittest.TestCase):
with self.subTest(option=option, flag=flag): with self.subTest(option=option, flag=flag):
seed = random.randrange(sys.maxsize) seed = random.randrange(sys.maxsize)
rand = random.Random(seed) rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option, world_options = {EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}) ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}
multiworld = setup_solo_multiworld(world_options)
regions_by_name = {region.name: region for region in vanilla_regions} 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: for connection in vanilla_connections:
if flag in connection.flag: if flag in connection.flag:

View File

@ -5,9 +5,12 @@ from typing import Dict, FrozenSet, Tuple, Any, ClassVar
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from test.TestBase import WorldTestBase from test.TestBase import WorldTestBase
from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld 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 ..mods.mod_data import ModNames
from worlds.AutoWorld import call_all 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): class SVTestBase(WorldTestBase):
@ -33,48 +36,48 @@ class SVTestBase(WorldTestBase):
def minimal_locations_maximal_items(self): def minimal_locations_maximal_items(self):
min_max_options = { min_max_options = {
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, Cropsanity.internal_name: Cropsanity.option_shuffled,
options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, BackpackProgression.internal_name: BackpackProgression.option_vanilla,
options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, ToolProgression.internal_name: ToolProgression.option_vanilla,
options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, SkillProgression.internal_name: SkillProgression.option_vanilla,
options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, BuildingProgression.internal_name: BuildingProgression.option_vanilla,
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled,
options.HelpWantedLocations.internal_name: 0, HelpWantedLocations.internal_name: 0,
options.Fishsanity.internal_name: options.Fishsanity.option_none, Fishsanity.internal_name: Fishsanity.option_none,
options.Museumsanity.internal_name: options.Museumsanity.option_none, Museumsanity.internal_name: Museumsanity.option_none,
options.Friendsanity.internal_name: options.Friendsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none,
options.NumberOfMovementBuffs.internal_name: 12, NumberOfMovementBuffs.internal_name: 12,
options.NumberOfLuckBuffs.internal_name: 12, NumberOfLuckBuffs.internal_name: 12,
} }
return min_max_options return min_max_options
def allsanity_options_without_mods(self): def allsanity_options_without_mods(self):
allsanity = { allsanity = {
options.Goal.internal_name: options.Goal.option_perfection, Goal.internal_name: Goal.option_perfection,
options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, BundleRandomization.internal_name: BundleRandomization.option_shuffled,
options.BundlePrice.internal_name: options.BundlePrice.option_expensive, BundlePrice.internal_name: BundlePrice.option_expensive,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, SeasonRandomization.internal_name: SeasonRandomization.option_randomized,
options.Cropsanity.internal_name: options.Cropsanity.option_shuffled, Cropsanity.internal_name: Cropsanity.option_shuffled,
options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, BackpackProgression.internal_name: BackpackProgression.option_progressive,
options.ToolProgression.internal_name: options.ToolProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive,
options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive,
options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, FestivalLocations.internal_name: FestivalLocations.option_hard,
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive,
options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
options.HelpWantedLocations.internal_name: 56, HelpWantedLocations.internal_name: 56,
options.Fishsanity.internal_name: options.Fishsanity.option_all, Fishsanity.internal_name: Fishsanity.option_all,
options.Museumsanity.internal_name: options.Museumsanity.option_all, Museumsanity.internal_name: Museumsanity.option_all,
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
options.FriendsanityHeartSize.internal_name: 1, FriendsanityHeartSize.internal_name: 1,
options.NumberOfMovementBuffs.internal_name: 12, NumberOfMovementBuffs.internal_name: 12,
options.NumberOfLuckBuffs.internal_name: 12, NumberOfLuckBuffs.internal_name: 12,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
options.TrapItems.internal_name: options.TrapItems.option_nightmare, TrapItems.internal_name: TrapItems.option_nightmare,
} }
return allsanity return allsanity
@ -89,7 +92,7 @@ class SVTestBase(WorldTestBase):
ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator 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 return allsanity
pre_generated_worlds = {} pre_generated_worlds = {}
@ -110,7 +113,7 @@ def setup_solo_multiworld(test_options=None, seed=None,
multiworld.set_seed(seed) multiworld.set_seed(seed)
# print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
args = Namespace() 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) value = option(test_options[name]) if name in test_options else option.from_any(option.default)
setattr(args, name, {1: value}) setattr(args, name, {1: value})
multiworld.set_options(args) multiworld.set_options(args)

View File

@ -1,11 +1,11 @@
from BaseClasses import MultiWorld 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 options
from .. import SVTestBase from .. import SVTestBase
def is_goal(multiworld: MultiWorld, goal: int) -> bool: 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: 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): 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): def assert_walnut_hunter_world_is_valid(tester: SVTestBase, multiworld: MultiWorld):

View File

@ -1,5 +1,3 @@
from typing import Union
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from .world_checks import get_all_item_names, get_all_location_names from .world_checks import get_all_item_names, get_all_location_names
from .. import SVTestBase from .. import SVTestBase
@ -8,32 +6,16 @@ from ...locations import LocationTags
from ...strings.ap_names.transport_names import Transportation 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: for world_key in multiworld.worlds:
world = multiworld.worlds[world_key] world = multiworld.worlds[world_key]
if isinstance(world, StardewValleyWorld): if isinstance(world, StardewValleyWorld):
return world return world
return None raise ValueError("no stardew world in this multiworld")
def is_setting(multiworld: MultiWorld, setting_name: str, setting_value: int) -> bool: def get_stardew_options(multiworld: MultiWorld) -> options.StardewValleyOptions:
stardew_world = get_stardew_world(multiworld) return get_stardew_world(multiworld).options
if not stardew_world:
return False
current_value = stardew_world.options[setting_name]
return current_value == setting_value
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 assert_can_reach_island(tester: SVTestBase, multiworld: MultiWorld): 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): 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: if include_island:
assert_can_reach_island(tester, multiworld) assert_can_reach_island(tester, multiworld)
else: 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): 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: if not is_cropsanity:
return 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): 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: if not has_festivals:
return return
assert_all_rarecrows_exist(tester, multiworld) assert_all_rarecrows_exist(tester, multiworld)
assert_has_deluxe_scarecrow_recipe(tester, multiworld) assert_has_deluxe_scarecrow_recipe(tester, multiworld)

View File

@ -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)

View File

@ -24,7 +24,6 @@ class TestGenerateDynamicOptions(SVTestBase):
def test_given_option_pair_when_generate_then_basic_checks(self): def test_given_option_pair_when_generate_then_basic_checks(self):
if self.skip_long_tests: if self.skip_long_tests:
return return
num_options = len(options_to_include) num_options = len(options_to_include)
for option1_index in range(0, num_options): for option1_index in range(0, num_options):
for option2_index in range(option1_index + 1, num_options): for option2_index in range(option1_index + 1, num_options):

View File

@ -1,4 +1,4 @@
from typing import Dict, List from typing import Dict
import random import random
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
@ -9,7 +9,6 @@ from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_w
from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \ 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 assert_festivals_give_access_to_deluxe_scarecrow
from ..checks.world_checks import assert_same_number_items_locations, assert_victory_exists from ..checks.world_checks import assert_same_number_items_locations, assert_victory_exists
from ... import options
def get_option_choices(option) -> Dict[str, int]: def get_option_choices(option) -> Dict[str, int]:

View File

@ -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", options_to_exclude = ["profit_margin", "starting_money", "multiple_day_sleep_enabled", "multiple_day_sleep_cost",
"experience_multiplier", "friendship_multiplier", "debris_multiplier", "experience_multiplier", "friendship_multiplier", "debris_multiplier",
"quick_start", "gifting", "gift_tax"] "quick_start", "gifting", "gift_tax", "progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
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] options_to_include = [option for option_name, option in StardewValleyWorld.options_dataclass.type_hints.items()
if option_name not in options_to_exclude]

View File

@ -4,21 +4,21 @@ import random
import sys import sys
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.stardew_valley.test import setup_solo_multiworld from ...mods.mod_data import ModNames
from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase from .. import setup_solo_multiworld
from worlds.stardew_valley import options, locations, items, Group, ItemClassification, StardewOptions from ..TestOptions import basic_checks, SVTestBase
from worlds.stardew_valley.mods.mod_data import ModNames from ... import items, Group, ItemClassification
from worlds.stardew_valley.regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions
from worlds.stardew_valley.items import item_table, items_by_group from ...items import item_table, items_by_group
from worlds.stardew_valley.locations import location_table, LocationTags from ...locations import location_table
from worlds.stardew_valley.options import stardew_valley_option_classes, Mods, EntranceRandomization from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems
mod_list = ["DeepWoods", "Tractor Mod", "Bigger Backpack", all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack,
"Luck Skill", "Magic", "Socializing Skill", "Archaeology", ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology,
"Cooking Skill", "Binning Skill", "Juna - Roommate NPC", ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna,
"Professor Jasper Thomas", "Alec Revisited", "Custom NPC - Yoba", "Custom NPC Eugene", ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene,
"'Prophet' Wellwick", "Mister Ginger (cat npc)", "Shiko - New Custom NPC", "Delores - Custom NPC", ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores,
"Ayeisha - The Postal Worker (Custom NPC)", "Custom NPC - Riley", "Skull Cavern Elevator"] ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator})
def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): 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): class TestGenerateModsOptions(SVTestBase):
def test_given_single_mods_when_generate_then_basic_checks(self): 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}"): with self.subTest(f"Mod: {mod}"):
multi_world = setup_solo_multiworld({Mods: mod}) multi_world = setup_solo_multiworld({Mods: mod})
basic_checks(self, multi_world) basic_checks(self, multi_world)
check_stray_mod_items(mod, 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): def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self):
for option in EntranceRandomization.options: for option in EntranceRandomization.options:
for mod in mod_list: for mod in all_mods:
with self.subTest(f"entrance_randomization: {option}, Mod: {mod}"): with self.subTest(f"entrance_randomization: {option}, Mod: {mod}"):
multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod}) multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod})
basic_checks(self, multiworld) basic_checks(self, multiworld)
check_stray_mod_items(mod, 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): class TestBaseItemGeneration(SVTestBase):
options = { options = {
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
options.Mods.internal_name: mod_list Mods.internal_name: all_mods
} }
def test_all_progression_items_are_added_to_the_pool(self): def test_all_progression_items_are_added_to_the_pool(self):
@ -105,10 +78,10 @@ class TestBaseItemGeneration(SVTestBase):
class TestNoGingerIslandModItemGeneration(SVTestBase): class TestNoGingerIslandModItemGeneration(SVTestBase):
options = { options = {
options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, Friendsanity.internal_name: Friendsanity.option_all_with_marriage,
options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, SeasonRandomization.internal_name: SeasonRandomization.option_progressive,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
options.Mods.internal_name: mod_list Mods.internal_name: all_mods
} }
def test_all_progression_items_except_island_are_added_to_the_pool(self): 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): def test_mod_entrance_randomization(self):
for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN),
(options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION),
(options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]:
with self.subTest(option=option, flag=flag): with self.subTest(option=option, flag=flag):
seed = random.randrange(sys.maxsize) seed = random.randrange(sys.maxsize)
rand = random.Random(seed) rand = random.Random(seed)
world_options = StardewOptions({options.EntranceRandomization.internal_name: option, world_options = {EntranceRandomization.internal_name: option,
options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
options.Mods.internal_name: mod_list}) Mods.internal_name: all_mods}
final_regions = create_final_regions(world_options) multiworld = setup_solo_multiworld(world_options)
final_connections = create_final_connections(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} 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: for connection in final_connections:
if flag in connection.flag: if flag in connection.flag:
connection_in_randomized = connection.name in randomized_connections connection_in_randomized = connection.name in randomized_connections
reverse_in_randomized = connection.reverse in randomized_connections reverse_in_randomized = connection.reverse in randomized_connections
self.assertTrue(connection_in_randomized, 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, 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()), self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization. Seed = {seed}") f"Connections are duplicated in randomization. Seed = {seed}")
@ -164,12 +139,11 @@ class TestModEntranceRando(unittest.TestCase):
class TestModTraps(SVTestBase): class TestModTraps(SVTestBase):
def test_given_traps_when_generate_then_all_traps_in_pool(self): def test_given_traps_when_generate_then_all_traps_in_pool(self):
trap_option = options.TrapItems for value in TrapItems.options:
for value in trap_option.options:
if value == "no_traps": if value == "no_traps":
continue continue
world_options = self.allsanity_options_without_mods() 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) 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] 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()] multiworld_items = [item.name for item in multi_world.get_items()]

View File

@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial MultiWorld, Item, CollectionState, Entrance, Tutorial
from .logic import cs_to_zz_locs from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion 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, \ 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, \ loc_name_to_id as _loc_name_to_id, make_id_to_others, \
zz_reg_name_to_reg_name, base_id zz_reg_name_to_reg_name, base_id
@ -70,7 +70,9 @@ class ZillionWorld(World):
game = "Zillion" game = "Zillion"
web = ZillionWebWorld() web = ZillionWebWorld()
option_definitions = zillion_options options_dataclass = ZillionOptions
options: ZillionOptions
settings: typing.ClassVar[ZillionSettings] settings: typing.ClassVar[ZillionSettings]
topology_present = True # indicate if world type has any meaningful layout/pathing topology_present = True # indicate if world type has any meaningful layout/pathing
@ -142,7 +144,10 @@ class ZillionWorld(World):
if not hasattr(self.multiworld, "zillion_logic_cache"): if not hasattr(self.multiworld, "zillion_logic_cache"):
setattr(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 self._item_counts = item_counts
@ -299,7 +304,8 @@ class ZillionWorld(World):
elif start_char_counts["Champ"] > start_char_counts["Apple"]: elif start_char_counts["Champ"] > start_char_counts["Apple"]:
to_stay = "Champ" to_stay = "Champ"
else: # equal 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: for p, sc in players_start_chars:
if sc != to_stay: if sc != to_stay:

View File

@ -1,13 +1,14 @@
from collections import Counter from collections import Counter
# import logging from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, Tuple, cast from typing import Dict, Tuple
from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice from typing_extensions import TypeGuard # remove when Python >= 3.10
from Options import DefaultOnToggle, PerGameCommonOptions, Range, SpecialRange, Toggle, Choice
from zilliandomizer.options import \ from zilliandomizer.options import \
Options as ZzOptions, char_to_gun, char_to_jump, ID, \ Options as ZzOptions, char_to_gun, char_to_jump, ID, \
VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
from zilliandomizer.options.parsing import validate as zz_validate from zilliandomizer.options.parsing import validate as zz_validate
if TYPE_CHECKING:
from BaseClasses import MultiWorld
class ZillionContinues(SpecialRange): class ZillionContinues(SpecialRange):
@ -41,6 +42,19 @@ class VBLR(Choice):
option_restrictive = 3 option_restrictive = 3
default = 1 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): class ZillionGunLevels(VBLR):
""" """
@ -225,27 +239,27 @@ class ZillionRoomGen(Toggle):
display_name = "room generation" display_name = "room generation"
zillion_options: Dict[str, AssembleOptions] = { @dataclass
"continues": ZillionContinues, class ZillionOptions(PerGameCommonOptions):
"floppy_req": ZillionFloppyReq, continues: ZillionContinues
"gun_levels": ZillionGunLevels, floppy_req: ZillionFloppyReq
"jump_levels": ZillionJumpLevels, gun_levels: ZillionGunLevels
"randomize_alarms": ZillionRandomizeAlarms, jump_levels: ZillionJumpLevels
"max_level": ZillionMaxLevel, randomize_alarms: ZillionRandomizeAlarms
"start_char": ZillionStartChar, max_level: ZillionMaxLevel
"opas_per_level": ZillionOpasPerLevel, start_char: ZillionStartChar
"id_card_count": ZillionIDCardCount, opas_per_level: ZillionOpasPerLevel
"bread_count": ZillionBreadCount, id_card_count: ZillionIDCardCount
"opa_opa_count": ZillionOpaOpaCount, bread_count: ZillionBreadCount
"zillion_count": ZillionZillionCount, opa_opa_count: ZillionOpaOpaCount
"floppy_disk_count": ZillionFloppyDiskCount, zillion_count: ZillionZillionCount
"scope_count": ZillionScopeCount, floppy_disk_count: ZillionFloppyDiskCount
"red_id_card_count": ZillionRedIDCardCount, scope_count: ZillionScopeCount
"early_scope": ZillionEarlyScope, red_id_card_count: ZillionRedIDCardCount
"skill": ZillionSkill, early_scope: ZillionEarlyScope
"starting_cards": ZillionStartingCards, skill: ZillionSkill
"room_gen": ZillionRoomGen, starting_cards: ZillionStartingCards
} room_gen: ZillionRoomGen
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts: def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
@ -262,47 +276,34 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
return tr 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 adjusts options to make game completion possible
`world` parameter is MultiWorld object that has my options on it `options` parameter is ZillionOptions object that was put on my world by the core
`p` is my player id
""" """
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 = options.jump_levels.to_zz_vblr()
jump_option = jump_levels.current_key required_level = char_to_jump["Apple"][jump_option].index(3) + 1
required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1
if skill == 0: if skill == 0:
# because of hp logic on final boss # because of hp logic on final boss
required_level = 8 required_level = 8
gun_levels = cast(ZillionGunLevels, wo.gun_levels[p]) gun_option = options.gun_levels.to_zz_vblr()
gun_option = gun_levels.current_key guns_required = char_to_gun["Champ"][gun_option].index(3)
guns_required = char_to_gun["Champ"][cast(ZzVBLR, 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({ item_counts = Counter({
"ID Card": card, "ID Card": options.id_card_count,
"Bread": bread, "Bread": options.bread_count,
"Opa-Opa": opa, "Opa-Opa": options.opa_opa_count,
"Zillion": gun, "Zillion": options.zillion_count,
"Floppy Disk": floppy, "Floppy Disk": options.floppy_disk_count,
"Scope": scope, "Scope": options.scope_count,
"Red ID Card": red "Red ID Card": options.red_id_card_count
}) })
minimums = Counter({ minimums = Counter({
"ID Card": 0, "ID Card": 0,
@ -335,10 +336,10 @@ def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
item_counts["Empty"] += diff item_counts["Empty"] += diff
assert sum(item_counts.values()) == 144 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) 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): while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
# logging.warning( # logging.warning(
# "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count" # "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 # that should be all of the level requirements met
name_capitalization = { name_capitalization: Dict[str, Chars] = {
"jj": "JJ", "jj": "JJ",
"apple": "Apple", "apple": "Apple",
"champ": "Champ", "champ": "Champ",
} }
start_char = cast(ZillionStartChar, wo.start_char[p]) start_char = options.start_char
start_char_name = name_capitalization[start_char.current_key] start_char_name = name_capitalization[start_char.current_key]
assert start_char_name in chars assert start_char_name in chars
start_char_name = cast(Chars, start_char_name)
starting_cards = cast(ZillionStartingCards, wo.starting_cards[p]) starting_cards = options.starting_cards
room_gen = cast(ZillionRoomGen, wo.room_gen[p]) room_gen = options.room_gen
early_scope = cast(ZillionEarlyScope, wo.early_scope[p])
if early_scope:
world.early_items[p]["Scope"] = 1
zz_item_counts = convert_item_counts(item_counts) zz_item_counts = convert_item_counts(item_counts)
zz_op = ZzOptions( zz_op = ZzOptions(
zz_item_counts, zz_item_counts,
cast(ZzVBLR, jump_option), jump_option,
cast(ZzVBLR, gun_option), gun_option,
opas_per_level.value, opas_per_level.value,
max_level.value, max_level.value,
False, # tutorial False, # tutorial
skill, skill,
start_char_name, start_char_name,
floppy_req.value, floppy_req.value,
wo.continues[p].value, options.continues.value,
wo.randomize_alarms[p].value, bool(options.randomize_alarms.value),
False, # early scope is done with AP early_items API bool(options.early_scope.value),
True, # balance defense True, # balance defense
starting_cards.value, starting_cards.value,
bool(room_gen.value) bool(room_gen.value)

View File

@ -1 +1,2 @@
zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4 zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@d7122bcbeda40da5db26d60fad06246a1331706f#0.5.4
typing-extensions>=4.7, <5

View File

@ -1,6 +1,6 @@
from . import ZillionTestBase 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 from zilliandomizer.options import VBLR_CHOICES
@ -9,7 +9,9 @@ class OptionsTest(ZillionTestBase):
def test_validate_default(self) -> None: def test_validate_default(self) -> None:
self.world_setup() 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: def test_vblr_ap_to_zz(self) -> None:
""" all of the valid values for the AP options map to valid values for ZZ options """ """ all of the valid values for the AP options map to valid values for ZZ options """
@ -20,7 +22,9 @@ class OptionsTest(ZillionTestBase):
for value in vblr_class.name_lookup.values(): for value in vblr_class.name_lookup.values():
self.options = {option_name: value} self.options = {option_name: value}
self.world_setup() 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 assert getattr(zz_options, option_name) in VBLR_CHOICES
# TODO: test validate with invalid combinations of options # TODO: test validate with invalid combinations of options