From 93617fa54609108fe115cf549a124ff444961548 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Tue, 2 Jul 2024 23:59:26 +0200 Subject: [PATCH 01/78] The Witness: mypy compliance (#3112) * Make witness apworld mostly pass mypy * Fix all remaining mypy errors except the core ones * I'm a goofy stupid poopoo head * Two more fixes * ruff after merge * Mypy for new stuff * Oops * Stricter ruff rules (that I already comply with :3) * Deprecated ruff thing * wait no i lied * lol super nevermind * I can actually be slightly more specific * lint --- worlds/witness/__init__.py | 28 +-- worlds/witness/data/static_items.py | 12 +- worlds/witness/data/static_locations.py | 14 +- worlds/witness/data/static_logic.py | 68 +++---- worlds/witness/data/utils.py | 30 +-- worlds/witness/hints.py | 73 ++++--- worlds/witness/locations.py | 6 +- worlds/witness/player_items.py | 25 ++- worlds/witness/player_logic.py | 246 ++++++++++++------------ worlds/witness/regions.py | 40 ++-- worlds/witness/ruff.toml | 6 +- worlds/witness/rules.py | 20 +- 12 files changed, 299 insertions(+), 269 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index ecab25db3d71..455c87d8e0d1 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -11,11 +11,12 @@ from worlds.AutoWorld import WebWorld, World from .data import static_items as static_witness_items +from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemData from .data.utils import get_audio_logs from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints -from .locations import WitnessPlayerLocations, static_witness_locations +from .locations import WitnessPlayerLocations from .options import TheWitnessOptions, witness_option_groups from .player_items import WitnessItem, WitnessPlayerItems from .player_logic import WitnessPlayerLogic @@ -53,7 +54,8 @@ class WitnessWorld(World): options: TheWitnessOptions item_name_to_id = { - name: data.ap_code for name, data in static_witness_items.ITEM_DATA.items() + # ITEM_DATA doesn't have any event items in it + name: cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() } location_name_to_id = static_witness_locations.ALL_LOCATIONS_TO_ID item_name_groups = static_witness_items.ITEM_GROUPS @@ -142,7 +144,7 @@ def generate_early(self) -> None: ) self.player_regions: WitnessPlayerRegions = WitnessPlayerRegions(self.player_locations, self) - self.log_ids_to_hints = dict() + self.log_ids_to_hints = {} self.determine_sufficient_progression() @@ -279,7 +281,7 @@ def create_items(self) -> None: remaining_item_slots = pool_size - sum(item_pool.values()) # Add puzzle skips. - num_puzzle_skips = self.options.puzzle_skip_amount + num_puzzle_skips = self.options.puzzle_skip_amount.value if num_puzzle_skips > remaining_item_slots: warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations" @@ -301,21 +303,21 @@ def create_items(self) -> None: if self.player_items.item_data[item_name].local_only: self.options.local_items.value.add(item_name) - def fill_slot_data(self) -> dict: - self.log_ids_to_hints: Dict[int, CompactItemData] = dict() - self.laser_ids_to_hints: Dict[int, CompactItemData] = dict() + def fill_slot_data(self) -> Dict[str, Any]: + self.log_ids_to_hints: Dict[int, CompactItemData] = {} + self.laser_ids_to_hints: Dict[int, CompactItemData] = {} already_hinted_locations = set() # Laser hints if self.options.laser_hints: - laser_hints = make_laser_hints(self, static_witness_items.ITEM_GROUPS["Lasers"]) + laser_hints = make_laser_hints(self, sorted(static_witness_items.ITEM_GROUPS["Lasers"])) for item_name, hint in laser_hints.items(): item_def = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]) self.laser_ids_to_hints[int(item_def.panel_id_hexes[0], 16)] = make_compact_hint_data(hint, self.player) - already_hinted_locations.add(hint.location) + already_hinted_locations.add(cast(Location, hint.location)) # Audio Log Hints @@ -378,13 +380,13 @@ class WitnessLocation(Location): game: str = "The Witness" entity_hex: int = -1 - def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1) -> None: + def __init__(self, player: int, name: str, address: Optional[int], parent: Region, ch_hex: int = -1) -> None: super().__init__(player, name, address, parent) self.entity_hex = ch_hex def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlayerLocations, - region_locations=None, exits=None) -> Region: + region_locations: Optional[List[str]] = None, exits: Optional[List[str]] = None) -> Region: """ Create an Archipelago Region for The Witness """ @@ -399,11 +401,11 @@ def create_region(world: WitnessWorld, name: str, player_locations: WitnessPlaye entity_hex = int( static_witness_logic.ENTITIES_BY_NAME[location]["entity_hex"], 0 ) - location = WitnessLocation( + location_obj = WitnessLocation( world.player, location, loc_id, ret, entity_hex ) - ret.locations.append(location) + ret.locations.append(location_obj) if exits: for single_exit in exits: ret.exits.append(Entrance(world.player, single_exit, ret)) diff --git a/worlds/witness/data/static_items.py b/worlds/witness/data/static_items.py index 8eb889f8203a..b0d8fc3c4f6e 100644 --- a/worlds/witness/data/static_items.py +++ b/worlds/witness/data/static_items.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Set from BaseClasses import ItemClassification @@ -7,7 +7,7 @@ from .static_locations import ID_START ITEM_DATA: Dict[str, ItemData] = {} -ITEM_GROUPS: Dict[str, List[str]] = {} +ITEM_GROUPS: Dict[str, Set[str]] = {} # Useful items that are treated specially at generation time and should not be automatically added to the player's # item list during get_progression_items. @@ -22,13 +22,13 @@ def populate_items() -> None: if definition.category is ItemCategory.SYMBOL: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Symbols", []).append(item_name) + ITEM_GROUPS.setdefault("Symbols", set()).add(item_name) elif definition.category is ItemCategory.DOOR: classification = ItemClassification.progression - ITEM_GROUPS.setdefault("Doors", []).append(item_name) + ITEM_GROUPS.setdefault("Doors", set()).add(item_name) elif definition.category is ItemCategory.LASER: classification = ItemClassification.progression_skip_balancing - ITEM_GROUPS.setdefault("Lasers", []).append(item_name) + ITEM_GROUPS.setdefault("Lasers", set()).add(item_name) elif definition.category is ItemCategory.USEFUL: classification = ItemClassification.useful elif definition.category is ItemCategory.FILLER: @@ -47,7 +47,7 @@ def populate_items() -> None: def get_item_to_door_mappings() -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} for item_name, item_data in ITEM_DATA.items(): - if not isinstance(item_data.definition, DoorItemDefinition): + if not isinstance(item_data.definition, DoorItemDefinition) or item_data.ap_code is None: continue output[item_data.ap_code] = [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] return output diff --git a/worlds/witness/data/static_locations.py b/worlds/witness/data/static_locations.py index e11544235ffc..de321d20c0f9 100644 --- a/worlds/witness/data/static_locations.py +++ b/worlds/witness/data/static_locations.py @@ -1,3 +1,5 @@ +from typing import Dict, Set, cast + from . import static_logic as static_witness_logic ID_START = 158000 @@ -441,17 +443,17 @@ "Town Obelisk Side 6", } -ALL_LOCATIONS_TO_ID = dict() +ALL_LOCATIONS_TO_ID: Dict[str, int] = {} -AREA_LOCATION_GROUPS = dict() +AREA_LOCATION_GROUPS: Dict[str, Set[str]] = {} -def get_id(entity_hex: str) -> str: +def get_id(entity_hex: str) -> int: """ Calculates the location ID for any given location """ - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"] + return cast(int, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["id"]) def get_event_name(entity_hex: str) -> str: @@ -461,7 +463,7 @@ def get_event_name(entity_hex: str) -> str: action = " Opened" if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "Door" else " Solved" - return static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"] + action + return cast(str, static_witness_logic.ENTITIES_BY_HEX[entity_hex]["checkName"]) + action ALL_LOCATIONS_TO_IDS = { @@ -479,4 +481,4 @@ def get_event_name(entity_hex: str) -> str: for loc in ALL_LOCATIONS_TO_IDS: area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"] - AREA_LOCATION_GROUPS.setdefault(area, []).append(loc) + AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc) diff --git a/worlds/witness/data/static_logic.py b/worlds/witness/data/static_logic.py index ecd95ea6c0fa..a9175c0c30b3 100644 --- a/worlds/witness/data/static_logic.py +++ b/worlds/witness/data/static_logic.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import Dict, List, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from Utils import cache_argsless @@ -24,13 +24,37 @@ class StaticWitnessLogicObj: - def read_logic_file(self, lines) -> None: + def __init__(self, lines: Optional[List[str]] = None) -> None: + if lines is None: + lines = get_sigma_normal_logic() + + # All regions with a list of panels in them and the connections to other regions, before logic adjustments + self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set)) + self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {} + + self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {} + self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {} + self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = {} + + self.OBELISK_SIDE_ID_TO_EP_HEXES: Dict[int, Set[int]] = {} + + self.EP_TO_OBELISK_SIDE: Dict[str, str] = {} + + self.ENTITY_ID_TO_NAME: Dict[str, str] = {} + + self.read_logic_file(lines) + self.reverse_connections() + self.combine_connections() + + def read_logic_file(self, lines: List[str]) -> None: """ Reads the logic file and does the initial population of data structures """ - current_region = dict() - current_area = { + current_region = {} + current_area: Dict[str, Any] = { "name": "Misc", "regions": [], } @@ -155,7 +179,7 @@ def read_logic_file(self, lines) -> None: current_region["entities"].append(entity_hex) current_region["physical_entities"].append(entity_hex) - def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]): + def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None: target = connection[0] traversal_options = connection[1] @@ -169,13 +193,13 @@ def reverse_connection(self, source_region: str, connection: Tuple[str, Set[Witn if remaining_options: self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options)) - def reverse_connections(self): + def reverse_connections(self) -> None: # Iterate all connections for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()): for connection in connections.items(): self.reverse_connection(region_name, connection) - def combine_connections(self): + def combine_connections(self) -> None: # All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice. self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME} @@ -184,30 +208,6 @@ def combine_connections(self): combined_req = logical_or_witness_rules(requirement) self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req)) - def __init__(self, lines=None) -> None: - if lines is None: - lines = get_sigma_normal_logic() - - # All regions with a list of panels in them and the connections to other regions, before logic adjustments - self.ALL_REGIONS_BY_NAME = dict() - self.ALL_AREAS_BY_NAME = dict() - self.CONNECTIONS_WITH_DUPLICATES = defaultdict(lambda: defaultdict(lambda: set())) - self.STATIC_CONNECTIONS_BY_REGION_NAME = dict() - - self.ENTITIES_BY_HEX = dict() - self.ENTITIES_BY_NAME = dict() - self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX = dict() - - self.OBELISK_SIDE_ID_TO_EP_HEXES = dict() - - self.EP_TO_OBELISK_SIDE = dict() - - self.ENTITY_ID_TO_NAME = dict() - - self.read_logic_file(lines) - self.reverse_connections() - self.combine_connections() - # Item data parsed from WitnessItems.txt ALL_ITEMS: Dict[str, ItemDefinition] = {} @@ -276,12 +276,12 @@ def get_sigma_expert() -> StaticWitnessLogicObj: return StaticWitnessLogicObj(get_sigma_expert_logic()) -def __getattr__(name): +def __getattr__(name: str) -> StaticWitnessLogicObj: if name == "vanilla": return get_vanilla() - elif name == "sigma_normal": + if name == "sigma_normal": return get_sigma_normal() - elif name == "sigma_expert": + if name == "sigma_expert": return get_sigma_expert() raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/worlds/witness/data/utils.py b/worlds/witness/data/utils.py index 2934308df3ec..f89aaf7d3e18 100644 --- a/worlds/witness/data/utils.py +++ b/worlds/witness/data/utils.py @@ -1,7 +1,9 @@ from math import floor from pkgutil import get_data -from random import random -from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple +from random import Random +from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple, TypeVar + +T = TypeVar("T") # A WitnessRule is just an or-chain of and-conditions. # It represents the set of all options that could fulfill this requirement. @@ -11,9 +13,9 @@ WitnessRule = FrozenSet[FrozenSet[str]] -def weighted_sample(world_random: random, population: List, weights: List[float], k: int) -> List: +def weighted_sample(world_random: Random, population: List[T], weights: List[float], k: int) -> List[T]: positions = range(len(population)) - indices = [] + indices: List[int] = [] while True: needed = k - len(indices) if not needed: @@ -82,13 +84,13 @@ def define_new_region(region_string: str) -> Tuple[Dict[str, Any], Set[Tuple[str region_obj = { "name": region_name, "shortName": region_name_simple, - "entities": list(), - "physical_entities": list(), + "entities": [], + "physical_entities": [], } return region_obj, options -def parse_lambda(lambda_string) -> WitnessRule: +def parse_lambda(lambda_string: str) -> WitnessRule: """ Turns a lambda String literal like this: a | b & c into a set of sets like this: {{a}, {b, c}} @@ -97,18 +99,18 @@ def parse_lambda(lambda_string) -> WitnessRule: if lambda_string == "True": return frozenset([frozenset()]) split_ands = set(lambda_string.split(" | ")) - lambda_set = frozenset({frozenset(a.split(" & ")) for a in split_ands}) - - return lambda_set + return frozenset({frozenset(a.split(" & ")) for a in split_ands}) -_adjustment_file_cache = dict() +_adjustment_file_cache = {} def get_adjustment_file(adjustment_file: str) -> List[str]: if adjustment_file not in _adjustment_file_cache: - data = get_data(__name__, adjustment_file).decode("utf-8") - _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.split("\n")] + data = get_data(__name__, adjustment_file) + if data is None: + raise FileNotFoundError(f"Could not find {adjustment_file}") + _adjustment_file_cache[adjustment_file] = [line.strip() for line in data.decode("utf-8").split("\n")] return _adjustment_file_cache[adjustment_file] @@ -237,7 +239,7 @@ def logical_and_witness_rules(witness_rules: Iterable[WitnessRule]) -> WitnessRu A logical formula might look like this: {{a, b}, {c, d}}, which would mean "a & b | c & d". These can be easily and-ed by just using the boolean distributive law: (a | b) & c = a & c | a & b. """ - current_overall_requirement = frozenset({frozenset()}) + current_overall_requirement: FrozenSet[FrozenSet[str]] = frozenset({frozenset()}) for next_dnf_requirement in witness_rules: new_requirement: Set[FrozenSet[str]] = set() diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 535a36e13b6f..a1ca1b081d3c 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -1,11 +1,12 @@ import logging from dataclasses import dataclass -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from .data import static_logic as static_witness_logic from .data.utils import weighted_sample +from .player_items import WitnessItem if TYPE_CHECKING: from . import WitnessWorld @@ -22,7 +23,9 @@ class WitnessLocationHint: def __hash__(self) -> int: return hash(self.location) - def __eq__(self, other) -> bool: + def __eq__(self, other: Any) -> bool: + if not isinstance(other, WitnessLocationHint): + return False return self.location == other.location @@ -171,9 +174,13 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item - item_name = item.name - if item.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item.player) + ")" + + item_name = "Nothing" + if item is not None: + item_name = item.name + + if item.player != world.player: + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" if hint.hint_came_from_location: hint_text = f"{location_name} contains {item_name}." @@ -183,14 +190,17 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes return WitnessWordedHint(hint_text, hint.location) -def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]: - def get_real_location(multiworld: MultiWorld, location: Location): +def hint_from_item(world: "WitnessWorld", item_name: str, + own_itempool: List["WitnessItem"]) -> Optional[WitnessLocationHint]: + def get_real_location(multiworld: MultiWorld, location: Location) -> Location: """If this location is from an item_link pseudo-world, get the location that the item_link item is on. Return the original location otherwise / as a fallback.""" if location.player not in world.multiworld.groups: return location try: + if not location.item: + return location return multiworld.find_item(location.item.name, location.player) except StopIteration: return location @@ -209,17 +219,11 @@ def get_real_location(multiworld: MultiWorld, location: Location): def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]: - location_obj = world.get_location(location) - item_obj = location_obj.item - item_name = item_obj.name - if item_obj.player != world.player: - item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")" - - return WitnessLocationHint(location_obj, True) + return WitnessLocationHint(world.get_location(location), True) def get_items_and_locations_in_random_order(world: "WitnessWorld", - own_itempool: List[Item]) -> Tuple[List[str], List[str]]: + own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]: prog_items_in_this_world = sorted( item.name for item in own_itempool if item.advancement and item.code and item.location @@ -235,7 +239,7 @@ def get_items_and_locations_in_random_order(world: "WitnessWorld", return prog_items_in_this_world, locations_in_this_world -def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item], +def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location] ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]: prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) @@ -282,14 +286,14 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Ite return always_hints, priority_hints -def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item], +def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"], already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint], unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]: prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool) next_random_hint_is_location = world.random.randrange(0, 2) - hints = [] + hints: List[WitnessWordedHint] = [] # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a] area_reverse_lookup = { @@ -304,6 +308,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp logging.warning(f"Ran out of items/locations to hint for player {player_name}.") break + location_hint: Optional[WitnessLocationHint] if hints_to_use_first: location_hint = hints_to_use_first.pop() elif next_random_hint_is_location and locations_in_this_world: @@ -317,7 +322,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp next_random_hint_is_location = not next_random_hint_is_location continue - if not location_hint or location_hint.location in already_hinted_locations: + if location_hint is None or location_hint.location in already_hinted_locations: continue # Don't hint locations in areas that are almost fully hinted out already @@ -344,8 +349,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st When this happens, they are made less likely to receive an area hint. """ - unhinted_locations_per_area = dict() - unhinted_location_percentage_per_area = dict() + unhinted_locations_per_area = {} + unhinted_location_percentage_per_area = {} for area_name, locations in locations_per_area.items(): not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations) @@ -368,8 +373,8 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]: potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys()) - locations_per_area = dict() - items_per_area = dict() + locations_per_area = {} + items_per_area = {} for area in potential_areas: regions = [ @@ -533,7 +538,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int, location_hints_created_in_round_1 = len(generated_hints) - unhinted_locations_per_area: Dict[str, Set[Location]] = dict() + unhinted_locations_per_area: Dict[str, Set[Location]] = {} # Then, make area hints. if area_hints: @@ -584,17 +589,29 @@ def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> location = hint.location area_amount = hint.area_amount - # None if junk hint, address if location hint, area string if area hint - arg_1 = location.address if location else (hint.area if hint.area else None) + # -1 if junk hint, address if location hint, area string if area hint + arg_1: Union[str, int] + if location and location.address is not None: + arg_1 = location.address + elif hint.area is not None: + arg_1 = hint.area + else: + arg_1 = -1 # self.player if junk hint, player if location hint, progression amount if area hint - arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number) + arg_2: int + if area_amount is not None: + arg_2 = area_amount + elif location is not None: + arg_2 = location.player + else: + arg_2 = local_player_number return hint.wording, arg_1, arg_2 def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]: - laser_hints_by_name = dict() + laser_hints_by_name = {} for item_name in laser_names: location_hint = hint_from_item(world, item_name, world.own_itempool) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index df8214ac9221..1796f051b896 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -61,9 +61,7 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1]) ) - event_locations = { - p for p in player_logic.USED_EVENT_NAMES_BY_HEX - } + event_locations = set(player_logic.USED_EVENT_NAMES_BY_HEX) self.EVENT_LOCATION_TABLE = { static_witness_locations.get_event_name(entity_hex): None @@ -80,5 +78,5 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> N def add_location_late(self, entity_name: str) -> None: entity_hex = static_witness_logic.ENTITIES_BY_NAME[entity_name]["entity_hex"] - self.CHECK_LOCATION_TABLE[entity_hex] = entity_name + self.CHECK_LOCATION_TABLE[entity_hex] = static_witness_locations.get_id(entity_hex) self.CHECK_PANELHEX_TO_ID[entity_hex] = static_witness_locations.get_id(entity_hex) diff --git a/worlds/witness/player_items.py b/worlds/witness/player_items.py index 627e5acccb90..718fd7d172ba 100644 --- a/worlds/witness/player_items.py +++ b/worlds/witness/player_items.py @@ -2,7 +2,7 @@ Defines progression, junk and event items for The Witness """ import copy -from typing import TYPE_CHECKING, Dict, List, Set +from typing import TYPE_CHECKING, Dict, List, Set, cast from BaseClasses import Item, ItemClassification, MultiWorld @@ -87,7 +87,8 @@ def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic, if data.classification == ItemClassification.useful}.items(): if item_name in static_witness_items._special_usefuls: continue - elif item_name == "Energy Capacity": + + if item_name == "Energy Capacity": self._mandatory_items[item_name] = NUM_ENERGY_UPGRADES elif isinstance(item_data.classification, ProgressiveItemDefinition): self._mandatory_items[item_name] = len(item_data.mappings) @@ -184,15 +185,16 @@ def get_early_items(self) -> List[str]: output -= {item for item, weight in inner_item.items() if weight} # Sort the output for consistency across versions if the implementation changes but the logic does not. - return sorted(list(output)) + return sorted(output) def get_door_ids_in_pool(self) -> List[int]: """ Returns the total set of all door IDs that are controlled by items in the pool. """ output: List[int] = [] - for item_name, item_data in {name: data for name, data in self.item_data.items() - if isinstance(data.definition, DoorItemDefinition)}.items(): + for item_name, item_data in dict(self.item_data.items()).items(): + if not isinstance(item_data.definition, DoorItemDefinition): + continue output += [int(hex_string, 16) for hex_string in item_data.definition.panel_id_hexes] return output @@ -201,18 +203,21 @@ def get_symbol_ids_not_in_pool(self) -> List[int]: """ Returns the item IDs of symbol items that were defined in the configuration file but are not in the pool. """ - return [data.ap_code for name, data in static_witness_items.ITEM_DATA.items() - if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL] + return [ + # data.ap_code is guaranteed for a symbol definition + cast(int, data.ap_code) for name, data in static_witness_items.ITEM_DATA.items() + if name not in self.item_data.keys() and data.definition.category is ItemCategory.SYMBOL + ] def get_progressive_item_ids_in_pool(self) -> Dict[int, List[int]]: output: Dict[int, List[int]] = {} - for item_name, quantity in {name: quantity for name, quantity in self._mandatory_items.items()}.items(): + for item_name, quantity in dict(self._mandatory_items.items()).items(): item = self.item_data[item_name] if isinstance(item.definition, ProgressiveItemDefinition): # Note: we need to reference the static table here rather than the player-specific one because the child # items were removed from the pool when we pruned out all progression items not in the settings. - output[item.ap_code] = [static_witness_items.ITEM_DATA[child_item].ap_code - for child_item in item.definition.child_item_names] + output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code) + for child_item in item.definition.child_item_names] return output diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 05b3cf3a98e4..b62c59b00ae1 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ from .data import static_logic as static_witness_logic from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition +from .data.static_logic import StaticWitnessLogicObj from .data.utils import ( WitnessRule, define_new_region, @@ -58,6 +59,95 @@ class WitnessPlayerLogic: """WITNESS LOGIC CLASS""" + VICTORY_LOCATION: str + + def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: + self.YAML_DISABLED_LOCATIONS: Set[str] = disabled_locations + self.YAML_ADDED_ITEMS: Dict[str, int] = start_inv + + self.EVENT_PANELS_FROM_PANELS: Set[str] = set() + self.EVENT_PANELS_FROM_REGIONS: Set[str] = set() + + self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: Set[str] = set() + + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY: Set[str] = set() + + self.UNREACHABLE_REGIONS: Set[str] = set() + + self.THEORETICAL_ITEMS: Set[str] = set() + self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set() + self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1) + self.MULTI_LISTS: Dict[str, List[str]] = {} + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set() + self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set() + self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} + self.STARTING_INVENTORY: Set[str] = set() + + self.DIFFICULTY = world.options.puzzle_randomization + + self.REFERENCE_LOGIC: StaticWitnessLogicObj + if self.DIFFICULTY == "sigma_expert": + self.REFERENCE_LOGIC = static_witness_logic.sigma_expert + elif self.DIFFICULTY == "none": + self.REFERENCE_LOGIC = static_witness_logic.vanilla + else: + self.REFERENCE_LOGIC = static_witness_logic.sigma_normal + + self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME + ) + self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy( + self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX + ) + self.REQUIREMENTS_BY_HEX: Dict[str, WitnessRule] = {} + + self.EVENT_ITEM_PAIRS: Dict[str, str] = {} + self.COMPLETELY_DISABLED_ENTITIES: Set[str] = set() + self.DISABLE_EVERYTHING_BEHIND: Set[str] = set() + self.PRECOMPLETED_LOCATIONS: Set[str] = set() + self.EXCLUDED_LOCATIONS: Set[str] = set() + self.ADDED_CHECKS: Set[str] = set() + self.VICTORY_LOCATION = "0x0356B" + + self.ALWAYS_EVENT_NAMES_BY_HEX = { + "0x00509": "+1 Laser (Symmetry Laser)", + "0x012FB": "+1 Laser (Desert Laser)", + "0x09F98": "Desert Laser Redirection", + "0x01539": "+1 Laser (Quarry Laser)", + "0x181B3": "+1 Laser (Shadows Laser)", + "0x014BB": "+1 Laser (Keep Laser)", + "0x17C65": "+1 Laser (Monastery Laser)", + "0x032F9": "+1 Laser (Town Laser)", + "0x00274": "+1 Laser (Jungle Laser)", + "0x0C2B2": "+1 Laser (Bunker Laser)", + "0x00BF6": "+1 Laser (Swamp Laser)", + "0x028A4": "+1 Laser (Treehouse Laser)", + "0x17C34": "Mountain Entry", + "0xFFF00": "Bottom Floor Discard Turns On", + } + + self.USED_EVENT_NAMES_BY_HEX: Dict[str, str] = {} + self.CONDITIONAL_EVENTS: Dict[Tuple[str, str], str] = {} + + # The basic requirements to solve each entity come from StaticWitnessLogic. + # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. + self.make_options_adjustments(world) + self.determine_unrequired_entities(world) + self.find_unsolvable_entities(world) + + # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. + # This will make the access conditions way faster, instead of recursively checking dependent entities each time. + self.make_dependency_reduced_checklist() + + # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. + self.finalize_items() + + # Create event-item pairs for specific panels in the game. + self.make_event_panel_lists() + def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: """ Panels in this game often only turn on when other panels are solved. @@ -77,9 +167,9 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # For the requirement of an entity, we consider two things: # 1. Any items this entity needs (e.g. Symbols or Door Items) - these_items = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) + these_items: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex].get("items", frozenset({frozenset()})) # 2. Any entities that this entity depends on (e.g. one panel powering on the next panel in a set) - these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] + these_panels: WitnessRule = self.DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex]["entities"] # Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off) these_items = frozenset({ @@ -91,47 +181,49 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: for subset in these_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset) - # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. - # Also, remove any original power requirements this entity might have had. + # Handle door entities (door shuffle) if entity_hex in self.DOOR_ITEMS_BY_ID: + # If this entity is opened by a door item that exists in the itempool, add that item to its requirements. door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]}) for dependent_item in door_items: self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item) - all_options = logical_and_witness_rules([door_items, these_items]) + these_items = logical_and_witness_rules([door_items, these_items]) - # If this entity is not an EP, and it has an associated door item, ignore the original power dependencies - if static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP": + # A door entity is opened by its door item instead of previous entities powering it. + # That means we need to ignore any dependent requirements. + # However, there are some entities that depend on other entities because of an environmental reason. + # Those requirements need to be preserved even in door shuffle. + entity_dependencies_need_to_be_preserved = ( + # EPs keep all their entity dependencies + static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP" # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies. - if entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels): - these_items = all_options - + or entity_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels) # Another dependency that is not power-based: The Symmetry Island Upper Panel latches - elif entity_hex == "0x1C349": - these_items = all_options - - else: - return frozenset(all_options) + or entity_hex == "0x1C349" + ) - else: - these_items = all_options + # If this is not one of those special cases, solving this door entity only needs its own item requirement. + # Dependent entities from these_panels are ignored, and we just return these_items directly. + if not entity_dependencies_need_to_be_preserved: + return these_items # Now that we have item requirements and entity dependencies, it's time for the dependency reduction. # For each entity that this entity depends on (e.g. a panel turning on another panel), # Add that entities requirements to this entity. # If there are multiple options, consider each, and then or-chain them. - all_options = list() + all_options = [] for option in these_panels: - dependent_items_for_option = frozenset({frozenset()}) + dependent_items_for_option: WitnessRule = frozenset({frozenset()}) # For each entity in this option, resolve it to its actual requirement. for option_entity in option: - dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) + dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity, {}) if option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", "PP2 Weirdness", "Theater to Tunnels"}: @@ -525,13 +617,16 @@ def make_options_adjustments(self, world: "WitnessWorld") -> None: current_adjustment_type = line[:-1] continue + if current_adjustment_type is None: + raise ValueError(f"Adjustment lineset {adjustment_lineset} is malformed") + self.make_single_adjustment(current_adjustment_type, line) for entity_id in self.COMPLETELY_DISABLED_ENTITIES: if entity_id in self.DOOR_ITEMS_BY_ID: del self.DOOR_ITEMS_BY_ID[entity_id] - def discover_reachable_regions(self): + def discover_reachable_regions(self) -> Set[str]: """ Some options disable panels or remove specific items. This can make entire regions completely unreachable, because all their incoming connections are invalid. @@ -640,7 +735,7 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> # Check each traversal option individually for option in connection[1]: - individual_entity_requirements = [] + individual_entity_requirements: List[WitnessRule] = [] for entity in option: # If a connection requires solving a disabled entity, it is not valid. if not self.solvability_guaranteed(entity) or entity in self.DISABLE_EVERYTHING_BEHIND: @@ -664,7 +759,7 @@ def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> return logical_or_witness_rules(all_possibilities) - def make_dependency_reduced_checklist(self): + def make_dependency_reduced_checklist(self) -> None: """ Every entity has a requirement. This requirement may involve other entities. Example: Solving a panel powers a cable, and that cable turns on the next panel. @@ -679,12 +774,12 @@ def make_dependency_reduced_checklist(self): # Requirements are cached per entity. However, we might redo the whole reduction process multiple times. # So, we first clear this cache. - self.REQUIREMENTS_BY_HEX = dict() + self.REQUIREMENTS_BY_HEX = {} # We also clear any data structures that we might have filled in a previous dependency reduction - self.REQUIREMENTS_BY_HEX = dict() - self.USED_EVENT_NAMES_BY_HEX = dict() - self.CONNECTIONS_BY_REGION_NAME = dict() + self.REQUIREMENTS_BY_HEX = {} + self.USED_EVENT_NAMES_BY_HEX = {} + self.CONNECTIONS_BY_REGION_NAME = {} self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() # Make independent requirements for entities @@ -695,22 +790,18 @@ def make_dependency_reduced_checklist(self): # Make independent region connection requirements based on the entities they require for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items(): - self.CONNECTIONS_BY_REGION_NAME[region] = [] - - new_connections = [] + new_connections = set() for connection in connections: overall_requirement = self.reduce_connection_requirement(connection) # If there is a way to use this connection, add it. if overall_requirement: - new_connections.append((connection[0], overall_requirement)) + new_connections.add((connection[0], overall_requirement)) - # If there are any usable outgoing connections from this region, add them. - if new_connections: - self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + self.CONNECTIONS_BY_REGION_NAME[region] = new_connections - def finalize_items(self): + def finalize_items(self) -> None: """ Finalise which items are used in the world, and handle their progressive versions. """ @@ -808,8 +899,7 @@ def make_event_item_pair(self, entity_hex: str) -> Tuple[str, str]: if entity_hex not in self.USED_EVENT_NAMES_BY_HEX: warning(f'Entity "{name}" does not have an associated event name.') self.USED_EVENT_NAMES_BY_HEX[entity_hex] = name + " Event" - pair = (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) - return pair + return (name, self.USED_EVENT_NAMES_BY_HEX[entity_hex]) def make_event_panel_lists(self) -> None: """ @@ -828,85 +918,3 @@ def make_event_panel_lists(self) -> None: for panel in self.USED_EVENT_NAMES_BY_HEX: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] - - def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_inv: Dict[str, int]) -> None: - self.YAML_DISABLED_LOCATIONS = disabled_locations - self.YAML_ADDED_ITEMS = start_inv - - self.EVENT_PANELS_FROM_PANELS = set() - self.EVENT_PANELS_FROM_REGIONS = set() - - self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() - - self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() - - self.UNREACHABLE_REGIONS = set() - - self.THEORETICAL_ITEMS = set() - self.THEORETICAL_ITEMS_NO_MULTI = set() - self.MULTI_AMOUNTS = defaultdict(lambda: 1) - self.MULTI_LISTS = dict() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set() - self.PROG_ITEMS_ACTUALLY_IN_THE_GAME = set() - self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {} - self.STARTING_INVENTORY = set() - - self.DIFFICULTY = world.options.puzzle_randomization - - if self.DIFFICULTY == "sigma_normal": - self.REFERENCE_LOGIC = static_witness_logic.sigma_normal - elif self.DIFFICULTY == "sigma_expert": - self.REFERENCE_LOGIC = static_witness_logic.sigma_expert - elif self.DIFFICULTY == "none": - self.REFERENCE_LOGIC = static_witness_logic.vanilla - - self.CONNECTIONS_BY_REGION_NAME_THEORETICAL = copy.deepcopy( - self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME - ) - self.CONNECTIONS_BY_REGION_NAME = dict() - self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) - self.REQUIREMENTS_BY_HEX = dict() - - self.EVENT_ITEM_PAIRS = dict() - self.COMPLETELY_DISABLED_ENTITIES = set() - self.DISABLE_EVERYTHING_BEHIND = set() - self.PRECOMPLETED_LOCATIONS = set() - self.EXCLUDED_LOCATIONS = set() - self.ADDED_CHECKS = set() - self.VICTORY_LOCATION: str - - self.ALWAYS_EVENT_NAMES_BY_HEX = { - "0x00509": "+1 Laser (Symmetry Laser)", - "0x012FB": "+1 Laser (Desert Laser)", - "0x09F98": "Desert Laser Redirection", - "0x01539": "+1 Laser (Quarry Laser)", - "0x181B3": "+1 Laser (Shadows Laser)", - "0x014BB": "+1 Laser (Keep Laser)", - "0x17C65": "+1 Laser (Monastery Laser)", - "0x032F9": "+1 Laser (Town Laser)", - "0x00274": "+1 Laser (Jungle Laser)", - "0x0C2B2": "+1 Laser (Bunker Laser)", - "0x00BF6": "+1 Laser (Swamp Laser)", - "0x028A4": "+1 Laser (Treehouse Laser)", - "0x17C34": "Mountain Entry", - "0xFFF00": "Bottom Floor Discard Turns On", - } - - self.USED_EVENT_NAMES_BY_HEX = {} - self.CONDITIONAL_EVENTS = {} - - # The basic requirements to solve each entity come from StaticWitnessLogic. - # However, for any given world, the options (e.g. which item shuffles are enabled) affect the requirements. - self.make_options_adjustments(world) - self.determine_unrequired_entities(world) - self.find_unsolvable_entities(world) - - # After we have adjusted the raw requirements, we perform a dependency reduction for the entity requirements. - # This will make the access conditions way faster, instead of recursively checking dependent entities each time. - self.make_dependency_reduced_checklist() - - # Finalize which items actually exist in the MultiWorld and which get grouped into progressive items. - self.finalize_items() - - # Create event-item pairs for specific panels in the game. - self.make_event_panel_lists() diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index 35f4e9544212..2528c8abe22b 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -9,9 +9,11 @@ from worlds.generic.Rules import CollectionRule +from .data import static_locations as static_witness_locations from .data import static_logic as static_witness_logic +from .data.static_logic import StaticWitnessLogicObj from .data.utils import WitnessRule, optimize_witness_rule -from .locations import WitnessPlayerLocations, static_witness_locations +from .locations import WitnessPlayerLocations from .player_logic import WitnessPlayerLogic if TYPE_CHECKING: @@ -21,8 +23,20 @@ class WitnessPlayerRegions: """Class that defines Witness Regions""" - player_locations = None - logic = None + def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: + difficulty = world.options.puzzle_randomization + + self.reference_logic: StaticWitnessLogicObj + if difficulty == "sigma_normal": + self.reference_logic = static_witness_logic.sigma_normal + elif difficulty == "sigma_expert": + self.reference_logic = static_witness_logic.sigma_expert + else: + self.reference_logic = static_witness_logic.vanilla + + self.player_locations = player_locations + self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) + self.created_region_names: Set[str] = set() @staticmethod def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule: @@ -36,7 +50,7 @@ def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Collect return _meets_item_requirements(item_requirement, world) def connect_if_possible(self, world: "WitnessWorld", source: str, target: str, req: WitnessRule, - regions_by_name: Dict[str, Region]): + regions_by_name: Dict[str, Region]) -> None: """ connect two regions and set the corresponding requirement """ @@ -89,8 +103,8 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic """ from . import create_region - all_locations = set() - regions_by_name = dict() + all_locations: Set[str] = set() + regions_by_name: Dict[str, Region] = {} regions_to_create = { k: v for k, v in self.reference_logic.ALL_REGIONS_BY_NAME.items() @@ -121,17 +135,3 @@ def create_regions(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic for region_name, region in regions_to_create.items(): for connection in player_logic.CONNECTIONS_BY_REGION_NAME[region_name]: self.connect_if_possible(world, region_name, connection[0], connection[1], regions_by_name) - - def __init__(self, player_locations: WitnessPlayerLocations, world: "WitnessWorld") -> None: - difficulty = world.options.puzzle_randomization - - if difficulty == "sigma_normal": - self.reference_logic = static_witness_logic.sigma_normal - elif difficulty == "sigma_expert": - self.reference_logic = static_witness_logic.sigma_expert - elif difficulty == "none": - self.reference_logic = static_witness_logic.vanilla - - self.player_locations = player_locations - self.two_way_entrance_register: Dict[Tuple[str, str], List[Entrance]] = defaultdict(lambda: []) - self.created_region_names: Set[str] = set() diff --git a/worlds/witness/ruff.toml b/worlds/witness/ruff.toml index d42361a4aaa9..a35711cce66d 100644 --- a/worlds/witness/ruff.toml +++ b/worlds/witness/ruff.toml @@ -1,10 +1,10 @@ line-length = 120 [lint] -select = ["E", "F", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] -ignore = ["RUF012", "RUF100"] +select = ["C", "E", "F", "R", "W", "I", "N", "Q", "UP", "RUF", "ISC", "T20"] +ignore = ["C9", "RUF012", "RUF100"] -[per-file-ignores] +[lint.per-file-ignores] # The way options definitions work right now, I am forced to break line length requirements. "options.py" = ["E501"] # The import list would just be so big if I imported every option individually in presets.py diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index b4982d1830b2..12a9a1ed4b59 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -37,8 +37,8 @@ def _has_laser(laser_hex: str, world: "WitnessWorld", player: int, redirect_requ _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations)(state) and state.has("Desert Laser Redirection", player) ) - else: - return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) + + return _can_solve_panel(laser_hex, world, world.player, world.player_logic, world.player_locations) def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule: @@ -63,8 +63,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi if entity_name + " Solved" in player_locations.EVENT_LOCATION_TABLE: return lambda state: state.has(player_logic.EVENT_ITEM_PAIRS[entity_name + " Solved"], player) - else: - return make_lambda(panel, world) + + return make_lambda(panel, world) def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: @@ -175,12 +175,10 @@ def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool: # We can get to Hedge 3 from Hedge 2. If we can get from Keep to Hedge 2, we're good. # This covers both Hedge 1 Exit and Hedge 2 Shortcut, because Hedge 1 is just part of the Keep region. - hedge_2_from_keep = any( + return any( e.can_reach(state) for e in player_regions.two_way_entrance_register["Keep 2nd Maze", "Keep"] ) - return hedge_2_from_keep - def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> bool: """ @@ -211,14 +209,12 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") -> # We also need a way from Town to Tunnels. - tunnels_from_town = ( + return ( any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Windmill Interior"]) and any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Town", "Windmill Interior"]) or any(e.can_reach(state) for e in player_regions.two_way_entrance_register["Tunnels", "Town"]) ) - return tunnels_from_town - def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic, player_locations: WitnessPlayerLocations) -> CollectionRule: @@ -237,9 +233,9 @@ def _has_item(item: str, world: "WitnessWorld", player: int, if item == "11 Lasers + Redirect": laser_req = world.options.challenge_lasers.value return _has_lasers(laser_req, world, True) - elif item == "PP2 Weirdness": + if item == "PP2 Weirdness": return lambda state: _can_do_expert_pp2(state, world) - elif item == "Theater to Tunnels": + if item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) if item in player_logic.USED_EVENT_NAMES_BY_HEX: return _can_solve_panel(item, world, player, player_logic, player_locations) From 95110c478733a315cb61ccd919fd3ca5f4801dfa Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:34:17 +0200 Subject: [PATCH 02/78] The Witness: Fix door shuffle being completely broken --- worlds/witness/player_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index b62c59b00ae1..e8d11f43f51c 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -197,7 +197,7 @@ def reduce_req_within_region(self, entity_hex: str) -> WitnessRule: # Those requirements need to be preserved even in door shuffle. entity_dependencies_need_to_be_preserved = ( # EPs keep all their entity dependencies - static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] != "EP" + static_witness_logic.ENTITIES_BY_HEX[entity_hex]["entityType"] == "EP" # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved, # except in Expert, where that dependency doesn't exist, but now there *is* a power dependency. # In the future, it'd be wise to make a distinction between "power dependencies" and other dependencies. From 50f7a79ea75e2b7eb0ff4c3881408485b4e9ec4e Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 2 Jul 2024 19:32:01 -0700 Subject: [PATCH 03/78] Zillion: new map generation feature (#3604) --- worlds/zillion/__init__.py | 6 +++--- worlds/zillion/options.py | 34 +++++++++++++++++++++++++-------- worlds/zillion/requirements.txt | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 205cc9ad6ba1..cf61d93ca4ce 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -145,10 +145,10 @@ def generate_early(self) -> None: self._item_counts = item_counts with redirect_stdout(self.lsi): # type: ignore - self.zz_system.make_randomizer(zz_op) - - self.zz_system.seed(self.multiworld.seed) + self.zz_system.set_options(zz_op) + self.zz_system.seed(self.random.randrange(1999999999)) self.zz_system.make_map() + self.zz_system.make_randomizer() # just in case the options changed anything (I don't think they do) assert self.zz_system.randomizer, "init failed" diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index d75dd1a1c22c..5de0b65c82f0 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -1,9 +1,9 @@ from collections import Counter from dataclasses import dataclass -from typing import ClassVar, Dict, Tuple +from typing import ClassVar, Dict, Literal, Tuple from typing_extensions import TypeGuard # remove when Python >= 3.10 -from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle from zilliandomizer.options import ( Options as ZzOptions, char_to_gun, char_to_jump, ID, @@ -251,9 +251,25 @@ class ZillionStartingCards(NamedRange): } -class ZillionRoomGen(Toggle): - """ whether to generate rooms with random terrain """ - display_name = "room generation" +class ZillionMapGen(Choice): + """ + - none: vanilla map + - rooms: random terrain inside rooms, but path through base is vanilla + - full: random path through base + """ + display_name = "map generation" + option_none = 0 + option_rooms = 1 + option_full = 2 + default = 0 + + def zz_value(self) -> Literal['none', 'rooms', 'full']: + if self.value == ZillionMapGen.option_none: + return "none" + if self.value == ZillionMapGen.option_rooms: + return "rooms" + assert self.value == ZillionMapGen.option_full + return "full" @dataclass @@ -276,7 +292,9 @@ class ZillionOptions(PerGameCommonOptions): early_scope: ZillionEarlyScope skill: ZillionSkill starting_cards: ZillionStartingCards - room_gen: ZillionRoomGen + map_gen: ZillionMapGen + + room_gen: Removed z_option_groups = [ @@ -375,7 +393,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": starting_cards = options.starting_cards - room_gen = options.room_gen + map_gen = options.map_gen.zz_value() zz_item_counts = convert_item_counts(item_counts) zz_op = ZzOptions( @@ -393,7 +411,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]": bool(options.early_scope.value), True, # balance defense starting_cards.value, - bool(room_gen.value) + map_gen ) zz_validate(zz_op) return zz_op, item_counts diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index ae7d9b173308..b4f554902f48 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@1dd2ce01c9d818caba5844529699b3ad026d6a07#0.7.1 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4a2fec0aa1c529df866e510cdfcf6dca4d53679b#0.8.0 typing-extensions>=4.7, <5 From f6735745b619a83d1631a37cf1be7c8208cc5a83 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Wed, 3 Jul 2024 06:39:08 -0700 Subject: [PATCH 04/78] Core: Fix !remaining (#3611) --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index dc5e3d21ac89..f59855fca6a4 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1352,7 +1352,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1365,7 +1365,7 @@ def _cmd_remaining(self) -> bool: if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.client.slot.game][item_id] + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") From 315e0c89e2f0f851e72bec651da10f917b485713 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 3 Jul 2024 12:13:16 -0400 Subject: [PATCH 05/78] Docs: Lastest -> Latest (#3616) --- worlds/generic/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index 6e65459851e3..22622cd0e94d 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -12,7 +12,7 @@ Some steps also assume use of Windows, so may vary with your OS. ## Installing the Archipelago software The most recent public release of Archipelago can be found on GitHub: -[Archipelago Lastest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). +[Archipelago Latest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). Run the exe file, and after accepting the license agreement you will be asked which components you would like to install. From d4d0a3e945274b6470c86dbd853922578679e97c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 5 Jul 2024 16:36:55 -0400 Subject: [PATCH 06/78] TUNIC: Make the shop checks require a sword --- worlds/tunic/er_rules.py | 10 ++++++++++ worlds/tunic/rules.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index d9348628ce9c..2652a5b148de 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1538,3 +1538,13 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: has_ability(state, player, prayer, options, ability_unlocks)) set_rule(multiworld.get_location("Library Fuse", player), lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + + # Shop + set_rule(multiworld.get_location("Shop - Potion 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Potion 2", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 2", player), + lambda state: has_sword(state, player)) diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 97270b5a2a81..b9dbc1e226b1 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -337,3 +337,13 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + + # Shop + set_rule(multiworld.get_location("Shop - Potion 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Potion 2", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 1", player), + lambda state: has_sword(state, player)) + set_rule(multiworld.get_location("Shop - Coin 2", player), + lambda state: has_sword(state, player)) From ca766288137e3f7973f71a13517cd32d3e08fc8e Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:37:32 -0700 Subject: [PATCH 07/78] sc2: Fixing typo in itemgroups.py causing spurious item groups with 2 letters chopped off (#3612) --- worlds/sc2/ItemGroups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sc2/ItemGroups.py b/worlds/sc2/ItemGroups.py index a77fb920f64d..3a3733044579 100644 --- a/worlds/sc2/ItemGroups.py +++ b/worlds/sc2/ItemGroups.py @@ -51,7 +51,7 @@ item_name_groups.setdefault(data.type, []).append(item) # Numbered flaggroups get sorted into an unnumbered group # Currently supports numbers of one or two digits - if data.type[-2:].strip().isnumeric: + if data.type[-2:].strip().isnumeric(): type_group = data.type[:-2].strip() item_name_groups.setdefault(type_group, []).append(item) # Flaggroups with numbers are unlisted From 4054a9f15fb86af4de5f4a2fe2241098b0193290 Mon Sep 17 00:00:00 2001 From: Louis M Date: Fri, 5 Jul 2024 16:40:26 -0400 Subject: [PATCH 08/78] Aquaria: Renaming some locations for consistency (#3533) * Change 'The Body main area' by 'The Body center area' for consistency * Renaming some locations for consistency * Adding a line for standard * Replacing Cathedral by Mithalas Cathedral and addin Blind goal option * Client option renaming for consistency * Fix death link not working * Removing death link from the option to put it client side * Changing Left to Right --- worlds/aquaria/Locations.py | 13 +++++++------ worlds/aquaria/Options.py | 12 ++++++++++-- worlds/aquaria/Regions.py | 18 +++++++++--------- worlds/aquaria/__init__.py | 3 ++- worlds/aquaria/test/__init__.py | 10 +++++----- worlds/aquaria/test/test_energy_form_access.py | 4 ++-- worlds/aquaria/test/test_li_song_access.py | 2 +- worlds/aquaria/test/test_nature_form_access.py | 2 +- ...est_no_progression_hard_hidden_locations.py | 4 ++-- .../test_progression_hard_hidden_locations.py | 4 ++-- worlds/aquaria/test/test_spirit_form_access.py | 2 +- 11 files changed, 42 insertions(+), 32 deletions(-) diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index 33d165db411a..2eb9d1e9a29d 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -30,7 +30,7 @@ class AquariaLocations: locations_verse_cave_r = { "Verse Cave, bulb in the skeleton room": 698107, - "Verse Cave, bulb in the path left of the skeleton room": 698108, + "Verse Cave, bulb in the path right of the skeleton room": 698108, "Verse Cave right area, Big Seed": 698175, } @@ -122,6 +122,7 @@ class AquariaLocations: "Open Water top right area, second urn in the Mithalas exit": 698149, "Open Water top right area, third urn in the Mithalas exit": 698150, } + locations_openwater_tr_turtle = { "Open Water top right area, bulb in the turtle room": 698009, "Open Water top right area, Transturtle": 698211, @@ -195,7 +196,7 @@ class AquariaLocations: locations_cathedral_l = { "Mithalas City Castle, bulb in the flesh hole": 698042, - "Mithalas City Castle, Blue banner": 698165, + "Mithalas City Castle, Blue Banner": 698165, "Mithalas City Castle, urn in the bedroom": 698130, "Mithalas City Castle, first urn of the single lamp path": 698131, "Mithalas City Castle, second urn of the single lamp path": 698132, @@ -226,7 +227,7 @@ class AquariaLocations: "Mithalas Cathedral, third urn in the path behind the flesh vein": 698146, "Mithalas Cathedral, fourth urn in the top right room": 698147, "Mithalas Cathedral, Mithalan Dress": 698189, - "Mithalas Cathedral right area, urn below the left entrance": 698198, + "Mithalas Cathedral, urn below the left entrance": 698198, } locations_cathedral_underground = { @@ -239,7 +240,7 @@ class AquariaLocations: } locations_cathedral_boss = { - "Cathedral boss area, beating Mithalan God": 698202, + "Mithalas boss area, beating Mithalan God": 698202, } locations_forest_tl = { @@ -269,7 +270,7 @@ class AquariaLocations: locations_forest_bl = { "Kelp Forest bottom left area, bulb close to the spirit crystals": 698054, - "Kelp Forest bottom left area, Walker baby": 698186, + "Kelp Forest bottom left area, Walker Baby": 698186, "Kelp Forest bottom left area, Transturtle": 698212, } @@ -451,7 +452,7 @@ class AquariaLocations: locations_body_c = { "The Body center area, breaking Li's cage": 698201, - "The Body main area, bulb on the main path blocking tube": 698097, + "The Body center area, bulb on the main path blocking tube": 698097, } locations_body_l = { diff --git a/worlds/aquaria/Options.py b/worlds/aquaria/Options.py index 4c795d350898..8c0142debff0 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -5,7 +5,7 @@ """ from dataclasses import dataclass -from Options import Toggle, Choice, Range, DeathLink, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool +from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle, StartInventoryPool class IngredientRandomizer(Choice): @@ -111,6 +111,14 @@ class BindSongNeededToGetUnderRockBulb(Toggle): display_name = "Bind song needed to get sing bulbs under rocks" +class BlindGoal(Toggle): + """ + Hide the goal's requirements from the help page so that you have to go to the last boss door to know + what is needed to access the boss. + """ + display_name = "Hide the goal's requirements" + + class UnconfineHomeWater(Choice): """ Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song. @@ -142,4 +150,4 @@ class AquariaOptions(PerGameCommonOptions): dish_randomizer: DishRandomizer aquarian_translation: AquarianTranslation skip_first_vision: SkipFirstVision - death_link: DeathLink + blind_goal: BlindGoal diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 28120259254c..93c02d4e6766 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -300,7 +300,7 @@ def __create_mithalas(self) -> None: AquariaLocations.locations_cathedral_l_sc) self.cathedral_r = self.__add_region("Mithalas Cathedral", AquariaLocations.locations_cathedral_r) - self.cathedral_underground = self.__add_region("Mithalas Cathedral Underground area", + self.cathedral_underground = self.__add_region("Mithalas Cathedral underground", AquariaLocations.locations_cathedral_underground) self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", AquariaLocations.locations_cathedral_boss) @@ -597,22 +597,22 @@ def __connect_mithalas_regions(self) -> None: lambda state: _has_beast_form(state, self.player) and _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral underground", + self.__connect_regions("Mithalas castle", "Mithalas Cathedral underground", self.cathedral_l, self.cathedral_underground, lambda state: _has_beast_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral right area", + self.__connect_regions("Mithalas castle", "Mithalas Cathedral", self.cathedral_l, self.cathedral_r, lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) - self.__connect_regions("Cathedral right area", "Cathedral underground", + self.__connect_regions("Mithalas Cathedral", "Mithalas Cathedral underground", self.cathedral_r, self.cathedral_underground, lambda state: _has_energy_form(state, self.player)) - self.__connect_one_way_regions("Cathedral underground", "Cathedral boss left area", + self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss left area", self.cathedral_underground, self.cathedral_boss_r, lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) - self.__connect_one_way_regions("Cathedral boss left area", "Cathedral underground", + self.__connect_one_way_regions("Cathedral boss left area", "Mithalas Cathedral underground", self.cathedral_boss_r, self.cathedral_underground, lambda state: _has_beast_form(state, self.player)) self.__connect_regions("Cathedral boss right area", "Cathedral boss left area", @@ -1099,7 +1099,7 @@ def __adjusting_manual_rules(self) -> None: lambda state: _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Open Water bottom left area, bulb inside the lowest fish pass", self.player), lambda state: _has_fish_form(state, self.player)) - add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", self.player), + add_rule(self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player), lambda state: _has_spirit_form(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb hidden behind the blocking rock", self.player), lambda state: _has_bind_song(state, self.player)) @@ -1134,7 +1134,7 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Cathedral boss area, beating Mithalan God", + self.multiworld.get_location("Mithalas boss area, beating Mithalan God", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", @@ -1191,7 +1191,7 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("Kelp Forest bottom left area, Walker baby", + self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, Sun Key", diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index ce46aeea75aa..1fb04036d81b 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -204,7 +204,8 @@ def generate_basic(self) -> None: def fill_slot_data(self) -> Dict[str, Any]: return {"ingredientReplacement": self.ingredients_substitution, - "aquarianTranslate": bool(self.options.aquarian_translation.value), + "aquarian_translate": bool(self.options.aquarian_translation.value), + "blind_goal": bool(self.options.blind_goal.value), "secret_needed": self.options.objective.value > 0, "minibosses_to_kill": self.options.mini_bosses_to_beat.value, "bigbosses_to_kill": self.options.big_bosses_to_beat.value, diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 5c63c9bb2968..029db691b66b 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -60,7 +60,7 @@ "Mithalas City, Doll", "Mithalas City, urn inside a home fish pass", "Mithalas City Castle, bulb in the flesh hole", - "Mithalas City Castle, Blue banner", + "Mithalas City Castle, Blue Banner", "Mithalas City Castle, urn in the bedroom", "Mithalas City Castle, first urn of the single lamp path", "Mithalas City Castle, second urn of the single lamp path", @@ -82,14 +82,14 @@ "Mithalas Cathedral, third urn in the path behind the flesh vein", "Mithalas Cathedral, fourth urn in the top right room", "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral right area, urn below the left entrance", + "Mithalas Cathedral, urn below the left entrance", "Cathedral Underground, bulb in the center part", "Cathedral Underground, first bulb in the top left part", "Cathedral Underground, second bulb in the top left part", "Cathedral Underground, third bulb in the top left part", "Cathedral Underground, bulb close to the save crystal", "Cathedral Underground, bulb in the bottom right path", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", @@ -104,7 +104,7 @@ "Kelp Forest top right area, Black Pearl", "Kelp Forest top right area, bulb in the top fish pass", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Kelp Forest bottom left area, Transturtle", "Kelp Forest bottom right area, Odd Container", "Kelp Forest boss area, beating Drunian God", @@ -175,7 +175,7 @@ "Sunken City left area, Girl Costume", "Sunken City, bulb on top of the boss area", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index bf0ace478e2e..82d8e89a0066 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -39,8 +39,8 @@ def test_energy_form_location(self) -> None: "Mithalas Cathedral, third urn in the path behind the flesh vein", "Mithalas Cathedral, fourth urn in the top right room", "Mithalas Cathedral, Mithalan Dress", - "Mithalas Cathedral right area, urn below the left entrance", - "Cathedral boss area, beating Mithalan God", + "Mithalas Cathedral, urn below the left entrance", + "Mithalas boss area, beating Mithalan God", "Kelp Forest top left area, bulb close to the Verse Egg", "Kelp Forest top left area, Verse Egg", "Kelp Forest boss area, beating Drunian God", diff --git a/worlds/aquaria/test/test_li_song_access.py b/worlds/aquaria/test/test_li_song_access.py index 85fc2bd45a66..f615fb10c640 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -24,7 +24,7 @@ def test_li_song_location(self) -> None: "Sunken City left area, Girl Costume", "Sunken City, bulb on top of the boss area", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_nature_form_access.py b/worlds/aquaria/test/test_nature_form_access.py index 71432539d0da..1d3b8f4150eb 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -38,7 +38,7 @@ def test_nature_form_location(self) -> None: "Beating the Golem", "Sunken City cleared", "The Body center area, breaking Li's cage", - "The Body main area, bulb on the main path blocking tube", + "The Body center area, bulb on the main path blocking tube", "The Body left area, first bulb in the top face room", "The Body left area, second bulb in the top face room", "The Body left area, bulb below the water stream", diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 40cb5336484e..f015b26de10b 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -16,7 +16,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): unfillable_locations = [ "Energy Temple boss area, Fallen God Tooth", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest boss area, beating Drunian God", "Sun Temple boss area, beating Sun God", "Sunken City, bulb on top of the boss area", @@ -35,7 +35,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Sun Temple, Sun Key", "The Body bottom area, Mutant Costume", "Sun Temple, bulb in the hidden room of the right part", diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 026a175206fc..a1493c5d0f39 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -15,7 +15,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): unfillable_locations = [ "Energy Temple boss area, Fallen God Tooth", - "Cathedral boss area, beating Mithalan God", + "Mithalas boss area, beating Mithalan God", "Kelp Forest boss area, beating Drunian God", "Sun Temple boss area, beating Sun God", "Sunken City, bulb on top of the boss area", @@ -34,7 +34,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", "Kelp Forest bottom left area, bulb close to the spirit crystals", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Sun Temple, Sun Key", "The Body bottom area, Mutant Costume", "Sun Temple, bulb in the hidden room of the right part", diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 40a087a7fb5a..3bcbd7d72e02 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -16,7 +16,7 @@ def test_spirit_form_location(self) -> None: "The Veil bottom area, bulb in the spirit path", "Mithalas City Castle, Trident Head", "Open Water skeleton path, King Skull", - "Kelp Forest bottom left area, Walker baby", + "Kelp Forest bottom left area, Walker Baby", "Abyss right area, bulb behind the rock in the whale room", "The Whale, Verse Egg", "Ice Cave, bulb in the room to the right", From e7a8e195e67b8ef3d220e0c93083157786bd2e5c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Fri, 5 Jul 2024 16:50:12 -0400 Subject: [PATCH 09/78] TUNIC: Use fewer parameters in helper functions (#3356) * Clean these functions up, get the hell out of here 5 parameter function * Clean up a bunch of rules that no longer need to be multi-lined since the functions are shorter * Clean up some range functions * Update to use world instead of player like Vi recommended * Fix merge conflict * Fix after merge --- worlds/tunic/__init__.py | 12 +- worlds/tunic/er_rules.py | 447 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 2 +- worlds/tunic/rules.py | 188 ++++++++-------- 4 files changed, 314 insertions(+), 335 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 92834d96b07f..f63193e6aeef 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -200,7 +200,7 @@ def create_items(self) -> None: # Remove filler to make room for other items def remove_filler(amount: int) -> None: - for _ in range(0, amount): + for _ in range(amount): if not available_filler: fill = "Fool Trap" else: @@ -258,7 +258,7 @@ def remove_filler(amount: int) -> None: items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): - for i in range(0, quantity): + for _ in range(quantity): tunic_item: TunicItem = self.create_item(item) if item in slot_data_item_names: self.slot_data_items.append(tunic_item) @@ -309,10 +309,10 @@ def create_regions(self) -> None: def set_rules(self) -> None: if self.options.entrance_rando or self.options.shuffle_ladders: - set_er_location_rules(self, self.ability_unlocks) + set_er_location_rules(self) else: - set_region_rules(self, self.ability_unlocks) - set_location_rules(self, self.ability_unlocks) + set_region_rules(self) + set_location_rules(self) def get_filler_item_name(self) -> str: return self.random.choice(filler_items) @@ -387,7 +387,7 @@ def fill_slot_data(self) -> Dict[str, Any]: if start_item in slot_data_item_names: if start_item not in slot_data: slot_data[start_item] = [] - for i in range(0, self.options.start_inventory_from_pool[start_item]): + for _ in range(self.options.start_inventory_from_pool[start_item]): slot_data[start_item].extend(["Your Pocket", self.player]) for plando_item in self.multiworld.plando_items[self.player]: diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 2652a5b148de..81e9d48b4afc 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -2,7 +2,6 @@ from worlds.generic.Rules import set_rule, forbid_item from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage from .er_data import Portal -from .options import TunicOptions from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -28,12 +27,11 @@ gold_hexagon = "Gold Questagon" -def has_ladder(ladder: str, state: CollectionState, player: int, options: TunicOptions): - return not options.shuffle_ladders or state.has(ladder, player) +def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: + return not world.options.shuffle_ladders or state.has(ladder, world.player) -def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], - portal_pairs: Dict[Portal, Portal]) -> None: +def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: player = world.player options = world.options @@ -43,16 +41,16 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Overworld regions["Overworld"].connect( connecting_region=regions["Overworld Holy Cross"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) # grapple on the west side, down the stairs from moss wall, across from ruined shop regions["Overworld"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) regions["Overworld Beach"].connect( @@ -64,10 +62,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Beach"].connect( connecting_region=regions["Overworld to Atoll Upper"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld to Atoll Upper"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld to Atoll Upper"], @@ -84,14 +82,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world)) # long dong, do not make a reverse connection here or to belltower regions["Overworld above Patrol Cave"].connect( @@ -109,52 +107,52 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + or has_ice_grapple_logic(True, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Overworld"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) regions["Above Ruined Passage"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) regions["After Ruined Passage"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) + or has_ice_grapple_logic(True, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], - rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + rule=lambda state: has_ladder("Ladders near Weathervane", state, world) or state.has(laurels, player)) # nmg: ice grapple the slimes, works both ways consistently regions["East Overworld"].connect( connecting_region=regions["After Ruined Passage"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Overworld"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) + or has_ice_grapple_logic(True, state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld at Patrol Cave"]) @@ -164,35 +162,35 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld at Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) or state.has(grapple, player)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], - rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) or state.has(grapple, player)) regions["Upper Overworld"].connect( @@ -204,18 +202,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Upper Overworld"].connect( connecting_region=regions["Overworld after Temple Rafters"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)) regions["Overworld after Temple Rafters"].connect( connecting_region=regions["Upper Overworld"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld above Quarry Entrance"], - rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], @@ -230,18 +228,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + rule=lambda state: has_ladder("Ladder to Quarry", state, world)) # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -252,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["Overworld Swamp Lower Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld Special Shop Entry"], @@ -266,41 +264,41 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Well Ladder"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Overworld Well Ladder"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) # nmg: can ice grapple through the door regions["Overworld"].connect( connecting_region=regions["Overworld Old House Door"], rule=lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) # not including ice grapple through this because it's very tedious to get an enemy here regions["Overworld"].connect( connecting_region=regions["Overworld Southeast Cross Door"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Overworld Southeast Cross Door"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) # not including ice grapple through this because we're not including it on the other door regions["Overworld"].connect( connecting_region=regions["Overworld Fountain Cross Door"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Town Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Town Portal"].connect( connecting_region=regions["Overworld"]) regions["Overworld"].connect( connecting_region=regions["Overworld Spawn Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Spawn Portal"].connect( connecting_region=regions["Overworld"]) @@ -308,7 +306,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -316,17 +314,17 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld Beach"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has(grapple, player)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) + or has_ice_grapple_logic(True, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) @@ -368,7 +366,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Hourglass Cave"].connect( connecting_region=regions["Hourglass Cave Tower"], - rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, player, options)) + rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, world)) # East Forest regions["Forest Belltower Upper"].connect( @@ -376,32 +374,31 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Forest Belltower Main"].connect( connecting_region=regions["Forest Belltower Lower"], - rule=lambda state: has_ladder("Ladder to East Forest", state, player, options)) + rule=lambda state: has_ladder("Ladder to East Forest", state, world)) # nmg: ice grapple up to dance fox spot, and vice versa regions["East Forest"].connect( connecting_region=regions["East Forest Dance Fox Spot"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["East Forest Dance Fox Spot"].connect( connecting_region=regions["East Forest"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["East Forest Portal"].connect( connecting_region=regions["East Forest"]) regions["East Forest"].connect( connecting_region=regions["Lower Forest"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options) - or (state.has_all({grapple, fire_wand, ice_dagger}, player) # do ice slime, then go to the lower hook - and has_ability(state, player, icebolt, options, ability_unlocks))) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world) + or (state.has_all({grapple, fire_wand, ice_dagger}, player) and has_ability(icebolt, state, world))) regions["Lower Forest"].connect( connecting_region=regions["East Forest"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 1 East"].connect( connecting_region=regions["Guard House 1 West"]) @@ -411,16 +408,16 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Guard House 2 Upper"].connect( connecting_region=regions["Guard House 2 Lower"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) regions["Guard House 2 Lower"].connect( connecting_region=regions["Guard House 2 Upper"], - rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) # nmg: ice grapple from upper grave path exit to the rest of it regions["Forest Grave Path Upper"].connect( connecting_region=regions["Forest Grave Path Main"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path Upper"], rule=lambda state: state.has(laurels, player)) @@ -430,23 +427,22 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: ice grapple or laurels through the gate regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Grave Path Main"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks) + rule=lambda state: has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Hero's Grave"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Forest Hero's Grave"].connect( connecting_region=regions["Forest Grave Path by Grave"]) # Beneath the Well and Dark Tomb - # don't need the ladder when entering at the ladder spot regions["Beneath the Well Ladder Exit"].connect( connecting_region=regions["Beneath the Well Front"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Ladder Exit"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Main"], @@ -457,10 +453,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Beneath the Well Main"].connect( connecting_region=regions["Beneath the Well Back"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Beneath the Well Back"].connect( connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_ladder("Ladders in Well", state, player, options) + rule=lambda state: has_ladder("Ladders in Well", state, world) and (has_stick(state, player) or state.has(fire_wand, player))) regions["Well Boss"].connect( @@ -472,22 +468,22 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Entry Point"]) regions["Dark Tomb Upper"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Upper"], - rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)) regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Dark Exit"]) regions["Dark Tomb Dark Exit"].connect( connecting_region=regions["Dark Tomb Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) # West Garden regions["West Garden Laurels Exit Region"].connect( @@ -506,7 +502,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["West Garden"].connect( connecting_region=regions["West Garden Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["West Garden Hero's Grave Region"].connect( connecting_region=regions["West Garden"]) @@ -515,29 +511,29 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["West Garden Portal Item"].connect( connecting_region=regions["West Garden Portal"], - rule=lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) # nmg: can ice grapple to and from the item behind the magic dagger house regions["West Garden Portal Item"].connect( connecting_region=regions["West Garden"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) # Atoll and Frog's Domain # nmg: ice grapple the bird below the portal regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Lower Entry Area"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Ruined Atoll Lower Entry Area"].connect( connecting_region=regions["Ruined Atoll"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Ladder Tops"], - rule=lambda state: has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ladder("Ladders in South Atoll", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Mouth"], @@ -548,48 +544,48 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Eye"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll Frog Eye"].connect( connecting_region=regions["Ruined Atoll"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Ruined Atoll Portal"].connect( connecting_region=regions["Ruined Atoll"]) regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in South Atoll", state, player, options)) + rule=lambda state: has_ability(prayer, state, world) + and has_ladder("Ladders in South Atoll", state, world)) regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) regions["Frog Stairs Eye Exit"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Eye Exit"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Upper"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs Upper"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs Lower"].connect( connecting_region=regions["Frog Stairs to Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog Stairs to Frog's Domain"].connect( connecting_region=regions["Frog Stairs Lower"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain Entry"].connect( connecting_region=regions["Frog's Domain"], - rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain"].connect( connecting_region=regions["Frog's Domain Back"], @@ -599,71 +595,71 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Library Exterior Tree Region"].connect( connecting_region=regions["Library Exterior Ladder Region"], rule=lambda state: state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Exterior Ladder Region"].connect( connecting_region=regions["Library Exterior Tree Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and (state.has(grapple, player) or (state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)))) + rule=lambda state: has_ability(prayer, state, world) + and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world)) + or state.has(grapple, player))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall Bookshelf"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Library Hero's Grave Region"].connect( connecting_region=regions["Library Hall"]) regions["Library Hall to Rotunda"].connect( connecting_region=regions["Library Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Hall"].connect( connecting_region=regions["Library Hall to Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Hall"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Hall"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda"].connect( connecting_region=regions["Library Rotunda to Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Rotunda to Lab"].connect( connecting_region=regions["Library Rotunda"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab Lower"].connect( connecting_region=regions["Library Lab"], rule=lambda state: state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Lab Lower"], rule=lambda state: state.has(laurels, player) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ability(prayer, state, world) + and has_ladder("Ladders in Library", state, world)) regions["Library Portal"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options) + rule=lambda state: has_ladder("Ladders in Library", state, world) or state.has(laurels, player)) regions["Library Lab"].connect( connecting_region=regions["Library Lab to Librarian"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) regions["Library Lab to Librarian"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + rule=lambda state: has_ladder("Ladders in Library", state, world)) # Eastern Vault Fortress regions["Fortress Exterior from East Forest"].connect( @@ -678,14 +674,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) regions["Fortress Exterior near cave"].connect( connecting_region=regions["Beneath the Vault Entry"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Entry"].connect( connecting_region=regions["Fortress Exterior near cave"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Exterior from Overworld"], @@ -694,48 +690,48 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Courtyard"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(True, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Courtyard"]) # nmg: can ice grapple to the upper ledge regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Courtyard Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) regions["Beneath the Vault Ladder Exit"].connect( connecting_region=regions["Beneath the Vault Main"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options) - and has_lantern(state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) + and has_lantern(state, world)) regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], - rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)) regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Back"]) regions["Beneath the Vault Back"].connect( connecting_region=regions["Beneath the Vault Main"], - rule=lambda state: has_lantern(state, player, options)) + rule=lambda state: has_lantern(state, world)) regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) # nmg: can ice grapple upwards regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", "Activate Eastern Vault East Fuse"}, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or has_ice_grapple_logic(False, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], @@ -746,14 +742,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Fortress Hero's Grave Region"].connect( connecting_region=regions["Fortress Grave Path"]) # nmg: ice grapple from upper grave path to lower regions["Fortress Grave Path Upper"].connect( connecting_region=regions["Fortress Grave Path"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -764,10 +760,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Quarry regions["Lower Mountain"].connect( connecting_region=regions["Lower Mountain Stairs"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Lower Mountain Stairs"].connect( connecting_region=regions["Lower Mountain"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Quarry Entry"].connect( connecting_region=regions["Quarry Portal"], @@ -805,25 +801,24 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Quarry"].connect( connecting_region=regions["Lower Quarry"], - rule=lambda state: has_mask(state, player, options)) + rule=lambda state: has_mask(state, world)) # need the ladder, or you can ice grapple down in nmg regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], - rule=lambda state: has_ladder("Ladders in Lower Quarry", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) + or has_ice_grapple_logic(True, state, world)) # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock regions["Even Lower Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], rule=lambda state: state.has("Activate Quarry Fuse", player) - or (has_ice_grapple_logic(False, state, player, options, ability_unlocks) and options.entrance_rando)) + or (has_ice_grapple_logic(False, state, world) and options.entrance_rando)) # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on regions["Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and options.entrance_rando) + rule=lambda state: has_ice_grapple_logic(True, state, world) and options.entrance_rando) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) @@ -834,7 +829,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Monastery Back"].connect( connecting_region=regions["Monastery Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Monastery Hero's Grave Region"].connect( connecting_region=regions["Monastery Back"]) @@ -855,20 +850,19 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Lower Front"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) - or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))) + or (has_sword(state, player) and has_ability(prayer, state, world))) # unrestricted: use ladder storage to get to the front, get hit by one of the many enemies # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Lower Front"], - rule=lambda state: ((state.has(laurels, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) - and has_ability(state, player, prayer, options, ability_unlocks) + rule=lambda state: ((state.has(laurels, player) or has_ice_grapple_logic(True, state, world)) + and has_ability(prayer, state, world) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or can_ladder_storage(state, world)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) @@ -880,41 +874,40 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has("Activate Ziggurat Fuse", player)) regions["Rooted Ziggurat Portal Room Exit"].connect( connecting_region=regions["Rooted Ziggurat Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) # Swamp and Cathedral regions["Swamp Front"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate regions["Swamp Mid"].connect( connecting_region=regions["Swamp Front"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate # nmg: ice grapple through cathedral door, can do it both ways regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(state, player, prayer, options, ability_unlocks) - and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player)) + or has_ice_grapple_logic(False, state, world)) regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(False, state, world)) regions["Swamp Mid"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options)) + rule=lambda state: has_ladder("Ladders in Swamp", state, world)) regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) # nmg: ice grapple the enemy at door + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, state, world)) # nmg: ice grapple the enemy at door regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], - rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + rule=lambda state: has_ability(holy_cross, state, world)) regions["Swamp to Cathedral Treasure Room"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"]) @@ -929,11 +922,11 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Back of Swamp Laurels Area"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: state.has(laurels, player) - and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + and has_ice_grapple_logic(True, state, world)) regions["Back of Swamp"].connect( connecting_region=regions["Swamp Hero's Grave Region"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(prayer, state, world)) regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) @@ -992,7 +985,8 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if world.options.hexagon_quest else (state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player) - and state.has_group_unique("Hero Relics", player, 6)))) + and state.has_group_unique("Hero Relics", player, 6) + and has_sword(state, player)))) # connecting the regions portals are in to other portals you can access via ladder storage # using has_stick instead of can_ladder_storage since it's already checking the logic rules @@ -1228,9 +1222,9 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[paired_region], name=portal_name + " (LS) " + region_name, rule=lambda state: has_stick(state, player) - and has_ability(state, player, holy_cross, options, ability_unlocks) - and (has_ladder("Ladders in Swamp", state, player, options) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and has_ability(holy_cross, state, world) + and (has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, state, world) or not options.entrance_rando)) # soft locked without this ladder elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando: @@ -1253,8 +1247,7 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) # soft locked if you can't get past garden knight backwards or up the belltower ladders elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando: @@ -1268,24 +1261,21 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has("Ladder to Beneath the Vault", player) - and has_lantern(state, player, options)) + rule=lambda state: has_stick(state, player) and state.has("Ladder to Beneath the Vault", player) + and has_lantern(state, world)) elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) and (state.has_any({"Ladders in Overworld Town", grapple}, player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks))) + or has_ice_grapple_logic(True, state, world))) elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player) - and state.has(grapple, player) or has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) + and state.has(grapple, player) or has_ability(prayer, state, world)) # soft lock potential elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance", "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando: @@ -1304,7 +1294,7 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: or state.has("Ladder to Quarry", player) and (state.has(fire_wand, player) or has_sword(state, player)))) or state.has("Ladders near Overworld Checkpoint", player) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks))))) + or has_ice_grapple_logic(True, state, world))))) # if no ladder items are required, just do the basic stick only lambda elif not ladders or not options.shuffle_ladders: regions[region_name].connect( @@ -1317,54 +1307,53 @@ def get_portal_info(portal_sd: str) -> Tuple[str, str]: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has(ladder, player)) + rule=lambda state: has_stick(state, player) and state.has(ladder, player)) # if multiple ladders can be used else: regions[region_name].connect( regions[paired_region], name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and state.has_any(ladders, player)) + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player)) -def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_er_location_rules(world: "TunicWorld") -> None: player = world.player multiworld = world.multiworld options = world.options + forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) # Ability Shuffle Exclusive Rules set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), lambda state: state.has("Activate Furnace Fuse", player)) set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) # Overworld set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), @@ -1380,35 +1369,37 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), lambda state: state.has(grapple, player)) set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), lambda state: state.has(fairies, player, 10)) set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6)) + set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + lambda state: state.has(coins, player, 3)) + set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + lambda state: state.has(coins, player, 6)) set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), lambda state: state.has(coins, player, 10)) set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), @@ -1420,8 +1411,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), lambda state: state.has_all({grapple, laurels}, player)) set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( - state.has_all({grapple, ice_dagger, fire_wand}, player) and - has_ability(state, player, icebolt, options, ability_unlocks))) + state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world))) # West Garden set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), @@ -1429,8 +1419,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options, - ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), @@ -1470,7 +1459,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) or options.entrance_rando))) set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player), - lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), @@ -1478,7 +1467,7 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) # nmg - kill Librarian with a lure, or gun I guess set_rule(multiworld.get_location("Librarian - Hexagon Green", player), lambda state: (has_sword(state, player) or options.logic_rules) - and has_ladder("Ladders in Library", state, player, options)) + and has_ladder("Ladders in Library", state, world)) # nmg - kill boss scav with orb + firecracker, or similar set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) @@ -1516,11 +1505,11 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) set_rule(multiworld.get_location("Western Bell", player), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) set_rule(multiworld.get_location("Furnace Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Beneath the Vault Fuse", player), lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) set_rule(multiworld.get_location("Eastern Vault West Fuses", player), @@ -1529,15 +1518,15 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses", "Activate South and West Fortress Exterior Fuses"}, player)) set_rule(multiworld.get_location("Quarry Connector Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player)) + lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) set_rule(multiworld.get_location("Quarry Fuse", player), lambda state: state.has("Activate Quarry Connector Fuse", player)) set_rule(multiworld.get_location("Ziggurat Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("West Garden Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Library Fuse", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) # Shop set_rule(multiworld.get_location("Shop - Potion 1", player), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 9d25137ba469..8689a51b7601 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -34,7 +34,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: for region_name, region_data in tunic_er_regions.items(): regions[region_name] = Region(region_name, world.player, world.multiworld) - set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) + set_er_region_rules(world, regions, portal_pairs) for location_name, location_id in world.location_name_to_id.items(): region = regions[location_table[location_name].er_region] diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index b9dbc1e226b1..73eb8118901b 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -38,102 +38,98 @@ def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str return dict(zip(abilities, ability_requirement)) -def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: +def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool: + options = world.options + ability_unlocks = world.ability_unlocks if not options.ability_shuffling: return True if options.hexagon_quest: - return state.has(gold_hexagon, player, ability_unlocks[ability]) - return state.has(ability, player) + return state.has(gold_hexagon, world.player, ability_unlocks[ability]) + return state.has(ability, world.player) # a check to see if you can whack things in melee at all def has_stick(state: CollectionState, player: int) -> bool: - return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player) + return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1) + or state.has("Sword", player)) def has_sword(state: CollectionState, player: int) -> bool: return state.has("Sword", player) or state.has("Sword Upgrade", player, 2) -def has_ice_grapple_logic(long_range: bool, state: CollectionState, player: int, options: TunicOptions, - ability_unlocks: Dict[str, int]) -> bool: - if not options.logic_rules: +def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool: + player = world.player + if not world.options.logic_rules: return False - if not long_range: return state.has_all({ice_dagger, grapple}, player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and \ - has_ability(state, player, icebolt, options, ability_unlocks) + return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) -def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.logic_rules == "unrestricted" and has_stick(state, player): - return True - else: - return False +def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) -def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.maskless: +def has_mask(state: CollectionState, world: "TunicWorld") -> bool: + if world.options.maskless: return True else: - return state.has(mask, player) + return state.has(mask, world.player) -def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.lanternless: +def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: + if world.options.lanternless: return True else: - return state.has(lantern, player) + return state.has(lantern, world.player) -def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_region_rules(world: "TunicWorld") -> None: multiworld = world.multiworld player = world.player options = world.options multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \ - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) + lambda state: has_ability(holy_cross, state, world) multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ lambda state: has_stick(state, player) or state.has(fire_wand, player) multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ - lambda state: has_lantern(state, player, options) + lambda state: has_lantern(state, world) multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ lambda state: state.has(laurels, player) \ - or can_ladder_storage(state, player, options) + or can_ladder_storage(state, world) multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ - or can_ladder_storage(state, player, options) + or has_ice_grapple_logic(True, state, world) \ + or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ - lambda state: has_lantern(state, player, options) and \ - has_ability(state, player, prayer, options, ability_unlocks) + lambda state: has_lantern(state, world) and has_ability(prayer, state, world) multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ - lambda state: state.has_any({grapple, laurels}, player) and \ - has_ability(state, player, prayer, options, ability_unlocks) + lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ - and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options)) + and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, world)) multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ - lambda state: has_mask(state, player, options) + lambda state: has_mask(state, world) multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ - lambda state: state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks) + lambda state: state.has(grapple, player) and has_ability(prayer, state, world) multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \ - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ + or has_ice_grapple_logic(False, state, world) multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \ - lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value - else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) and state.has_group_unique("Hero Relics", player, 6)) and \ - has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player) and \ - state.has_any({lantern, laurels}, player) + lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value + else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player) + and state.has_group_unique("Hero Relics", player, 6)) + and has_ability(prayer, state, world) and has_sword(state, player) + and state.has_any({lantern, laurels}, player)) -def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: +def set_location_rules(world: "TunicWorld") -> None: multiworld = world.multiworld player = world.player options = world.options @@ -142,37 +138,36 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Ability Shuffle Exclusive Rules set_rule(multiworld.get_location("Far Shore - Page Pickup", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) + lambda state: has_ability(prayer, state, world) + or state.has(laurels, player) + or can_ladder_storage(state, world) + or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player) - or can_ladder_storage(state, player, options) - or (has_ice_grapple_logic(True, state, player, options, ability_unlocks) - and has_lantern(state, player, options))) + lambda state: has_ability(prayer, state, world) or state.has(laurels, player) + or can_ladder_storage(state, world) + or (has_ice_grapple_logic(True, state, world) and has_lantern(state, world))) set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), - lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: has_ability(prayer, state, world)) set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: has_ability(holy_cross, state, world)) # Overworld set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), @@ -182,21 +177,21 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), lambda state: state.has_any({grapple, laurels}, player)) set_rule(multiworld.get_location("Far Shore - Secret Chest", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Old House - Normal Chest", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) - or (state.has(laurels, player) and options.logic_rules))) + lambda state: has_ability(holy_cross, state, world) and ( + state.has(house_key, player) + or has_ice_grapple_logic(False, state, world) + or (state.has(laurels, player) and options.logic_rules))) set_rule(multiworld.get_location("Old House - Shield Pickup", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + or has_ice_grapple_logic(False, state, world) or (state.has(laurels, player) and options.logic_rules)) set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), lambda state: state.has(laurels, player)) @@ -204,8 +199,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_lantern(state, world) and has_sword(state, player)) + or can_ladder_storage(state, world)) set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), @@ -213,15 +208,14 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), - lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and - (state.has(laurels, player) - or (has_lantern(state, player, options) and - (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: has_ability(holy_cross, state, world) + and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) + or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) + or has_ice_grapple_logic(False, state, world)) set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -245,7 +239,7 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has_all({grapple, laurels}, player)) set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player) - and has_ability(state, player, icebolt, options, ability_unlocks)) + and has_ability(icebolt, state, world)) # West Garden set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player), @@ -253,17 +247,16 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), - lambda state: state.has(laurels, player) - and has_ability(state, player, holy_cross, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player), - lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) - or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) + or has_ice_grapple_logic(True, state, world)) set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player)) - or can_ladder_storage(state, player, options)) + or (has_lantern(state, world) and has_sword(state, player)) + or can_ladder_storage(state, world)) # Ruined Atoll set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), @@ -287,19 +280,17 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), - lambda state: has_sword(state, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: has_sword(state, player) + and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), - lambda state: state.has(vault_key, player) and - (has_ability(state, player, prayer, options, ability_unlocks) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + lambda state: state.has(vault_key, player) + and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) # Beneath the Vault set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player)) set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player), - lambda state: has_stick(state, player) and has_lantern(state, player, options)) + lambda state: has_stick(state, player) and has_lantern(state, world)) # Quarry set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), @@ -313,8 +304,7 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Swamp set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks))) + and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), @@ -326,17 +316,17 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> # Hero's Grave set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), - lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks)) + lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) # Shop set_rule(multiworld.get_location("Shop - Potion 1", player), From bfac100567d56a088a0538c61c10c61fbab06100 Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Fri, 5 Jul 2024 16:54:35 -0400 Subject: [PATCH 10/78] MLSS: Fix for missing cutscene trigger --- worlds/mlss/data/basepatch.bsdiff | Bin 17615 -> 17596 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/worlds/mlss/data/basepatch.bsdiff b/worlds/mlss/data/basepatch.bsdiff index 156d28f346e8fa38fb2d1c4a3ce1b9e01342e848..8f9324995ec4c9ef9992397d64b33847c207600d 100644 GIT binary patch literal 17596 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 literal 17615 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 D+|RcY From f99ee77325c2c19a5658459e09a90a4ce48ed69b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 6 Jul 2024 13:40:55 +0200 Subject: [PATCH 11/78] The Witness: Add some unit tests (#3328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add hidden early symbol item option, make some unit tests * Add early symbol item false to the arrows test * I guess it's not an issue * more tests * assertEqual * cleanup * add minimum symbols test for all 3 modes * Formatting * Add more minimal beatability tests * one more for the road * I HATE THIS AAAAAAAAAAAHHHHHHHHHHH WHY DID WE GO WITH OPTIONS * loiaqeäsdhgalikSDGHjasDÖKHGASKLDÖGHJASKLJGHJSAÖkfaöslifjasöfASGJÖASDLFGJ'sklgösLGIKsdhJLGÖsdfjälghklDASFJghjladshfgjasdfälkjghasdöLfghasd-kjgjASDLÖGHAESKDLJGJÖsdaLGJHsadöKGjFDSLAkgjölSÄDghbASDFKGjasdLJGhjLÖSDGHLJASKDkgjldafjghjÖLADSFghäasdökgjäsadjlgkjsadkLHGsaDÖLGSADGÖLwSdlgkJLwDSFÄLHBJsaöfdkHweaFGIoeWjvlkdösmVJÄlsafdJKhvjdsJHFGLsdaövhWDsköLV-ksdFJHGVöSEKD * fix imports (within apworld needs to be relative) * Update worlds/witness/options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Sure * good suggestion * subtest * Add some EP shuffle unit tests, also an explicit event-checking unit test * add more tests yay * oops * mypy * Update worlds/witness/options.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Collapse into one test :( * More efficiency * line length * More collapsing * Cleanup and docstrings --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/witness/__init__.py | 31 +-- worlds/witness/options.py | 11 +- worlds/witness/test/__init__.py | 161 +++++++++++++++ worlds/witness/test/test_auto_elevators.py | 66 +++++++ .../test/test_disable_non_randomized.py | 37 ++++ worlds/witness/test/test_door_shuffle.py | 24 +++ worlds/witness/test/test_ep_shuffle.py | 54 +++++ worlds/witness/test/test_lasers.py | 185 ++++++++++++++++++ .../witness/test/test_roll_other_options.py | 58 ++++++ worlds/witness/test/test_symbol_shuffle.py | 74 +++++++ 10 files changed, 685 insertions(+), 16 deletions(-) create mode 100644 worlds/witness/test/__init__.py create mode 100644 worlds/witness/test/test_auto_elevators.py create mode 100644 worlds/witness/test/test_disable_non_randomized.py create mode 100644 worlds/witness/test/test_door_shuffle.py create mode 100644 worlds/witness/test/test_ep_shuffle.py create mode 100644 worlds/witness/test/test_lasers.py create mode 100644 worlds/witness/test/test_roll_other_options.py create mode 100644 worlds/witness/test/test_symbol_shuffle.py diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 455c87d8e0d1..254064098db9 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -185,21 +185,22 @@ def create_regions(self) -> None: self.items_placed_early.append("Puzzle Skip") - # Pick an early item to place on the tutorial gate. - early_items = [ - item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() - ] - if early_items: - random_early_item = self.random.choice(early_items) - if self.options.puzzle_randomization == "sigma_expert": - # In Expert, only tag the item as early, rather than forcing it onto the gate. - self.multiworld.local_early_items[self.player][random_early_item] = 1 - else: - # Force the item onto the tutorial gate check and remove it from our random pool. - gate_item = self.create_item(random_early_item) - self.get_location("Tutorial Gate Open").place_locked_item(gate_item) - self.own_itempool.append(gate_item) - self.items_placed_early.append(random_early_item) + if self.options.early_symbol_item: + # Pick an early item to place on the tutorial gate. + early_items = [ + item for item in self.player_items.get_early_items() if item in self.player_items.get_mandatory_items() + ] + if early_items: + random_early_item = self.random.choice(early_items) + if self.options.puzzle_randomization == "sigma_expert": + # In Expert, only tag the item as early, rather than forcing it onto the gate. + self.multiworld.local_early_items[self.player][random_early_item] = 1 + else: + # Force the item onto the tutorial gate check and remove it from our random pool. + gate_item = self.create_item(random_early_item) + self.get_location("Tutorial Gate Open").place_locked_item(gate_item) + self.own_itempool.append(gate_item) + self.items_placed_early.append(random_early_item) # There are some really restrictive settings in The Witness. # They are rarely played, but when they are, we add some extra sphere 1 locations. diff --git a/worlds/witness/options.py b/worlds/witness/options.py index f51d86ba22f3..4855fc715933 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,7 +2,7 @@ from schema import And, Schema -from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle +from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility from .data import static_logic as static_witness_logic from .data.item_definition_classes import ItemCategory, WeightedItemDefinition @@ -35,6 +35,14 @@ class EarlyCaves(Choice): alias_on = 2 +class EarlySymbolItem(DefaultOnToggle): + """ + Put a random helpful symbol item on an early check, specifically Tutorial Gate Open if it is available early. + """ + + visibility = Visibility.none + + class ShuffleSymbols(DefaultOnToggle): """ If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. @@ -325,6 +333,7 @@ class TheWitnessOptions(PerGameCommonOptions): mountain_lasers: MountainLasers challenge_lasers: ChallengeLasers early_caves: EarlyCaves + early_symbol_item: EarlySymbolItem elevators_come_to_you: ElevatorsComeToYou trap_percentage: TrapPercentage trap_weights: TrapWeights diff --git a/worlds/witness/test/__init__.py b/worlds/witness/test/__init__.py new file mode 100644 index 000000000000..0a24467feab2 --- /dev/null +++ b/worlds/witness/test/__init__.py @@ -0,0 +1,161 @@ +from test.bases import WorldTestBase +from test.general import gen_steps, setup_multiworld +from test.multiworld.test_multiworlds import MultiworldTestBase +from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast + +from BaseClasses import CollectionState, Entrance, Item, Location, Region + +from .. import WitnessWorld + + +class WitnessTestBase(WorldTestBase): + game = "The Witness" + player: ClassVar[int] = 1 + + world: WitnessWorld + + def can_beat_game_with_items(self, items: Iterable[Item]) -> bool: + """ + Check that the items listed are enough to beat the game. + """ + + state = CollectionState(self.multiworld) + for item in items: + state.collect(item) + return state.multiworld.can_beat_game(state) + + def assert_dependency_on_event_item(self, spot: Union[Location, Region, Entrance], item_name: str) -> None: + """ + WorldTestBase.assertAccessDependency, but modified & simplified to work with event items + """ + event_items = [item for item in self.multiworld.get_items() if item.name == item_name] + self.assertTrue(event_items, f"Event item {item_name} does not exist.") + + event_locations = [cast(Location, event_item.location) for event_item in event_items] + + # Checking for an access dependency on an event item requires a bit of extra work, + # as state.remove forces a sweep, which will pick up the event item again right after we tried to remove it. + # So, we temporarily set the access rules of the event locations to be impossible. + original_rules = {event_location.name: event_location.access_rule for event_location in event_locations} + for event_location in event_locations: + event_location.access_rule = lambda _: False + + # We can't use self.assertAccessDependency here, it doesn't work for event items. (As of 2024-06-30) + test_state = self.multiworld.get_all_state(False) + + self.assertFalse(spot.can_reach(test_state), f"{spot.name} is reachable without {item_name}") + + test_state.collect(event_items[0]) + + self.assertTrue(spot.can_reach(test_state), f"{spot.name} is not reachable despite having {item_name}") + + # Restore original access rules. + for event_location in event_locations: + event_location.access_rule = original_rules[event_location.name] + + def assert_location_exists(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, also make sure that this (non-event) location COULD exist. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + try: + self.world.get_location(location_name) + except KeyError: + self.fail(f"Location {location_name} does not exist.") + + def assert_location_does_not_exist(self, location_name: str, strict_check: bool = True) -> None: + """ + Assert that a location exists in this world. + If strict_check, be explicit about whether the location could exist in the first place. + """ + + if strict_check: + self.assertIn(location_name, self.world.location_name_to_id, f"Location {location_name} can never exist") + + self.assertRaises( + KeyError, + lambda _: self.world.get_location(location_name), + f"Location {location_name} exists, but is not supposed to.", + ) + + def assert_can_beat_with_minimally(self, required_item_counts: Mapping[str, int]) -> None: + """ + Assert that the specified mapping of items is enough to beat the game, + and that having one less of any item would result in the game being unbeatable. + """ + # Find the actual items + found_items = [item for item in self.multiworld.get_items() if item.name in required_item_counts] + actual_items: Dict[str, List[Item]] = {item_name: [] for item_name in required_item_counts} + for item in found_items: + if len(actual_items[item.name]) < required_item_counts[item.name]: + actual_items[item.name].append(item) + + # Assert that enough items exist in the item pool to satisfy the specified required counts + for item_name, item_objects in actual_items.items(): + self.assertEqual( + len(item_objects), + required_item_counts[item_name], + f"Couldn't find {required_item_counts[item_name]} copies of item {item_name} available in the pool, " + f"only found {len(item_objects)}", + ) + + # assert that multiworld is beatable with the items specified + self.assertTrue( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Could not beat game with items: {required_item_counts}", + ) + + # assert that one less copy of any item would result in the multiworld being unbeatable + for item_name, item_objects in actual_items.items(): + with self.subTest(f"Verify cannot beat game with one less copy of {item_name}"): + removed_item = item_objects.pop() + self.assertFalse( + self.can_beat_game_with_items(item for items in actual_items.values() for item in items), + f"Game was beatable despite having {len(item_objects)} copies of {item_name} " + f"instead of the specified {required_item_counts[item_name]}", + ) + item_objects.append(removed_item) + + +class WitnessMultiworldTestBase(MultiworldTestBase): + options_per_world: List[Dict[str, Any]] + common_options: Dict[str, Any] = {} + + def setUp(self) -> None: + """ + Set up a multiworld with multiple players, each using different options. + """ + + self.multiworld = setup_multiworld([WitnessWorld] * len(self.options_per_world), ()) + + for world, options in zip(self.multiworld.worlds.values(), self.options_per_world): + for option_name, option_value in {**self.common_options, **options}.items(): + option = getattr(world.options, option_name) + self.assertIsNotNone(option) + + option.value = option.from_any(option_value).value + + self.assertSteps(gen_steps) + + def collect_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Collect all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + items = self.get_items_by_name(item_names, player) + for item in items: + self.multiworld.state.collect(item) + return items + + def get_items_by_name(self, item_names: Union[str, Iterable[str]], player: int) -> List[Item]: + """ + Return all copies of a specified item name (or list of item names) for a player in the multiworld item pool. + """ + + if isinstance(item_names, str): + item_names = (item_names,) + return [item for item in self.multiworld.itempool if item.name in item_names and item.player == player] diff --git a/worlds/witness/test/test_auto_elevators.py b/worlds/witness/test/test_auto_elevators.py new file mode 100644 index 000000000000..16b1b5a56d37 --- /dev/null +++ b/worlds/witness/test/test_auto_elevators.py @@ -0,0 +1,66 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestElevatorsComeToYou(WitnessTestBase): + options = { + "elevators_come_to_you": True, + "shuffle_doors": "mixed", + "shuffle_symbols": False, + } + + def test_bunker_laser(self) -> None: + """ + In elevators_come_to_you, Bunker can be entered from the back. + This means that you can access the laser with just Bunker Elevator Control (Panel). + It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door. + """ + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) + + self.collect_by_name("Bunker Elevator Control (Panel)") + + self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player)) + self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) + + self.collect_by_name("Bunker Elevator Room Entry (Door)") + self.collect_by_name("Bunker Drop-Down Door Controls (Panel)") + + self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player)) + + +class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase): + options_per_world = [ + { + "elevators_come_to_you": False, + }, + { + "elevators_come_to_you": True, + }, + { + "elevators_come_to_you": False, + }, + ] + + common_options = { + "shuffle_symbols": False, + "shuffle_doors": "panels", + } + + def test_correct_access_per_player(self) -> None: + """ + Test that in a multiworld with players that alternate the elevators_come_to_you option, + the actual behavior alternates as well and doesn't bleed over from slot to slot. + (This is essentially a "does connection info bleed over" test). + """ + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) + + self.collect_by_name(["Bunker Elevator Control (Panel)"], 1) + self.collect_by_name(["Bunker Elevator Control (Panel)"], 2) + self.collect_by_name(["Bunker Elevator Control (Panel)"], 3) + + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1)) + self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2)) + self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3)) diff --git a/worlds/witness/test/test_disable_non_randomized.py b/worlds/witness/test/test_disable_non_randomized.py new file mode 100644 index 000000000000..e7cb1597b2ba --- /dev/null +++ b/worlds/witness/test/test_disable_non_randomized.py @@ -0,0 +1,37 @@ +from ..rules import _has_lasers +from ..test import WitnessTestBase + + +class TestDisableNonRandomized(WitnessTestBase): + options = { + "disable_non_randomized_puzzles": True, + "shuffle_doors": "panels", + "early_symbol_item": False, + } + + def test_locations_got_disabled_and_alternate_activation_triggers_work(self) -> None: + """ + Test the different behaviors of the disable_non_randomized mode: + + 1. Unrandomized locations like Orchard Apple Tree 5 are disabled. + 2. Certain doors or lasers that would usually be activated by unrandomized panels depend on event items instead. + 3. These alternate activations are tied to solving Discarded Panels. + """ + + with self.subTest("Test that unrandomized locations are disabled."): + self.assert_location_does_not_exist("Orchard Apple Tree 5") + + with self.subTest("Test that alternate activation trigger events exist."): + self.assert_dependency_on_event_item( + self.world.get_entrance("Town Tower After Third Door to Town Tower Top"), + "Town Tower 4th Door Opens", + ) + + with self.subTest("Test that alternate activation triggers award lasers."): + self.assertFalse(_has_lasers(1, self.world, False)(self.multiworld.state)) + + self.collect_by_name("Triangles") + + # Alternate triggers yield Bunker Laser (Mountainside Discard) and Monastery Laser (Desert Discard) + self.assertTrue(_has_lasers(2, self.world, False)(self.multiworld.state)) + self.assertFalse(_has_lasers(3, self.world, False)(self.multiworld.state)) diff --git a/worlds/witness/test/test_door_shuffle.py b/worlds/witness/test/test_door_shuffle.py new file mode 100644 index 000000000000..0e38c32d69e2 --- /dev/null +++ b/worlds/witness/test/test_door_shuffle.py @@ -0,0 +1,24 @@ +from ..test import WitnessTestBase + + +class TestIndividualDoors(WitnessTestBase): + options = { + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_swamp_laser_shortcut(self) -> None: + """ + Test that Door Shuffle grants early access to Swamp Laser from the back shortcut. + """ + + self.assertTrue(self.get_items_by_name("Swamp Laser Shortcut (Door)")) + + self.assertAccessDependency( + ["Swamp Laser Panel"], + [ + ["Swamp Laser Shortcut (Door)"], + ["Swamp Red Underwater Exit (Door)"], + ], + only_check_listed=True, + ) diff --git a/worlds/witness/test/test_ep_shuffle.py b/worlds/witness/test/test_ep_shuffle.py new file mode 100644 index 000000000000..342390916675 --- /dev/null +++ b/worlds/witness/test/test_ep_shuffle.py @@ -0,0 +1,54 @@ +from ..test import WitnessTestBase + + +class TestIndividualEPs(WitnessTestBase): + options = { + "shuffle_EPs": "individual", + "EP_difficulty": "normal", + "obelisk_keys": True, + "disable_non_randomized_puzzles": True, + "shuffle_postgame": False, + "victory_condition": "mountain_box_short", + "early_caves": "off", + } + + def test_correct_eps_exist_and_are_locked(self) -> None: + """ + Test that EP locations exist in shuffle_EPs, but only the ones that actually should (based on options) + """ + + # Test Tutorial First Hallways EP as a proxy for "EPs exist at all" + # Don't wrap in a subtest - If this fails, there is no point. + self.assert_location_exists("Tutorial First Hallway EP") + + with self.subTest("Test that disable_non_randomized disables Monastery Garden Left EP"): + self.assert_location_does_not_exist("Monastery Garden Left EP") + + with self.subTest("Test that shuffle_postgame being off disables postgame EPs."): + self.assert_location_does_not_exist("Caves Skylight EP") + + with self.subTest("Test that ep_difficulty being set to normal excludes tedious EPs."): + self.assert_location_does_not_exist("Shipwreck Couch EP") + + with self.subTest("Test that EPs are being locked by Obelisk Keys."): + self.assertAccessDependency(["Desert Sand Snake EP"], [["Desert Obelisk Key"]], True) + + +class TestObeliskSides(WitnessTestBase): + options = { + "shuffle_EPs": "obelisk_sides", + "EP_difficulty": "eclipse", + "shuffle_vault_boxes": True, + "shuffle_postgame": True, + } + + def test_eclipse_required_for_town_side_6(self) -> None: + """ + Test that Obelisk Sides require the appropriate event items from the individual EPs. + Specifically, assert that Town Obelisk Side 6 needs Theater Eclipse EP. + This doubles as a test for Theater Eclipse EP existing with the right options. + """ + + self.assert_dependency_on_event_item( + self.world.get_location("Town Obelisk Side 6"), "Town Obelisk Side 6 - Theater Eclipse EP" + ) diff --git a/worlds/witness/test/test_lasers.py b/worlds/witness/test/test_lasers.py new file mode 100644 index 000000000000..f09897ce4053 --- /dev/null +++ b/worlds/witness/test/test_lasers.py @@ -0,0 +1,185 @@ +from ..test import WitnessTestBase + + +class TestSymbolsRequiredToWinElevatorNormal(WitnessTestBase): + options = { + "shuffle_lasers": True, + "puzzle_randomization": "sigma_normal", + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Sigma Normal Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Eraser": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorExpert(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "sigma_expert", + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Sigma Expert Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 2, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Negative Shapers": 1, + "Eraser": 1, + "Triangles": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "puzzle_randomization": "none", + } + + def test_symbols_to_win(self) -> None: + """ + In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires a very specific set of symbol items per puzzle randomization mode. + In this case, we check Vanilla Puzzles. + """ + + exact_requirement = { + "Monastery Laser": 1, + "Progressive Dots": 2, + "Progressive Stars": 2, + "Progressive Symmetry": 1, + "Black/White Squares": 1, + "Colored Squares": 1, + "Shapers": 1, + "Rotated Shapers": 1, + "Eraser": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestPanelsRequiredToWinElevator(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "panels", + "door_groupings": "off", + } + + def test_panels_to_win(self) -> None: + """ + In door panel shuffle , the only way to reach the Elevator is through Mountain Entry by descending the Mountain. + This requires some control panels for each of the Mountain Floors. + """ + + exact_requirement = { + "Desert Laser": 1, + "Town Desert Laser Redirect Control (Panel)": 1, + "Mountain Floor 1 Light Bridge (Panel)": 1, + "Mountain Floor 2 Light Bridge Near (Panel)": 1, + "Mountain Floor 2 Light Bridge Far (Panel)": 1, + "Mountain Floor 2 Elevator Control (Panel)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + +class TestDoorsRequiredToWinElevator(WitnessTestBase): + options = { + "shuffle_lasers": True, + "mountain_lasers": 1, + "victory_condition": "elevator", + "early_symbol_item": False, + "shuffle_symbols": False, + "shuffle_doors": "doors", + "door_groupings": "off", + } + + def test_doors_to_elevator_paths(self) -> None: + """ + In remote door shuffle, there are three ways to win. + + - Through the normal route (Mountain Entry -> Descend through Mountain -> Reach Bottom Floor) + - Through the Caves using the Caves Shortcuts (Caves -> Reach Bottom Floor) + - Through the Caves via Challenge (Tunnels -> Challenge -> Caves -> Reach Bottom Floor) + """ + + with self.subTest("Test Elevator victory in shuffle_doors through Mountain Entry."): + exact_requirement = { + "Monastery Laser": 1, + "Mountain Floor 1 Exit (Door)": 1, + "Mountain Floor 2 Staircase Near (Door)": 1, + "Mountain Floor 2 Staircase Far (Door)": 1, + "Mountain Floor 2 Exit (Door)": 1, + "Mountain Bottom Floor Giant Puzzle Exit (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Caves Shortcuts."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Caves Mountain Shortcut (Door)": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) + + with self.subTest("Test Elevator victory in shuffle_doors through Tunnels->Challenge->Caves."): + exact_requirement = { + "Monastery Laser": 1, # Elevator Panel itself has a laser lock + "Windmill Entry (Door)": 1, + "Tunnels Theater Shortcut (Door)": 1, + "Tunnels Entry (Door)": 1, + "Challenge Entry (Door)": 1, + "Caves Pillar Door": 1, + "Caves Entry (Door)": 1, + "Mountain Bottom Floor Rock (Door)": 1, + "Mountain Bottom Floor Pillars Room Entry (Door)": 1, + } + + self.assert_can_beat_with_minimally(exact_requirement) diff --git a/worlds/witness/test/test_roll_other_options.py b/worlds/witness/test/test_roll_other_options.py new file mode 100644 index 000000000000..71743c326038 --- /dev/null +++ b/worlds/witness/test/test_roll_other_options.py @@ -0,0 +1,58 @@ +from ..test import WitnessTestBase + +# These are just some random options combinations, just to catch whether I broke anything obvious + + +class TestExpertNonRandomizedEPs(WitnessTestBase): + options = { + "disable_non_randomized": True, + "puzzle_randomization": "sigma_expert", + "shuffle_EPs": "individual", + "ep_difficulty": "eclipse", + "victory_condition": "challenge", + "shuffle_discarded_panels": False, + "shuffle_boat": False, + } + + +class TestVanillaAutoElevatorsPanels(WitnessTestBase): + options = { + "puzzle_randomization": "none", + "elevators_come_to_you": True, + "shuffle_doors": "panels", + "victory_condition": "mountain_box_short", + "early_caves": True, + "shuffle_vault_boxes": True, + "mountain_lasers": 11, + } + + +class TestMiscOptions(WitnessTestBase): + options = { + "death_link": True, + "death_link_amnesty": 3, + "laser_hints": True, + "hint_amount": 40, + "area_hint_percentage": 100, + } + + +class TestMaxEntityShuffle(WitnessTestBase): + options = { + "shuffle_symbols": False, + "shuffle_doors": "mixed", + "shuffle_EPs": "individual", + "obelisk_keys": True, + "shuffle_lasers": "anywhere", + "victory_condition": "mountain_box_long", + } + + +class TestPostgameGroupedDoors(WitnessTestBase): + options = { + "shuffle_postgame": True, + "shuffle_discarded_panels": True, + "shuffle_doors": "doors", + "door_groupings": "regional", + "victory_condition": "elevator", + } diff --git a/worlds/witness/test/test_symbol_shuffle.py b/worlds/witness/test/test_symbol_shuffle.py new file mode 100644 index 000000000000..8012480075a7 --- /dev/null +++ b/worlds/witness/test/test_symbol_shuffle.py @@ -0,0 +1,74 @@ +from ..test import WitnessMultiworldTestBase, WitnessTestBase + + +class TestSymbols(WitnessTestBase): + options = { + "early_symbol_item": False, + } + + def test_progressive_symbols(self) -> None: + """ + Test that Dots & Full Dots are correctly replaced by 2x Progressive Dots, + and test that Dots puzzles and Full Dots puzzles require 1 and 2 copies of this item respectively. + """ + + progressive_dots = self.get_items_by_name("Progressive Dots") + self.assertEqual(len(progressive_dots), 2) + + self.assertFalse(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertFalse( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + self.collect(progressive_dots.pop()) + + self.assertTrue(self.multiworld.state.can_reach("Outside Tutorial Shed Row 5", "Location", self.player)) + self.assertTrue( + self.multiworld.state.can_reach("Outside Tutorial Outpost Entry Panel", "Location", self.player) + ) + + +class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase): + options_per_world = [ + { + "puzzle_randomization": "sigma_normal", + }, + { + "puzzle_randomization": "sigma_expert", + }, + { + "puzzle_randomization": "none", + }, + ] + + common_options = { + "shuffle_discarded_panels": True, + "early_symbol_item": False, + } + + def test_arrows_exist_and_are_required_in_expert_seeds_only(self) -> None: + """ + In sigma_expert, Discarded Panels require Arrows. + In sigma_normal, Discarded Panels require Triangles, and Arrows shouldn't exist at all as an item. + """ + + with self.subTest("Test that Arrows exist only in the expert seed."): + self.assertFalse(self.get_items_by_name("Arrows", 1)) + self.assertTrue(self.get_items_by_name("Arrows", 2)) + self.assertFalse(self.get_items_by_name("Arrows", 3)) + + with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."): + desert_discard = "0x17CE7" + triangles = frozenset({frozenset({"Triangles"})}) + arrows = frozenset({frozenset({"Arrows"})}) + + self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) + self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows) + self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) From 9b22458f44083e7dd54f261f6fd24db720d6de29 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 7 Jul 2024 16:04:25 +0300 Subject: [PATCH 12/78] Stardew Valley 6.x.x: The Content Update (#3478) Focus of the Update: Compatibility with Stardew Valley 1.6 Released on March 19th 2024 This includes randomization for pretty much all of the new content, including but not limited to - Raccoon Bundles - Booksanity - Skill Masteries - New Recipes, Craftables, Fish, Maps, Farm Type, Festivals and Quests This also includes a significant reorganisation of the code into "Content Packs", to allow for easier modularity of various game mechanics between the settings and the supported mods. This improves maintainability quite a bit. In addition to that, a few **very** requested new features have been introduced, although they weren't the focus of this update - Walnutsanity - Player Buffs - More customizability in settings, such as shorter special orders, ER without farmhouse - New Remixed Bundles --- worlds/stardew_valley/__init__.py | 99 ++- worlds/stardew_valley/bundles/bundle.py | 98 ++- worlds/stardew_valley/bundles/bundle_item.py | 38 +- worlds/stardew_valley/bundles/bundle_room.py | 25 +- worlds/stardew_valley/bundles/bundles.py | 144 ++-- worlds/stardew_valley/content/__init__.py | 107 +++ .../stardew_valley/content/content_packs.py | 31 + .../content/feature/__init__.py | 4 + .../content/feature/booksanity.py | 72 ++ .../content/feature/cropsanity.py | 42 + .../content/feature/fishsanity.py | 101 +++ .../content/feature/friendsanity.py | 139 +++ worlds/stardew_valley/content/game_content.py | 117 +++ worlds/stardew_valley/content/mod_registry.py | 7 + .../stardew_valley/content/mods/__init__.py | 0 .../stardew_valley/content/mods/archeology.py | 20 + .../content/mods/big_backpack.py | 7 + .../content/mods/boarding_house.py | 13 + .../stardew_valley/content/mods/deepwoods.py | 28 + .../content/mods/distant_lands.py | 17 + worlds/stardew_valley/content/mods/jasper.py | 14 + worlds/stardew_valley/content/mods/magic.py | 10 + .../stardew_valley/content/mods/npc_mods.py | 88 ++ .../stardew_valley/content/mods/skill_mods.py | 25 + .../content/mods/skull_cavern_elevator.py | 7 + worlds/stardew_valley/content/mods/sve.py | 126 +++ worlds/stardew_valley/content/mods/tractor.py | 7 + worlds/stardew_valley/content/override.py | 7 + worlds/stardew_valley/content/unpacking.py | 97 +++ .../content/vanilla/__init__.py | 0 worlds/stardew_valley/content/vanilla/base.py | 172 ++++ .../content/vanilla/ginger_island.py | 81 ++ .../content/vanilla/pelican_town.py | 393 +++++++++ .../content/vanilla/qi_board.py | 36 + .../content/vanilla/the_desert.py | 46 + .../content/vanilla/the_farm.py | 43 + .../content/vanilla/the_mines.py | 35 + worlds/stardew_valley/data/__init__.py | 2 - worlds/stardew_valley/data/artisan.py | 10 + worlds/stardew_valley/data/bundle_data.py | 221 ++++- worlds/stardew_valley/data/craftable_data.py | 163 ++-- worlds/stardew_valley/data/crops.csv | 41 - worlds/stardew_valley/data/crops_data.py | 50 -- worlds/stardew_valley/data/fish_data.py | 149 ++-- worlds/stardew_valley/data/game_item.py | 86 ++ worlds/stardew_valley/data/harvest.py | 66 ++ worlds/stardew_valley/data/items.csv | 123 ++- worlds/stardew_valley/data/locations.csv | 492 +++++++++-- worlds/stardew_valley/data/museum_data.py | 15 +- worlds/stardew_valley/data/recipe_data.py | 36 +- worlds/stardew_valley/data/recipe_source.py | 10 + worlds/stardew_valley/data/requirement.py | 31 + worlds/stardew_valley/data/shop.py | 40 + worlds/stardew_valley/data/skill.py | 9 + worlds/stardew_valley/data/villagers_data.py | 86 +- .../stardew_valley/docs/en_Stardew Valley.md | 101 +-- worlds/stardew_valley/docs/setup_en.md | 16 +- worlds/stardew_valley/early_items.py | 66 +- worlds/stardew_valley/items.py | 298 ++++--- worlds/stardew_valley/locations.py | 214 +++-- worlds/stardew_valley/logic/ability_logic.py | 1 + worlds/stardew_valley/logic/action_logic.py | 16 +- worlds/stardew_valley/logic/artisan_logic.py | 62 +- worlds/stardew_valley/logic/base_logic.py | 10 +- worlds/stardew_valley/logic/book_logic.py | 24 + worlds/stardew_valley/logic/buff_logic.py | 23 - worlds/stardew_valley/logic/building_logic.py | 12 +- worlds/stardew_valley/logic/bundle_logic.py | 36 +- worlds/stardew_valley/logic/combat_logic.py | 17 +- worlds/stardew_valley/logic/cooking_logic.py | 10 +- worlds/stardew_valley/logic/crafting_logic.py | 22 +- worlds/stardew_valley/logic/crop_logic.py | 72 -- worlds/stardew_valley/logic/farming_logic.py | 59 +- worlds/stardew_valley/logic/fishing_logic.py | 71 +- worlds/stardew_valley/logic/grind_logic.py | 74 ++ .../stardew_valley/logic/harvesting_logic.py | 56 ++ worlds/stardew_valley/logic/has_logic.py | 39 +- worlds/stardew_valley/logic/logic.py | 438 +++++----- .../logic/logic_and_mods_design.md | 19 +- worlds/stardew_valley/logic/logic_event.py | 33 + worlds/stardew_valley/logic/mine_logic.py | 17 +- worlds/stardew_valley/logic/money_logic.py | 35 +- worlds/stardew_valley/logic/monster_logic.py | 25 +- worlds/stardew_valley/logic/museum_logic.py | 21 +- worlds/stardew_valley/logic/pet_logic.py | 33 +- worlds/stardew_valley/logic/quality_logic.py | 33 + worlds/stardew_valley/logic/quest_logic.py | 20 +- worlds/stardew_valley/logic/received_logic.py | 17 +- worlds/stardew_valley/logic/region_logic.py | 10 +- .../logic/relationship_logic.py | 191 +++-- .../stardew_valley/logic/requirement_logic.py | 52 ++ worlds/stardew_valley/logic/season_logic.py | 29 +- worlds/stardew_valley/logic/shipping_logic.py | 6 +- worlds/stardew_valley/logic/skill_logic.py | 67 +- worlds/stardew_valley/logic/source_logic.py | 106 +++ .../logic/special_order_logic.py | 80 +- worlds/stardew_valley/logic/time_logic.py | 90 +- worlds/stardew_valley/logic/tool_logic.py | 21 +- .../mods/logic/deepwoods_logic.py | 14 +- .../stardew_valley/mods/logic/item_logic.py | 95 +-- .../mods/logic/mod_skills_levels.py | 13 +- .../stardew_valley/mods/logic/quests_logic.py | 4 +- .../stardew_valley/mods/logic/skills_logic.py | 38 +- .../mods/logic/special_orders_logic.py | 7 +- worlds/stardew_valley/mods/logic/sve_logic.py | 25 +- worlds/stardew_valley/mods/mod_data.py | 11 - worlds/stardew_valley/mods/mod_regions.py | 46 +- worlds/stardew_valley/option_groups.py | 125 +-- worlds/stardew_valley/options.py | 165 +++- worlds/stardew_valley/presets.py | 65 +- worlds/stardew_valley/region_classes.py | 34 +- worlds/stardew_valley/regions.py | 363 ++++---- worlds/stardew_valley/requirements.txt | 1 + worlds/stardew_valley/rules.py | 320 +++++-- .../scripts/export_locations.py | 8 +- worlds/stardew_valley/stardew_rule/base.py | 101 ++- .../stardew_rule/indirect_connection.py | 22 +- worlds/stardew_valley/stardew_rule/literal.py | 6 - .../stardew_valley/stardew_rule/protocol.py | 4 - .../stardew_rule/rule_explain.py | 164 ++++ worlds/stardew_valley/stardew_rule/state.py | 41 +- .../strings/ap_names/ap_option_names.py | 16 + .../strings/ap_names/buff_names.py | 12 +- .../ap_names/community_upgrade_names.py | 2 + .../strings/ap_names/event_names.py | 24 +- .../strings/ap_names/mods/mod_items.py | 10 +- .../strings/artisan_good_names.py | 39 + worlds/stardew_valley/strings/book_names.py | 65 ++ worlds/stardew_valley/strings/bundle_names.py | 164 ++-- .../stardew_valley/strings/craftable_names.py | 33 +- worlds/stardew_valley/strings/crop_names.py | 97 +-- .../stardew_valley/strings/currency_names.py | 3 + .../stardew_valley/strings/entrance_names.py | 53 +- .../strings/festival_check_names.py | 43 + worlds/stardew_valley/strings/fish_names.py | 166 ++-- worlds/stardew_valley/strings/food_names.py | 12 + .../strings/forageable_names.py | 42 +- .../stardew_valley/strings/machine_names.py | 8 + .../stardew_valley/strings/material_names.py | 1 + worlds/stardew_valley/strings/metal_names.py | 1 + .../strings/monster_drop_names.py | 4 + worlds/stardew_valley/strings/quest_names.py | 4 +- worlds/stardew_valley/strings/region_names.py | 47 +- worlds/stardew_valley/strings/season_names.py | 1 + worlds/stardew_valley/strings/seed_names.py | 49 +- worlds/stardew_valley/strings/skill_names.py | 2 + worlds/stardew_valley/strings/tool_names.py | 1 + .../strings/wallet_item_names.py | 1 + worlds/stardew_valley/test/TestBooksanity.py | 207 +++++ worlds/stardew_valley/test/TestBundles.py | 65 +- worlds/stardew_valley/test/TestData.py | 45 +- worlds/stardew_valley/test/TestFarmType.py | 31 + worlds/stardew_valley/test/TestFill.py | 30 + worlds/stardew_valley/test/TestFishsanity.py | 405 +++++++++ .../stardew_valley/test/TestFriendsanity.py | 159 ++++ worlds/stardew_valley/test/TestGeneration.py | 441 +--------- worlds/stardew_valley/test/TestItems.py | 77 +- worlds/stardew_valley/test/TestLogic.py | 111 +-- .../test/TestMultiplePlayers.py | 2 +- .../test/TestNumberLocations.py | 98 +++ worlds/stardew_valley/test/TestOptions.py | 200 +++-- .../stardew_valley/test/TestOptionsPairs.py | 2 +- worlds/stardew_valley/test/TestRegions.py | 13 +- worlds/stardew_valley/test/TestRules.py | 797 ------------------ worlds/stardew_valley/test/TestStardewRule.py | 83 +- .../stardew_valley/test/TestStartInventory.py | 8 +- .../stardew_valley/test/TestWalnutsanity.py | 209 +++++ worlds/stardew_valley/test/__init__.py | 434 ++++++---- .../test/assertion/mod_assert.py | 6 +- .../test/assertion/option_assert.py | 8 +- .../test/assertion/rule_assert.py | 46 +- .../test/assertion/rule_explain.py | 102 --- .../test/assertion/world_assert.py | 2 +- .../test/content/TestArtisanEquipment.py | 54 ++ .../test/content/TestGingerIsland.py | 55 ++ .../test/content/TestPelicanTown.py | 112 +++ .../test/content/TestQiBoard.py | 27 + .../stardew_valley/test/content/__init__.py | 23 + .../test/content/feature/TestFriendsanity.py | 33 + .../test/content/feature/__init__.py | 0 .../test/content/mods/TestDeepwoods.py | 14 + .../test/content/mods/TestJasper.py | 27 + .../test/content/mods/TestSVE.py | 143 ++++ .../test/content/mods/__init__.py | 0 .../stardew_valley/test/long/TestModsLong.py | 44 +- .../test/long/TestOptionsLong.py | 20 +- .../test/long/TestPreRolledRandomness.py | 5 +- .../test/long/TestRandomWorlds.py | 12 +- .../stardew_valley/test/mods/TestModFish.py | 226 ----- .../test/mods/TestModVillagers.py | 132 --- worlds/stardew_valley/test/mods/TestMods.py | 56 +- .../test/performance/TestPerformance.py | 70 +- .../stardew_valley/test/rules/TestArcades.py | 97 +++ .../test/rules/TestBuildings.py | 62 ++ .../stardew_valley/test/rules/TestBundles.py | 66 ++ .../test/rules/TestCookingRecipes.py | 83 ++ .../test/rules/TestCraftingRecipes.py | 123 +++ .../test/rules/TestDonations.py | 73 ++ .../test/rules/TestFriendship.py | 58 ++ .../stardew_valley/test/rules/TestMuseum.py | 16 + .../stardew_valley/test/rules/TestShipping.py | 82 ++ .../stardew_valley/test/rules/TestSkills.py | 40 + .../test/rules/TestStateRules.py | 12 + worlds/stardew_valley/test/rules/TestTools.py | 141 ++++ .../stardew_valley/test/rules/TestWeapons.py | 75 ++ worlds/stardew_valley/test/rules/__init__.py | 0 worlds/stardew_valley/test/script/__init__.py | 0 .../test/script/benchmark_locations.py | 140 +++ .../test/stability/StabilityOutputScript.py | 4 +- .../test/stability/TestStability.py | 12 +- 210 files changed, 10308 insertions(+), 4550 deletions(-) create mode 100644 worlds/stardew_valley/content/__init__.py create mode 100644 worlds/stardew_valley/content/content_packs.py create mode 100644 worlds/stardew_valley/content/feature/__init__.py create mode 100644 worlds/stardew_valley/content/feature/booksanity.py create mode 100644 worlds/stardew_valley/content/feature/cropsanity.py create mode 100644 worlds/stardew_valley/content/feature/fishsanity.py create mode 100644 worlds/stardew_valley/content/feature/friendsanity.py create mode 100644 worlds/stardew_valley/content/game_content.py create mode 100644 worlds/stardew_valley/content/mod_registry.py create mode 100644 worlds/stardew_valley/content/mods/__init__.py create mode 100644 worlds/stardew_valley/content/mods/archeology.py create mode 100644 worlds/stardew_valley/content/mods/big_backpack.py create mode 100644 worlds/stardew_valley/content/mods/boarding_house.py create mode 100644 worlds/stardew_valley/content/mods/deepwoods.py create mode 100644 worlds/stardew_valley/content/mods/distant_lands.py create mode 100644 worlds/stardew_valley/content/mods/jasper.py create mode 100644 worlds/stardew_valley/content/mods/magic.py create mode 100644 worlds/stardew_valley/content/mods/npc_mods.py create mode 100644 worlds/stardew_valley/content/mods/skill_mods.py create mode 100644 worlds/stardew_valley/content/mods/skull_cavern_elevator.py create mode 100644 worlds/stardew_valley/content/mods/sve.py create mode 100644 worlds/stardew_valley/content/mods/tractor.py create mode 100644 worlds/stardew_valley/content/override.py create mode 100644 worlds/stardew_valley/content/unpacking.py create mode 100644 worlds/stardew_valley/content/vanilla/__init__.py create mode 100644 worlds/stardew_valley/content/vanilla/base.py create mode 100644 worlds/stardew_valley/content/vanilla/ginger_island.py create mode 100644 worlds/stardew_valley/content/vanilla/pelican_town.py create mode 100644 worlds/stardew_valley/content/vanilla/qi_board.py create mode 100644 worlds/stardew_valley/content/vanilla/the_desert.py create mode 100644 worlds/stardew_valley/content/vanilla/the_farm.py create mode 100644 worlds/stardew_valley/content/vanilla/the_mines.py create mode 100644 worlds/stardew_valley/data/artisan.py delete mode 100644 worlds/stardew_valley/data/crops.csv delete mode 100644 worlds/stardew_valley/data/crops_data.py create mode 100644 worlds/stardew_valley/data/game_item.py create mode 100644 worlds/stardew_valley/data/harvest.py create mode 100644 worlds/stardew_valley/data/requirement.py create mode 100644 worlds/stardew_valley/data/shop.py create mode 100644 worlds/stardew_valley/data/skill.py create mode 100644 worlds/stardew_valley/logic/book_logic.py delete mode 100644 worlds/stardew_valley/logic/buff_logic.py delete mode 100644 worlds/stardew_valley/logic/crop_logic.py create mode 100644 worlds/stardew_valley/logic/grind_logic.py create mode 100644 worlds/stardew_valley/logic/harvesting_logic.py create mode 100644 worlds/stardew_valley/logic/logic_event.py create mode 100644 worlds/stardew_valley/logic/quality_logic.py create mode 100644 worlds/stardew_valley/logic/requirement_logic.py create mode 100644 worlds/stardew_valley/logic/source_logic.py create mode 100644 worlds/stardew_valley/stardew_rule/rule_explain.py create mode 100644 worlds/stardew_valley/strings/ap_names/ap_option_names.py create mode 100644 worlds/stardew_valley/strings/book_names.py create mode 100644 worlds/stardew_valley/test/TestBooksanity.py create mode 100644 worlds/stardew_valley/test/TestFarmType.py create mode 100644 worlds/stardew_valley/test/TestFill.py create mode 100644 worlds/stardew_valley/test/TestFishsanity.py create mode 100644 worlds/stardew_valley/test/TestFriendsanity.py create mode 100644 worlds/stardew_valley/test/TestNumberLocations.py delete mode 100644 worlds/stardew_valley/test/TestRules.py create mode 100644 worlds/stardew_valley/test/TestWalnutsanity.py delete mode 100644 worlds/stardew_valley/test/assertion/rule_explain.py create mode 100644 worlds/stardew_valley/test/content/TestArtisanEquipment.py create mode 100644 worlds/stardew_valley/test/content/TestGingerIsland.py create mode 100644 worlds/stardew_valley/test/content/TestPelicanTown.py create mode 100644 worlds/stardew_valley/test/content/TestQiBoard.py create mode 100644 worlds/stardew_valley/test/content/__init__.py create mode 100644 worlds/stardew_valley/test/content/feature/TestFriendsanity.py create mode 100644 worlds/stardew_valley/test/content/feature/__init__.py create mode 100644 worlds/stardew_valley/test/content/mods/TestDeepwoods.py create mode 100644 worlds/stardew_valley/test/content/mods/TestJasper.py create mode 100644 worlds/stardew_valley/test/content/mods/TestSVE.py create mode 100644 worlds/stardew_valley/test/content/mods/__init__.py delete mode 100644 worlds/stardew_valley/test/mods/TestModFish.py delete mode 100644 worlds/stardew_valley/test/mods/TestModVillagers.py create mode 100644 worlds/stardew_valley/test/rules/TestArcades.py create mode 100644 worlds/stardew_valley/test/rules/TestBuildings.py create mode 100644 worlds/stardew_valley/test/rules/TestBundles.py create mode 100644 worlds/stardew_valley/test/rules/TestCookingRecipes.py create mode 100644 worlds/stardew_valley/test/rules/TestCraftingRecipes.py create mode 100644 worlds/stardew_valley/test/rules/TestDonations.py create mode 100644 worlds/stardew_valley/test/rules/TestFriendship.py create mode 100644 worlds/stardew_valley/test/rules/TestMuseum.py create mode 100644 worlds/stardew_valley/test/rules/TestShipping.py create mode 100644 worlds/stardew_valley/test/rules/TestSkills.py create mode 100644 worlds/stardew_valley/test/rules/TestStateRules.py create mode 100644 worlds/stardew_valley/test/rules/TestTools.py create mode 100644 worlds/stardew_valley/test/rules/TestWeapons.py create mode 100644 worlds/stardew_valley/test/rules/__init__.py create mode 100644 worlds/stardew_valley/test/script/__init__.py create mode 100644 worlds/stardew_valley/test/script/benchmark_locations.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 61c866631690..07235ad2983a 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,12 +1,13 @@ import logging from typing import Dict, Any, Iterable, Optional, Union, List, TextIO -from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld +from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState from Options import PerGameCommonOptions from worlds.AutoWorld import World, WebWorld from . import rules from .bundles.bundle_room import BundleRoom from .bundles.bundles import get_all_bundles +from .content import content_packs, StardewContent, unpack_content, create_content from .early_items import setup_early_items from .items import item_table, create_items, ItemData, Group, items_by_group, get_all_filler_items, remove_limited_amount_packs from .locations import location_table, create_locations, LocationData, locations_by_tag @@ -14,16 +15,17 @@ from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS from .option_groups import sv_option_groups -from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, NumberOfLuckBuffs, NumberOfMovementBuffs, \ - BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization +from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, BundlePrice, EnabledFillerBuffs, NumberOfMovementBuffs, \ + BackpackProgression, BuildingProgression, ExcludeGingerIsland, TrapItems, EntranceRandomization, FarmType, Walnutsanity from .presets import sv_options_presets from .regions import create_regions from .rules import set_rules -from .stardew_rule import True_, StardewRule, HasProgressionPercent +from .stardew_rule import True_, StardewRule, HasProgressionPercent, true_ from .strings.ap_names.event_names import Event from .strings.entrance_names import Entrance as EntranceName from .strings.goal_names import Goal as GoalName -from .strings.region_names import Region as RegionName +from .strings.metal_names import Ore +from .strings.region_names import Region as RegionName, LogicRegion client_version = 0 @@ -77,6 +79,7 @@ class StardewValleyWorld(World): options_dataclass = StardewValleyOptions options: StardewValleyOptions + content: StardewContent logic: StardewLogic web = StardewWebWorld() @@ -94,6 +97,7 @@ def __init__(self, multiworld: MultiWorld, player: int): def generate_early(self): self.force_change_options_if_incompatible() + self.content = create_content(self.options) def force_change_options_if_incompatible(self): goal_is_walnut_hunter = self.options.goal == Goal.option_greatest_walnut_hunter @@ -106,6 +110,11 @@ def force_change_options_if_incompatible(self): player_name = self.multiworld.player_name[self.player] logging.warning( f"Goal '{goal_name}' requires Ginger Island. Exclude Ginger Island setting forced to 'False' for player {self.player} ({player_name})") + if exclude_ginger_island and self.options.walnutsanity != Walnutsanity.preset_none: + self.options.walnutsanity.value = Walnutsanity.preset_none + player_name = self.multiworld.player_name[self.player] + logging.warning( + f"Walnutsanity requires Ginger Island. Ginger Island was excluded from {self.player} ({player_name})'s world, so walnutsanity was force disabled") def create_regions(self): def create_region(name: str, exits: Iterable[str]) -> Region: @@ -115,9 +124,10 @@ def create_region(name: str, exits: Iterable[str]) -> Region: world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options) - self.logic = StardewLogic(self.player, self.options, world_regions.keys()) + self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.modified_bundles = get_all_bundles(self.random, self.logic, + self.content, self.options) def add_location(name: str, code: Optional[int], region: str): @@ -125,11 +135,12 @@ def add_location(name: str, code: Optional[int], region: str): location = StardewLocation(self.player, name, code, region) region.locations.append(location) - create_locations(add_location, self.modified_bundles, self.options, self.random) + create_locations(add_location, self.modified_bundles, self.options, self.content, self.random) self.multiworld.regions.extend(world_regions.values()) def create_items(self): self.precollect_starting_season() + self.precollect_farm_type_items() items_to_exclude = [excluded_items for excluded_items in self.multiworld.precollected_items[self.player] if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, @@ -143,7 +154,7 @@ def create_items(self): for location in self.multiworld.get_locations(self.player) if location.address is not None]) - created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, + created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options, self.content, self.random) self.multiworld.itempool += created_items @@ -173,10 +184,15 @@ def precollect_starting_season(self): starting_season = self.create_starting_item(self.random.choice(season_pool)) self.multiworld.push_precollected(starting_season) + def precollect_farm_type_items(self): + if self.options.farm_type == FarmType.option_meadowlands and self.options.building_progression & BuildingProgression.option_progressive: + self.multiworld.push_precollected(self.create_starting_item("Progressive Coop")) + def setup_player_events(self): self.setup_construction_events() self.setup_quest_events() self.setup_action_events() + self.setup_logic_events() def setup_construction_events(self): can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings) @@ -187,10 +203,26 @@ def setup_quest_events(self): self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest) def setup_action_events(self): - can_ship_event = LocationData(None, RegionName.shipping, Event.can_ship_items) - self.create_event_location(can_ship_event, True_(), Event.can_ship_items) + can_ship_event = LocationData(None, LogicRegion.shipping, Event.can_ship_items) + self.create_event_location(can_ship_event, true_, Event.can_ship_items) can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre) - self.create_event_location(can_shop_pierre_event, True_(), Event.can_shop_at_pierre) + self.create_event_location(can_shop_pierre_event, true_, Event.can_shop_at_pierre) + + spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming) + self.create_event_location(spring_farming, true_, Event.spring_farming) + summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming) + self.create_event_location(summer_farming, true_, Event.summer_farming) + fall_farming = LocationData(None, LogicRegion.fall_farming, Event.fall_farming) + self.create_event_location(fall_farming, true_, Event.fall_farming) + winter_farming = LocationData(None, LogicRegion.winter_farming, Event.winter_farming) + self.create_event_location(winter_farming, true_, Event.winter_farming) + + def setup_logic_events(self): + def register_event(name: str, region: str, rule: StardewRule): + event_location = LocationData(None, region, name) + self.create_event_location(event_location, rule, name) + + self.logic.setup_events(register_event) def setup_victory(self): if self.options.goal == Goal.option_community_center: @@ -211,7 +243,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_master_angler: self.create_event_location(location_table[GoalName.master_angler], - self.logic.fishing.can_catch_every_fish_in_slot(self.get_all_location_names()), + self.logic.fishing.can_catch_every_fish_for_fishsanity(), Event.victory) elif self.options.goal == Goal.option_complete_collection: self.create_event_location(location_table[GoalName.complete_museum], @@ -270,18 +302,13 @@ def create_item(self, item: Union[str, ItemData], override_classification: ItemC if override_classification is None: override_classification = item.classification - if override_classification == ItemClassification.progression and item.name != Event.victory: + if override_classification == ItemClassification.progression: self.total_progression_items += 1 - # if item.name not in self.all_progression_items: - # self.all_progression_items[item.name] = 0 - # self.all_progression_items[item.name] += 1 return StardewItem(item.name, override_classification, item.code, self.player) def delete_item(self, item: Item): if item.classification & ItemClassification.progression: self.total_progression_items -= 1 - # if item.name in self.all_progression_items: - # self.all_progression_items[item.name] -= 1 def create_starting_item(self, item: Union[str, ItemData]) -> StardewItem: if isinstance(item, str): @@ -299,7 +326,11 @@ def create_event_location(self, location_data: LocationData, rule: StardewRule = location = StardewLocation(self.player, location_data.name, None, region) location.access_rule = rule region.locations.append(location) - location.place_locked_item(self.create_item(item)) + location.place_locked_item(StardewItem(item, ItemClassification.progression, None, self.player)) + + # This is not ideal, but the rule count them so... + if item != Event.victory: + self.total_progression_items += 1 def set_rules(self): set_rules(self) @@ -358,7 +389,7 @@ def add_bundles_to_spoiler_log(self, spoiler_handle: TextIO): quality = "" else: quality = f" ({item.quality.split(' ')[0]})" - spoiler_handle.write(f"\t\t{item.amount}x {item.item_name}{quality}\n") + spoiler_handle.write(f"\t\t{item.amount}x {item.get_item()}{quality}\n") def add_entrances_to_spoiler_log(self): if self.options.entrance_randomization == EntranceRandomization.option_disabled: @@ -373,9 +404,9 @@ def fill_slot_data(self) -> Dict[str, Any]: for bundle in room.bundles: bundles[room.name][bundle.name] = {"number_required": bundle.number_required} for i, item in enumerate(bundle.items): - bundles[room.name][bundle.name][i] = f"{item.item_name}|{item.amount}|{item.quality}" + bundles[room.name][bundle.name][i] = f"{item.get_item()}|{item.amount}|{item.quality}" - excluded_options = [BundleRandomization, NumberOfMovementBuffs, NumberOfLuckBuffs] + excluded_options = [BundleRandomization, NumberOfMovementBuffs, EnabledFillerBuffs] excluded_option_names = [option.internal_name for option in excluded_options] generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints] excluded_option_names.extend(generic_option_names) @@ -385,7 +416,29 @@ def fill_slot_data(self) -> Dict[str, Any]: "seed": self.random.randrange(1000000000), # Seed should be max 9 digits "randomized_entrances": self.randomized_entrances, "modified_bundles": bundles, - "client_version": "5.0.0", + "client_version": "6.0.0", }) return slot_data + + def collect(self, state: CollectionState, item: StardewItem) -> bool: + change = super().collect(state, item) + if change: + state.prog_items[self.player][Event.received_walnuts] += self.get_walnut_amount(item.name) + return change + + def remove(self, state: CollectionState, item: StardewItem) -> bool: + change = super().remove(state, item) + if change: + state.prog_items[self.player][Event.received_walnuts] -= self.get_walnut_amount(item.name) + return change + + @staticmethod + def get_walnut_amount(item_name: str) -> int: + if item_name == "Golden Walnut": + return 1 + if item_name == "3 Golden Walnuts": + return 3 + if item_name == "5 Golden Walnuts": + return 5 + return 0 diff --git a/worlds/stardew_valley/bundles/bundle.py b/worlds/stardew_valley/bundles/bundle.py index 199826b96bc8..43afc750b87a 100644 --- a/worlds/stardew_valley/bundles/bundle.py +++ b/worlds/stardew_valley/bundles/bundle.py @@ -1,8 +1,10 @@ +import math from dataclasses import dataclass from random import Random -from typing import List +from typing import List, Tuple from .bundle_item import BundleItem +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions, ExcludeGingerIsland, FestivalLocations from ..strings.currency_names import Currency @@ -26,7 +28,8 @@ class BundleTemplate: number_possible_items: int number_required_items: int - def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, items: List[BundleItem], number_possible_items: int, + number_required_items: int): self.room = room self.name = name self.items = items @@ -35,17 +38,12 @@ def __init__(self, room: str, name: str, items: List[BundleItem], number_possibl @staticmethod def extend_from(template, items: List[BundleItem]): - return BundleTemplate(template.room, template.name, items, template.number_possible_items, template.number_required_items) + return BundleTemplate(template.room, template.name, items, template.number_possible_items, + template.number_required_items) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value - number_required = max(1, number_required) - filtered_items = [item for item in self.items if item.can_appear(options)] + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) + filtered_items = [item for item in self.items if item.can_appear(content, options)] number_items = len(filtered_items) number_chosen_items = self.number_possible_items if number_chosen_items < number_required: @@ -55,6 +53,7 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option chosen_items = filtered_items + random.choices(filtered_items, k=number_chosen_items - number_items) else: chosen_items = random.sample(filtered_items, number_chosen_items) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) def can_appear(self, options: StardewValleyOptions) -> bool: @@ -68,19 +67,13 @@ def __init__(self, room: str, name: str, item: BundleItem): super().__init__(room, name, [item], 1, 1) self.item = item - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) return Bundle(self.room, self.name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount def can_appear(self, options: StardewValleyOptions) -> bool: @@ -95,11 +88,11 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class MoneyBundleTemplate(CurrencyBundleTemplate): - def __init__(self, room: str, item: BundleItem): - super().__init__(room, "", item) + def __init__(self, room: str, default_name: str, item: BundleItem): + super().__init__(room, default_name, item) - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - currency_amount = self.get_currency_amount(bundle_price_option) + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + currency_amount = self.get_currency_amount(options.bundle_price) currency_name = "g" if currency_amount >= 1000: unit_amount = currency_amount % 1000 @@ -111,13 +104,8 @@ def create_bundle(self, bundle_price_option: BundlePrice, random: Random, option return Bundle(self.room, name, [BundleItem(self.item.item_name, currency_amount)], 1) def get_currency_amount(self, bundle_price_option: BundlePrice): - if bundle_price_option == BundlePrice.option_minimum: - price_multiplier = 0.1 - elif bundle_price_option == BundlePrice.option_maximum: - price_multiplier = 4 - else: - price_multiplier = round(1 + (bundle_price_option.value * 0.4), 2) - currency_amount = int(self.item.amount * price_multiplier) + _, price_multiplier = get_bundle_final_prices(bundle_price_option, self.number_required_items, True) + currency_amount = max(1, int(self.item.amount * price_multiplier)) return currency_amount @@ -134,30 +122,54 @@ def can_appear(self, options: StardewValleyOptions) -> bool: class DeepBundleTemplate(BundleTemplate): categories: List[List[BundleItem]] - def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, number_required_items: int): + def __init__(self, room: str, name: str, categories: List[List[BundleItem]], number_possible_items: int, + number_required_items: int): super().__init__(room, name, [], number_possible_items, number_required_items) self.categories = categories - def create_bundle(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions) -> Bundle: - if bundle_price_option == BundlePrice.option_minimum: - number_required = 1 - elif bundle_price_option == BundlePrice.option_maximum: - number_required = 8 - else: - number_required = self.number_required_items + bundle_price_option.value + def create_bundle(self, random: Random, content: StardewContent, options: StardewValleyOptions) -> Bundle: + number_required, price_multiplier = get_bundle_final_prices(options.bundle_price, self.number_required_items, False) number_categories = len(self.categories) number_chosen_categories = self.number_possible_items if number_chosen_categories < number_required: number_chosen_categories = number_required if number_chosen_categories > number_categories: - chosen_categories = self.categories + random.choices(self.categories, k=number_chosen_categories - number_categories) + chosen_categories = self.categories + random.choices(self.categories, + k=number_chosen_categories - number_categories) else: chosen_categories = random.sample(self.categories, number_chosen_categories) chosen_items = [] for category in chosen_categories: - filtered_items = [item for item in category if item.can_appear(options)] + filtered_items = [item for item in category if item.can_appear(content, options)] chosen_items.append(random.choice(filtered_items)) + chosen_items = [item.as_amount(max(1, math.floor(item.amount * price_multiplier))) for item in chosen_items] return Bundle(self.room, self.name, chosen_items, number_required) + + +def get_bundle_final_prices(bundle_price_option: BundlePrice, default_required_items: int, is_currency: bool) -> Tuple[int, float]: + number_required_items = get_number_required_items(bundle_price_option, default_required_items) + price_multiplier = get_price_multiplier(bundle_price_option, is_currency) + return number_required_items, price_multiplier + + +def get_number_required_items(bundle_price_option: BundlePrice, default_required_items: int) -> int: + if bundle_price_option == BundlePrice.option_minimum: + return 1 + if bundle_price_option == BundlePrice.option_maximum: + return 8 + number_required = default_required_items + bundle_price_option.value + return min(8, max(1, number_required)) + + +def get_price_multiplier(bundle_price_option: BundlePrice, is_currency: bool) -> float: + if bundle_price_option == BundlePrice.option_minimum: + return 0.1 if is_currency else 0.2 + if bundle_price_option == BundlePrice.option_maximum: + return 4 if is_currency else 1.4 + price_factor = 0.4 if is_currency else (0.2 if bundle_price_option.value <= 0 else 0.1) + price_multiplier_difference = bundle_price_option.value * price_factor + price_multiplier = 1 + price_multiplier_difference + return round(price_multiplier, 2) diff --git a/worlds/stardew_valley/bundles/bundle_item.py b/worlds/stardew_valley/bundles/bundle_item.py index 8aaa67c5f242..7dc9c0e1a3b5 100644 --- a/worlds/stardew_valley/bundles/bundle_item.py +++ b/worlds/stardew_valley/bundles/bundle_item.py @@ -3,7 +3,8 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations +from ..content import StardewContent +from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations, SkillProgression from ..strings.crop_names import Fruit from ..strings.currency_names import Currency from ..strings.quality_names import CropQuality, FishQuality, ForageQuality @@ -30,27 +31,50 @@ def can_appear(self, options: StardewValleyOptions) -> bool: return options.festival_locations != FestivalLocations.option_disabled +class MasteryItemSource(BundleItemSource): + def can_appear(self, options: StardewValleyOptions) -> bool: + return options.skill_progression == SkillProgression.option_progressive_with_masteries + + +class ContentItemSource(BundleItemSource): + """This is meant to be used for items that are managed by the content packs.""" + + def can_appear(self, options: StardewValleyOptions) -> bool: + raise ValueError("This should not be called, check if the item is in the content instead.") + + @dataclass(frozen=True, order=True) class BundleItem: class Sources: vanilla = VanillaItemSource() island = IslandItemSource() festival = FestivalItemSource() + masteries = MasteryItemSource() + content = ContentItemSource() item_name: str amount: int = 1 quality: str = CropQuality.basic source: BundleItemSource = Sources.vanilla + flavor: str = None + can_have_quality: bool = True @staticmethod def money_bundle(amount: int) -> BundleItem: return BundleItem(Currency.money, amount) + def get_item(self) -> str: + if self.flavor is None: + return self.item_name + return f"{self.item_name} [{self.flavor}]" + def as_amount(self, amount: int) -> BundleItem: - return BundleItem(self.item_name, amount, self.quality, self.source) + return BundleItem(self.item_name, amount, self.quality, self.source, self.flavor) def as_quality(self, quality: str) -> BundleItem: - return BundleItem(self.item_name, self.amount, quality, self.source) + if self.can_have_quality: + return BundleItem(self.item_name, self.amount, quality, self.source, self.flavor) + return BundleItem(self.item_name, self.amount, self.quality, self.source, self.flavor) def as_quality_crop(self) -> BundleItem: amount = 5 @@ -67,7 +91,11 @@ def as_quality_forage(self) -> BundleItem: def __repr__(self): quality = "" if self.quality == CropQuality.basic else self.quality - return f"{self.amount} {quality} {self.item_name}" + return f"{self.amount} {quality} {self.get_item()}" + + def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool: + if isinstance(self.source, ContentItemSource): + return self.get_item() in content.game_items - def can_appear(self, options: StardewValleyOptions) -> bool: return self.source.can_appear(options) + diff --git a/worlds/stardew_valley/bundles/bundle_room.py b/worlds/stardew_valley/bundles/bundle_room.py index a5cdb89144f5..8068ff17ac83 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -3,6 +3,7 @@ from typing import List from .bundle import Bundle, BundleTemplate +from ..content import StardewContent from ..options import BundlePrice, StardewValleyOptions @@ -18,7 +19,25 @@ class BundleRoomTemplate: bundles: List[BundleTemplate] number_bundles: int - def create_bundle_room(self, bundle_price_option: BundlePrice, random: Random, options: StardewValleyOptions): + def create_bundle_room(self, random: Random, content: StardewContent, options: StardewValleyOptions): filtered_bundles = [bundle for bundle in self.bundles if bundle.can_appear(options)] - chosen_bundles = random.sample(filtered_bundles, self.number_bundles) - return BundleRoom(self.name, [bundle.create_bundle(bundle_price_option, random, options) for bundle in chosen_bundles]) + + priority_bundles = [] + unpriority_bundles = [] + for bundle in filtered_bundles: + if bundle.name in options.bundle_plando: + priority_bundles.append(bundle) + else: + unpriority_bundles.append(bundle) + + if self.number_bundles <= len(priority_bundles): + chosen_bundles = random.sample(priority_bundles, self.number_bundles) + else: + chosen_bundles = priority_bundles + num_remaining_bundles = self.number_bundles - len(priority_bundles) + if num_remaining_bundles > len(unpriority_bundles): + chosen_bundles.extend(random.choices(unpriority_bundles, k=num_remaining_bundles)) + else: + chosen_bundles.extend(random.sample(unpriority_bundles, num_remaining_bundles)) + + return BundleRoom(self.name, [bundle.create_bundle(random, content, options) for bundle in chosen_bundles]) diff --git a/worlds/stardew_valley/bundles/bundles.py b/worlds/stardew_valley/bundles/bundles.py index 260ee17cbe82..99619e09aadf 100644 --- a/worlds/stardew_valley/bundles/bundles.py +++ b/worlds/stardew_valley/bundles/bundles.py @@ -1,65 +1,102 @@ from random import Random -from typing import List +from typing import List, Tuple -from .bundle_room import BundleRoom +from .bundle import Bundle +from .bundle_room import BundleRoom, BundleRoomTemplate +from ..content import StardewContent from ..data.bundle_data import pantry_vanilla, crafts_room_vanilla, fish_tank_vanilla, boiler_room_vanilla, bulletin_board_vanilla, vault_vanilla, \ pantry_thematic, crafts_room_thematic, fish_tank_thematic, boiler_room_thematic, bulletin_board_thematic, vault_thematic, pantry_remixed, \ crafts_room_remixed, fish_tank_remixed, boiler_room_remixed, bulletin_board_remixed, vault_remixed, all_bundle_items_except_money, \ - abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed + abandoned_joja_mart_thematic, abandoned_joja_mart_vanilla, abandoned_joja_mart_remixed, raccoon_vanilla, raccoon_thematic, raccoon_remixed, \ + community_center_remixed_anywhere from ..logic.logic import StardewLogic -from ..options import BundleRandomization, StardewValleyOptions, ExcludeGingerIsland +from ..options import BundleRandomization, StardewValleyOptions -def get_all_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: +def get_all_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: if options.bundle_randomization == BundleRandomization.option_vanilla: - return get_vanilla_bundles(random, options) + return get_vanilla_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_thematic: - return get_thematic_bundles(random, options) + return get_thematic_bundles(random, content, options) elif options.bundle_randomization == BundleRandomization.option_remixed: - return get_remixed_bundles(random, options) + return get_remixed_bundles(random, content, options) + elif options.bundle_randomization == BundleRandomization.option_remixed_anywhere: + return get_remixed_bundles_anywhere(random, content, options) elif options.bundle_randomization == BundleRandomization.option_shuffled: - return get_shuffled_bundles(random, logic, options) + return get_shuffled_bundles(random, logic, content, options) raise NotImplementedError -def get_vanilla_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_vanilla.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_vanilla.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_vanilla.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_vanilla.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_vanilla.create_bundle_room(options.bundle_price, random, options) - vault = vault_vanilla.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_thematic_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_thematic.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_thematic.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_thematic.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_thematic.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_thematic.create_bundle_room(options.bundle_price, random, options) - vault = vault_thematic.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_remixed_bundles(random: Random, options: StardewValleyOptions) -> List[BundleRoom]: - pantry = pantry_remixed.create_bundle_room(options.bundle_price, random, options) - crafts_room = crafts_room_remixed.create_bundle_room(options.bundle_price, random, options) - fish_tank = fish_tank_remixed.create_bundle_room(options.bundle_price, random, options) - boiler_room = boiler_room_remixed.create_bundle_room(options.bundle_price, random, options) - bulletin_board = bulletin_board_remixed.create_bundle_room(options.bundle_price, random, options) - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) - abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(options.bundle_price, random, options) - return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart] - - -def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewValleyOptions) -> List[BundleRoom]: - valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(options)] - - rooms = [room for room in get_remixed_bundles(random, options) if room.name != "Vault"] +def get_vanilla_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_vanilla.create_bundle_room(random, content, options) + crafts_room = crafts_room_vanilla.create_bundle_room(random, content, options) + fish_tank = fish_tank_vanilla.create_bundle_room(random, content, options) + boiler_room = boiler_room_vanilla.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_vanilla.create_bundle_room(random, content, options) + vault = vault_vanilla.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_vanilla.create_bundle_room(random, content, options) + raccoon = raccoon_vanilla.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_thematic_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_thematic.create_bundle_room(random, content, options) + crafts_room = crafts_room_thematic.create_bundle_room(random, content, options) + fish_tank = fish_tank_thematic.create_bundle_room(random, content, options) + boiler_room = boiler_room_thematic.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_thematic.create_bundle_room(random, content, options) + vault = vault_thematic.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_thematic.create_bundle_room(random, content, options) + raccoon = raccoon_thematic.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + pantry = pantry_remixed.create_bundle_room(random, content, options) + crafts_room = crafts_room_remixed.create_bundle_room(random, content, options) + fish_tank = fish_tank_remixed.create_bundle_room(random, content, options) + boiler_room = boiler_room_remixed.create_bundle_room(random, content, options) + bulletin_board = bulletin_board_remixed.create_bundle_room(random, content, options) + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def get_remixed_bundles_anywhere(random: Random, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + big_room = community_center_remixed_anywhere.create_bundle_room(random, content, options) + all_chosen_bundles = big_room.bundles + random.shuffle(all_chosen_bundles) + + end_index = 0 + + pantry, end_index = create_room_from_bundles(pantry_remixed, all_chosen_bundles, end_index) + crafts_room, end_index = create_room_from_bundles(crafts_room_remixed, all_chosen_bundles, end_index) + fish_tank, end_index = create_room_from_bundles(fish_tank_remixed, all_chosen_bundles, end_index) + boiler_room, end_index = create_room_from_bundles(boiler_room_remixed, all_chosen_bundles, end_index) + bulletin_board, end_index = create_room_from_bundles(bulletin_board_remixed, all_chosen_bundles, end_index) + + vault = vault_remixed.create_bundle_room(random, content, options) + abandoned_joja_mart = abandoned_joja_mart_remixed.create_bundle_room(random, content, options) + raccoon = raccoon_remixed.create_bundle_room(random, content, options) + fix_raccoon_bundle_names(raccoon) + return [pantry, crafts_room, fish_tank, boiler_room, bulletin_board, vault, abandoned_joja_mart, raccoon] + + +def create_room_from_bundles(template: BundleRoomTemplate, all_bundles: List[Bundle], end_index: int) -> Tuple[BundleRoom, int]: + start_index = end_index + end_index += template.number_bundles + return BundleRoom(template.name, all_bundles[start_index:end_index]), end_index + + +def get_shuffled_bundles(random: Random, logic: StardewLogic, content: StardewContent, options: StardewValleyOptions) -> List[BundleRoom]: + valid_bundle_items = [bundle_item for bundle_item in all_bundle_items_except_money if bundle_item.can_appear(content, options)] + + rooms = [room for room in get_remixed_bundles(random, content, options) if room.name != "Vault"] required_items = 0 for room in rooms: for bundle in room.bundles: @@ -67,14 +104,21 @@ def get_shuffled_bundles(random: Random, logic: StardewLogic, options: StardewVa random.shuffle(room.bundles) random.shuffle(rooms) + # Remove duplicates of the same item + valid_bundle_items = [item1 for i, item1 in enumerate(valid_bundle_items) + if not any(item1.item_name == item2.item_name and item1.quality == item2.quality for item2 in valid_bundle_items[:i])] chosen_bundle_items = random.sample(valid_bundle_items, required_items) - sorted_bundle_items = sorted(chosen_bundle_items, key=lambda x: logic.has(x.item_name).get_difficulty()) for room in rooms: for bundle in room.bundles: num_items = len(bundle.items) - bundle.items = sorted_bundle_items[:num_items] - sorted_bundle_items = sorted_bundle_items[num_items:] + bundle.items = chosen_bundle_items[:num_items] + chosen_bundle_items = chosen_bundle_items[num_items:] - vault = vault_remixed.create_bundle_room(options.bundle_price, random, options) + vault = vault_remixed.create_bundle_room(random, content, options) return [*rooms, vault] + +def fix_raccoon_bundle_names(raccoon): + for i in range(len(raccoon.bundles)): + raccoon_bundle = raccoon.bundles[i] + raccoon_bundle.name = f"Raccoon Request {i + 1}" diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py new file mode 100644 index 000000000000..9130873fa405 --- /dev/null +++ b/worlds/stardew_valley/content/__init__.py @@ -0,0 +1,107 @@ +from . import content_packs +from .feature import cropsanity, friendsanity, fishsanity, booksanity +from .game_content import ContentPack, StardewContent, StardewFeatures +from .unpacking import unpack_content +from .. import options + + +def create_content(player_options: options.StardewValleyOptions) -> StardewContent: + active_packs = choose_content_packs(player_options) + features = choose_features(player_options) + return unpack_content(features, active_packs) + + +def choose_content_packs(player_options: options.StardewValleyOptions): + active_packs = [content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines] + + if player_options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + active_packs.append(content_packs.ginger_island_content_pack) + + if player_options.special_order_locations & options.SpecialOrderLocations.value_qi: + active_packs.append(content_packs.qi_board_content_pack) + + for mod in player_options.mods.value: + active_packs.append(content_packs.by_mod[mod]) + + return active_packs + + +def choose_features(player_options: options.StardewValleyOptions) -> StardewFeatures: + return StardewFeatures( + choose_booksanity(player_options.booksanity), + choose_cropsanity(player_options.cropsanity), + choose_fishsanity(player_options.fishsanity), + choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size) + ) + + +booksanity_by_option = { + options.Booksanity.option_none: booksanity.BooksanityDisabled(), + options.Booksanity.option_power: booksanity.BooksanityPower(), + options.Booksanity.option_power_skill: booksanity.BooksanityPowerSkill(), + options.Booksanity.option_all: booksanity.BooksanityAll(), +} + + +def choose_booksanity(booksanity_option: options.Booksanity) -> booksanity.BooksanityFeature: + booksanity_feature = booksanity_by_option.get(booksanity_option) + + if booksanity_feature is None: + raise ValueError(f"No booksanity feature mapped to {str(booksanity_option.value)}") + + return booksanity_feature + + +cropsanity_by_option = { + options.Cropsanity.option_disabled: cropsanity.CropsanityDisabled(), + options.Cropsanity.option_enabled: cropsanity.CropsanityEnabled(), +} + + +def choose_cropsanity(cropsanity_option: options.Cropsanity) -> cropsanity.CropsanityFeature: + cropsanity_feature = cropsanity_by_option.get(cropsanity_option) + + if cropsanity_feature is None: + raise ValueError(f"No cropsanity feature mapped to {str(cropsanity_option.value)}") + + return cropsanity_feature + + +fishsanity_by_option = { + options.Fishsanity.option_none: fishsanity.FishsanityNone(), + options.Fishsanity.option_legendaries: fishsanity.FishsanityLegendaries(), + options.Fishsanity.option_special: fishsanity.FishsanitySpecial(), + options.Fishsanity.option_randomized: fishsanity.FishsanityAll(randomization_ratio=0.4), + options.Fishsanity.option_all: fishsanity.FishsanityAll(), + options.Fishsanity.option_exclude_legendaries: fishsanity.FishsanityExcludeLegendaries(), + options.Fishsanity.option_exclude_hard_fish: fishsanity.FishsanityExcludeHardFish(), + options.Fishsanity.option_only_easy_fish: fishsanity.FishsanityOnlyEasyFish(), +} + + +def choose_fishsanity(fishsanity_option: options.Fishsanity) -> fishsanity.FishsanityFeature: + fishsanity_feature = fishsanity_by_option.get(fishsanity_option) + + if fishsanity_feature is None: + raise ValueError(f"No fishsanity feature mapped to {str(fishsanity_option.value)}") + + return fishsanity_feature + + +def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: options.FriendsanityHeartSize) -> friendsanity.FriendsanityFeature: + if friendsanity_option == options.Friendsanity.option_none: + return friendsanity.FriendsanityNone() + + if friendsanity_option == options.Friendsanity.option_bachelors: + return friendsanity.FriendsanityBachelors(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_starting_npcs: + return friendsanity.FriendsanityStartingNpc(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all: + return friendsanity.FriendsanityAll(heart_size.value) + + if friendsanity_option == options.Friendsanity.option_all_with_marriage: + return friendsanity.FriendsanityAllWithMarriage(heart_size.value) + + raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}") diff --git a/worlds/stardew_valley/content/content_packs.py b/worlds/stardew_valley/content/content_packs.py new file mode 100644 index 000000000000..fb8df8c70cba --- /dev/null +++ b/worlds/stardew_valley/content/content_packs.py @@ -0,0 +1,31 @@ +import importlib +import pkgutil + +from . import mods +from .mod_registry import by_mod +from .vanilla.base import base_game +from .vanilla.ginger_island import ginger_island_content_pack +from .vanilla.pelican_town import pelican_town +from .vanilla.qi_board import qi_board_content_pack +from .vanilla.the_desert import the_desert +from .vanilla.the_farm import the_farm +from .vanilla.the_mines import the_mines + +assert base_game +assert ginger_island_content_pack +assert pelican_town +assert qi_board_content_pack +assert the_desert +assert the_farm +assert the_mines + +# Dynamically register everything currently in the mods folder. This would ideally be done through a metaclass, but I have not looked into that yet. +mod_modules = pkgutil.iter_modules(mods.__path__) + +loaded_modules = {} +for mod_module in mod_modules: + module_name = mod_module.name + module = importlib.import_module("." + module_name, mods.__name__) + loaded_modules[module_name] = module + +assert by_mod diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py new file mode 100644 index 000000000000..74249c808257 --- /dev/null +++ b/worlds/stardew_valley/content/feature/__init__.py @@ -0,0 +1,4 @@ +from . import booksanity +from . import cropsanity +from . import fishsanity +from . import friendsanity diff --git a/worlds/stardew_valley/content/feature/booksanity.py b/worlds/stardew_valley/content/feature/booksanity.py new file mode 100644 index 000000000000..5eade5932535 --- /dev/null +++ b/worlds/stardew_valley/content/feature/booksanity.py @@ -0,0 +1,72 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional, Iterable + +from ...data.game_item import GameItem, ItemTag +from ...strings.book_names import ordered_lost_books + +item_prefix = "Power: " +location_prefix = "Read " + + +def to_item_name(book: str) -> str: + return item_prefix + book + + +def to_location_name(book: str) -> str: + return location_prefix + book + + +def extract_book_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class BooksanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_item_name = staticmethod(to_item_name) + progressive_lost_book = "Progressive Lost Book" + to_location_name = staticmethod(to_location_name) + extract_book_from_location_name = staticmethod(extract_book_from_location_name) + + @abstractmethod + def is_included(self, book: GameItem) -> bool: + ... + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return [] + + +class BooksanityDisabled(BooksanityFeature): + is_enabled = False + + def is_included(self, book: GameItem) -> bool: + return False + + +class BooksanityPower(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags + + +class BooksanityPowerSkill(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + +class BooksanityAll(BooksanityFeature): + is_enabled = True + + def is_included(self, book: GameItem) -> bool: + return ItemTag.BOOK_POWER in book.tags or ItemTag.BOOK_SKILL in book.tags + + @staticmethod + def get_randomized_lost_books() -> Iterable[str]: + return ordered_lost_books diff --git a/worlds/stardew_valley/content/feature/cropsanity.py b/worlds/stardew_valley/content/feature/cropsanity.py new file mode 100644 index 000000000000..18ef370815ee --- /dev/null +++ b/worlds/stardew_valley/content/feature/cropsanity.py @@ -0,0 +1,42 @@ +from abc import ABC, abstractmethod +from typing import ClassVar, Optional + +from ...data.game_item import GameItem, ItemTag + +location_prefix = "Harvest " + + +def to_location_name(crop: str) -> str: + return location_prefix + crop + + +def extract_crop_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +class CropsanityFeature(ABC): + is_enabled: ClassVar[bool] + + to_location_name = staticmethod(to_location_name) + extract_crop_from_location_name = staticmethod(extract_crop_from_location_name) + + @abstractmethod + def is_included(self, crop: GameItem) -> bool: + ... + + +class CropsanityDisabled(CropsanityFeature): + is_enabled = False + + def is_included(self, crop: GameItem) -> bool: + return False + + +class CropsanityEnabled(CropsanityFeature): + is_enabled = True + + def is_included(self, crop: GameItem) -> bool: + return ItemTag.CROPSANITY_SEED in crop.tags diff --git a/worlds/stardew_valley/content/feature/fishsanity.py b/worlds/stardew_valley/content/feature/fishsanity.py new file mode 100644 index 000000000000..02f9a632a873 --- /dev/null +++ b/worlds/stardew_valley/content/feature/fishsanity.py @@ -0,0 +1,101 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar, Optional + +from ...data.fish_data import FishItem +from ...strings.fish_names import Fish + +location_prefix = "Fishsanity: " + + +def to_location_name(fish: str) -> str: + return location_prefix + fish + + +def extract_fish_from_location_name(location_name: str) -> Optional[str]: + if not location_name.startswith(location_prefix): + return None + + return location_name[len(location_prefix):] + + +@dataclass(frozen=True) +class FishsanityFeature(ABC): + is_enabled: ClassVar[bool] + + randomization_ratio: float = 1 + + to_location_name = staticmethod(to_location_name) + extract_fish_from_location_name = staticmethod(extract_fish_from_location_name) + + @property + def is_randomized(self) -> bool: + return self.randomization_ratio != 1 + + @abstractmethod + def is_included(self, fish: FishItem) -> bool: + ... + + +class FishsanityNone(FishsanityFeature): + is_enabled = False + + def is_included(self, fish: FishItem) -> bool: + return False + + +class FishsanityLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.legendary + + +class FishsanitySpecial(FishsanityFeature): + is_enabled = True + + included_fishes = { + Fish.angler, + Fish.crimsonfish, + Fish.glacierfish, + Fish.legend, + Fish.mutant_carp, + Fish.blobfish, + Fish.lava_eel, + Fish.octopus, + Fish.scorpion_carp, + Fish.ice_pip, + Fish.super_cucumber, + Fish.dorado + } + + def is_included(self, fish: FishItem) -> bool: + return fish.name in self.included_fishes + + +class FishsanityAll(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return True + + +class FishsanityExcludeLegendaries(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return not fish.legendary + + +class FishsanityExcludeHardFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 80 + + +class FishsanityOnlyEasyFish(FishsanityFeature): + is_enabled = True + + def is_included(self, fish: FishItem) -> bool: + return fish.difficulty < 50 diff --git a/worlds/stardew_valley/content/feature/friendsanity.py b/worlds/stardew_valley/content/feature/friendsanity.py new file mode 100644 index 000000000000..3e1581b4e2f1 --- /dev/null +++ b/worlds/stardew_valley/content/feature/friendsanity.py @@ -0,0 +1,139 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from functools import lru_cache +from typing import Optional, Tuple, ClassVar + +from ...data.villagers_data import Villager +from ...strings.villager_names import NPC + +suffix = " <3" +location_prefix = "Friendsanity: " + + +def to_item_name(npc_name: str) -> str: + return npc_name + suffix + + +def to_location_name(npc_name: str, heart: int) -> str: + return location_prefix + npc_name + " " + str(heart) + suffix + + +pet_heart_item_name = to_item_name(NPC.pet) + + +def extract_npc_from_item_name(item_name: str) -> Optional[str]: + if not item_name.endswith(suffix): + return None + + return item_name[:-len(suffix)] + + +def extract_npc_from_location_name(location_name: str) -> Tuple[Optional[str], int]: + if not location_name.endswith(suffix): + return None, 0 + + trimmed = location_name[len(location_prefix):-len(suffix)] + last_space = trimmed.rindex(" ") + return trimmed[:last_space], int(trimmed[last_space + 1:]) + + +@lru_cache(maxsize=32) # Should not go pass 32 values if every friendsanity options are in the multi world +def get_heart_steps(max_heart: int, heart_size: int) -> Tuple[int, ...]: + return tuple(range(heart_size, max_heart + 1, heart_size)) + ((max_heart,) if max_heart % heart_size else ()) + + +@dataclass(frozen=True) +class FriendsanityFeature(ABC): + is_enabled: ClassVar[bool] + + heart_size: int + + to_item_name = staticmethod(to_item_name) + to_location_name = staticmethod(to_location_name) + pet_heart_item_name = pet_heart_item_name + extract_npc_from_item_name = staticmethod(extract_npc_from_item_name) + extract_npc_from_location_name = staticmethod(extract_npc_from_location_name) + + @abstractmethod + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + ... + + @property + def is_pet_randomized(self): + return bool(self.get_pet_randomized_hearts()) + + @abstractmethod + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + ... + + +class FriendsanityNone(FriendsanityFeature): + is_enabled = False + + def __init__(self): + super().__init__(1) + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + return () + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityBachelors(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.bachelor: + return () + + return get_heart_steps(8, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return () + + +@dataclass(frozen=True) +class FriendsanityStartingNpc(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if not villager.available: + return () + + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAll(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(8, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) + + +@dataclass(frozen=True) +class FriendsanityAllWithMarriage(FriendsanityFeature): + is_enabled = True + + def get_randomized_hearts(self, villager: Villager) -> Tuple[int, ...]: + if villager.bachelor: + return get_heart_steps(14, self.heart_size) + + return get_heart_steps(10, self.heart_size) + + def get_pet_randomized_hearts(self) -> Tuple[int, ...]: + return get_heart_steps(5, self.heart_size) diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py new file mode 100644 index 000000000000..8dcf933145e3 --- /dev/null +++ b/worlds/stardew_valley/content/game_content.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union + +from .feature import booksanity, cropsanity, fishsanity, friendsanity +from ..data.fish_data import FishItem +from ..data.game_item import GameItem, ItemSource, ItemTag +from ..data.skill import Skill +from ..data.villagers_data import Villager + + +@dataclass(frozen=True) +class StardewContent: + features: StardewFeatures + registered_packs: Set[str] = field(default_factory=set) + + # regions -> To be used with can reach rule + + game_items: Dict[str, GameItem] = field(default_factory=dict) + fishes: Dict[str, FishItem] = field(default_factory=dict) + villagers: Dict[str, Villager] = field(default_factory=dict) + skills: Dict[str, Skill] = field(default_factory=dict) + quests: Dict[str, Any] = field(default_factory=dict) + + def find_sources_of_type(self, types: Union[Type[ItemSource], Tuple[Type[ItemSource]]]) -> Iterable[ItemSource]: + for item in self.game_items.values(): + for source in item.sources: + if isinstance(source, types): + yield source + + def source_item(self, item_name: str, *sources: ItemSource): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + def tag_item(self, item_name: str, *tags: ItemTag): + item = self.game_items.setdefault(item_name, GameItem(item_name)) + item.add_tags(tags) + + def untag_item(self, item_name: str, tag: ItemTag): + self.game_items[item_name].tags.remove(tag) + + def find_tagged_items(self, tag: ItemTag) -> Iterable[GameItem]: + # TODO might be worth caching this, but it need to only be cached once the content is finalized... + for item in self.game_items.values(): + if tag in item.tags: + yield item + + +@dataclass(frozen=True) +class StardewFeatures: + booksanity: booksanity.BooksanityFeature + cropsanity: cropsanity.CropsanityFeature + fishsanity: fishsanity.FishsanityFeature + friendsanity: friendsanity.FriendsanityFeature + + +@dataclass(frozen=True) +class ContentPack: + name: str + + dependencies: Iterable[str] = () + """ Hard requirement, generation will fail if it's missing. """ + weak_dependencies: Iterable[str] = () + """ Not a strict dependency, only used only for ordering the packs to make sure hooks are applied correctly. """ + + # items + # def item_hook + # ... + + harvest_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + """Harvest sources contains both crops and forageables, but also fruits from trees, the cave farm and stuff harvested from tapping like maple syrup.""" + + def harvest_source_hook(self, content: StardewContent): + ... + + shop_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def shop_source_hook(self, content: StardewContent): + ... + + fishes: Iterable[FishItem] = () + + def fish_hook(self, content: StardewContent): + ... + + crafting_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def crafting_hook(self, content: StardewContent): + ... + + artisan_good_sources: Mapping[str, Iterable[ItemSource]] = field(default_factory=dict) + + def artisan_good_hook(self, content: StardewContent): + ... + + villagers: Iterable[Villager] = () + + def villager_hook(self, content: StardewContent): + ... + + skills: Iterable[Skill] = () + + def skill_hook(self, content: StardewContent): + ... + + quests: Iterable[Any] = () + + def quest_hook(self, content: StardewContent): + ... + + def finalize_hook(self, content: StardewContent): + """Last hook called on the pack, once all other content packs have been registered. + + This is the place to do any final adjustments to the content, like adding rules based on tags applied by other packs. + """ + ... diff --git a/worlds/stardew_valley/content/mod_registry.py b/worlds/stardew_valley/content/mod_registry.py new file mode 100644 index 000000000000..c598fcbad295 --- /dev/null +++ b/worlds/stardew_valley/content/mod_registry.py @@ -0,0 +1,7 @@ +from .game_content import ContentPack + +by_mod = {} + + +def register_mod_content_pack(content_pack: ContentPack): + by_mod[content_pack.name] = content_pack diff --git a/worlds/stardew_valley/content/mods/__init__.py b/worlds/stardew_valley/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py new file mode 100644 index 000000000000..97d38085d3b2 --- /dev/null +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -0,0 +1,20 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.game_item import ItemTag, Tag +from ...data.shop import ShopSource +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.book_names import ModBook +from ...strings.region_names import LogicRegion +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.archaeology, + shop_sources={ + ModBook.digging_like_worms: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=500, shop_region=LogicRegion.bookseller_1),), + }, + skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), + +)) diff --git a/worlds/stardew_valley/content/mods/big_backpack.py b/worlds/stardew_valley/content/mods/big_backpack.py new file mode 100644 index 000000000000..27b4ea1f816c --- /dev/null +++ b/worlds/stardew_valley/content/mods/big_backpack.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.big_backpack, +)) diff --git a/worlds/stardew_valley/content/mods/boarding_house.py b/worlds/stardew_valley/content/mods/boarding_house.py new file mode 100644 index 000000000000..f3ad138fa7c2 --- /dev/null +++ b/worlds/stardew_valley/content/mods/boarding_house.py @@ -0,0 +1,13 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.boarding_house, + villagers=( + villagers_data.gregory, + villagers_data.sheila, + villagers_data.joel, + ) +)) diff --git a/worlds/stardew_valley/content/mods/deepwoods.py b/worlds/stardew_valley/content/mods/deepwoods.py new file mode 100644 index 000000000000..a78629da57c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/deepwoods.py @@ -0,0 +1,28 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.harvest import ForagingSource +from ...mods.mod_data import ModNames +from ...strings.crop_names import Fruit +from ...strings.flower_names import Flower +from ...strings.region_names import DeepWoodsRegion +from ...strings.season_names import Season + +register_mod_content_pack(ContentPack( + ModNames.deepwoods, + harvest_sources={ + # Deep enough to have seen such a tree at least once + Fruit.apple: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.apricot: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.cherry: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.orange: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.peach: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.pomegranate: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Fruit.mango: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + + Flower.tulip: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.blue_jazz: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + Flower.summer_spangle: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.poppy: (ForagingSource(seasons=Season.not_winter, regions=(DeepWoodsRegion.floor_10,)),), + Flower.fairy_rose: (ForagingSource(regions=(DeepWoodsRegion.floor_10,)),), + } +)) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py new file mode 100644 index 000000000000..19380d4ff565 --- /dev/null +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -0,0 +1,17 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data, fish_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.distant_lands, + fishes=( + fish_data.void_minnow, + fish_data.purple_algae, + fish_data.swamp_leech, + fish_data.giant_horsehoe_crab, + ), + villagers=( + villagers_data.zic, + ) +)) diff --git a/worlds/stardew_valley/content/mods/jasper.py b/worlds/stardew_valley/content/mods/jasper.py new file mode 100644 index 000000000000..146b291d800a --- /dev/null +++ b/worlds/stardew_valley/content/mods/jasper.py @@ -0,0 +1,14 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ..override import override +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.jasper, + villagers=( + villagers_data.jasper, + override(villagers_data.gunther, mod_name=ModNames.jasper), + override(villagers_data.marlon, mod_name=ModNames.jasper), + ) +)) diff --git a/worlds/stardew_valley/content/mods/magic.py b/worlds/stardew_valley/content/mods/magic.py new file mode 100644 index 000000000000..aae3617cb00c --- /dev/null +++ b/worlds/stardew_valley/content/mods/magic.py @@ -0,0 +1,10 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.magic, + skills=(Skill(name=ModSkill.magic, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py new file mode 100644 index 000000000000..3172a55dbf32 --- /dev/null +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -0,0 +1,88 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.alec, + villagers=( + villagers_data.alec, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ayeisha, + villagers=( + villagers_data.ayeisha, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.delores, + villagers=( + villagers_data.delores, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.eugene, + villagers=( + villagers_data.eugene, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.juna, + villagers=( + villagers_data.juna, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.ginger, + villagers=( + villagers_data.kitty, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.shiko, + villagers=( + villagers_data.shiko, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.wellwick, + villagers=( + villagers_data.wellwick, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.yoba, + villagers=( + villagers_data.yoba, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.riley, + villagers=( + villagers_data.riley, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.alecto, + villagers=( + villagers_data.alecto, + ) +)) + +register_mod_content_pack(ContentPack( + ModNames.lacey, + villagers=( + villagers_data.lacey, + ) +)) diff --git a/worlds/stardew_valley/content/mods/skill_mods.py b/worlds/stardew_valley/content/mods/skill_mods.py new file mode 100644 index 000000000000..7f88b2ebf2dc --- /dev/null +++ b/worlds/stardew_valley/content/mods/skill_mods.py @@ -0,0 +1,25 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...data.skill import Skill +from ...mods.mod_data import ModNames +from ...strings.skill_names import ModSkill + +register_mod_content_pack(ContentPack( + ModNames.luck_skill, + skills=(Skill(name=ModSkill.luck, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.socializing_skill, + skills=(Skill(name=ModSkill.socializing, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.cooking_skill, + skills=(Skill(name=ModSkill.cooking, has_mastery=False),) +)) + +register_mod_content_pack(ContentPack( + ModNames.binning_skill, + skills=(Skill(name=ModSkill.binning, has_mastery=False),) +)) diff --git a/worlds/stardew_valley/content/mods/skull_cavern_elevator.py b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py new file mode 100644 index 000000000000..ff8c089608e5 --- /dev/null +++ b/worlds/stardew_valley/content/mods/skull_cavern_elevator.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.skull_cavern_elevator, +)) diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py new file mode 100644 index 000000000000..f74b80948c96 --- /dev/null +++ b/worlds/stardew_valley/content/mods/sve.py @@ -0,0 +1,126 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ..override import override +from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack +from ...data import villagers_data, fish_data +from ...data.harvest import ForagingSource +from ...data.requirement import YearRequirement +from ...mods.mod_data import ModNames +from ...strings.crop_names import Fruit +from ...strings.fish_names import WaterItem +from ...strings.flower_names import Flower +from ...strings.forageable_names import Mushroom, Forageable +from ...strings.region_names import Region, SVERegion +from ...strings.season_names import Season + + +class SVEContentPack(ContentPack): + + def fish_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + content.fishes.pop(fish_data.baby_lunaloo.name) + content.fishes.pop(fish_data.clownfish.name) + content.fishes.pop(fish_data.lunaloo.name) + content.fishes.pop(fish_data.seahorse.name) + content.fishes.pop(fish_data.shiny_lunaloo.name) + content.fishes.pop(fish_data.starfish.name) + content.fishes.pop(fish_data.sea_sponge.name) + + # Remove Highlands fishes at it requires 2 Lance hearts for the quest to access it + content.fishes.pop(fish_data.daggerfish.name) + content.fishes.pop(fish_data.gemfish.name) + + # Remove Fable Reef fishes at it requires 8 Lance hearts for the event to access it + content.fishes.pop(fish_data.torpedo_trout.name) + + def villager_hook(self, content: StardewContent): + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge + content.villagers.pop(villagers_data.lance.name) + + +register_mod_content_pack(SVEContentPack( + ModNames.sve, + weak_dependencies=( + ginger_island_content_pack.name, + ModNames.jasper, # To override Marlon and Gunther + ), + harvest_sources={ + Mushroom.red: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.purple: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.morel: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Mushroom.chanterelle: ( + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ), + Flower.tulip: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.blue_jazz: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring,)),), + Flower.summer_spangle: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), + Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), + Fruit.ancient_fruit: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring_cave,)), + ), + Fruit.sweet_gem_berry: ( + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ), + + # Fable Reef + WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), + Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), + WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + }, + fishes=( + fish_data.baby_lunaloo, # Removed when no ginger island + fish_data.bonefish, + fish_data.bull_trout, + fish_data.butterfish, + fish_data.clownfish, # Removed when no ginger island + fish_data.daggerfish, + fish_data.frog, + fish_data.gemfish, + fish_data.goldenfish, + fish_data.grass_carp, + fish_data.king_salmon, + fish_data.kittyfish, + fish_data.lunaloo, # Removed when no ginger island + fish_data.meteor_carp, + fish_data.minnow, + fish_data.puppyfish, + fish_data.radioactive_bass, + fish_data.seahorse, # Removed when no ginger island + fish_data.shiny_lunaloo, # Removed when no ginger island + fish_data.snatcher_worm, + fish_data.starfish, # Removed when no ginger island + fish_data.torpedo_trout, + fish_data.undeadfish, + fish_data.void_eel, + fish_data.water_grub, + fish_data.sea_sponge, # Removed when no ginger island + + ), + villagers=( + villagers_data.claire, + villagers_data.lance, # Removed when no ginger island + villagers_data.mommy, + villagers_data.sophia, + villagers_data.victor, + villagers_data.andy, + villagers_data.apples, + villagers_data.gunther, + villagers_data.martin, + villagers_data.marlon, + villagers_data.morgan, + villagers_data.scarlett, + villagers_data.susan, + villagers_data.morris, + # The wizard leaves his tower on sunday, for like 1 hour... Good enough for entrance rando! + override(villagers_data.wizard, locations=(Region.wizard_tower, Region.forest), bachelor=True, mod_name=ModNames.sve), + ) +)) diff --git a/worlds/stardew_valley/content/mods/tractor.py b/worlds/stardew_valley/content/mods/tractor.py new file mode 100644 index 000000000000..8f143001791c --- /dev/null +++ b/worlds/stardew_valley/content/mods/tractor.py @@ -0,0 +1,7 @@ +from ..game_content import ContentPack +from ..mod_registry import register_mod_content_pack +from ...mods.mod_data import ModNames + +register_mod_content_pack(ContentPack( + ModNames.tractor, +)) diff --git a/worlds/stardew_valley/content/override.py b/worlds/stardew_valley/content/override.py new file mode 100644 index 000000000000..adfc64c95b49 --- /dev/null +++ b/worlds/stardew_valley/content/override.py @@ -0,0 +1,7 @@ +from typing import Any + + +def override(content: Any, **kwargs) -> Any: + attributes = dict(content.__dict__) + attributes.update(kwargs) + return type(content)(**attributes) diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py new file mode 100644 index 000000000000..f069866d56cd --- /dev/null +++ b/worlds/stardew_valley/content/unpacking.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Iterable, Mapping, Callable + +from .game_content import StardewContent, ContentPack, StardewFeatures +from .vanilla.base import base_game as base_game_content_pack +from ..data.game_item import GameItem, ItemSource + +try: + from graphlib import TopologicalSorter +except ImportError: + from graphlib_backport import TopologicalSorter # noqa + + +def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: + # Base game is always registered first. + content = StardewContent(features) + packs_to_finalize = [base_game_content_pack] + register_pack(content, base_game_content_pack) + + # Content packs are added in order based on their dependencies + sorter = TopologicalSorter() + packs_by_name = {p.name: p for p in packs} + + # Build the dependency graph + for name, pack in packs_by_name.items(): + sorter.add(name, + *pack.dependencies, + *(wd for wd in pack.weak_dependencies if wd in packs_by_name)) + + # Graph is traversed in BFS + sorter.prepare() + while sorter.is_active(): + # Packs get shuffled in TopologicalSorter, most likely due to hash seeding. + for pack_name in sorted(sorter.get_ready()): + pack = packs_by_name[pack_name] + register_pack(content, pack) + sorter.done(pack_name) + packs_to_finalize.append(pack) + + prune_inaccessible_items(content) + + for pack in packs_to_finalize: + pack.finalize_hook(content) + + # Maybe items without source should be removed at some point + return content + + +def register_pack(content: StardewContent, pack: ContentPack): + # register regions + + # register entrances + + register_sources_and_call_hook(content, pack.harvest_sources, pack.harvest_source_hook) + register_sources_and_call_hook(content, pack.shop_sources, pack.shop_source_hook) + register_sources_and_call_hook(content, pack.crafting_sources, pack.crafting_hook) + register_sources_and_call_hook(content, pack.artisan_good_sources, pack.artisan_good_hook) + + for fish in pack.fishes: + content.fishes[fish.name] = fish + pack.fish_hook(content) + + for villager in pack.villagers: + content.villagers[villager.name] = villager + pack.villager_hook(content) + + for skill in pack.skills: + content.skills[skill.name] = skill + pack.skill_hook(content) + + # register_quests + + # ... + + content.registered_packs.add(pack.name) + + +def register_sources_and_call_hook(content: StardewContent, + sources_by_item_name: Mapping[str, Iterable[ItemSource]], + hook: Callable[[StardewContent], None]): + for item_name, sources in sources_by_item_name.items(): + item = content.game_items.setdefault(item_name, GameItem(item_name)) + item.add_sources(sources) + + for source in sources: + for requirement_name, tags in source.requirement_tags.items(): + requirement_item = content.game_items.setdefault(requirement_name, GameItem(requirement_name)) + requirement_item.add_tags(tags) + + hook(content) + + +def prune_inaccessible_items(content: StardewContent): + for item in list(content.game_items.values()): + if not item.sources: + content.game_items.pop(item.name) diff --git a/worlds/stardew_valley/content/vanilla/__init__.py b/worlds/stardew_valley/content/vanilla/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py new file mode 100644 index 000000000000..2c910df5d00f --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/base.py @@ -0,0 +1,172 @@ +from ..game_content import ContentPack, StardewContent +from ...data.artisan import MachineSource +from ...data.game_item import ItemTag, CustomRuleSource, GameItem +from ...data.harvest import HarvestFruitTreeSource, HarvestCropSource +from ...data.skill import Skill +from ...strings.artisan_good_names import ArtisanGood +from ...strings.craftable_names import WildSeeds +from ...strings.crop_names import Fruit, Vegetable +from ...strings.flower_names import Flower +from ...strings.food_names import Beverage +from ...strings.forageable_names import all_edible_mushrooms, Mushroom, Forageable +from ...strings.fruit_tree_names import Sapling +from ...strings.machine_names import Machine +from ...strings.monster_names import Monster +from ...strings.season_names import Season +from ...strings.seed_names import Seed +from ...strings.skill_names import Skill as SkillName + +all_fruits = ( + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Fruit.banana, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pineapple, Fruit.pomegranate, Fruit.powdermelon, Fruit.qi_fruit, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, + Fruit.strawberry +) + +all_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.hops, Vegetable.kale, + Vegetable.parsnip, Vegetable.potato, Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.taro_root, + Vegetable.tea_leaves, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.wheat, Vegetable.yam +) + +non_juiceable_vegetables = (Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat, Vegetable.tea_leaves) + + +# This will hold items, skills and stuff that is available everywhere across the game, but not directly needing pelican town (crops, ore, foraging, etc.) +class BaseGameContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + coffee_starter = content.game_items[Seed.coffee_starter] + content.game_items[Seed.coffee_starter] = GameItem(Seed.coffee, sources=coffee_starter.sources, tags=coffee_starter.tags) + + content.untag_item(WildSeeds.ancient, ItemTag.CROPSANITY_SEED) + + for fruit in all_fruits: + content.tag_item(fruit, ItemTag.FRUIT) + + for vegetable in all_vegetables: + content.tag_item(vegetable, ItemTag.VEGETABLE) + + for edible_mushroom in all_edible_mushrooms: + if edible_mushroom == Mushroom.magma_cap: + continue + + content.tag_item(edible_mushroom, ItemTag.EDIBLE_MUSHROOM) + + def finalize_hook(self, content: StardewContent): + # FIXME I hate this design. A listener design pattern would be more appropriate so artisan good are register at the exact moment a FRUIT tag is added. + for fruit in tuple(content.find_tagged_items(ItemTag.FRUIT)): + wine = ArtisanGood.specific_wine(fruit.name) + content.source_item(wine, MachineSource(item=fruit.name, machine=Machine.keg)) + content.source_item(ArtisanGood.wine, MachineSource(item=fruit.name, machine=Machine.keg)) + + if fruit.name == Fruit.grape: + content.source_item(ArtisanGood.raisins, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + else: + dried_fruit = ArtisanGood.specific_dried_fruit(fruit.name) + content.source_item(dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_fruit, MachineSource(item=fruit.name, machine=Machine.dehydrator)) + + jelly = ArtisanGood.specific_jelly(fruit.name) + content.source_item(jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.jelly, MachineSource(item=fruit.name, machine=Machine.preserves_jar)) + + for vegetable in tuple(content.find_tagged_items(ItemTag.VEGETABLE)): + if vegetable.name not in non_juiceable_vegetables: + juice = ArtisanGood.specific_juice(vegetable.name) + content.source_item(juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + content.source_item(ArtisanGood.juice, MachineSource(item=vegetable.name, machine=Machine.keg)) + + pickles = ArtisanGood.specific_pickles(vegetable.name) + content.source_item(pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + content.source_item(ArtisanGood.pickles, MachineSource(item=vegetable.name, machine=Machine.preserves_jar)) + + for mushroom in tuple(content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM)): + dried_mushroom = ArtisanGood.specific_dried_mushroom(mushroom.name) + content.source_item(dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + content.source_item(ArtisanGood.dried_mushroom, MachineSource(item=mushroom.name, machine=Machine.dehydrator)) + + # for fish in tuple(content.find_tagged_items(ItemTag.FISH)): + # smoked_fish = ArtisanGood.specific_smoked_fish(fish.name) + # content.source_item(smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + # content.source_item(ArtisanGood.smoked_fish, MachineSource(item=fish.name, machine=Machine.fish_smoker)) + + +base_game = BaseGameContentPack( + "Base game (Vanilla)", + harvest_sources={ + # Fruit tree + Fruit.apple: (HarvestFruitTreeSource(sapling=Sapling.apple, seasons=(Season.fall,)),), + Fruit.apricot: (HarvestFruitTreeSource(sapling=Sapling.apricot, seasons=(Season.spring,)),), + Fruit.cherry: (HarvestFruitTreeSource(sapling=Sapling.cherry, seasons=(Season.spring,)),), + Fruit.orange: (HarvestFruitTreeSource(sapling=Sapling.orange, seasons=(Season.summer,)),), + Fruit.peach: (HarvestFruitTreeSource(sapling=Sapling.peach, seasons=(Season.summer,)),), + Fruit.pomegranate: (HarvestFruitTreeSource(sapling=Sapling.pomegranate, seasons=(Season.fall,)),), + + # Crops + Vegetable.parsnip: (HarvestCropSource(seed=Seed.parsnip, seasons=(Season.spring,)),), + Vegetable.green_bean: (HarvestCropSource(seed=Seed.bean, seasons=(Season.spring,)),), + Vegetable.cauliflower: (HarvestCropSource(seed=Seed.cauliflower, seasons=(Season.spring,)),), + Vegetable.potato: (HarvestCropSource(seed=Seed.potato, seasons=(Season.spring,)),), + Flower.tulip: (HarvestCropSource(seed=Seed.tulip, seasons=(Season.spring,)),), + Vegetable.kale: (HarvestCropSource(seed=Seed.kale, seasons=(Season.spring,)),), + Flower.blue_jazz: (HarvestCropSource(seed=Seed.jazz, seasons=(Season.spring,)),), + Vegetable.garlic: (HarvestCropSource(seed=Seed.garlic, seasons=(Season.spring,)),), + Vegetable.unmilled_rice: (HarvestCropSource(seed=Seed.rice, seasons=(Season.spring,)),), + + Fruit.melon: (HarvestCropSource(seed=Seed.melon, seasons=(Season.summer,)),), + Vegetable.tomato: (HarvestCropSource(seed=Seed.tomato, seasons=(Season.summer,)),), + Fruit.blueberry: (HarvestCropSource(seed=Seed.blueberry, seasons=(Season.summer,)),), + Fruit.hot_pepper: (HarvestCropSource(seed=Seed.pepper, seasons=(Season.summer,)),), + Vegetable.wheat: (HarvestCropSource(seed=Seed.wheat, seasons=(Season.summer, Season.fall)),), + Vegetable.radish: (HarvestCropSource(seed=Seed.radish, seasons=(Season.summer,)),), + Flower.poppy: (HarvestCropSource(seed=Seed.poppy, seasons=(Season.summer,)),), + Flower.summer_spangle: (HarvestCropSource(seed=Seed.spangle, seasons=(Season.summer,)),), + Vegetable.hops: (HarvestCropSource(seed=Seed.hops, seasons=(Season.summer,)),), + Vegetable.corn: (HarvestCropSource(seed=Seed.corn, seasons=(Season.summer, Season.fall)),), + Flower.sunflower: (HarvestCropSource(seed=Seed.sunflower, seasons=(Season.summer, Season.fall)),), + Vegetable.red_cabbage: (HarvestCropSource(seed=Seed.red_cabbage, seasons=(Season.summer,)),), + + Vegetable.eggplant: (HarvestCropSource(seed=Seed.eggplant, seasons=(Season.fall,)),), + Vegetable.pumpkin: (HarvestCropSource(seed=Seed.pumpkin, seasons=(Season.fall,)),), + Vegetable.bok_choy: (HarvestCropSource(seed=Seed.bok_choy, seasons=(Season.fall,)),), + Vegetable.yam: (HarvestCropSource(seed=Seed.yam, seasons=(Season.fall,)),), + Fruit.cranberries: (HarvestCropSource(seed=Seed.cranberry, seasons=(Season.fall,)),), + Flower.fairy_rose: (HarvestCropSource(seed=Seed.fairy, seasons=(Season.fall,)),), + Vegetable.amaranth: (HarvestCropSource(seed=Seed.amaranth, seasons=(Season.fall,)),), + Fruit.grape: (HarvestCropSource(seed=Seed.grape, seasons=(Season.fall,)),), + Vegetable.artichoke: (HarvestCropSource(seed=Seed.artichoke, seasons=(Season.fall,)),), + + Vegetable.broccoli: (HarvestCropSource(seed=Seed.broccoli, seasons=(Season.fall,)),), + Vegetable.carrot: (HarvestCropSource(seed=Seed.carrot, seasons=(Season.spring,)),), + Fruit.powdermelon: (HarvestCropSource(seed=Seed.powdermelon, seasons=(Season.summer,)),), + Vegetable.summer_squash: (HarvestCropSource(seed=Seed.summer_squash, seasons=(Season.summer,)),), + + Fruit.strawberry: (HarvestCropSource(seed=Seed.strawberry, seasons=(Season.spring,)),), + Fruit.sweet_gem_berry: (HarvestCropSource(seed=Seed.rare_seed, seasons=(Season.fall,)),), + Fruit.ancient_fruit: (HarvestCropSource(seed=WildSeeds.ancient, seasons=(Season.spring, Season.summer, Season.fall,)),), + + Seed.coffee_starter: (CustomRuleSource(lambda logic: logic.traveling_merchant.has_days(3) & logic.monster.can_kill_many(Monster.dust_sprite)),), + Seed.coffee: (HarvestCropSource(seed=Seed.coffee_starter, seasons=(Season.spring, Season.summer,)),), + + Vegetable.tea_leaves: (CustomRuleSource(lambda logic: logic.has(Sapling.tea) & logic.time.has_lived_months(2) & logic.season.has_any_not_winter()),), + }, + artisan_good_sources={ + Beverage.beer: (MachineSource(item=Vegetable.wheat, machine=Machine.keg),), + # Ingredient.vinegar: (MachineSource(item=Ingredient.rice, machine=Machine.keg),), + Beverage.coffee: (MachineSource(item=Seed.coffee, machine=Machine.keg), + CustomRuleSource(lambda logic: logic.has(Machine.coffee_maker)), + CustomRuleSource(lambda logic: logic.has("Hot Java Ring"))), + ArtisanGood.green_tea: (MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg),), + ArtisanGood.mead: (MachineSource(item=ArtisanGood.honey, machine=Machine.keg),), + ArtisanGood.pale_ale: (MachineSource(item=Vegetable.hops, machine=Machine.keg),), + }, + skills=( + Skill(SkillName.farming, has_mastery=True), + Skill(SkillName.foraging, has_mastery=True), + Skill(SkillName.fishing, has_mastery=True), + Skill(SkillName.mining, has_mastery=True), + Skill(SkillName.combat, has_mastery=True), + ) +) diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py new file mode 100644 index 000000000000..d824deff3903 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -0,0 +1,81 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.shop import ShopSource +from ...strings.book_names import Book +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.metal_names import Fossil, Mineral +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class GingerIslandContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.tag_item(Fruit.banana, ItemTag.FRUIT) + content.tag_item(Fruit.pineapple, ItemTag.FRUIT) + content.tag_item(Fruit.mango, ItemTag.FRUIT) + content.tag_item(Vegetable.taro_root, ItemTag.VEGETABLE) + content.tag_item(Mushroom.magma_cap, ItemTag.EDIBLE_MUSHROOM) + + +ginger_island_content_pack = GingerIslandContentPack( + "Ginger Island (Vanilla)", + weak_dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Foraging + Forageable.dragon_tooth: ( + ForagingSource(regions=(Region.volcano_floor_10,)), + ), + Forageable.ginger: ( + ForagingSource(regions=(Region.island_west,)), + ), + Mushroom.magma_cap: ( + ForagingSource(regions=(Region.volcano_floor_5,)), + ), + + # Fruit tree + Fruit.banana: (HarvestFruitTreeSource(sapling=Sapling.banana, seasons=(Season.summer,)),), + Fruit.mango: (HarvestFruitTreeSource(sapling=Sapling.mango, seasons=(Season.summer,)),), + + # Crop + Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),), + Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),), + + }, + shop_sources={ + Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),), + Seed.pineapple: (ShopSource(items_price=((1, Mushroom.magma_cap),), shop_region=Region.island_trader),), + Sapling.banana: (ShopSource(items_price=((5, Forageable.dragon_tooth),), shop_region=Region.island_trader),), + Sapling.mango: (ShopSource(items_price=((75, Fish.mussel_node),), shop_region=Region.island_trader),), + + # This one is 10 diamonds, should maybe add time? + Book.the_diamond_hunter: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), + ), + + }, + fishes=( + # TODO override region so no need to add inaccessible regions in logic + fish_data.blue_discus, + fish_data.lionfish, + fish_data.midnight_carp, + fish_data.pufferfish, + fish_data.stingray, + fish_data.super_cucumber, + fish_data.tilapia, + fish_data.tuna + ), + villagers=( + villagers_data.leo, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py new file mode 100644 index 000000000000..2c687eacbdde --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -0,0 +1,393 @@ +from ..game_content import ContentPack +from ...data import villagers_data, fish_data +from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource +from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource +from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement +from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource +from ...strings.book_names import Book +from ...strings.crop_names import Fruit +from ...strings.fish_names import WaterItem +from ...strings.food_names import Beverage, Meal +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.fruit_tree_names import Sapling +from ...strings.generic_names import Generic +from ...strings.material_names import Material +from ...strings.region_names import Region, LogicRegion +from ...strings.season_names import Season +from ...strings.seed_names import Seed, TreeSeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial + +pelican_town = ContentPack( + "Pelican Town (Vanilla)", + harvest_sources={ + # Spring + Forageable.daffodil: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.town, Region.railroad)), + ), + Forageable.dandelion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.leek: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_horseradish: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.backwoods, Region.mountain, Region.forest, Region.secret_woods)), + ), + Forageable.salmonberry: ( + SeasonalForagingSource(season=Season.spring, days=(15, 16, 17, 18), + regions=(Region.backwoods, Region.mountain, Region.town, Region.forest, Region.tunnel_entrance, Region.railroad)), + ), + Forageable.spring_onion: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.forest,)), + ), + + # Summer + Fruit.grape: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.spice_berry: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.forest, Region.railroad)), + ), + Forageable.sweet_pea: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.fiddlehead_fern: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.secret_woods,)), + ), + + # Fall + Forageable.blackberry: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.town, Region.forest, Region.railroad)), + SeasonalForagingSource(season=Season.fall, days=(8, 9, 10, 11), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.tunnel_entrance, + Region.railroad)), + ), + Forageable.hazelnut: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.railroad)), + ), + Forageable.wild_plum: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.mountain, Region.bus_stop, Region.railroad)), + ), + + # Winter + Forageable.crocus: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.secret_woods)), + ), + Forageable.crystal_fruit: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.holly: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad)), + ), + Forageable.snow_yam: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Forageable.winter_root: ( + ForagingSource(seasons=(Season.winter,), + regions=(Region.farm, Region.backwoods, Region.mountain, Region.bus_stop, Region.town, Region.forest, Region.railroad, + Region.secret_woods, Region.beach), + other_requirements=(ToolRequirement(Tool.hoe),)), + ), + + # Mushrooms + Mushroom.common: ( + ForagingSource(seasons=(Season.spring,), regions=(Region.secret_woods,)), + ForagingSource(seasons=(Season.fall,), regions=(Region.backwoods, Region.mountain, Region.forest)), + ), + Mushroom.chanterelle: ( + ForagingSource(seasons=(Season.fall,), regions=(Region.secret_woods,)), + ), + Mushroom.morel: ( + ForagingSource(seasons=(Season.spring, Season.fall), regions=(Region.secret_woods,)), + ), + Mushroom.red: ( + ForagingSource(seasons=(Season.summer, Season.fall), regions=(Region.secret_woods,)), + ), + + # Beach + WaterItem.coral: ( + ForagingSource(regions=(Region.tide_pools,)), + SeasonalForagingSource(season=Season.summer, days=(12, 13, 14), regions=(Region.beach,)), + ), + WaterItem.nautilus_shell: ( + ForagingSource(seasons=(Season.winter,), regions=(Region.beach,)), + ), + Forageable.rainbow_shell: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.beach,)), + ), + WaterItem.sea_urchin: ( + ForagingSource(regions=(Region.tide_pools,)), + ), + + Seed.mixed: ( + ForagingSource(seasons=(Season.spring, Season.summer, Season.fall,), regions=(Region.town, Region.farm, Region.forest)), + ), + + Seed.mixed_flower: ( + ForagingSource(seasons=(Season.summer,), regions=(Region.town, Region.farm, Region.forest)), + ), + + # Books + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ArtifactSpotSource(amount=22),), # After 22 spots, there are 50.48% chances player received the book. + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=(Region.forest, Region.mountain), + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), SkillRequirement(Skill.foraging, 5))),), + }, + shop_sources={ + # Saplings + Sapling.apple: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.apricot: (ShopSource(money_price=2000, shop_region=Region.pierre_store),), + Sapling.cherry: (ShopSource(money_price=3400, shop_region=Region.pierre_store),), + Sapling.orange: (ShopSource(money_price=4000, shop_region=Region.pierre_store),), + Sapling.peach: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + Sapling.pomegranate: (ShopSource(money_price=6000, shop_region=Region.pierre_store),), + + # Crop seeds, assuming they are bought in season, otherwise price is different with missing stock list. + Seed.parsnip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.bean: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.cauliflower: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.potato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.tulip: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.kale: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.jazz: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.garlic: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + Seed.rice: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.spring,)),), + + Seed.melon: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.tomato: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.blueberry: (ShopSource(money_price=80, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.pepper: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.wheat: (ShopSource(money_price=10, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.radish: (ShopSource(money_price=40, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.poppy: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.spangle: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.hops: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + Seed.corn: (ShopSource(money_price=150, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.sunflower: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.summer, Season.fall)),), + Seed.red_cabbage: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.summer,)),), + + Seed.eggplant: (ShopSource(money_price=20, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.pumpkin: (ShopSource(money_price=100, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.bok_choy: (ShopSource(money_price=50, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.yam: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.cranberry: (ShopSource(money_price=240, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.fairy: (ShopSource(money_price=200, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.amaranth: (ShopSource(money_price=70, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.grape: (ShopSource(money_price=60, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + Seed.artichoke: (ShopSource(money_price=30, shop_region=Region.pierre_store, seasons=(Season.fall,)),), + + Seed.broccoli: (ShopSource(items_price=((5, Material.moss),), shop_region=LogicRegion.raccoon_shop),), + Seed.carrot: (ShopSource(items_price=((1, TreeSeed.maple),), shop_region=LogicRegion.raccoon_shop),), + Seed.powdermelon: (ShopSource(items_price=((2, TreeSeed.acorn),), shop_region=LogicRegion.raccoon_shop),), + Seed.summer_squash: (ShopSource(items_price=((15, Material.sap),), shop_region=LogicRegion.raccoon_shop),), + + Seed.strawberry: (ShopSource(money_price=100, shop_region=LogicRegion.egg_festival, seasons=(Season.spring,)),), + Seed.rare_seed: (ShopSource(money_price=1000, shop_region=LogicRegion.traveling_cart, seasons=(Season.spring, Season.summer)),), + + # Saloon + Beverage.beer: (ShopSource(money_price=400, shop_region=Region.saloon),), + Meal.salad: (ShopSource(money_price=220, shop_region=Region.saloon),), + Meal.bread: (ShopSource(money_price=100, shop_region=Region.saloon),), + Meal.spaghetti: (ShopSource(money_price=240, shop_region=Region.saloon),), + Meal.pizza: (ShopSource(money_price=600, shop_region=Region.saloon),), + Beverage.coffee: (ShopSource(money_price=300, shop_region=Region.saloon),), + + # Books + Book.animal_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=5000, shop_region=Region.ranch),), + Book.book_of_mysteries: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book. + Book.dwarvish_safety_manual: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=4000, shop_region=LogicRegion.mines_dwarf_shop), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.friendship_101: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + PrizeMachineSource(amount=9), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.horse_the_book: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.jack_be_nimble_jack_be_thick: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.jewels_of_the_sea: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + FishingTreasureChestSource(amount=21), # After 21 chests, there are 49.44% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.mapping_cave_systems: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.adventurer_guild_bedroom), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.monster_compendium: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + CustomRuleSource(create_rule=lambda logic: logic.monster.can_kill_many(Generic.any)), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.ol_slitherlegs: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=25000, shop_region=LogicRegion.bookseller_2),), + Book.price_catalogue: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), + Book.the_alleyway_buffet: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.town, + other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.the_art_o_crabbing: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + GenericSource(regions=Region.beach, + other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), + SkillRequirement(Skill.fishing, 6), + SeasonRequirement(Season.winter))), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.treasure_appraisal_guide: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ArtifactTroveSource(amount=18), # After 18 troves, there is 49,88% chances player received the book. + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + Book.raccoon_journal: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3), + ShopSource(items_price=((999, Material.fiber),), shop_region=LogicRegion.raccoon_shop),), + Book.way_of_the_wind_pt_1: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=15000, shop_region=LogicRegion.bookseller_2),), + Book.way_of_the_wind_pt_2: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=35000, shop_region=LogicRegion.bookseller_2, other_requirements=(BookRequirement(Book.way_of_the_wind_pt_1),)),), + Book.woodys_secret: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), + ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), + + # Experience Books + Book.book_of_stars: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.bait_and_bobber: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.combat_quarterly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.mining_monthly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.stardew_valley_almanac: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.woodcutters_weekly: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2),), # Worst book ever + }, + fishes=( + fish_data.albacore, + fish_data.anchovy, + fish_data.bream, + fish_data.bullhead, + fish_data.carp, + fish_data.catfish, + fish_data.chub, + fish_data.dorado, + fish_data.eel, + fish_data.flounder, + fish_data.goby, + fish_data.halibut, + fish_data.herring, + fish_data.largemouth_bass, + fish_data.lingcod, + fish_data.midnight_carp, # Ginger island override + fish_data.octopus, + fish_data.perch, + fish_data.pike, + fish_data.pufferfish, # Ginger island override + fish_data.rainbow_trout, + fish_data.red_mullet, + fish_data.red_snapper, + fish_data.salmon, + fish_data.sardine, + fish_data.sea_cucumber, + fish_data.shad, + fish_data.slimejack, + fish_data.smallmouth_bass, + fish_data.squid, + fish_data.sturgeon, + fish_data.sunfish, + fish_data.super_cucumber, # Ginger island override + fish_data.tiger_trout, + fish_data.tilapia, # Ginger island override + fish_data.tuna, # Ginger island override + fish_data.void_salmon, + fish_data.walleye, + fish_data.woodskip, + fish_data.blobfish, + fish_data.midnight_squid, + fish_data.spook_fish, + + # Legendaries + fish_data.angler, + fish_data.crimsonfish, + fish_data.glacierfish, + fish_data.legend, + fish_data.mutant_carp, + + # Crab pot + fish_data.clam, + fish_data.cockle, + fish_data.crab, + fish_data.crayfish, + fish_data.lobster, + fish_data.mussel, + fish_data.oyster, + fish_data.periwinkle, + fish_data.shrimp, + fish_data.snail, + ), + villagers=( + villagers_data.josh, + villagers_data.elliott, + villagers_data.harvey, + villagers_data.sam, + villagers_data.sebastian, + villagers_data.shane, + villagers_data.abigail, + villagers_data.emily, + villagers_data.haley, + villagers_data.leah, + villagers_data.maru, + villagers_data.penny, + villagers_data.caroline, + villagers_data.clint, + villagers_data.demetrius, + villagers_data.evelyn, + villagers_data.george, + villagers_data.gus, + villagers_data.jas, + villagers_data.jodi, + villagers_data.kent, + villagers_data.krobus, + villagers_data.lewis, + villagers_data.linus, + villagers_data.marnie, + villagers_data.pam, + villagers_data.pierre, + villagers_data.robin, + villagers_data.vincent, + villagers_data.willy, + villagers_data.wizard, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/qi_board.py b/worlds/stardew_valley/content/vanilla/qi_board.py new file mode 100644 index 000000000000..d859d3b16ff7 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/qi_board.py @@ -0,0 +1,36 @@ +from .ginger_island import ginger_island_content_pack as ginger_island_content_pack +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack, StardewContent +from ...data import fish_data +from ...data.game_item import GenericSource, ItemTag +from ...data.harvest import HarvestCropSource +from ...strings.crop_names import Fruit +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + + +class QiBoardContentPack(ContentPack): + def harvest_source_hook(self, content: StardewContent): + content.untag_item(Seed.qi_bean, ItemTag.CROPSANITY_SEED) + + +qi_board_content_pack = QiBoardContentPack( + "Qi Board (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ginger_island_content_pack.name, + ), + harvest_sources={ + # This one is a bit special, because it's only available during the special order, but it can be found from like, everywhere. + Seed.qi_bean: (GenericSource(regions=(Region.qi_walnut_room,)),), + Fruit.qi_fruit: (HarvestCropSource(seed=Seed.qi_bean),), + }, + fishes=( + fish_data.ms_angler, + fish_data.son_of_crimsonfish, + fish_data.glacierfish_jr, + fish_data.legend_ii, + fish_data.radioactive_carp, + ) +) diff --git a/worlds/stardew_valley/content/vanilla/the_desert.py b/worlds/stardew_valley/content/vanilla/the_desert.py new file mode 100644 index 000000000000..a207e169ca46 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_desert.py @@ -0,0 +1,46 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.shop import ShopSource +from ...strings.crop_names import Fruit, Vegetable +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import Seed + +the_desert = ContentPack( + "The Desert (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cactus_fruit: ( + ForagingSource(regions=(Region.desert,)), + HarvestCropSource(seed=Seed.cactus, seasons=()) + ), + Forageable.coconut: ( + ForagingSource(regions=(Region.desert,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.skull_cavern_25,)), + ), + + Fruit.rhubarb: (HarvestCropSource(seed=Seed.rhubarb, seasons=(Season.spring,)),), + Fruit.starfruit: (HarvestCropSource(seed=Seed.starfruit, seasons=(Season.summer,)),), + Vegetable.beet: (HarvestCropSource(seed=Seed.beet, seasons=(Season.fall,)),), + }, + shop_sources={ + Seed.cactus: (ShopSource(money_price=150, shop_region=Region.oasis),), + Seed.rhubarb: (ShopSource(money_price=100, shop_region=Region.oasis, seasons=(Season.spring,)),), + Seed.starfruit: (ShopSource(money_price=400, shop_region=Region.oasis, seasons=(Season.summer,)),), + Seed.beet: (ShopSource(money_price=20, shop_region=Region.oasis, seasons=(Season.fall,)),), + }, + fishes=( + fish_data.sandfish, + fish_data.scorpion_carp, + ), + villagers=( + villagers_data.sandy, + ), +) diff --git a/worlds/stardew_valley/content/vanilla/the_farm.py b/worlds/stardew_valley/content/vanilla/the_farm.py new file mode 100644 index 000000000000..68d0bf10f6b8 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_farm.py @@ -0,0 +1,43 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data.harvest import FruitBatsSource, MushroomCaveSource +from ...strings.forageable_names import Forageable, Mushroom + +the_farm = ContentPack( + "The Farm (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + # Fruit cave + Forageable.blackberry: ( + FruitBatsSource(), + ), + Forageable.salmonberry: ( + FruitBatsSource(), + ), + Forageable.spice_berry: ( + FruitBatsSource(), + ), + Forageable.wild_plum: ( + FruitBatsSource(), + ), + + # Mushrooms + Mushroom.common: ( + MushroomCaveSource(), + ), + Mushroom.chanterelle: ( + MushroomCaveSource(), + ), + Mushroom.morel: ( + MushroomCaveSource(), + ), + Mushroom.purple: ( + MushroomCaveSource(), + ), + Mushroom.red: ( + MushroomCaveSource(), + ), + } +) diff --git a/worlds/stardew_valley/content/vanilla/the_mines.py b/worlds/stardew_valley/content/vanilla/the_mines.py new file mode 100644 index 000000000000..729b195f7b06 --- /dev/null +++ b/worlds/stardew_valley/content/vanilla/the_mines.py @@ -0,0 +1,35 @@ +from .pelican_town import pelican_town as pelican_town_content_pack +from ..game_content import ContentPack +from ...data import fish_data, villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import ToolRequirement +from ...strings.forageable_names import Forageable, Mushroom +from ...strings.region_names import Region +from ...strings.tool_names import Tool + +the_mines = ContentPack( + "The Mines (Vanilla)", + dependencies=( + pelican_town_content_pack.name, + ), + harvest_sources={ + Forageable.cave_carrot: ( + ForagingSource(regions=(Region.mines_floor_10,), other_requirements=(ToolRequirement(Tool.hoe),)), + ), + Mushroom.red: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ), + Mushroom.purple: ( + ForagingSource(regions=(Region.mines_floor_95,)), + ) + }, + fishes=( + fish_data.ghostfish, + fish_data.ice_pip, + fish_data.lava_eel, + fish_data.stonefish, + ), + villagers=( + villagers_data.dwarf, + ), +) diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py index d14d9cfb8e97..e69de29bb2d1 100644 --- a/worlds/stardew_valley/data/__init__.py +++ b/worlds/stardew_valley/data/__init__.py @@ -1,2 +0,0 @@ -from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds -from .fish_data import FishItem, all_fish diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py new file mode 100644 index 000000000000..593ab6a3ddf0 --- /dev/null +++ b/worlds/stardew_valley/data/artisan.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from .game_item import kw_only, ItemSource + + +@dataclass(frozen=True, **kw_only) +class MachineSource(ItemSource): + item: str # this should be optional (worm bin) + machine: str + # seasons diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 7e7a08c16b37..8b2e189c796e 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -1,17 +1,19 @@ from ..bundles.bundle import BundleTemplate, IslandBundleTemplate, DeepBundleTemplate, CurrencyBundleTemplate, MoneyBundleTemplate, FestivalBundleTemplate from ..bundles.bundle_item import BundleItem from ..bundles.bundle_room import BundleRoomTemplate +from ..content import content_packs +from ..content.vanilla.base import all_fruits, all_vegetables, all_edible_mushrooms from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.bundle_names import CCRoom, BundleName -from ..strings.craftable_names import Fishing, Craftable, Bomb +from ..strings.craftable_names import Fishing, Craftable, Bomb, Consumable, Lighting from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem, Trash +from ..strings.fish_names import Fish, WaterItem, Trash, all_fish from ..strings.flower_names import Flower from ..strings.food_names import Beverage, Meal -from ..strings.forageable_names import Forageable +from ..strings.forageable_names import Forageable, Mushroom from ..strings.geode_names import Geode from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient @@ -19,27 +21,27 @@ from ..strings.metal_names import MetalBar, Artifact, Fossil, Ore, Mineral from ..strings.monster_drop_names import Loot from ..strings.quality_names import ForageQuality, ArtisanQuality, FishQuality -from ..strings.seed_names import Seed +from ..strings.seed_names import Seed, TreeSeed wild_horseradish = BundleItem(Forageable.wild_horseradish) daffodil = BundleItem(Forageable.daffodil) leek = BundleItem(Forageable.leek) dandelion = BundleItem(Forageable.dandelion) -morel = BundleItem(Forageable.morel) -common_mushroom = BundleItem(Forageable.common_mushroom) +morel = BundleItem(Mushroom.morel) +common_mushroom = BundleItem(Mushroom.common) salmonberry = BundleItem(Forageable.salmonberry) spring_onion = BundleItem(Forageable.spring_onion) grape = BundleItem(Fruit.grape) spice_berry = BundleItem(Forageable.spice_berry) sweet_pea = BundleItem(Forageable.sweet_pea) -red_mushroom = BundleItem(Forageable.red_mushroom) +red_mushroom = BundleItem(Mushroom.red) fiddlehead_fern = BundleItem(Forageable.fiddlehead_fern) wild_plum = BundleItem(Forageable.wild_plum) hazelnut = BundleItem(Forageable.hazelnut) blackberry = BundleItem(Forageable.blackberry) -chanterelle = BundleItem(Forageable.chanterelle) +chanterelle = BundleItem(Mushroom.chanterelle) winter_root = BundleItem(Forageable.winter_root) crystal_fruit = BundleItem(Forageable.crystal_fruit) @@ -50,7 +52,7 @@ coconut = BundleItem(Forageable.coconut) cactus_fruit = BundleItem(Forageable.cactus_fruit) cave_carrot = BundleItem(Forageable.cave_carrot) -purple_mushroom = BundleItem(Forageable.purple_mushroom) +purple_mushroom = BundleItem(Mushroom.purple) maple_syrup = BundleItem(ArtisanGood.maple_syrup) oak_resin = BundleItem(ArtisanGood.oak_resin) pine_tar = BundleItem(ArtisanGood.pine_tar) @@ -62,13 +64,25 @@ cockle = BundleItem(Fish.cockle) mussel = BundleItem(Fish.mussel) oyster = BundleItem(Fish.oyster) -seaweed = BundleItem(WaterItem.seaweed) +seaweed = BundleItem(WaterItem.seaweed, can_have_quality=False) wood = BundleItem(Material.wood, 99) stone = BundleItem(Material.stone, 99) hardwood = BundleItem(Material.hardwood, 10) clay = BundleItem(Material.clay, 10) fiber = BundleItem(Material.fiber, 99) +moss = BundleItem(Material.moss, 10) + +mixed_seeds = BundleItem(Seed.mixed) +acorn = BundleItem(TreeSeed.acorn) +maple_seed = BundleItem(TreeSeed.maple) +pine_cone = BundleItem(TreeSeed.pine) +mahogany_seed = BundleItem(TreeSeed.mahogany) +mushroom_tree_seed = BundleItem(TreeSeed.mushroom, source=BundleItem.Sources.island) +mystic_tree_seed = BundleItem(TreeSeed.mystic, source=BundleItem.Sources.masteries) +mossy_seed = BundleItem(TreeSeed.mossy) + +strawberry_seeds = BundleItem(Seed.strawberry) blue_jazz = BundleItem(Flower.blue_jazz) cauliflower = BundleItem(Vegetable.cauliflower) @@ -106,8 +120,13 @@ red_cabbage = BundleItem(Vegetable.red_cabbage) starfruit = BundleItem(Fruit.starfruit) artichoke = BundleItem(Vegetable.artichoke) -pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.island) -taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.island, ) +pineapple = BundleItem(Fruit.pineapple, source=BundleItem.Sources.content) +taro_root = BundleItem(Vegetable.taro_root, source=BundleItem.Sources.content) + +carrot = BundleItem(Vegetable.carrot) +summer_squash = BundleItem(Vegetable.summer_squash) +broccoli = BundleItem(Vegetable.broccoli) +powdermelon = BundleItem(Fruit.powdermelon) egg = BundleItem(AnimalProduct.egg) large_egg = BundleItem(AnimalProduct.large_egg) @@ -151,8 +170,8 @@ peach = BundleItem(Fruit.peach) pomegranate = BundleItem(Fruit.pomegranate) cherry = BundleItem(Fruit.cherry) -banana = BundleItem(Fruit.banana, source=BundleItem.Sources.island) -mango = BundleItem(Fruit.mango, source=BundleItem.Sources.island) +banana = BundleItem(Fruit.banana, source=BundleItem.Sources.content) +mango = BundleItem(Fruit.mango, source=BundleItem.Sources.content) basic_fertilizer = BundleItem(Fertilizer.basic, 100) quality_fertilizer = BundleItem(Fertilizer.quality, 20) @@ -300,6 +319,13 @@ rhubarb_pie = BundleItem(Meal.rhubarb_pie) shrimp_cocktail = BundleItem(Meal.shrimp_cocktail) pina_colada = BundleItem(Beverage.pina_colada, source=BundleItem.Sources.island) +stuffing = BundleItem(Meal.stuffing) +magic_rock_candy = BundleItem(Meal.magic_rock_candy) +spicy_eel = BundleItem(Meal.spicy_eel) +crab_cakes = BundleItem(Meal.crab_cakes) +eggplant_parmesan = BundleItem(Meal.eggplant_parmesan) +pumpkin_soup = BundleItem(Meal.pumpkin_soup) +lucky_lunch = BundleItem(Meal.lucky_lunch) green_algae = BundleItem(WaterItem.green_algae) white_algae = BundleItem(WaterItem.white_algae) @@ -370,6 +396,7 @@ spinner = BundleItem(Fishing.spinner) dressed_spinner = BundleItem(Fishing.dressed_spinner) trap_bobber = BundleItem(Fishing.trap_bobber) +sonar_bobber = BundleItem(Fishing.sonar_bobber) cork_bobber = BundleItem(Fishing.cork_bobber) lead_bobber = BundleItem(Fishing.lead_bobber) treasure_hunter = BundleItem(Fishing.treasure_hunter) @@ -377,18 +404,67 @@ curiosity_lure = BundleItem(Fishing.curiosity_lure) quality_bobber = BundleItem(Fishing.quality_bobber) bait = BundleItem(Fishing.bait, 100) +deluxe_bait = BundleItem(Fishing.deluxe_bait, 50) magnet = BundleItem(Fishing.magnet) -wild_bait = BundleItem(Fishing.wild_bait, 10) -magic_bait = BundleItem(Fishing.magic_bait, 5, source=BundleItem.Sources.island) +wild_bait = BundleItem(Fishing.wild_bait, 20) +magic_bait = BundleItem(Fishing.magic_bait, 10, source=BundleItem.Sources.island) pearl = BundleItem(Gift.pearl) +challenge_bait = BundleItem(Fishing.challenge_bait, 25, source=BundleItem.Sources.masteries) +targeted_bait = BundleItem(ArtisanGood.targeted_bait, 25, source=BundleItem.Sources.content) -ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.island) -magma_cap = BundleItem(Forageable.magma_cap, source=BundleItem.Sources.island) +ginger = BundleItem(Forageable.ginger, source=BundleItem.Sources.content) +magma_cap = BundleItem(Mushroom.magma_cap, source=BundleItem.Sources.content) wheat_flour = BundleItem(Ingredient.wheat_flour) sugar = BundleItem(Ingredient.sugar) vinegar = BundleItem(Ingredient.vinegar) +jack_o_lantern = BundleItem(Lighting.jack_o_lantern) +prize_ticket = BundleItem(Currency.prize_ticket) +mystery_box = BundleItem(Consumable.mystery_box) +gold_mystery_box = BundleItem(Consumable.gold_mystery_box, source=BundleItem.Sources.masteries) +calico_egg = BundleItem(Currency.calico_egg) + +raccoon_crab_pot_fish_items = [periwinkle.as_amount(5), snail.as_amount(5), crayfish.as_amount(5), mussel.as_amount(5), + oyster.as_amount(5), cockle.as_amount(5), clam.as_amount(5)] +raccoon_smoked_fish_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in + [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, + Fish.rainbow_trout, Fish.tilapia, Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch]] +raccoon_fish_items_flat = [*raccoon_crab_pot_fish_items, *raccoon_smoked_fish_items] +raccoon_fish_items_deep = [raccoon_crab_pot_fish_items, raccoon_smoked_fish_items] +raccoon_fish_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_deep, 2, 2) +raccoon_fish_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_fish, raccoon_fish_items_flat, 3, 2) + +all_specific_jellies = [BundleItem(ArtisanGood.jelly, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +all_specific_pickles = [BundleItem(ArtisanGood.pickles, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +all_specific_dried_fruits = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +all_specific_juices = [BundleItem(ArtisanGood.juice, flavor=vegetable, source=BundleItem.Sources.content) for vegetable in all_vegetables] +raccoon_artisan_items = [*all_specific_jellies, *all_specific_pickles, *all_specific_dried_fruits, *all_specific_juices] +raccoon_artisan_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 2, 2) +raccoon_artisan_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_artisan, raccoon_artisan_items, 3, 2) + +all_specific_dried_mushrooms = [BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms] +raccoon_food_items = [egg.as_amount(5), cave_carrot.as_amount(5), white_algae.as_amount(5)] +raccoon_food_items_vanilla = [all_specific_dried_mushrooms, raccoon_food_items] +raccoon_food_items_thematic = [*all_specific_dried_mushrooms, *raccoon_food_items, brown_egg.as_amount(5), large_egg.as_amount(2), large_brown_egg.as_amount(2), + green_algae.as_amount(10)] +raccoon_food_bundle_vanilla = DeepBundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_vanilla, 2, 2) +raccoon_food_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_food, raccoon_food_items_thematic, 3, 2) + +raccoon_foraging_items = [moss, rusty_spoon, trash.as_amount(5), slime.as_amount(99), bat_wing.as_amount(10), geode.as_amount(8), + frozen_geode.as_amount(5), magma_geode.as_amount(3), coral.as_amount(4), sea_urchin.as_amount(2), bug_meat.as_amount(10), + diamond, topaz.as_amount(3), ghostfish.as_amount(3)] +raccoon_foraging_bundle_vanilla = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 2, 2) +raccoon_foraging_bundle_thematic = BundleTemplate(CCRoom.raccoon_requests, BundleName.raccoon_foraging, raccoon_foraging_items, 3, 2) + +raccoon_bundles_vanilla = [raccoon_fish_bundle_vanilla, raccoon_artisan_bundle_vanilla, raccoon_food_bundle_vanilla, raccoon_foraging_bundle_vanilla] +raccoon_bundles_thematic = [raccoon_fish_bundle_thematic, raccoon_artisan_bundle_thematic, raccoon_food_bundle_thematic, raccoon_foraging_bundle_thematic] +raccoon_bundles_remixed = raccoon_bundles_thematic +raccoon_vanilla = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_vanilla, 8) +raccoon_thematic = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_thematic, 8) +raccoon_remixed = BundleRoomTemplate(CCRoom.raccoon_requests, raccoon_bundles_remixed, 8) + # Crafts Room spring_foraging_items_vanilla = [wild_horseradish, daffodil, leek, dandelion] spring_foraging_items_thematic = [*spring_foraging_items_vanilla, spring_onion, salmonberry, morel] @@ -436,42 +512,50 @@ sticky_items = [sap.as_amount(500), sap.as_amount(500)] sticky_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.sticky, sticky_items, 1, 1) +forest_items = [moss, fiber.as_amount(200), acorn.as_amount(10), maple_seed.as_amount(10), pine_cone.as_amount(10), mahogany_seed, + mushroom_tree_seed, mossy_seed.as_amount(5), mystic_tree_seed] +forest_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.forest, forest_items, 4, 2) + wild_medicine_items = [item.as_amount(5) for item in [purple_mushroom, fiddlehead_fern, white_algae, hops, blackberry, dandelion]] wild_medicine_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.wild_medicine, wild_medicine_items, 4, 3) -quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(1) +quality_foraging_items = sorted({item.as_quality(ForageQuality.gold).as_amount(3) for item in [*spring_foraging_items_thematic, *summer_foraging_items_thematic, *fall_foraging_items_thematic, - *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap]}) + *winter_foraging_items_thematic, *beach_foraging_items, *desert_foraging_items, magma_cap] if item.can_have_quality}) quality_foraging_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.quality_foraging, quality_foraging_items, 4, 3) +green_rain_items = [moss.as_amount(200), fiber.as_amount(200), mossy_seed.as_amount(20), fiddlehead_fern.as_amount(10)] +green_rain_bundle = BundleTemplate(CCRoom.crafts_room, BundleName.green_rain, green_rain_items, 4, 3) + crafts_room_bundles_vanilla = [spring_foraging_bundle_vanilla, summer_foraging_bundle_vanilla, fall_foraging_bundle_vanilla, winter_foraging_bundle_vanilla, construction_bundle_vanilla, exotic_foraging_bundle_vanilla] crafts_room_bundles_thematic = [spring_foraging_bundle_thematic, summer_foraging_bundle_thematic, fall_foraging_bundle_thematic, winter_foraging_bundle_thematic, construction_bundle_thematic, exotic_foraging_bundle_thematic] crafts_room_bundles_remixed = [*crafts_room_bundles_thematic, beach_foraging_bundle, mines_foraging_bundle, desert_foraging_bundle, - island_foraging_bundle, sticky_bundle, wild_medicine_bundle, quality_foraging_bundle] + island_foraging_bundle, sticky_bundle, forest_bundle, wild_medicine_bundle, quality_foraging_bundle, green_rain_bundle] crafts_room_vanilla = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_vanilla, 6) crafts_room_thematic = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_thematic, 6) crafts_room_remixed = BundleRoomTemplate(CCRoom.crafts_room, crafts_room_bundles_remixed, 6) # Pantry spring_crops_items_vanilla = [parsnip, green_bean, cauliflower, potato] -spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice] +spring_crops_items_thematic = [*spring_crops_items_vanilla, blue_jazz, coffee_bean, garlic, kale, rhubarb, strawberry, tulip, unmilled_rice, carrot] spring_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.spring_crops, spring_crops_items_vanilla, 4, 4) spring_crops_bundle_thematic = BundleTemplate.extend_from(spring_crops_bundle_vanilla, spring_crops_items_thematic) summer_crops_items_vanilla = [tomato, hot_pepper, blueberry, melon] -summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat] +summer_crops_items_thematic = [*summer_crops_items_vanilla, corn, hops, poppy, radish, red_cabbage, starfruit, summer_spangle, sunflower, wheat, summer_squash] summer_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.summer_crops, summer_crops_items_vanilla, 4, 4) summer_crops_bundle_thematic = BundleTemplate.extend_from(summer_crops_bundle_vanilla, summer_crops_items_thematic) fall_crops_items_vanilla = [corn, eggplant, pumpkin, yam] -fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, sunflower, wheat, sweet_gem_berry] +fall_crops_items_thematic = [*fall_crops_items_vanilla, amaranth, artichoke, beet, bok_choy, cranberries, fairy_rose, grape, + sunflower, wheat, sweet_gem_berry, broccoli] fall_crops_bundle_vanilla = BundleTemplate(CCRoom.pantry, BundleName.fall_crops, fall_crops_items_vanilla, 4, 4) fall_crops_bundle_thematic = BundleTemplate.extend_from(fall_crops_bundle_vanilla, fall_crops_items_thematic) -all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic}) +all_crops_items = sorted({*spring_crops_items_thematic, *summer_crops_items_thematic, *fall_crops_items_thematic, powdermelon}) quality_crops_items_vanilla = [item.as_quality_crop() for item in [parsnip, melon, pumpkin, corn]] quality_crops_items_thematic = [item.as_quality_crop() for item in all_crops_items] @@ -492,7 +576,8 @@ rare_crops_items = [ancient_fruit, sweet_gem_berry] rare_crops_bundle = BundleTemplate(CCRoom.pantry, BundleName.rare_crops, rare_crops_items, 2, 2) -fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(15), squid_ink] +# all_specific_roes = [BundleItem(AnimalProduct.roe, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fish] +fish_farmer_items = [roe.as_amount(15), aged_roe.as_amount(5), squid_ink, caviar.as_amount(5)] fish_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.fish_farmer, fish_farmer_items, 3, 2) garden_items = [tulip, blue_jazz, summer_spangle, sunflower, fairy_rose, poppy, bouquet] @@ -516,12 +601,20 @@ purple_slime_egg, green_slime_egg, tiger_slime_egg] slime_farmer_bundle = BundleTemplate(CCRoom.pantry, BundleName.slime_farmer, slime_farmer_items, 4, 3) +sommelier_items = [BundleItem(ArtisanGood.wine, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits] +sommelier_bundle = BundleTemplate(CCRoom.pantry, BundleName.sommelier, sommelier_items, 6, 3) + +dry_items = [*[BundleItem(ArtisanGood.dried_fruit, flavor=fruit, source=BundleItem.Sources.content) for fruit in all_fruits], + *[BundleItem(ArtisanGood.dried_mushroom, flavor=mushroom, source=BundleItem.Sources.content) for mushroom in all_edible_mushrooms], + BundleItem(ArtisanGood.raisins, source=BundleItem.Sources.content)] +dry_bundle = BundleTemplate(CCRoom.pantry, BundleName.dry, dry_items, 6, 3) + pantry_bundles_vanilla = [spring_crops_bundle_vanilla, summer_crops_bundle_vanilla, fall_crops_bundle_vanilla, quality_crops_bundle_vanilla, animal_bundle_vanilla, artisan_bundle_vanilla] pantry_bundles_thematic = [spring_crops_bundle_thematic, summer_crops_bundle_thematic, fall_crops_bundle_thematic, quality_crops_bundle_thematic, animal_bundle_thematic, artisan_bundle_thematic] pantry_bundles_remixed = [*pantry_bundles_thematic, rare_crops_bundle, fish_farmer_bundle, garden_bundle, - brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle] + brewer_bundle, orchard_bundle, island_crops_bundle, agronomist_bundle, slime_farmer_bundle, sommelier_bundle, dry_bundle] pantry_vanilla = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_vanilla, 6) pantry_thematic = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_thematic, 6) pantry_remixed = BundleRoomTemplate(CCRoom.pantry, pantry_bundles_remixed, 6) @@ -579,8 +672,11 @@ rain_fish_items = [red_snapper, shad, catfish, eel, walleye] rain_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.rain_fish, rain_fish_items, 3, 3) -quality_fish_items = sorted({item.as_quality(FishQuality.gold) for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic]}) -quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 4) +quality_fish_items = sorted({ + item.as_quality(FishQuality.gold).as_amount(2) + for item in [*river_fish_items_thematic, *lake_fish_items_thematic, *ocean_fish_items_thematic] +}) +quality_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.quality_fish, quality_fish_items, 4, 3) master_fisher_items = [lava_eel, scorpion_carp, octopus, blobfish, lingcod, ice_pip, super_cucumber, stingray, void_salmon, pufferfish] master_fisher_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.master_fisher, master_fisher_items, 4, 2) @@ -591,21 +687,31 @@ island_fish_items = [lionfish, blue_discus, stingray] island_fish_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.island_fish, island_fish_items, 3, 3) -tackle_items = [spinner, dressed_spinner, trap_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] -tackle_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) +tackle_items = [spinner, dressed_spinner, trap_bobber, sonar_bobber, cork_bobber, lead_bobber, treasure_hunter, barbed_hook, curiosity_lure, quality_bobber] +tackle_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.tackle, tackle_items, 3, 2) -bait_items = [bait, magnet, wild_bait, magic_bait] -bait_bundle = IslandBundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 2, 2) +bait_items = [bait, magnet, wild_bait, magic_bait, challenge_bait, deluxe_bait, targeted_bait] +bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.bait, bait_items, 3, 2) + +# This bundle could change based on content packs, once the fish are properly in it. Until then, I'm not sure how, so pelican town only +specific_bait_items = [BundleItem(ArtisanGood.targeted_bait, flavor=fish.name).as_amount(10) for fish in content_packs.pelican_town.fishes] +specific_bait_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.specific_bait, specific_bait_items, 6, 3) deep_fishing_items = [blobfish, spook_fish, midnight_squid, sea_cucumber, super_cucumber, octopus, pearl, seaweed] deep_fishing_bundle = FestivalBundleTemplate(CCRoom.fish_tank, BundleName.deep_fishing, deep_fishing_items, 4, 3) +smokeable_fish = [Fish.largemouth_bass, Fish.bream, Fish.bullhead, Fish.chub, Fish.ghostfish, Fish.flounder, Fish.shad, Fish.rainbow_trout, Fish.tilapia, + Fish.red_mullet, Fish.tuna, Fish.midnight_carp, Fish.salmon, Fish.perch] +fish_smoker_items = [BundleItem(ArtisanGood.smoked_fish, flavor=fish) for fish in smokeable_fish] +fish_smoker_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.fish_smoker, fish_smoker_items, 6, 3) + fish_tank_bundles_vanilla = [river_fish_bundle_vanilla, lake_fish_bundle_vanilla, ocean_fish_bundle_vanilla, night_fish_bundle_vanilla, crab_pot_bundle_vanilla, specialty_fish_bundle_vanilla] fish_tank_bundles_thematic = [river_fish_bundle_thematic, lake_fish_bundle_thematic, ocean_fish_bundle_thematic, night_fish_bundle_thematic, crab_pot_bundle_thematic, specialty_fish_bundle_thematic] fish_tank_bundles_remixed = [*fish_tank_bundles_thematic, spring_fish_bundle, summer_fish_bundle, fall_fish_bundle, winter_fish_bundle, trash_bundle, - rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle] + rain_fish_bundle, quality_fish_bundle, master_fisher_bundle, legendary_fish_bundle, tackle_bundle, bait_bundle, + specific_bait_bundle, deep_fishing_bundle, fish_smoker_bundle] # In Remixed, the trash items are in the recycling bundle, so we don't use the thematic version of the crab pot bundle that added trash items to it fish_tank_bundles_remixed.remove(crab_pot_bundle_thematic) @@ -670,12 +776,12 @@ chef_bundle_thematic = BundleTemplate.extend_from(chef_bundle_vanilla, chef_items_thematic) dye_items_vanilla = [red_mushroom, sea_urchin, sunflower, duck_feather, aquamarine, red_cabbage] -dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip, red_mushroom] dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] -dye_yellow_items = [corn, parsnip, summer_spangle, sunflower] -dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean] -dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit] -dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea] +dye_yellow_items = [corn, parsnip, summer_spangle, sunflower, starfruit] +dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean, cactus_fruit, duck_feather, dinosaur_egg] +dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit, aquamarine] +dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea, iridium_bar, sea_urchin, amaranth] dye_items_thematic = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] dye_bundle_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_vanilla, 6, 6) dye_bundle_thematic = DeepBundleTemplate(CCRoom.bulletin_board, BundleName.dye, dye_items_thematic, 6, 6) @@ -710,12 +816,31 @@ chocolate_cake, pancakes, rhubarb_pie] home_cook_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.home_cook, home_cook_items, 3, 3) +helper_items = [prize_ticket, mystery_box.as_amount(5), gold_mystery_box] +helper_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.helper, helper_items, 2, 2) + +spirit_eve_items = [jack_o_lantern, corn.as_amount(10), bat_wing.as_amount(10)] +spirit_eve_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.spirit_eve, spirit_eve_items, 3, 3) + +winter_star_items = [holly.as_amount(5), plum_pudding, stuffing, powdermelon.as_amount(5)] +winter_star_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.winter_star, winter_star_items, 2, 2) + bartender_items = [shrimp_cocktail, triple_shot_espresso, ginger_ale, cranberry_candy, beer, pale_ale, pina_colada] bartender_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.bartender, bartender_items, 3, 3) +calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg.as_amount(200), + magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50), + strawberry_seeds.as_amount(20), + spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5), + pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),] +calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2) + +raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4) + bulletin_board_bundles_vanilla = [chef_bundle_vanilla, dye_bundle_vanilla, field_research_bundle_vanilla, fodder_bundle_vanilla, enchanter_bundle_vanilla] bulletin_board_bundles_thematic = [chef_bundle_thematic, dye_bundle_thematic, field_research_bundle_thematic, fodder_bundle_thematic, enchanter_bundle_thematic] -bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, bartender_bundle] +bulletin_board_bundles_remixed = [*bulletin_board_bundles_thematic, children_bundle, forager_bundle, home_cook_bundle, + helper_bundle, spirit_eve_bundle, winter_star_bundle, bartender_bundle, calico_bundle, raccoon_bundle] bulletin_board_vanilla = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_vanilla, 5) bulletin_board_thematic = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_thematic, 5) bulletin_board_remixed = BundleRoomTemplate(CCRoom.bulletin_board, bulletin_board_bundles_remixed, 5) @@ -738,16 +863,15 @@ abandoned_joja_mart_thematic = BundleRoomTemplate(CCRoom.abandoned_joja_mart, abandoned_joja_mart_bundles_thematic, 1) abandoned_joja_mart_remixed = abandoned_joja_mart_thematic -# Make thematic with other currencies vault_2500_gold = BundleItem.money_bundle(2500) vault_5000_gold = BundleItem.money_bundle(5000) vault_10000_gold = BundleItem.money_bundle(10000) vault_25000_gold = BundleItem.money_bundle(25000) -vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, vault_2500_gold) -vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_5000_gold) -vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_10000_gold) -vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, vault_25000_gold) +vault_2500_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_2500, vault_2500_gold) +vault_5000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_5000, vault_5000_gold) +vault_10000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_10000, vault_10000_gold) +vault_25000_bundle = MoneyBundleTemplate(CCRoom.vault, BundleName.money_25000, vault_25000_gold) vault_gambler_items = BundleItem(Currency.qi_coin, 10000) vault_gambler_bundle = CurrencyBundleTemplate(CCRoom.vault, BundleName.gambler, vault_gambler_items) @@ -768,9 +892,14 @@ vault_thematic = BundleRoomTemplate(CCRoom.vault, vault_bundles_thematic, 4) vault_remixed = BundleRoomTemplate(CCRoom.vault, vault_bundles_remixed, 4) +all_cc_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed] +community_center_remixed_anywhere = BundleRoomTemplate("Community Center", all_cc_remixed_bundles, 26) + all_bundle_items_except_money = [] all_remixed_bundles = [*crafts_room_bundles_remixed, *pantry_bundles_remixed, *fish_tank_bundles_remixed, - *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic] + *boiler_room_bundles_remixed, *bulletin_board_bundles_remixed, missing_bundle_thematic, + *raccoon_bundles_remixed] for bundle in all_remixed_bundles: all_bundle_items_except_money.extend(bundle.items) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index bfb2d25ec6b8..d83478a62051 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,25 +1,28 @@ from typing import Dict, List, Optional -from ..mods.mod_data import ModNames from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource +from ..mods.mod_data import ModNames +from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood -from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, Craftable, \ - ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable +from ..strings.craftable_names import Bomb, Fence, Sprinkler, WildSeeds, Floor, Fishing, Ring, Consumable, Edible, Lighting, Storage, Furniture, Sign, \ + Craftable, \ + ModEdible, ModCraftable, ModMachine, ModFloor, ModConsumable, Statue from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro -from ..strings.fish_names import Fish, WaterItem +from ..strings.fish_names import Fish, WaterItem, ModTrash from ..strings.flower_names import Flower from ..strings.food_names import Meal -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom +from ..strings.gift_names import Gift from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material from ..strings.metal_names import Ore, MetalBar, Fossil, Artifact, Mineral, ModFossil -from ..strings.monster_drop_names import Loot +from ..strings.monster_drop_names import Loot, ModLoot from ..strings.quest_names import Quest -from ..strings.region_names import Region, SVERegion +from ..strings.region_names import Region, SVERegion, LogicRegion from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill, ModSkill from ..strings.special_order_names import SpecialOrder @@ -61,6 +64,11 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = MasterySource(skill) + return create_recipe(name, ingredients, source, mod_name) + + def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = ShopSource(region, price) return create_recipe(name, ingredients, source, mod_name) @@ -133,27 +141,37 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, cheese_press = skill_recipe(Machine.cheese_press, Skill.farming, 6, {Material.wood: 45, Material.stone: 45, Material.hardwood: 10, MetalBar.copper: 1}) keg = skill_recipe(Machine.keg, Skill.farming, 8, {Material.wood: 30, MetalBar.copper: 1, MetalBar.iron: 1, ArtisanGood.oak_resin: 1}) loom = skill_recipe(Machine.loom, Skill.farming, 7, {Material.wood: 60, Material.fiber: 30, ArtisanGood.pine_tar: 1}) -mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) +mayonnaise_machine = skill_recipe(Machine.mayonnaise_machine, Skill.farming, 2, + {Material.wood: 15, Material.stone: 15, Mineral.earth_crystal: 10, MetalBar.copper: 1}) oil_maker = skill_recipe(Machine.oil_maker, Skill.farming, 8, {Loot.slime: 50, Material.hardwood: 20, MetalBar.gold: 1}) preserves_jar = skill_recipe(Machine.preserves_jar, Skill.farming, 4, {Material.wood: 50, Material.stone: 40, Material.coal: 8}) +fish_smoker = shop_recipe(Machine.fish_smoker, Region.fish_shop, 10000, + {Material.hardwood: 10, WaterItem.sea_jelly: 1, WaterItem.river_jelly: 1, WaterItem.cave_jelly: 1}) +dehydrator = shop_recipe(Machine.dehydrator, Region.pierre_store, 10000, {Material.wood: 30, Material.clay: 2, Mineral.fire_quartz: 1}) basic_fertilizer = skill_recipe(Fertilizer.basic, Skill.farming, 1, {Material.sap: 2}) -quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 2, Fish.any: 1}) + +quality_fertilizer = skill_recipe(Fertilizer.quality, Skill.farming, 9, {Material.sap: 4, Fish.any: 1}) deluxe_fertilizer = ap_recipe(Fertilizer.deluxe, {MetalBar.iridium: 1, Material.sap: 40}) -basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Fish.clam: 1}) -deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, WaterItem.coral: 1}) + +basic_speed_gro = skill_recipe(SpeedGro.basic, Skill.farming, 3, {ArtisanGood.pine_tar: 1, Material.moss: 5}) +deluxe_speed_gro = skill_recipe(SpeedGro.deluxe, Skill.farming, 8, {ArtisanGood.oak_resin: 1, Fossil.bone_fragment: 5}) hyper_speed_gro = ap_recipe(SpeedGro.hyper, {Ore.radioactive: 1, Fossil.bone_fragment: 3, Loot.solar_essence: 1}) basic_retaining_soil = skill_recipe(RetainingSoil.basic, Skill.farming, 4, {Material.stone: 2}) quality_retaining_soil = skill_recipe(RetainingSoil.quality, Skill.farming, 7, {Material.stone: 3, Material.clay: 1}) -deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) +deluxe_retaining_soil = shop_trade_recipe(RetainingSoil.deluxe, Region.island_trader, Currency.cinder_shard, 50, + {Material.stone: 5, Material.fiber: 3, Material.clay: 1}) tree_fertilizer = skill_recipe(Fertilizer.tree, Skill.foraging, 7, {Material.fiber: 5, Material.stone: 5}) -spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) +spring_seeds = skill_recipe(WildSeeds.spring, Skill.foraging, 1, + {Forageable.wild_horseradish: 1, Forageable.daffodil: 1, Forageable.leek: 1, Forageable.dandelion: 1}) summer_seeds = skill_recipe(WildSeeds.summer, Skill.foraging, 4, {Forageable.spice_berry: 1, Fruit.grape: 1, Forageable.sweet_pea: 1}) -fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Forageable.common_mushroom: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) -winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) +fall_seeds = skill_recipe(WildSeeds.fall, Skill.foraging, 6, {Mushroom.common: 1, Forageable.wild_plum: 1, Forageable.hazelnut: 1, Forageable.blackberry: 1}) +winter_seeds = skill_recipe(WildSeeds.winter, Skill.foraging, 7, + {Forageable.winter_root: 1, Forageable.crystal_fruit: 1, Forageable.snow_yam: 1, Forageable.crocus: 1}) ancient_seeds = ap_recipe(WildSeeds.ancient, {Artifact.ancient_seed: 1}) grass_starter = shop_recipe(WildSeeds.grass_starter, Region.pierre_store, 1000, {Material.fiber: 10}) +blue_grass_starter = ap_recipe(WildSeeds.blue_grass_starter, {Material.fiber: 25, Material.moss: 10, ArtisanGood.mystic_syrup: 1}) for wild_seeds in [WildSeeds.spring, WildSeeds.summer, WildSeeds.fall, WildSeeds.winter]: tea_sapling = cutscene_recipe(WildSeeds.tea_sapling, Region.sunroom, NPC.caroline, 2, {wild_seeds: 2, Material.fiber: 5, Material.wood: 5}) fiber_seeds = special_order_recipe(WildSeeds.fiber, SpecialOrder.community_cleanup, {Seed.mixed: 1, Material.sap: 5, Material.clay: 1}) @@ -161,7 +179,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, wood_floor = shop_recipe(Floor.wood, Region.carpenter, 100, {Material.wood: 1}) rustic_floor = shop_recipe(Floor.rustic, Region.carpenter, 200, {Material.wood: 1}) straw_floor = shop_recipe(Floor.straw, Region.carpenter, 200, {Material.wood: 1, Material.fiber: 1}) -weathered_floor = shop_recipe(Floor.weathered, Region.mines_dwarf_shop, 500, {Material.wood: 1}) +weathered_floor = shop_recipe(Floor.weathered, LogicRegion.mines_dwarf_shop, 500, {Material.wood: 1}) crystal_floor = shop_recipe(Floor.crystal, Region.sewer, 500, {MetalBar.quartz: 1}) stone_floor = shop_recipe(Floor.stone, Region.carpenter, 100, {Material.stone: 1}) stone_walkway_floor = shop_recipe(Floor.stone_walkway, Region.carpenter, 200, {Material.stone: 1}) @@ -174,6 +192,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spinner = skill_recipe(Fishing.spinner, Skill.fishing, 6, {MetalBar.iron: 2}) trap_bobber = skill_recipe(Fishing.trap_bobber, Skill.fishing, 6, {MetalBar.copper: 1, Material.sap: 10}) +sonar_bobber = skill_recipe(Fishing.sonar_bobber, Skill.fishing, 6, {MetalBar.iron: 1, MetalBar.quartz: 2}) cork_bobber = skill_recipe(Fishing.cork_bobber, Skill.fishing, 7, {Material.wood: 10, Material.hardwood: 5, Loot.slime: 10}) quality_bobber = special_order_recipe(Fishing.quality_bobber, SpecialOrder.juicy_bugs_wanted, {MetalBar.copper: 1, Material.sap: 20, Loot.solar_essence: 5}) treasure_hunter = skill_recipe(Fishing.treasure_hunter, Skill.fishing, 7, {MetalBar.gold: 2}) @@ -181,6 +200,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, barbed_hook = skill_recipe(Fishing.barbed_hook, Skill.fishing, 8, {MetalBar.copper: 1, MetalBar.iron: 1, MetalBar.gold: 1}) magnet = skill_recipe(Fishing.magnet, Skill.fishing, 9, {MetalBar.iron: 1}) bait = skill_recipe(Fishing.bait, Skill.fishing, 2, {Loot.bug_meat: 1}) +deluxe_bait = skill_recipe(Fishing.deluxe_bait, Skill.fishing, 4, {Fishing.bait: 5, Material.moss: 2}) wild_bait = cutscene_recipe(Fishing.wild_bait, Region.tent, NPC.linus, 4, {Material.fiber: 10, Loot.bug_meat: 5, Loot.slime: 5}) magic_bait = ap_recipe(Fishing.magic_bait, {Ore.radioactive: 1, Loot.bug_meat: 3}) crab_pot = skill_recipe(Machine.crab_pot, Skill.fishing, 3, {Material.wood: 40, MetalBar.iron: 3}) @@ -191,11 +211,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, thorns_ring = skill_recipe(Ring.thorns_ring, Skill.combat, 7, {Fossil.bone_fragment: 50, Material.stone: 50, MetalBar.gold: 1}) glowstone_ring = skill_recipe(Ring.glowstone_ring, Skill.mining, 4, {Loot.solar_essence: 5, MetalBar.iron: 5}) iridium_band = skill_recipe(Ring.iridium_band, Skill.combat, 9, {MetalBar.iridium: 5, Loot.solar_essence: 50, Loot.void_essence: 50}) -wedding_ring = shop_recipe(Ring.wedding_ring, Region.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) +wedding_ring = shop_recipe(Ring.wedding_ring, LogicRegion.traveling_cart, 500, {MetalBar.iridium: 5, Mineral.prismatic_shard: 1}) field_snack = skill_recipe(Edible.field_snack, Skill.foraging, 1, {TreeSeed.acorn: 1, TreeSeed.maple: 1, TreeSeed.pine: 1}) bug_steak = skill_recipe(Edible.bug_steak, Skill.combat, 1, {Loot.bug_meat: 10}) -life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Forageable.red_mushroom: 1, Forageable.purple_mushroom: 1, Forageable.morel: 1, Forageable.chanterelle: 1}) +life_elixir = skill_recipe(Edible.life_elixir, Skill.combat, 2, {Mushroom.red: 1, Mushroom.purple: 1, Mushroom.morel: 1, Mushroom.chanterelle: 1}) oil_of_garlic = skill_recipe(Edible.oil_of_garlic, Skill.combat, 6, {Vegetable.garlic: 10, Ingredient.oil: 1}) monster_musk = special_order_recipe(Consumable.monster_musk, SpecialOrder.prismatic_jelly, {Loot.bat_wing: 30, Loot.slime: 30}) @@ -203,8 +223,10 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, warp_totem_beach = skill_recipe(Consumable.warp_totem_beach, Skill.foraging, 6, {Material.hardwood: 1, WaterItem.coral: 2, Material.fiber: 10}) warp_totem_mountains = skill_recipe(Consumable.warp_totem_mountains, Skill.foraging, 7, {Material.hardwood: 1, MetalBar.iron: 1, Material.stone: 25}) warp_totem_farm = skill_recipe(Consumable.warp_totem_farm, Skill.foraging, 8, {Material.hardwood: 1, ArtisanGood.honey: 1, Material.fiber: 20}) -warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) -warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) +warp_totem_desert = shop_trade_recipe(Consumable.warp_totem_desert, Region.desert, MetalBar.iridium, 10, + {Material.hardwood: 2, Forageable.coconut: 1, Ore.iridium: 4}) +warp_totem_island = shop_recipe(Consumable.warp_totem_island, Region.volcano_dwarf_shop, 10000, + {Material.hardwood: 5, Forageable.dragon_tooth: 1, Forageable.ginger: 1}) rain_totem = skill_recipe(Consumable.rain_totem, Skill.foraging, 9, {Material.hardwood: 1, ArtisanGood.truffle_oil: 1, ArtisanGood.pine_tar: 5}) torch = starter_recipe(Lighting.torch, {Material.wood: 1, Material.sap: 2}) @@ -219,13 +241,17 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, marble_brazier = shop_recipe(Lighting.marble_brazier, Region.carpenter, 5000, {Mineral.marble: 1, Mineral.aquamarine: 1, Material.stone: 100}) wood_lamp_post = shop_recipe(Lighting.wood_lamp_post, Region.carpenter, 500, {Material.wood: 50, ArtisanGood.battery_pack: 1}) iron_lamp_post = shop_recipe(Lighting.iron_lamp_post, Region.carpenter, 1000, {MetalBar.iron: 1, ArtisanGood.battery_pack: 1}) -jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, Region.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) +jack_o_lantern = festival_shop_recipe(Lighting.jack_o_lantern, LogicRegion.spirit_eve, 2000, {Vegetable.pumpkin: 1, Lighting.torch: 1}) bone_mill = special_order_recipe(Machine.bone_mill, SpecialOrder.fragments_of_the_past, {Fossil.bone_fragment: 10, Material.clay: 3, Material.stone: 20}) -charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 4, {Material.wood: 20, MetalBar.copper: 2}) +bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: 3, WaterItem.coral: 3, WaterItem.sea_urchin: 1}) + +charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) + crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) +mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) lightning_rod = skill_recipe(Machine.lightning_rod, Skill.foraging, 6, {MetalBar.iron: 1, MetalBar.quartz: 1, Loot.bat_wing: 5}) ostrich_incubator = ap_recipe(Machine.ostrich_incubator, {Fossil.bone_fragment: 50, Material.hardwood: 50, Currency.cinder_shard: 20}) @@ -234,20 +260,27 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, slime_egg_press = skill_recipe(Machine.slime_egg_press, Skill.combat, 6, {Material.coal: 25, Mineral.fire_quartz: 1, ArtisanGood.battery_pack: 1}) slime_incubator = skill_recipe(Machine.slime_incubator, Skill.combat, 8, {MetalBar.iridium: 2, Loot.slime: 100}) solar_panel = special_order_recipe(Machine.solar_panel, SpecialOrder.island_ingredients, {MetalBar.quartz: 10, MetalBar.iron: 5, MetalBar.gold: 5}) -tapper = skill_recipe(Machine.tapper, Skill.foraging, 3, {Material.wood: 40, MetalBar.copper: 2}) -worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 8, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) -tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, Region.flower_dance, 2000, {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) +tapper = skill_recipe(Machine.tapper, Skill.foraging, 4, {Material.wood: 40, MetalBar.copper: 2}) + +worm_bin = skill_recipe(Machine.worm_bin, Skill.fishing, 4, {Material.hardwood: 25, MetalBar.gold: 1, MetalBar.iron: 1, Material.fiber: 50}) +deluxe_worm_bin = skill_recipe(Machine.deluxe_worm_bin, Skill.fishing, 8, {Machine.worm_bin: 1, Material.moss: 30}) + +tub_o_flowers = festival_shop_recipe(Furniture.tub_o_flowers, LogicRegion.flower_dance, 2000, + {Material.wood: 15, Seed.tulip: 1, Seed.jazz: 1, Seed.poppy: 1, Seed.spangle: 1}) wicked_statue = shop_recipe(Furniture.wicked_statue, Region.sewer, 1000, {Material.stone: 25, Material.coal: 5}) flute_block = cutscene_recipe(Furniture.flute_block, Region.carpenter, NPC.robin, 6, {Material.wood: 10, Ore.copper: 2, Material.fiber: 20}) drum_block = cutscene_recipe(Furniture.drum_block, Region.carpenter, NPC.robin, 6, {Material.stone: 10, Ore.copper: 2, Material.fiber: 20}) chest = starter_recipe(Storage.chest, {Material.wood: 50}) stone_chest = special_order_recipe(Storage.stone_chest, SpecialOrder.robins_resource_rush, {Material.stone: 50}) +big_chest = shop_recipe(Storage.big_chest, Region.carpenter, 5000, {Material.wood: 120, MetalBar.copper: 2}) +big_stone_chest = shop_recipe(Storage.big_stone_chest, LogicRegion.mines_dwarf_shop, 5000, {Material.stone: 250}) wood_sign = starter_recipe(Sign.wood, {Material.wood: 25}) stone_sign = starter_recipe(Sign.stone, {Material.stone: 25}) dark_sign = friendship_recipe(Sign.dark, NPC.krobus, 3, {Loot.bat_wing: 5, Fossil.bone_fragment: 5}) +text_sign = starter_recipe(Sign.text, {Material.wood: 25}) garden_pot = ap_recipe(Craftable.garden_pot, {Material.clay: 1, Material.stone: 10, MetalBar.quartz: 1}, "Greenhouse") scarecrow = skill_recipe(Craftable.scarecrow, Skill.farming, 1, {Material.wood: 50, Material.coal: 1, Material.fiber: 20}) @@ -258,56 +291,84 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, transmute_au = skill_recipe(Craftable.transmute_au, Skill.mining, 7, {MetalBar.iron: 2}) mini_jukebox = cutscene_recipe(Craftable.mini_jukebox, Region.saloon, NPC.gus, 5, {MetalBar.iron: 2, ArtisanGood.battery_pack: 1}) mini_obelisk = special_order_recipe(Craftable.mini_obelisk, SpecialOrder.a_curious_substance, {Material.hardwood: 30, Loot.solar_essence: 20, MetalBar.gold: 3}) -farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) +farm_computer = special_order_recipe(Craftable.farm_computer, SpecialOrder.aquatic_overpopulation, + {Artifact.dwarf_gadget: 1, ArtisanGood.battery_pack: 1, MetalBar.quartz: 10}) hopper = ap_recipe(Craftable.hopper, {Material.hardwood: 10, MetalBar.iridium: 1, MetalBar.radioactive: 1}) -cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 9, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) + +cookout_kit = skill_recipe(Craftable.cookout_kit, Skill.foraging, 3, {Material.wood: 15, Material.fiber: 10, Material.coal: 3}) +tent_kit = skill_recipe(Craftable.tent_kit, Skill.foraging, 8, {Material.hardwood: 10, Material.fiber: 25, ArtisanGood.cloth: 1}) + +statue_of_blessings = mastery_recipe(Statue.blessings, Skill.farming, {Material.sap: 999, Material.fiber: 999, Material.stone: 999}) +statue_of_dwarf_king = mastery_recipe(Statue.dwarf_king, Skill.mining, {MetalBar.iridium: 20}) +heavy_furnace = mastery_recipe(Machine.heavy_furnace, Skill.mining, {Machine.furnace: 2, MetalBar.iron: 3, Material.stone: 50}) +mystic_tree_seed = mastery_recipe(TreeSeed.mystic, Skill.foraging, {TreeSeed.acorn: 5, TreeSeed.maple: 5, TreeSeed.pine: 5, TreeSeed.mahogany: 5}) +treasure_totem = mastery_recipe(Consumable.treasure_totem, Skill.foraging, {Material.hardwood: 5, ArtisanGood.mystic_syrup: 1, Material.moss: 10}) +challenge_bait = mastery_recipe(Fishing.challenge_bait, Skill.fishing, {Fossil.bone_fragment: 5, Material.moss: 2}) +anvil = mastery_recipe(Machine.anvil, Skill.combat, {MetalBar.iron: 50}) +mini_forge = mastery_recipe(Machine.mini_forge, Skill.combat, {Forageable.dragon_tooth: 5, MetalBar.iron: 10, MetalBar.gold: 10, MetalBar.iridium: 5}) travel_charm = shop_recipe(ModCraftable.travel_core, Region.adventurer_guild, 250, {Loot.solar_essence: 1, Loot.void_essence: 1}, ModNames.magic) -preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 2, {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, +preservation_chamber = skill_recipe(ModMachine.preservation_chamber, ModSkill.archaeology, 1, + {MetalBar.copper: 1, Material.wood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 7, {MetalBar.copper: 1, Material.hardwood: 15, +restoration_table = skill_recipe(ModMachine.restoration_table, ModSkill.archaeology, 1, {Material.wood: 15, MetalBar.copper: 1, MetalBar.iron: 1}, ModNames.archaeology) +preservation_chamber_h = skill_recipe(ModMachine.hardwood_preservation_chamber, ModSkill.archaeology, 6, {MetalBar.copper: 1, Material.hardwood: 15, ArtisanGood.oak_resin: 30}, ModNames.archaeology) -grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 8, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, ModNames.archaeology) -ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 6, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, +grinder = skill_recipe(ModMachine.grinder, ModSkill.archaeology, 2, {Artifact.rusty_cog: 10, MetalBar.iron: 5, ArtisanGood.battery_pack: 1}, + ModNames.archaeology) +ancient_battery = skill_recipe(ModMachine.ancient_battery, ModSkill.archaeology, 7, {Material.stone: 40, MetalBar.copper: 10, MetalBar.iron: 5}, ModNames.archaeology) -glass_bazier = skill_recipe(ModCraftable.glass_bazier, ModSkill.archaeology, 1, {Artifact.glass_shards: 10}, ModNames.archaeology) -glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 1, {Artifact.glass_shards: 1}, ModNames.archaeology) -glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 1, {Artifact.glass_shards: 5}, ModNames.archaeology) -bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 3, {Fossil.bone_fragment: 1}, ModNames.archaeology) +glass_bazier = skill_recipe(ModCraftable.glass_brazier, ModSkill.archaeology, 4, {Artifact.glass_shards: 10}, ModNames.archaeology) +glass_path = skill_recipe(ModFloor.glass_path, ModSkill.archaeology, 3, {Artifact.glass_shards: 1}, ModNames.archaeology) +glass_fence = skill_recipe(ModCraftable.glass_fence, ModSkill.archaeology, 7, {Artifact.glass_shards: 5}, ModNames.archaeology) +bone_path = skill_recipe(ModFloor.bone_path, ModSkill.archaeology, 4, {Fossil.bone_fragment: 1}, ModNames.archaeology) +rust_path = skill_recipe(ModFloor.rusty_path, ModSkill.archaeology, 2, {ModTrash.rusty_scrap: 2}, ModNames.archaeology) +rusty_brazier = skill_recipe(ModCraftable.rusty_brazier, ModSkill.archaeology, 3, {ModTrash.rusty_scrap: 10, Material.coal: 1, Material.fiber: 1}, ModNames.archaeology) +bone_fence = skill_recipe(ModCraftable.bone_fence, ModSkill.archaeology, 8, {Fossil.bone_fragment: 2}, ModNames.archaeology) water_shifter = skill_recipe(ModCraftable.water_shifter, ModSkill.archaeology, 4, {Material.wood: 40, MetalBar.copper: 4}, ModNames.archaeology) -wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 2, {Material.wood: 25}, ModNames.archaeology) +wooden_display = skill_recipe(ModCraftable.wooden_display, ModSkill.archaeology, 1, {Material.wood: 25}, ModNames.archaeology) hardwood_display = skill_recipe(ModCraftable.hardwood_display, ModSkill.archaeology, 7, {Material.hardwood: 10}, ModNames.archaeology) +lucky_ring = skill_recipe(Ring.lucky_ring, ModSkill.archaeology, 8, {Artifact.elvish_jewelry: 1, AnimalProduct.rabbit_foot: 5, Mineral.tigerseye: 1}, ModNames.archaeology) volcano_totem = skill_recipe(ModConsumable.volcano_totem, ModSkill.archaeology, 9, {Material.cinder_shard: 5, Artifact.rare_disc: 1, Artifact.dwarf_gadget: 1}, ModNames.archaeology) -haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, SVEForage.void_soul: 5, Ingredient.sugar: 1, +haste_elixir = shop_recipe(ModEdible.haste_elixir, SVERegion.alesia_shop, 35000, {Loot.void_essence: 35, ModLoot.void_soul: 5, Ingredient.sugar: 1, Meal.spicy_eel: 1}, ModNames.sve) -hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {SVEForage.void_pebble: 3, SVEForage.void_soul: 5, Ingredient.oil: 1, +hero_elixir = shop_recipe(ModEdible.hero_elixir, SVERegion.isaac_shop, 65000, {ModLoot.void_pebble: 3, ModLoot.void_soul: 5, Ingredient.oil: 1, Loot.slime: 10}, ModNames.sve) -armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, SVEForage.void_soul: 5, Ingredient.vinegar: 5, +armor_elixir = shop_recipe(ModEdible.armor_elixir, SVERegion.alesia_shop, 50000, {Loot.solar_essence: 30, ModLoot.void_soul: 5, Ingredient.vinegar: 5, Fossil.bone_fragment: 5}, ModNames.sve) ginger_tincture = friendship_recipe(ModConsumable.ginger_tincture, ModNPC.goblin, 4, {DistantLandsForageable.brown_amanita: 1, Forageable.ginger: 5, - Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, ModNames.distant_lands) - -neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, Region.mines_dwarf_shop, 5000, - {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, ModFossil.neanderthal_limb_bones: 1, - MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) -pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, Region.mines_dwarf_shop, 5000, + Material.cinder_shard: 1, DistantLandsForageable.swamp_herb: 1}, + ModNames.distant_lands) + +neanderthal_skeleton = shop_recipe(ModCraftable.neanderthal_skeleton, LogicRegion.mines_dwarf_shop, 5000, + {ModFossil.neanderthal_skull: 1, ModFossil.neanderthal_ribs: 1, ModFossil.neanderthal_pelvis: 1, + ModFossil.neanderthal_limb_bones: 1, + MetalBar.iron: 5, Material.hardwood: 10}, ModNames.boarding_house) +pterodactyl_skeleton_l = shop_recipe(ModCraftable.pterodactyl_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_skull: 1, ModFossil.pterodactyl_l_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_m = shop_recipe(ModCraftable.pterodactyl_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_vertebra: 1, ModFossil.pterodactyl_ribs: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, Region.mines_dwarf_shop, 5000, +pterodactyl_skeleton_r = shop_recipe(ModCraftable.pterodactyl_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.pterodactyl_phalange: 1, ModFossil.pterodactyl_claw: 1, ModFossil.pterodactyl_r_wing_bone: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, Region.mines_dwarf_shop, 5000, +trex_skeleton_l = shop_recipe(ModCraftable.trex_skeleton_l, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_tooth: 1, ModFossil.dinosaur_skull: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, Region.mines_dwarf_shop, 5000, +trex_skeleton_m = shop_recipe(ModCraftable.trex_skeleton_m, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_ribs: 1, ModFossil.dinosaur_claw: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) -trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, Region.mines_dwarf_shop, 5000, +trex_skeleton_r = shop_recipe(ModCraftable.trex_skeleton_r, LogicRegion.mines_dwarf_shop, 5000, {ModFossil.dinosaur_vertebra: 1, ModFossil.dinosaur_femur: 1, ModFossil.dinosaur_pelvis: 1, MetalBar.iron: 10, Material.hardwood: 15}, ModNames.boarding_house) +bouquet = skill_recipe(Gift.bouquet, ModSkill.socializing, 3, {Flower.tulip: 3}, ModNames.socializing_skill) +trash_bin = skill_recipe(ModMachine.trash_bin, ModSkill.binning, 2, {Material.stone: 30, MetalBar.iron: 2}, ModNames.binning_skill) +composter = skill_recipe(ModMachine.composter, ModSkill.binning, 4, {Material.wood: 70, Material.sap: 20, Material.fiber: 30}, ModNames.binning_skill) +recycling_bin = skill_recipe(ModMachine.recycling_bin, ModSkill.binning, 7, {MetalBar.iron: 3, Material.fiber: 10, MetalBar.gold: 2}, ModNames.binning_skill) +advanced_recycling_machine = skill_recipe(ModMachine.advanced_recycling_machine, ModSkill.binning, 9, + {MetalBar.iridium: 5, ArtisanGood.battery_pack: 2, MetalBar.quartz: 10}, ModNames.binning_skill) + all_crafting_recipes_by_name = {recipe.item: recipe for recipe in all_crafting_recipes} diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv deleted file mode 100644 index 0bf43a76764e..000000000000 --- a/worlds/stardew_valley/data/crops.csv +++ /dev/null @@ -1,41 +0,0 @@ -crop,farm_growth_seasons,seed,seed_seasons,seed_regions,requires_island -Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store",False -Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store",False -Beet,Fall,Beet Seeds,Fall,Oasis,False -Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store",False -Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store",False -Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store",False -Cactus Fruit,,Cactus Seeds,,Oasis,False -Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store",False -Coffee Bean,"Spring,Summer",Coffee Bean,"Summer,Fall","Traveling Cart",False -Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store",False -Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store",False -Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store",False -Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store",False -Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store",False -Grape,Fall,Grape Starter,Fall,"Pierre's General Store",False -Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store",False -Hops,Summer,Hops Starter,Summer,"Pierre's General Store",False -Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store",False -Kale,Spring,Kale Seeds,Spring,"Pierre's General Store",False -Melon,Summer,Melon Seeds,Summer,"Pierre's General Store",False -Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store",False -Pineapple,Summer,Pineapple Seeds,Summer,"Island Trader",True -Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store",False -Potato,Spring,Potato Seeds,Spring,"Pierre's General Store",False -Qi Fruit,"Spring,Summer,Fall,Winter",Qi Bean,"Spring,Summer,Fall,Winter","Qi's Walnut Room",True -Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store",False -Radish,Summer,Radish Seeds,Summer,"Pierre's General Store",False -Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store",False -Rhubarb,Spring,Rhubarb Seeds,Spring,Oasis,False -Starfruit,Summer,Starfruit Seeds,Summer,Oasis,False -Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store",False -Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store",False -Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store",False -Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart,False -Taro Root,Summer,Taro Tuber,Summer,"Island Trader",True -Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store",False -Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store",False -Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store",False -Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store",False -Yam,Fall,Yam Seeds,Fall,"Pierre's General Store",False diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py deleted file mode 100644 index 7144ccfbcf9b..000000000000 --- a/worlds/stardew_valley/data/crops_data.py +++ /dev/null @@ -1,50 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - -from .. import data - - -@dataclass(frozen=True) -class SeedItem: - name: str - seasons: Tuple[str] - regions: Tuple[str] - requires_island: bool - - -@dataclass(frozen=True) -class CropItem: - name: str - farm_growth_seasons: Tuple[str] - seed: SeedItem - - -def load_crop_csv(): - import csv - try: - from importlib.resources import files - except ImportError: - from importlib_resources import files # noqa - - with files(data).joinpath("crops.csv").open() as file: - reader = csv.DictReader(file) - crops = [] - seeds = [] - - for item in reader: - seeds.append(SeedItem(item["seed"], - tuple(season for season in item["seed_seasons"].split(",")) - if item["seed_seasons"] else tuple(), - tuple(region for region in item["seed_regions"].split(",")) - if item["seed_regions"] else tuple(), - item["requires_island"] == "True")) - crops.append(CropItem(item["crop"], - tuple(season for season in item["farm_growth_seasons"].split(",")) - if item["farm_growth_seasons"] else tuple(), - seeds[-1])) - return crops, seeds - - -# TODO Those two should probably be split to we can include rest of seeds -all_crops, all_purchasable_seeds = load_crop_csv() -crops_by_name = {crop.name: crop for crop in all_crops} diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index aeb416733950..c6f0c30d41ff 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Union, Optional, Set +from typing import Tuple, Union, Optional from . import season_data as season -from ..strings.fish_names import Fish, SVEFish, DistantLandsFish -from ..strings.region_names import Region, SVERegion from ..mods.mod_data import ModNames +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish +from ..strings.region_names import Region, SVERegion, LogicRegion @dataclass(frozen=True) @@ -30,6 +30,7 @@ def __repr__(self): mountain_lake = (Region.mountain,) forest_pond = (Region.forest,) forest_river = (Region.forest,) +forest_waterfall = (LogicRegion.forest_waterfall,) secret_woods = (Region.secret_woods,) mines_floor_20 = (Region.mines_floor_20,) mines_floor_60 = (Region.mines_floor_60,) @@ -50,8 +51,6 @@ def __repr__(self): fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) -all_fish: List[FishItem] = [] - def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]], difficulty: int, legendary: bool = False, extended_family: bool = False, mod_name: Optional[str] = None) -> FishItem: @@ -59,63 +58,63 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple seasons = (seasons,) fish_item = FishItem(name, locations, seasons, difficulty, legendary, extended_family, mod_name) - all_fish.append(fish_item) return fish_item -albacore = create_fish("Albacore", ocean, (season.fall, season.winter), 60) -anchovy = create_fish("Anchovy", ocean, (season.spring, season.fall), 30) -blue_discus = create_fish("Blue Discus", ginger_island_river, season.all_seasons, 60) -bream = create_fish("Bream", town_river + forest_river, season.all_seasons, 35) -bullhead = create_fish("Bullhead", mountain_lake, season.all_seasons, 46) +albacore = create_fish(Fish.albacore, ocean, (season.fall, season.winter), 60) +anchovy = create_fish(Fish.anchovy, ocean, (season.spring, season.fall), 30) +blue_discus = create_fish(Fish.blue_discus, ginger_island_river, season.all_seasons, 60) +bream = create_fish(Fish.bream, town_river + forest_river, season.all_seasons, 35) +bullhead = create_fish(Fish.bullhead, mountain_lake, season.all_seasons, 46) carp = create_fish(Fish.carp, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15) -catfish = create_fish("Catfish", town_river + forest_river + secret_woods, (season.spring, season.fall), 75) -chub = create_fish("Chub", forest_river + mountain_lake, season.all_seasons, 35) -dorado = create_fish("Dorado", forest_river, season.summer, 78) -eel = create_fish("Eel", ocean, (season.spring, season.fall), 70) -flounder = create_fish("Flounder", ocean, (season.spring, season.summer), 50) -ghostfish = create_fish("Ghostfish", mines_floor_20 + mines_floor_60, season.all_seasons, 50) -halibut = create_fish("Halibut", ocean, season.not_fall, 50) -herring = create_fish("Herring", ocean, (season.spring, season.winter), 25) -ice_pip = create_fish("Ice Pip", mines_floor_60, season.all_seasons, 85) -largemouth_bass = create_fish("Largemouth Bass", mountain_lake, season.all_seasons, 50) -lava_eel = create_fish("Lava Eel", mines_floor_100, season.all_seasons, 90) -lingcod = create_fish("Lingcod", town_river + forest_river + mountain_lake, season.winter, 85) -lionfish = create_fish("Lionfish", ginger_island_ocean, season.all_seasons, 50) -midnight_carp = create_fish("Midnight Carp", mountain_lake + forest_pond + ginger_island_river, +catfish = create_fish(Fish.catfish, town_river + forest_river + secret_woods, (season.spring, season.fall), 75) +chub = create_fish(Fish.chub, forest_river + mountain_lake, season.all_seasons, 35) +dorado = create_fish(Fish.dorado, forest_river, season.summer, 78) +eel = create_fish(Fish.eel, ocean, (season.spring, season.fall), 70) +flounder = create_fish(Fish.flounder, ocean, (season.spring, season.summer), 50) +ghostfish = create_fish(Fish.ghostfish, mines_floor_20 + mines_floor_60, season.all_seasons, 50) +goby = create_fish(Fish.goby, forest_waterfall, season.all_seasons, 55) +halibut = create_fish(Fish.halibut, ocean, season.not_fall, 50) +herring = create_fish(Fish.herring, ocean, (season.spring, season.winter), 25) +ice_pip = create_fish(Fish.ice_pip, mines_floor_60, season.all_seasons, 85) +largemouth_bass = create_fish(Fish.largemouth_bass, mountain_lake, season.all_seasons, 50) +lava_eel = create_fish(Fish.lava_eel, mines_floor_100, season.all_seasons, 90) +lingcod = create_fish(Fish.lingcod, town_river + forest_river + mountain_lake, season.winter, 85) +lionfish = create_fish(Fish.lionfish, ginger_island_ocean, season.all_seasons, 50) +midnight_carp = create_fish(Fish.midnight_carp, mountain_lake + forest_pond + ginger_island_river, (season.fall, season.winter), 55) -octopus = create_fish("Octopus", ocean, season.summer, 95) -perch = create_fish("Perch", town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) -pike = create_fish("Pike", town_river + forest_river + forest_pond, (season.summer, season.winter), 60) -pufferfish = create_fish("Pufferfish", ocean + ginger_island_ocean, season.summer, 80) -rainbow_trout = create_fish("Rainbow Trout", town_river + forest_river + mountain_lake, season.summer, 45) -red_mullet = create_fish("Red Mullet", ocean, (season.summer, season.winter), 55) -red_snapper = create_fish("Red Snapper", ocean, (season.summer, season.fall), 40) -salmon = create_fish("Salmon", town_river + forest_river, season.fall, 50) -sandfish = create_fish("Sandfish", desert, season.all_seasons, 65) -sardine = create_fish("Sardine", ocean, (season.spring, season.fall, season.winter), 30) -scorpion_carp = create_fish("Scorpion Carp", desert, season.all_seasons, 90) -sea_cucumber = create_fish("Sea Cucumber", ocean, (season.fall, season.winter), 40) -shad = create_fish("Shad", town_river + forest_river, season.not_winter, 45) -slimejack = create_fish("Slimejack", mutant_bug_lair, season.all_seasons, 55) -smallmouth_bass = create_fish("Smallmouth Bass", town_river + forest_river, (season.spring, season.fall), 28) -squid = create_fish("Squid", ocean, season.winter, 75) -stingray = create_fish("Stingray", pirate_cove, season.all_seasons, 80) -stonefish = create_fish("Stonefish", mines_floor_20, season.all_seasons, 65) -sturgeon = create_fish("Sturgeon", mountain_lake, (season.summer, season.winter), 78) -sunfish = create_fish("Sunfish", town_river + forest_river, (season.spring, season.summer), 30) -super_cucumber = create_fish("Super Cucumber", ocean + ginger_island_ocean, (season.summer, season.fall), 80) -tiger_trout = create_fish("Tiger Trout", town_river + forest_river, (season.fall, season.winter), 60) -tilapia = create_fish("Tilapia", ocean + ginger_island_ocean, (season.summer, season.fall), 50) +octopus = create_fish(Fish.octopus, ocean, season.summer, 95) +perch = create_fish(Fish.perch, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) +pike = create_fish(Fish.pike, town_river + forest_river + forest_pond, (season.summer, season.winter), 60) +pufferfish = create_fish(Fish.pufferfish, ocean + ginger_island_ocean, season.summer, 80) +rainbow_trout = create_fish(Fish.rainbow_trout, town_river + forest_river + mountain_lake, season.summer, 45) +red_mullet = create_fish(Fish.red_mullet, ocean, (season.summer, season.winter), 55) +red_snapper = create_fish(Fish.red_snapper, ocean, (season.summer, season.fall), 40) +salmon = create_fish(Fish.salmon, town_river + forest_river, season.fall, 50) +sandfish = create_fish(Fish.sandfish, desert, season.all_seasons, 65) +sardine = create_fish(Fish.sardine, ocean, (season.spring, season.fall, season.winter), 30) +scorpion_carp = create_fish(Fish.scorpion_carp, desert, season.all_seasons, 90) +sea_cucumber = create_fish(Fish.sea_cucumber, ocean, (season.fall, season.winter), 40) +shad = create_fish(Fish.shad, town_river + forest_river, season.not_winter, 45) +slimejack = create_fish(Fish.slimejack, mutant_bug_lair, season.all_seasons, 55) +smallmouth_bass = create_fish(Fish.smallmouth_bass, town_river + forest_river, (season.spring, season.fall), 28) +squid = create_fish(Fish.squid, ocean, season.winter, 75) +stingray = create_fish(Fish.stingray, pirate_cove, season.all_seasons, 80) +stonefish = create_fish(Fish.stonefish, mines_floor_20, season.all_seasons, 65) +sturgeon = create_fish(Fish.sturgeon, mountain_lake, (season.summer, season.winter), 78) +sunfish = create_fish(Fish.sunfish, town_river + forest_river, (season.spring, season.summer), 30) +super_cucumber = create_fish(Fish.super_cucumber, ocean + ginger_island_ocean, (season.summer, season.fall), 80) +tiger_trout = create_fish(Fish.tiger_trout, town_river + forest_river, (season.fall, season.winter), 60) +tilapia = create_fish(Fish.tilapia, ocean + ginger_island_ocean, (season.summer, season.fall), 50) # Tuna has different seasons on ginger island. Should be changed when the whole fish thing is refactored -tuna = create_fish("Tuna", ocean + ginger_island_ocean, (season.summer, season.winter), 70) -void_salmon = create_fish("Void Salmon", witch_swamp, season.all_seasons, 80) -walleye = create_fish("Walleye", town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) -woodskip = create_fish("Woodskip", secret_woods, season.all_seasons, 50) +tuna = create_fish(Fish.tuna, ocean + ginger_island_ocean, (season.summer, season.winter), 70) +void_salmon = create_fish(Fish.void_salmon, witch_swamp, season.all_seasons, 80) +walleye = create_fish(Fish.walleye, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) +woodskip = create_fish(Fish.woodskip, secret_woods, season.all_seasons, 50) -blob_fish = create_fish("Blobfish", night_market, season.winter, 75) -midnight_squid = create_fish("Midnight Squid", night_market, season.winter, 55) -spook_fish = create_fish("Spook Fish", night_market, season.winter, 60) +blobfish = create_fish(Fish.blobfish, night_market, season.winter, 75) +midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 55) +spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60) angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False) crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False) @@ -155,37 +154,21 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple void_eel = create_fish(SVEFish.void_eel, witch_swamp, season.all_seasons, 100, mod_name=ModNames.sve) water_grub = create_fish(SVEFish.water_grub, mutant_bug_lair, season.all_seasons, 60, mod_name=ModNames.sve) sea_sponge = create_fish(SVEFish.sea_sponge, ginger_island_ocean, season.all_seasons, 40, mod_name=ModNames.sve) -dulse_seaweed = create_fish(SVEFish.dulse_seaweed, vineyard, season.all_seasons, 50, mod_name=ModNames.sve) void_minnow = create_fish(DistantLandsFish.void_minnow, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) purple_algae = create_fish(DistantLandsFish.purple_algae, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) swamp_leech = create_fish(DistantLandsFish.swamp_leech, witch_swamp, season.all_seasons, 15, mod_name=ModNames.distant_lands) giant_horsehoe_crab = create_fish(DistantLandsFish.giant_horsehoe_crab, witch_swamp, season.all_seasons, 90, mod_name=ModNames.distant_lands) - -clam = create_fish("Clam", ocean, season.all_seasons, -1) -cockle = create_fish("Cockle", ocean, season.all_seasons, -1) -crab = create_fish("Crab", ocean, season.all_seasons, -1) -crayfish = create_fish("Crayfish", fresh_water, season.all_seasons, -1) -lobster = create_fish("Lobster", ocean, season.all_seasons, -1) -mussel = create_fish("Mussel", ocean, season.all_seasons, -1) -oyster = create_fish("Oyster", ocean, season.all_seasons, -1) -periwinkle = create_fish("Periwinkle", fresh_water, season.all_seasons, -1) -shrimp = create_fish("Shrimp", ocean, season.all_seasons, -1) -snail = create_fish("Snail", fresh_water, season.all_seasons, -1) - -legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] -extended_family = [ms_angler, son_of_crimsonfish, glacierfish_jr, legend_ii, radioactive_carp] -special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] -island_fish = [lionfish, blue_discus, stingray, *extended_family] - -all_fish_by_name = {fish.name: fish for fish in all_fish} - - -def get_fish_for_mods(mods: Set[str]) -> List[FishItem]: - fish_for_mods = [] - for fish in all_fish: - if fish.mod_name and fish.mod_name not in mods: - continue - fish_for_mods.append(fish) - return fish_for_mods +clam = create_fish(Fish.clam, ocean, season.all_seasons, -1) +cockle = create_fish(Fish.cockle, ocean, season.all_seasons, -1) +crab = create_fish(Fish.crab, ocean, season.all_seasons, -1) +crayfish = create_fish(Fish.crayfish, fresh_water, season.all_seasons, -1) +lobster = create_fish(Fish.lobster, ocean, season.all_seasons, -1) +mussel = create_fish(Fish.mussel, ocean, season.all_seasons, -1) +oyster = create_fish(Fish.oyster, ocean, season.all_seasons, -1) +periwinkle = create_fish(Fish.periwinkle, fresh_water, season.all_seasons, -1) +shrimp = create_fish(Fish.shrimp, ocean, season.all_seasons, -1) +snail = create_fish(Fish.snail, fresh_water, season.all_seasons, -1) + +vanilla_legendary_fish = [angler, crimsonfish, glacierfish, legend, mutant_carp] diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py new file mode 100644 index 000000000000..2107ca30d33a --- /dev/null +++ b/worlds/stardew_valley/data/game_item.py @@ -0,0 +1,86 @@ +import enum +import sys +from abc import ABC +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any + +from ..stardew_rule.protocol import StardewRule + +if sys.version_info >= (3, 10): + kw_only = {"kw_only": True} +else: + kw_only = {} + +DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) + + +@dataclass(frozen=True) +class Requirement(ABC): + ... + + +class ItemTag(enum.Enum): + CROPSANITY_SEED = enum.auto() + CROPSANITY = enum.auto() + FISH = enum.auto() + FRUIT = enum.auto() + VEGETABLE = enum.auto() + EDIBLE_MUSHROOM = enum.auto() + BOOK = enum.auto() + BOOK_POWER = enum.auto() + BOOK_SKILL = enum.auto() + + +@dataclass(frozen=True) +class ItemSource(ABC): + add_tags: ClassVar[Tuple[ItemTag]] = () + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return DEFAULT_REQUIREMENT_TAGS + + # FIXME this should just be an optional field, but kw_only requires python 3.10... + @property + def other_requirements(self) -> Iterable[Requirement]: + return () + + +@dataclass(frozen=True, **kw_only) +class GenericSource(ItemSource): + regions: Tuple[str, ...] = () + """No region means it's available everywhere.""" + other_requirements: Tuple[Requirement, ...] = () + + +@dataclass(frozen=True) +class CustomRuleSource(ItemSource): + """Hopefully once everything is migrated to sources, we won't need these custom logic anymore.""" + create_rule: Callable[[Any], StardewRule] + + +class Tag(ItemSource): + """Not a real source, just a way to add tags to an item. Will be removed from the item sources during unpacking.""" + tag: Tuple[ItemTag, ...] + + def __init__(self, *tag: ItemTag): + self.tag = tag # noqa + + @property + def add_tags(self): + return self.tag + + +@dataclass(frozen=True) +class GameItem: + name: str + sources: List[ItemSource] = field(default_factory=list) + tags: Set[ItemTag] = field(default_factory=set) + + def add_sources(self, sources: Iterable[ItemSource]): + self.sources.extend(source for source in sources if type(source) is not Tag) + for source in sources: + self.add_tags(source.add_tags) + + def add_tags(self, tags: Iterable[ItemTag]): + self.tags.update(tags) diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py new file mode 100644 index 000000000000..087d7c3fa86b --- /dev/null +++ b/worlds/stardew_valley/data/harvest.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass +from typing import Tuple, Sequence, Mapping + +from .game_item import ItemSource, kw_only, ItemTag, Requirement +from ..strings.season_names import Season + + +@dataclass(frozen=True, **kw_only) +class ForagingSource(ItemSource): + regions: Tuple[str, ...] + seasons: Tuple[str, ...] = Season.all + other_requirements: Tuple[Requirement, ...] = () + + +@dataclass(frozen=True, **kw_only) +class SeasonalForagingSource(ItemSource): + season: str + days: Sequence[int] + regions: Tuple[str, ...] + + def as_foraging_source(self) -> ForagingSource: + return ForagingSource(seasons=(self.season,), regions=self.regions) + + +@dataclass(frozen=True, **kw_only) +class FruitBatsSource(ItemSource): + ... + + +@dataclass(frozen=True, **kw_only) +class MushroomCaveSource(ItemSource): + ... + + +@dataclass(frozen=True, **kw_only) +class HarvestFruitTreeSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + sapling: str + seasons: Tuple[str, ...] = Season.all + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.sapling: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, **kw_only) +class HarvestCropSource(ItemSource): + add_tags = (ItemTag.CROPSANITY,) + + seed: str + seasons: Tuple[str, ...] = Season.all + """Empty means it can't be grown on the farm.""" + + @property + def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: + return { + self.seed: (ItemTag.CROPSANITY_SEED,) + } + + +@dataclass(frozen=True, **kw_only) +class ArtifactSpotSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 9ecb2ba3649e..2604ad2c46bd 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -54,7 +54,7 @@ id,name,classification,groups,mod_name 68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS, 69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS, 70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS, -71,Golden Scythe,useful,, +71,Golden Scythe,useful,DEPRECATED, 72,Progressive Mine Elevator,progression,, 73,Farming Level,progression,SKILL_LEVEL_UP, 74,Fishing Level,progression,SKILL_LEVEL_UP, @@ -92,8 +92,8 @@ id,name,classification,groups,mod_name 106,Galaxy Sword,filler,"WEAPON,DEPRECATED", 107,Galaxy Dagger,filler,"WEAPON,DEPRECATED", 108,Galaxy Hammer,filler,"WEAPON,DEPRECATED", -109,Movement Speed Bonus,progression,, -110,Luck Bonus,progression,, +109,Movement Speed Bonus,useful,, +110,Luck Bonus,filler,PLAYER_BUFF, 111,Lava Katana,filler,"WEAPON,DEPRECATED", 112,Progressive House,progression,, 113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY, @@ -104,7 +104,7 @@ id,name,classification,groups,mod_name 118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY, 119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY, 120,Traveling Merchant Stock Size,useful,, -121,Traveling Merchant Discount,useful,, +121,Traveling Merchant Discount,useful,DEPRECATED, 122,Return Scepter,useful,, 123,Progressive Season,progression,, 124,Spring,progression,SEASON, @@ -398,6 +398,7 @@ id,name,classification,groups,mod_name 417,Tropical Curry Recipe,progression,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 418,Trout Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_QOS", 419,Vegetable Medley Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +420,Moss Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL", 425,Gate Recipe,progression,CRAFTSANITY, 426,Wood Fence Recipe,progression,CRAFTSANITY, 427,Deluxe Retaining Soil Recipe,progression,"CRAFTSANITY,GINGER_ISLAND", @@ -430,7 +431,7 @@ id,name,classification,groups,mod_name 454,Marble Brazier Recipe,progression,CRAFTSANITY, 455,Wood Lamp-post Recipe,progression,CRAFTSANITY, 456,Iron Lamp-post Recipe,progression,CRAFTSANITY, -457,Furnace Recipe,progression,CRAFTSANITY, +457,Furnace Recipe,progression,"CRAFTSANITY", 458,Wicked Statue Recipe,progression,CRAFTSANITY, 459,Chest Recipe,progression,CRAFTSANITY, 460,Wood Sign Recipe,progression,CRAFTSANITY, @@ -439,6 +440,75 @@ id,name,classification,groups,mod_name 470,Fruit Bats,progression,, 471,Mushroom Boxes,progression,, 475,The Gateway Gazette,progression,TV_CHANNEL, +476,Carrot Seeds,progression,CROPSANITY, +477,Summer Squash Seeds,progression,CROPSANITY, +478,Broccoli Seeds,progression,CROPSANITY, +479,Powdermelon Seeds,progression,CROPSANITY, +480,Progressive Raccoon,progression,, +481,Farming Mastery,progression,SKILL_MASTERY, +482,Mining Mastery,progression,SKILL_MASTERY, +483,Foraging Mastery,progression,SKILL_MASTERY, +484,Fishing Mastery,progression,SKILL_MASTERY, +485,Combat Mastery,progression,SKILL_MASTERY, +486,Fish Smoker Recipe,progression,CRAFTSANITY, +487,Dehydrator Recipe,progression,CRAFTSANITY, +488,Big Chest Recipe,progression,CRAFTSANITY, +489,Big Stone Chest Recipe,progression,CRAFTSANITY, +490,Text Sign Recipe,progression,CRAFTSANITY, +491,Blue Grass Starter Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", +492,Mastery Of The Five Ways,progression,SKILL_MASTERY, +493,Progressive Scythe,useful,, +494,Progressive Pan,progression,PROGRESSIVE_TOOLS, +495,Calico Statue,filler,FESTIVAL, +496,Mummy Mask,filler,FESTIVAL, +497,Free Cactis,filler,FESTIVAL, +498,Gil's Hat,filler,FESTIVAL, +499,Bucket Hat,filler,FESTIVAL, +500,Mounted Trout,filler,FESTIVAL, +501,'Squid Kid',filler,FESTIVAL, +502,Squid Hat,filler,FESTIVAL, +503,Resource Pack: 200 Calico Egg,useful,"FESTIVAL", +504,Resource Pack: 120 Calico Egg,useful,"FESTIVAL", +505,Resource Pack: 100 Calico Egg,useful,"FESTIVAL", +506,Resource Pack: 50 Calico Egg,useful,"FESTIVAL", +507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL", +508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL", +509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL", +510,Book: The Art O' Crabbing,useful,"FESTIVAL", +511,Mr Qi's Plane Ride,progression,, +521,Power: Price Catalogue,useful,"BOOK_POWER", +522,Power: Mapping Cave Systems,useful,"BOOK_POWER", +523,Power: Way Of The Wind pt. 1,progression,"BOOK_POWER", +524,Power: Way Of The Wind pt. 2,useful,"BOOK_POWER", +525,Power: Monster Compendium,useful,"BOOK_POWER", +526,Power: Friendship 101,useful,"BOOK_POWER", +527,"Power: Jack Be Nimble, Jack Be Thick",useful,"BOOK_POWER", +528,Power: Woody's Secret,useful,"BOOK_POWER", +529,Power: Raccoon Journal,useful,"BOOK_POWER", +530,Power: Jewels Of The Sea,useful,"BOOK_POWER", +531,Power: Dwarvish Safety Manual,useful,"BOOK_POWER", +532,Power: The Art O' Crabbing,useful,"BOOK_POWER", +533,Power: The Alleyway Buffet,useful,"BOOK_POWER", +534,Power: The Diamond Hunter,useful,"BOOK_POWER", +535,Power: Book of Mysteries,progression,"BOOK_POWER", +536,Power: Horse: The Book,useful,"BOOK_POWER", +537,Power: Treasure Appraisal Guide,useful,"BOOK_POWER", +538,Power: Ol' Slitherlegs,useful,"BOOK_POWER", +539,Power: Animal Catalogue,useful,"BOOK_POWER", +541,Progressive Lost Book,progression,"LOST_BOOK", +551,Golden Walnut,progression,"RESOURCE_PACK,GINGER_ISLAND", +552,3 Golden Walnuts,progression,"GINGER_ISLAND", +553,5 Golden Walnuts,progression,"GINGER_ISLAND", +554,Damage Bonus,filler,PLAYER_BUFF, +555,Defense Bonus,filler,PLAYER_BUFF, +556,Immunity Bonus,filler,PLAYER_BUFF, +557,Health Bonus,filler,PLAYER_BUFF, +558,Energy Bonus,filler,PLAYER_BUFF, +559,Bite Rate Bonus,filler,PLAYER_BUFF, +560,Fish Trap Bonus,filler,PLAYER_BUFF, +561,Fishing Bar Size Bonus,filler,PLAYER_BUFF, +562,Quality Bonus,filler,PLAYER_BUFF, +563,Glow Bonus,filler,PLAYER_BUFF, 4001,Burnt Trap,trap,TRAP, 4002,Darkness Trap,trap,TRAP, 4003,Frozen Trap,trap,TRAP, @@ -464,6 +534,8 @@ id,name,classification,groups,mod_name 4023,Benjamin Budton Trap,trap,TRAP, 4024,Inflation Trap,trap,TRAP, 4025,Bomb Trap,trap,TRAP, +4026,Nudge Trap,trap,TRAP, +4501,Deflation Bonus,filler,, 5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", 5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK", @@ -701,9 +773,9 @@ id,name,classification,groups,mod_name 5234,Resource Pack: 10 Qi Seasoning,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5235,Mr. Qi's Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5236,Aquatic Sanctuary,filler,RESOURCE_PACK, +5237,Leprechaun Hat,filler,"MAXIMUM_ONE,RESOURCE_PACK", 5242,Exotic Double Bed,filler,RESOURCE_PACK, 5243,Resource Pack: 2 Qi Gem,filler,"GINGER_ISLAND,RESOURCE_PACK,DEPRECATED", -5245,Golden Walnut,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL,GINGER_ISLAND", 5247,Fairy Dust,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5248,Seed Maker,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5249,Keg,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", @@ -726,6 +798,27 @@ id,name,classification,groups,mod_name 5266,Resource Pack: 5 Staircase,filler,"RESOURCE_PACK", 5267,Auto-Petter,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", 5268,Auto-Grabber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5269,Resource Pack: 10 Calico Egg,filler,"RESOURCE_PACK", +5270,Resource Pack: 20 Calico Egg,filler,"RESOURCE_PACK", +5272,Tent Kit,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5273,Resource Pack: 4 Mystery Box,filler,"RESOURCE_PACK", +5274,Prize Ticket,filler,"RESOURCE_PACK", +5275,Resource Pack: 20 Deluxe Bait,filler,"RESOURCE_PACK", +5276,Resource Pack: 2 Triple Shot Espresso,filler,"RESOURCE_PACK", +5277,Dish O' The Sea,filler,"RESOURCE_PACK", +5278,Seafoam Pudding,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5279,Trap Bobber,useful,"RESOURCE_PACK,RESOURCE_PACK_USEFUL", +5280,Treasure Chest,filler,"RESOURCE_PACK", +5281,Resource Pack: 15 Mixed Seeds,filler,"RESOURCE_PACK", +5282,Resource Pack: 15 Mixed Flower Seeds,filler,"RESOURCE_PACK", +5283,Resource Pack: 5 Cherry Bomb,filler,"RESOURCE_PACK", +5284,Resource Pack: 3 Bomb,filler,"RESOURCE_PACK", +5285,Resource Pack: 2 Mega Bomb,filler,"RESOURCE_PACK", +5286,Resource Pack: 2 Life Elixir,filler,"RESOURCE_PACK", +5287,Resource Pack: 5 Coffee,filler,"RESOURCE_PACK", +5289,Prismatic Shard,filler,"RESOURCE_PACK", +5290,Stardrop Tea,filler,"RESOURCE_PACK", +5291,Resource Pack: 2 Artifact Trove,filler,"RESOURCE_PACK", 10001,Luck Level,progression,SKILL_LEVEL_UP,Luck Skill 10002,Magic Level,progression,SKILL_LEVEL_UP,Magic 10003,Socializing Level,progression,SKILL_LEVEL_UP,Socializing Skill @@ -802,11 +895,16 @@ id,name,classification,groups,mod_name 10409,Void Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10410,Void Salmon Sushi Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 10411,Mushroom Kebab Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10412,Crayfish Soup Recipe,progression,,Distant Lands - Witch Swamp Overhaul +10412,Crayfish Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10413,Pemmican Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul 10414,Void Mint Tea Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul -10415,Ginger Tincture Recipe,progression,GINGER_ISLAND,Distant Lands - Witch Swamp Overhaul -10416,Special Pumpkin Soup Recipe,progression,,Boarding House and Bus Stop Extension +10415,Ginger Tincture Recipe,progression,"GINGER_ISLAND,CHEFSANITY,CHEFSANITY_FRIENDSHIP",Distant Lands - Witch Swamp Overhaul +10416,Special Pumpkin Soup Recipe,progression,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Boarding House and Bus Stop Extension +10417,Rocky Root Coffee Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10418,Digger's Delight Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10419,Ancient Jello Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +10420,Grilled Cheese Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +10421,Fish Casserole Recipe,progression,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 10450,Void Mint Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10451,Vile Ancient Fruit Seeds,progression,DEPRECATED,Distant Lands - Witch Swamp Overhaul 10501,Marlon's Boat Paddle,progression,GINGER_ISLAND,Stardew Valley Expanded @@ -850,10 +948,15 @@ id,name,classification,groups,mod_name 10707,Resource Pack: 5 Wooden Display,filler,RESOURCE_PACK,Archaeology 10708,Grinder,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology 10709,Ancient Battery Production Station,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology -10710,Hero Elixir,filler,RESOURCE_PACK,Starde Valley Expanded +10710,Hero Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10711,Aegis Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10712,Haste Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10713,Lightning Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10714,Armor Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10715,Gravity Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded 10716,Barbarian Elixir,filler,RESOURCE_PACK,Stardew Valley Expanded +10717,Restoration Table,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Archaeology +10718,Trash Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10719,Composter,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10720,Recycling Bin,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill +10721,Advanced Recycling Machine,filler,"RESOURCE_PACK,RESOURCE_PACK_USEFUL",Binning Skill diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 68667ac5c4bf..bb2ed2e2ce1f 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -36,6 +36,7 @@ id,region,name,tags,mod_name 35,Boiler Room,Complete Boiler Room,COMMUNITY_CENTER_ROOM, 36,Bulletin Board,Complete Bulletin Board,COMMUNITY_CENTER_ROOM, 37,Vault,Complete Vault,COMMUNITY_CENTER_ROOM, +38,Crafts Room,Forest Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 39,Fish Tank,Deep Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE", 40,Crafts Room,Beach Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", 41,Crafts Room,Mines Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", @@ -78,7 +79,6 @@ id,region,name,tags,mod_name 78,Vault,500g Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 79,Vault,"1,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 80,Vault,"2,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", -81,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 82,Vault,"1,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 83,Vault,"3,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", 84,Vault,"3,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,VAULT_BUNDLE", @@ -124,6 +124,20 @@ id,region,name,tags,mod_name 124,Beach,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", 126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE", +127,Mountain,Copper Pan Cutscene,"TOOL_UPGRADE,PAN_UPGRADE", +128,Blacksmith Iron Upgrades,Iron Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +129,Blacksmith Gold Upgrades,Gold Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +130,Blacksmith Iridium Upgrades,Iridium Pan Upgrade,"TOOL_UPGRADE,PAN_UPGRADE", +151,Bulletin Board,Helper's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +152,Bulletin Board,Spirit's Eve Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +153,Bulletin Board,Winter Star Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +154,Bulletin Board,Calico Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +155,Pantry,Sommelier Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +156,Pantry,Dry Bundle,"PANTRY_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +157,Fish Tank,Fish Smoker Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +158,Bulletin Board,Raccoon Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", +159,Crafts Room,Green Rain Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE", +160,Fish Tank,Specific Fishing Bundle,"FISH_TANK_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE", 201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE", 202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE", 203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE", @@ -161,18 +175,18 @@ id,region,name,tags,mod_name 235,The Mines - Floor 110,Floor 110 Elevator,ELEVATOR, 236,The Mines - Floor 115,Floor 115 Elevator,ELEVATOR, 237,The Mines - Floor 120,Floor 120 Elevator,ELEVATOR, -250,Shipping,Demetrius's Breakthrough,MANDATORY +250,Shipping,Demetrius's Breakthrough,MANDATORY, 251,Volcano - Floor 10,Volcano Caldera Treasure,"GINGER_ISLAND,MANDATORY", -301,Farming,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", -302,Farming,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", -303,Farming,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", -304,Farming,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", -305,Farming,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", -306,Farming,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", -307,Farming,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", -308,Farming,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", -309,Farming,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", -310,Farming,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", +301,Farm,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL", +302,Farm,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL", +303,Farm,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL", +304,Farm,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL", +305,Farm,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL", +306,Farm,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL", +307,Farm,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL", +308,Farm,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL", +309,Farm,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL", +310,Farm,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL", 311,Fishing,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 312,Fishing,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL", 313,Fishing,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL", @@ -213,6 +227,11 @@ id,region,name,tags,mod_name 348,The Mines - Floor 90,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 349,The Mines - Floor 100,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL", 350,The Mines - Floor 110,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL", +351,Mastery Cave,Farming Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +352,Mastery Cave,Fishing Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +353,Mastery Cave,Foraging Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +354,Mastery Cave,Mining Mastery,"MASTERY_LEVEL,SKILL_LEVEL", +355,Mastery Cave,Combat Mastery,"MASTERY_LEVEL,SKILL_LEVEL", 401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT, 402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT, 403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT, @@ -279,6 +298,8 @@ id,region,name,tags,mod_name 546,Mutant Bug Lair,Dark Talisman,"STORY_QUEST", 547,Witch's Swamp,Goblin Problem,"STORY_QUEST", 548,Witch's Hut,Magic Ink,"STORY_QUEST", +549,Forest,The Giant Stump,"STORY_QUEST", +550,Farm,Feeding Animals,"STORY_QUEST", 601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK", 602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK", 603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK", @@ -307,6 +328,7 @@ id,region,name,tags,mod_name 705,Farmhouse,Have Another Baby,BABY, 706,Farmhouse,Spouse Stardrop,, 707,Sewer,Krobus Stardrop,MANDATORY, +708,Forest,Pot Of Gold,MANDATORY, 801,Forest,Help Wanted: Gathering 1,HELP_WANTED, 802,Forest,Help Wanted: Gathering 2,HELP_WANTED, 803,Forest,Help Wanted: Gathering 3,HELP_WANTED, @@ -454,6 +476,7 @@ id,region,name,tags,mod_name 1068,Beach,Fishsanity: Legend II,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1069,Beach,Fishsanity: Ms. Angler,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", 1070,Beach,Fishsanity: Radioactive Carp,"FISHSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +1071,Fishing,Fishsanity: Goby,FISHSANITY, 1100,Museum,Museumsanity: 5 Donations,MUSEUM_MILESTONES, 1101,Museum,Museumsanity: 10 Donations,MUSEUM_MILESTONES, 1102,Museum,Museumsanity: 15 Donations,MUSEUM_MILESTONES, @@ -1021,6 +1044,57 @@ id,region,name,tags,mod_name 2034,Dance of the Moonlight Jellies,Moonlight Jellies Banner,FESTIVAL, 2035,Dance of the Moonlight Jellies,Starport Decal,FESTIVAL, 2036,Casino,Rarecrow #3 (Alien),FESTIVAL, +2041,Desert Festival,Calico Race,FESTIVAL, +2042,Desert Festival,Mummy Mask,FESTIVAL_HARD, +2043,Desert Festival,Calico Statue,FESTIVAL, +2044,Desert Festival,Emily's Outfit Services,FESTIVAL, +2045,Desert Festival,Earthy Mousse,DESERT_FESTIVAL_CHEF, +2046,Desert Festival,Sweet Bean Cake,DESERT_FESTIVAL_CHEF, +2047,Desert Festival,Skull Cave Casserole,DESERT_FESTIVAL_CHEF, +2048,Desert Festival,Spicy Tacos,DESERT_FESTIVAL_CHEF, +2049,Desert Festival,Mountain Chili,DESERT_FESTIVAL_CHEF, +2050,Desert Festival,Crystal Cake,DESERT_FESTIVAL_CHEF, +2051,Desert Festival,Cave Kebab,DESERT_FESTIVAL_CHEF, +2052,Desert Festival,Hot Log,DESERT_FESTIVAL_CHEF, +2053,Desert Festival,Sour Salad,DESERT_FESTIVAL_CHEF, +2054,Desert Festival,Superfood Cake,DESERT_FESTIVAL_CHEF, +2055,Desert Festival,Warrior Smoothie,DESERT_FESTIVAL_CHEF, +2056,Desert Festival,Rumpled Fruit Skin,DESERT_FESTIVAL_CHEF, +2057,Desert Festival,Calico Pizza,DESERT_FESTIVAL_CHEF, +2058,Desert Festival,Stuffed Mushrooms,DESERT_FESTIVAL_CHEF, +2059,Desert Festival,Elf Quesadilla,DESERT_FESTIVAL_CHEF, +2060,Desert Festival,Nachos Of The Desert,DESERT_FESTIVAL_CHEF, +2061,Desert Festival,Cioppino,DESERT_FESTIVAL_CHEF, +2062,Desert Festival,Rainforest Shrimp,DESERT_FESTIVAL_CHEF, +2063,Desert Festival,Shrimp Donut,DESERT_FESTIVAL_CHEF, +2064,Desert Festival,Smell Of The Sea,DESERT_FESTIVAL_CHEF, +2065,Desert Festival,Desert Gumbo,DESERT_FESTIVAL_CHEF, +2066,Desert Festival,Free Cactis,FESTIVAL, +2067,Desert Festival,Monster Hunt,FESTIVAL_HARD, +2068,Desert Festival,Deep Dive,FESTIVAL_HARD, +2069,Desert Festival,Treasure Hunt,FESTIVAL_HARD, +2070,Desert Festival,Touch A Calico Statue,FESTIVAL, +2071,Desert Festival,Real Calico Egg Hunter,FESTIVAL, +2072,Desert Festival,Willy's Challenge,FESTIVAL_HARD, +2073,Desert Festival,Desert Scholar,FESTIVAL, +2074,Trout Derby,Trout Derby Reward 1,FESTIVAL, +2075,Trout Derby,Trout Derby Reward 2,FESTIVAL, +2076,Trout Derby,Trout Derby Reward 3,FESTIVAL, +2077,Trout Derby,Trout Derby Reward 4,FESTIVAL_HARD, +2078,Trout Derby,Trout Derby Reward 5,FESTIVAL_HARD, +2079,Trout Derby,Trout Derby Reward 6,FESTIVAL_HARD, +2080,Trout Derby,Trout Derby Reward 7,FESTIVAL_HARD, +2081,Trout Derby,Trout Derby Reward 8,FESTIVAL_HARD, +2082,Trout Derby,Trout Derby Reward 9,FESTIVAL_HARD, +2083,Trout Derby,Trout Derby Reward 10,FESTIVAL_HARD, +2084,SquidFest,SquidFest Day 1 Copper,FESTIVAL, +2085,SquidFest,SquidFest Day 1 Iron,FESTIVAL, +2086,SquidFest,SquidFest Day 1 Gold,FESTIVAL_HARD, +2087,SquidFest,SquidFest Day 1 Iridium,FESTIVAL_HARD, +2088,SquidFest,SquidFest Day 2 Copper,FESTIVAL, +2089,SquidFest,SquidFest Day 2 Iron,FESTIVAL, +2090,SquidFest,SquidFest Day 2 Gold,FESTIVAL_HARD, +2091,SquidFest,SquidFest Day 2 Iridium,FESTIVAL_HARD, 2101,Town,Island Ingredients,"GINGER_ISLAND,SPECIAL_ORDER_BOARD", 2102,The Mines - Floor 75,Cave Patrol,SPECIAL_ORDER_BOARD, 2103,Fishing,Aquatic Overpopulation,SPECIAL_ORDER_BOARD, @@ -1065,53 +1139,59 @@ id,region,name,tags,mod_name 2214,Island West,Parrot Express,"GINGER_ISLAND,WALNUT_PURCHASE", 2215,Dig Site,Open Professor Snail Cave,GINGER_ISLAND, 2216,Field Office,Complete Island Field Office,GINGER_ISLAND, -2301,Farming,Harvest Amaranth,CROPSANITY, -2302,Farming,Harvest Artichoke,CROPSANITY, -2303,Farming,Harvest Beet,CROPSANITY, -2304,Farming,Harvest Blue Jazz,CROPSANITY, -2305,Farming,Harvest Blueberry,CROPSANITY, -2306,Farming,Harvest Bok Choy,CROPSANITY, -2307,Farming,Harvest Cauliflower,CROPSANITY, -2308,Farming,Harvest Corn,CROPSANITY, -2309,Farming,Harvest Cranberries,CROPSANITY, -2310,Farming,Harvest Eggplant,CROPSANITY, -2311,Farming,Harvest Fairy Rose,CROPSANITY, -2312,Farming,Harvest Garlic,CROPSANITY, -2313,Farming,Harvest Grape,CROPSANITY, -2314,Farming,Harvest Green Bean,CROPSANITY, -2315,Farming,Harvest Hops,CROPSANITY, -2316,Farming,Harvest Hot Pepper,CROPSANITY, -2317,Farming,Harvest Kale,CROPSANITY, -2318,Farming,Harvest Melon,CROPSANITY, -2319,Farming,Harvest Parsnip,CROPSANITY, -2320,Farming,Harvest Poppy,CROPSANITY, -2321,Farming,Harvest Potato,CROPSANITY, -2322,Farming,Harvest Pumpkin,CROPSANITY, -2323,Farming,Harvest Radish,CROPSANITY, -2324,Farming,Harvest Red Cabbage,CROPSANITY, -2325,Farming,Harvest Rhubarb,CROPSANITY, -2326,Farming,Harvest Starfruit,CROPSANITY, -2327,Farming,Harvest Strawberry,CROPSANITY, -2328,Farming,Harvest Summer Spangle,CROPSANITY, -2329,Farming,Harvest Sunflower,CROPSANITY, -2330,Farming,Harvest Tomato,CROPSANITY, -2331,Farming,Harvest Tulip,CROPSANITY, -2332,Farming,Harvest Unmilled Rice,CROPSANITY, -2333,Farming,Harvest Wheat,CROPSANITY, -2334,Farming,Harvest Yam,CROPSANITY, -2335,Farming,Harvest Cactus Fruit,CROPSANITY, -2336,Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", -2337,Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", -2338,Farming,Harvest Sweet Gem Berry,CROPSANITY, -2339,Farming,Harvest Apple,CROPSANITY, -2340,Farming,Harvest Apricot,CROPSANITY, -2341,Farming,Harvest Cherry,CROPSANITY, -2342,Farming,Harvest Orange,CROPSANITY, -2343,Farming,Harvest Pomegranate,CROPSANITY, -2344,Farming,Harvest Peach,CROPSANITY, -2345,Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", -2346,Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", -2347,Farming,Harvest Coffee Bean,CROPSANITY, +2301,Fall Farming,Harvest Amaranth,CROPSANITY, +2302,Fall Farming,Harvest Artichoke,CROPSANITY, +2303,Fall Farming,Harvest Beet,CROPSANITY, +2304,Spring Farming,Harvest Blue Jazz,CROPSANITY, +2305,Summer Farming,Harvest Blueberry,CROPSANITY, +2306,Fall Farming,Harvest Bok Choy,CROPSANITY, +2307,Spring Farming,Harvest Cauliflower,CROPSANITY, +2308,Summer or Fall Farming,Harvest Corn,CROPSANITY, +2309,Fall Farming,Harvest Cranberries,CROPSANITY, +2310,Fall Farming,Harvest Eggplant,CROPSANITY, +2311,Fall Farming,Harvest Fairy Rose,CROPSANITY, +2312,Spring Farming,Harvest Garlic,CROPSANITY, +2313,Fall Farming,Harvest Grape,CROPSANITY, +2314,Spring Farming,Harvest Green Bean,CROPSANITY, +2315,Summer Farming,Harvest Hops,CROPSANITY, +2316,Summer Farming,Harvest Hot Pepper,CROPSANITY, +2317,Spring Farming,Harvest Kale,CROPSANITY, +2318,Summer Farming,Harvest Melon,CROPSANITY, +2319,Spring Farming,Harvest Parsnip,CROPSANITY, +2320,Summer Farming,Harvest Poppy,CROPSANITY, +2321,Spring Farming,Harvest Potato,CROPSANITY, +2322,Fall Farming,Harvest Pumpkin,CROPSANITY, +2323,Summer Farming,Harvest Radish,CROPSANITY, +2324,Summer Farming,Harvest Red Cabbage,CROPSANITY, +2325,Spring Farming,Harvest Rhubarb,CROPSANITY, +2326,Summer Farming,Harvest Starfruit,CROPSANITY, +2327,Spring Farming,Harvest Strawberry,CROPSANITY, +2328,Summer Farming,Harvest Summer Spangle,CROPSANITY, +2329,Summer or Fall Farming,Harvest Sunflower,CROPSANITY, +2330,Summer Farming,Harvest Tomato,CROPSANITY, +2331,Spring Farming,Harvest Tulip,CROPSANITY, +2332,Spring Farming,Harvest Unmilled Rice,CROPSANITY, +2333,Summer or Fall Farming,Harvest Wheat,CROPSANITY, +2334,Fall Farming,Harvest Yam,CROPSANITY, +2335,Indoor Farming,Harvest Cactus Fruit,CROPSANITY, +2336,Summer Farming,Harvest Pineapple,"CROPSANITY,GINGER_ISLAND", +2337,Summer Farming,Harvest Taro Root,"CROPSANITY,GINGER_ISLAND", +2338,Fall Farming,Harvest Sweet Gem Berry,CROPSANITY, +2339,Fall Farming,Harvest Apple,CROPSANITY, +2340,Spring Farming,Harvest Apricot,CROPSANITY, +2341,Spring Farming,Harvest Cherry,CROPSANITY, +2342,Summer Farming,Harvest Orange,CROPSANITY, +2343,Fall Farming,Harvest Pomegranate,CROPSANITY, +2344,Summer Farming,Harvest Peach,CROPSANITY, +2345,Summer Farming,Harvest Banana,"CROPSANITY,GINGER_ISLAND", +2346,Summer Farming,Harvest Mango,"CROPSANITY,GINGER_ISLAND", +2347,Indoor Farming,Harvest Coffee Bean,CROPSANITY, +2348,Fall Farming,Harvest Broccoli,CROPSANITY, +2349,Spring Farming,Harvest Carrot,CROPSANITY, +2350,Summer Farming,Harvest Powdermelon,CROPSANITY, +2351,Summer Farming,Harvest Summer Squash,CROPSANITY, +2352,Indoor Farming,Harvest Ancient Fruit,CROPSANITY, +2353,Indoor Farming,Harvest Qi Fruit,"CROPSANITY,GINGER_ISLAND", 2401,Shipping,Shipsanity: Duck Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2402,Shipping,Shipsanity: Duck Feather,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 2403,Shipping,Shipsanity: Egg,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", @@ -1431,7 +1511,7 @@ id,region,name,tags,mod_name 2717,Shipping,Shipsanity: Cactus Fruit,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2718,Shipping,Shipsanity: Cave Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2719,Shipping,Shipsanity: Chanterelle,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", -2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +2720,Shipping,Shipsanity: Clam,"SHIPSANITY,SHIPSANITY_FISH", 2721,Shipping,Shipsanity: Coconut,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2722,Shipping,Shipsanity: Common Mushroom,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", 2723,Shipping,Shipsanity: Coral,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", @@ -1683,7 +1763,7 @@ id,region,name,tags,mod_name 2969,Shipping,Shipsanity: Mango Sapling,"GINGER_ISLAND,SHIPSANITY", 2970,Shipping,Shipsanity: Mushroom Tree Seed,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2971,Shipping,Shipsanity: Pineapple Seeds,"GINGER_ISLAND,SHIPSANITY", -2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY", +2972,Shipping,Shipsanity: Qi Bean,"GINGER_ISLAND,SHIPSANITY,REQUIRES_QI_ORDERS", 2973,Shipping,Shipsanity: Taro Tuber,"GINGER_ISLAND,SHIPSANITY", 3001,Adventurer's Guild,Monster Eradication: Slimes,"MONSTERSANITY,MONSTERSANITY_GOALS", 3002,Adventurer's Guild,Monster Eradication: Void Spirits,"MONSTERSANITY,MONSTERSANITY_GOALS", @@ -1852,6 +1932,7 @@ id,region,name,tags,mod_name 3278,Kitchen,Cook Tropical Curry,"COOKSANITY,GINGER_ISLAND", 3279,Kitchen,Cook Trout Soup,"COOKSANITY,COOKSANITY_QOS", 3280,Kitchen,Cook Vegetable Medley,COOKSANITY, +3281,Kitchen,Cook Moss Soup,COOKSANITY, 3301,Farm,Algae Soup Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", 3302,The Queen of Sauce,Artichoke Dip Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3303,Farm,Autumn's Bounty Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", @@ -1932,6 +2013,7 @@ id,region,name,tags,mod_name 3378,Island Resort,Tropical Curry Recipe,"CHEFSANITY,GINGER_ISLAND,CHEFSANITY_PURCHASE", 3379,The Queen of Sauce,Trout Soup Recipe,"CHEFSANITY,CHEFSANITY_QOS", 3380,Farm,Vegetable Medley Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP", +3381,Farm,Moss Soup Recipe,"CHEFSANITY,CHEFSANITY_SKILL", 3401,Farm,Craft Cherry Bomb,CRAFTSANITY, 3402,Farm,Craft Bomb,CRAFTSANITY, 3403,Farm,Craft Mega Bomb,CRAFTSANITY, @@ -2062,6 +2144,26 @@ id,region,name,tags,mod_name 3528,Farm,Craft Farm Computer,CRAFTSANITY, 3529,Farm,Craft Hopper,"CRAFTSANITY,GINGER_ISLAND", 3530,Farm,Craft Cookout Kit,CRAFTSANITY, +3531,Farm,Craft Fish Smoker,"CRAFTSANITY", +3532,Farm,Craft Dehydrator,"CRAFTSANITY", +3533,Farm,Craft Blue Grass Starter,"CRAFTSANITY,GINGER_ISLAND", +3534,Farm,Craft Mystic Tree Seed,"CRAFTSANITY,REQUIRES_MASTERIES", +3535,Farm,Craft Sonar Bobber,"CRAFTSANITY", +3536,Farm,Craft Challenge Bait,"CRAFTSANITY,REQUIRES_MASTERIES", +3537,Farm,Craft Treasure Totem,"CRAFTSANITY,REQUIRES_MASTERIES", +3538,Farm,Craft Heavy Furnace,"CRAFTSANITY,REQUIRES_MASTERIES", +3539,Farm,Craft Deluxe Worm Bin,"CRAFTSANITY", +3540,Farm,Craft Mushroom Log,"CRAFTSANITY", +3541,Farm,Craft Big Chest,"CRAFTSANITY", +3542,Farm,Craft Big Stone Chest,"CRAFTSANITY", +3543,Farm,Craft Text Sign,"CRAFTSANITY", +3544,Farm,Craft Tent Kit,"CRAFTSANITY", +3545,Farm,Craft Statue Of The Dwarf King,"CRAFTSANITY,REQUIRES_MASTERIES", +3546,Farm,Craft Statue Of Blessings,"CRAFTSANITY,REQUIRES_MASTERIES", +3547,Farm,Craft Anvil,"CRAFTSANITY,REQUIRES_MASTERIES", +3548,Farm,Craft Mini-Forge,"CRAFTSANITY,GINGER_ISLAND,REQUIRES_MASTERIES", +3549,Farm,Craft Deluxe Bait,"CRAFTSANITY", +3550,Farm,Craft Bait Maker,"CRAFTSANITY", 3551,Pierre's General Store,Grass Starter Recipe,CRAFTSANITY, 3552,Carpenter Shop,Wood Floor Recipe,CRAFTSANITY, 3553,Carpenter Shop,Rustic Plank Floor Recipe,CRAFTSANITY, @@ -2088,6 +2190,226 @@ id,region,name,tags,mod_name 3574,Sewer,Wicked Statue Recipe,CRAFTSANITY, 3575,Desert,Warp Totem: Desert Recipe,"CRAFTSANITY", 3576,Island Trader,Deluxe Retaining Soil Recipe,"CRAFTSANITY,GINGER_ISLAND", +3577,Willy's Fish Shop,Fish Smoker Recipe,CRAFTSANITY, +3578,Pierre's General Store,Dehydrator Recipe,CRAFTSANITY, +3579,Carpenter Shop,Big Chest Recipe,CRAFTSANITY, +3580,Mines Dwarf Shop,Big Stone Chest Recipe,CRAFTSANITY, +3701,Raccoon Bundles,Raccoon Request 1,"BUNDLE,RACCOON_BUNDLES", +3702,Raccoon Bundles,Raccoon Request 2,"BUNDLE,RACCOON_BUNDLES", +3703,Raccoon Bundles,Raccoon Request 3,"BUNDLE,RACCOON_BUNDLES", +3704,Raccoon Bundles,Raccoon Request 4,"BUNDLE,RACCOON_BUNDLES", +3705,Raccoon Bundles,Raccoon Request 5,"BUNDLE,RACCOON_BUNDLES", +3706,Raccoon Bundles,Raccoon Request 6,"BUNDLE,RACCOON_BUNDLES", +3707,Raccoon Bundles,Raccoon Request 7,"BUNDLE,RACCOON_BUNDLES", +3708,Raccoon Bundles,Raccoon Request 8,"BUNDLE,RACCOON_BUNDLES", +3801,Shipping,Shipsanity: Goby,"SHIPSANITY,SHIPSANITY_FISH", +3802,Shipping,Shipsanity: Fireworks (Red),"SHIPSANITY", +3803,Shipping,Shipsanity: Fireworks (Purple),"SHIPSANITY", +3804,Shipping,Shipsanity: Fireworks (Green),"SHIPSANITY", +3805,Shipping,Shipsanity: Far Away Stone,"SHIPSANITY", +3806,Shipping,Shipsanity: Calico Egg,"SHIPSANITY", +3807,Shipping,Shipsanity: Mixed Flower Seeds,"SHIPSANITY", +3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY", +3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY", +3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", +3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", +3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", +3815,Shipping,Shipsanity: Mystic Tree Seed,"SHIPSANITY,REQUIRES_MASTERIES", +3816,Shipping,Shipsanity: Mystic Syrup,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", +3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", +3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", +3824,Shipping,Shipsanity: Carrot Seeds,"SHIPSANITY", +3825,Shipping,Shipsanity: Carrot,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3826,Shipping,Shipsanity: Summer Squash Seeds,"SHIPSANITY", +3827,Shipping,Shipsanity: Summer Squash,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3828,Shipping,Shipsanity: Broccoli Seeds,"SHIPSANITY", +3829,Shipping,Shipsanity: Broccoli,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3830,Shipping,Shipsanity: Powdermelon Seeds,"SHIPSANITY", +3831,Shipping,Shipsanity: Powdermelon,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT", +3832,Shipping,Shipsanity: Smoked Fish,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3833,Shipping,Shipsanity: Book Of Stars,"SHIPSANITY", +3834,Shipping,Shipsanity: Stardew Valley Almanac,"SHIPSANITY", +3835,Shipping,Shipsanity: Woodcutter's Weekly,"SHIPSANITY", +3836,Shipping,Shipsanity: Bait And Bobber,"SHIPSANITY", +3837,Shipping,Shipsanity: Mining Monthly,"SHIPSANITY", +3838,Shipping,Shipsanity: Combat Quarterly,"SHIPSANITY", +3839,Shipping,Shipsanity: The Alleyway Buffet,"SHIPSANITY", +3840,Shipping,Shipsanity: The Art O' Crabbing,"SHIPSANITY", +3841,Shipping,Shipsanity: Dwarvish Safety Manual,"SHIPSANITY", +3842,Shipping,Shipsanity: Jewels Of The Sea,"SHIPSANITY", +3843,Shipping,Shipsanity: Raccoon Journal,"SHIPSANITY", +3844,Shipping,Shipsanity: Woody's Secret,"SHIPSANITY", +3845,Shipping,"Shipsanity: Jack Be Nimble, Jack Be Thick","SHIPSANITY", +3846,Shipping,Shipsanity: Friendship 101,"SHIPSANITY", +3847,Shipping,Shipsanity: Monster Compendium,"SHIPSANITY", +3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", +3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", +3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY", +3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", +3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", +3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", +3855,Shipping,Shipsanity: Way Of The Wind pt. 2,"SHIPSANITY", +3856,Shipping,Shipsanity: Golden Animal Cracker,"SHIPSANITY,REQUIRES_MASTERIES", +3857,Shipping,Shipsanity: Golden Mystery Box,"SHIPSANITY,REQUIRES_MASTERIES", +3858,Shipping,Shipsanity: Sea Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3859,Shipping,Shipsanity: Cave Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3860,Shipping,Shipsanity: River Jelly,"SHIPSANITY,SHIPSANITY_FISH", +3861,Shipping,Shipsanity: Treasure Appraisal Guide,"SHIPSANITY", +3862,Shipping,Shipsanity: Horse: The Book,"SHIPSANITY", +3863,Shipping,Shipsanity: Butterfly Powder,"SHIPSANITY", +3864,Shipping,Shipsanity: Blue Grass Starter,"SHIPSANITY,GINGER_ISLAND,REQUIRES_QI_ORDERS", +3865,Shipping,Shipsanity: Moss Soup,"SHIPSANITY", +3866,Shipping,Shipsanity: Ol' Slitherlegs,"SHIPSANITY", +3867,Shipping,Shipsanity: Targeted Bait,"SHIPSANITY", +4001,Farm,Read Price Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4002,Farm,Read Mapping Cave Systems,"BOOKSANITY,BOOKSANITY_POWER", +4003,Farm,Read Way Of The Wind pt. 1,"BOOKSANITY,BOOKSANITY_POWER", +4004,Farm,Read Way Of The Wind pt. 2,"BOOKSANITY,BOOKSANITY_POWER", +4005,Farm,Read Monster Compendium,"BOOKSANITY,BOOKSANITY_POWER", +4006,Farm,Read Friendship 101,"BOOKSANITY,BOOKSANITY_POWER", +4007,Farm,"Read Jack Be Nimble, Jack Be Thick","BOOKSANITY,BOOKSANITY_POWER", +4008,Farm,Read Woody's Secret,"BOOKSANITY,BOOKSANITY_POWER", +4009,Farm,Read Raccoon Journal,"BOOKSANITY,BOOKSANITY_POWER", +4010,Farm,Read Jewels Of The Sea,"BOOKSANITY,BOOKSANITY_POWER", +4011,Farm,Read Dwarvish Safety Manual,"BOOKSANITY,BOOKSANITY_POWER", +4012,Farm,Read The Art O' Crabbing,"BOOKSANITY,BOOKSANITY_POWER", +4013,Farm,Read The Alleyway Buffet,"BOOKSANITY,BOOKSANITY_POWER", +4014,Farm,Read The Diamond Hunter,"BOOKSANITY,BOOKSANITY_POWER,GINGER_ISLAND", +4015,Farm,Read Book of Mysteries,"BOOKSANITY,BOOKSANITY_POWER", +4016,Farm,Read Horse: The Book,"BOOKSANITY,BOOKSANITY_POWER", +4017,Farm,Read Treasure Appraisal Guide,"BOOKSANITY,BOOKSANITY_POWER", +4018,Farm,Read Ol' Slitherlegs,"BOOKSANITY,BOOKSANITY_POWER", +4019,Farm,Read Animal Catalogue,"BOOKSANITY,BOOKSANITY_POWER", +4031,Farm,Read Bait And Bobber,"BOOKSANITY,BOOKSANITY_SKILL", +4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", +4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", +4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL", +4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", +4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", +4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", +4052,Museum,Read This is a book by Marnie,"BOOKSANITY,BOOKSANITY_LOST", +4053,Museum,Read On Foraging,"BOOKSANITY,BOOKSANITY_LOST", +4054,Museum,"Read The Fisherman, Act 1","BOOKSANITY,BOOKSANITY_LOST", +4055,Museum,Read How Deep do the mines go?,"BOOKSANITY,BOOKSANITY_LOST", +4056,Museum,Read An Old Farmer's Journal,"BOOKSANITY,BOOKSANITY_LOST", +4057,Museum,Read Scarecrows,"BOOKSANITY,BOOKSANITY_LOST", +4058,Museum,Read The Secret of the Stardrop,"BOOKSANITY,BOOKSANITY_LOST", +4059,Museum,Read Journey of the Prairie King -- The Smash Hit Video Game!,"BOOKSANITY,BOOKSANITY_LOST", +4060,Museum,Read A Study on Diamond Yields,"BOOKSANITY,BOOKSANITY_LOST", +4061,Museum,Read Brewmaster's Guide,"BOOKSANITY,BOOKSANITY_LOST", +4062,Museum,Read Mysteries of the Dwarves,"BOOKSANITY,BOOKSANITY_LOST", +4063,Museum,Read Highlights From The Book of Yoba,"BOOKSANITY,BOOKSANITY_LOST", +4064,Museum,Read Marriage Guide for Farmers,"BOOKSANITY,BOOKSANITY_LOST", +4065,Museum,"Read The Fisherman, Act II","BOOKSANITY,BOOKSANITY_LOST", +4066,Museum,Read Technology Report!,"BOOKSANITY,BOOKSANITY_LOST", +4067,Museum,Read Secrets of the Legendary Fish,"BOOKSANITY,BOOKSANITY_LOST", +4068,Museum,Read Gunther Tunnel Notice,"BOOKSANITY,BOOKSANITY_LOST", +4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST", +4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST", +4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST", +4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG", +4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG", +4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG", +4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG", +4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG", +4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG", +4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG", +4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", +4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH", +4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG", +4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG", +4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH", +4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH", +4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE", +4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH", +4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG", +4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE", +4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG", 5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill 5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill @@ -2578,6 +2900,7 @@ id,region,name,tags,mod_name 7055,Abandoned Mines - 3,Abandoned Treasure - Floor 3,MANDATORY,Boarding House and Bus Stop Extension 7056,Abandoned Mines - 4,Abandoned Treasure - Floor 4,MANDATORY,Boarding House and Bus Stop Extension 7057,Abandoned Mines - 5,Abandoned Treasure - Floor 5,MANDATORY,Boarding House and Bus Stop Extension +7351,Farm,Read Digging Like Worms,"BOOKSANITY,BOOKSANITY_SKILL",Archaeology 7401,Farm,Cook Magic Elixir,COOKSANITY,Magic 7402,Farm,Craft Travel Core,CRAFTSANITY,Magic 7403,Farm,Craft Haste Elixir,CRAFTSANITY,Stardew Valley Expanded @@ -2585,7 +2908,7 @@ id,region,name,tags,mod_name 7405,Farm,Craft Armor Elixir,CRAFTSANITY,Stardew Valley Expanded 7406,Witch's Swamp,Craft Ginger Tincture,"CRAFTSANITY,GINGER_ISLAND",Distant Lands - Witch Swamp Overhaul 7407,Farm,Craft Glass Path,CRAFTSANITY,Archaeology -7408,Farm,Craft Glass Bazier,CRAFTSANITY,Archaeology +7408,Farm,Craft Glass Brazier,CRAFTSANITY,Archaeology 7409,Farm,Craft Glass Fence,CRAFTSANITY,Archaeology 7410,Farm,Craft Bone Path,CRAFTSANITY,Archaeology 7411,Farm,Craft Water Shifter,CRAFTSANITY,Archaeology @@ -2603,13 +2926,23 @@ id,region,name,tags,mod_name 7423,Farm,Craft T-Rex Skeleton L,CRAFTSANITY,Boarding House and Bus Stop Extension 7424,Farm,Craft T-Rex Skeleton M,CRAFTSANITY,Boarding House and Bus Stop Extension 7425,Farm,Craft T-Rex Skeleton R,CRAFTSANITY,Boarding House and Bus Stop Extension +7426,Farm,Craft Restoration Table,CRAFTSANITY,Archaeology +7427,Farm,Craft Rusty Path,CRAFTSANITY,Archaeology +7428,Farm,Craft Rusty Brazier,CRAFTSANITY,Archaeology +7429,Farm,Craft Lucky Ring,CRAFTSANITY,Archaeology +7430,Farm,Craft Bone Fence,CRAFTSANITY,Archaeology +7431,Farm,Craft Bouquet,CRAFTSANITY,Socializing Skill +7432,Farm,Craft Trash Bin,CRAFTSANITY,Binning Skill +7433,Farm,Craft Composter,CRAFTSANITY,Binning Skill +7434,Farm,Craft Recycling Bin,CRAFTSANITY,Binning Skill +7435,Farm,Craft Advanced Recycling Machine,CRAFTSANITY,Binning Skill 7451,Adventurer's Guild,Magic Elixir Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic 7452,Adventurer's Guild,Travel Core Recipe,CRAFTSANITY,Magic 7453,Alesia Shop,Haste Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7454,Isaac Shop,Hero Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7455,Alesia Shop,Armor Elixir Recipe,CRAFTSANITY,Stardew Valley Expanded 7501,Mountain,Missing Envelope,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) -7502,Forest,Lost Emerald Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) +7502,Forest,Ayeisha's Lost Ring,"STORY_QUEST",Ayeisha - The Postal Worker (Custom NPC) 7503,Forest,Mr.Ginger's request,"STORY_QUEST",Mister Ginger (cat npc) 7504,Forest,Juna's Drink Request,"STORY_QUEST",Juna - Roommate NPC 7505,Forest,Juna's BFF Request,"STORY_QUEST",Juna - Roommate NPC @@ -2648,6 +2981,11 @@ id,region,name,tags,mod_name 7563,Kitchen,Cook Pemmican,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7564,Kitchen,Cook Void Mint Tea,COOKSANITY,Distant Lands - Witch Swamp Overhaul 7565,Kitchen,Cook Special Pumpkin Soup,COOKSANITY,Boarding House and Bus Stop Extension +7566,Kitchen,Cook Digger's Delight,COOKSANITY,Archaeology +7567,Kitchen,Cook Rocky Root Coffee,COOKSANITY,Archaeology +7568,Kitchen,Cook Ancient Jello,COOKSANITY,Archaeology +7569,Kitchen,Cook Grilled Cheese,COOKSANITY,Binning Skill +7570,Kitchen,Cook Fish Casserole,COOKSANITY,Binning Skill 7601,Bear Shop,Baked Berry Oatmeal Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7602,Bear Shop,Flower Cookie Recipe,"CHEFSANITY,CHEFSANITY_PURCHASE",Stardew Valley Expanded 7603,Saloon,Big Bark Burger Recipe,"CHEFSANITY,CHEFSANITY_FRIENDSHIP",Stardew Valley Expanded @@ -2668,6 +3006,11 @@ id,region,name,tags,mod_name 7620,Mines Dwarf Shop,T-Rex Skeleton L Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7621,Mines Dwarf Shop,T-Rex Skeleton M Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension 7622,Mines Dwarf Shop,T-Rex Skeleton R Recipe,CRAFTSANITY,Boarding House and Bus Stop Extension +7623,Farm,Digger's Delight Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7624,Farm,Rocky Root Coffee Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7625,Farm,Ancient Jello Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Archaeology +7627,Farm,Grilled Cheese Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill +7628,Farm,Fish Casserole Recipe,"CHEFSANITY,CHEFSANITY_SKILL",Binning Skill 7651,Alesia Shop,Tempered Galaxy Dagger,MANDATORY,Stardew Valley Expanded 7652,Isaac Shop,Tempered Galaxy Sword,MANDATORY,Stardew Valley Expanded 7653,Isaac Shop,Tempered Galaxy Hammer,MANDATORY,Stardew Valley Expanded @@ -2697,7 +3040,6 @@ id,region,name,tags,mod_name 7724,Mutant Bug Lair,Fishsanity: Water Grub,FISHSANITY,Stardew Valley Expanded 7725,Crimson Badlands,Fishsanity: Undeadfish,FISHSANITY,Stardew Valley Expanded 7726,Shearwater Bridge,Fishsanity: Kittyfish,FISHSANITY,Stardew Valley Expanded -7727,Blue Moon Vineyard,Fishsanity: Dulse Seaweed,FISHSANITY,Stardew Valley Expanded 7728,Witch's Swamp,Fishsanity: Void Minnow,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7729,Witch's Swamp,Fishsanity: Swamp Leech,FISHSANITY,Distant Lands - Witch Swamp Overhaul 7730,Witch's Swamp,Fishsanity: Giant Horsehoe Crab,FISHSANITY,Distant Lands - Witch Swamp Overhaul @@ -2714,15 +3056,15 @@ id,region,name,tags,mod_name 8002,Shipping,Shipsanity: Travel Core,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Magic 8003,Shipping,Shipsanity: Aegis Elixir,SHIPSANITY,Stardew Valley Expanded 8004,Shipping,Shipsanity: Aged Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded -8005,Shipping,Shipsanity: Ancient Ferns Seed,SHIPSANITY,Stardew Valley Expanded +8005,Shipping,Shipsanity: Ancient Fern Seed,SHIPSANITY,Stardew Valley Expanded 8006,Shipping,Shipsanity: Ancient Fiber,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8007,Shipping,Shipsanity: Armor Elixir,SHIPSANITY,Stardew Valley Expanded 8008,Shipping,Shipsanity: Baby Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8009,Shipping,Shipsanity: Baked Berry Oatmeal,SHIPSANITY,Stardew Valley Expanded 8010,Shipping,Shipsanity: Barbarian Elixir,SHIPSANITY,Stardew Valley Expanded -8011,Shipping,Shipsanity: Bearberrys,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8011,Shipping,Shipsanity: Bearberry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8012,Shipping,Shipsanity: Big Bark Burger,SHIPSANITY,Stardew Valley Expanded -8013,Shipping,Shipsanity: Big Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8013,Shipping,Shipsanity: Conch,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8014,Shipping,Shipsanity: Blue Moon Wine,SHIPSANITY,Stardew Valley Expanded 8015,Shipping,Shipsanity: Bonefish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8016,Shipping,Shipsanity: Bull Trout,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2730,8 +3072,7 @@ id,region,name,tags,mod_name 8018,Shipping,Shipsanity: Clownfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8019,Shipping,Shipsanity: Daggerfish,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8020,Shipping,Shipsanity: Dewdrop Berry,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8021,Shipping,Shipsanity: Dried Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded -8022,Shipping,Shipsanity: Dulse Seaweed,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded +8021,Shipping,Shipsanity: Sand Dollar,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8023,Shipping,Shipsanity: Ferngill Primrose,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8024,Shipping,Shipsanity: Flower Cookie,SHIPSANITY,Stardew Valley Expanded 8025,Shipping,Shipsanity: Frog,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2751,7 +3092,7 @@ id,region,name,tags,mod_name 8040,Shipping,Shipsanity: King Salmon,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8050,Shipping,Shipsanity: Kittyfish,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8051,Shipping,Shipsanity: Lightning Elixir,SHIPSANITY,Stardew Valley Expanded -8052,Shipping,Shipsanity: Lucky Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8052,Shipping,Shipsanity: Four Leaf Clover,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8053,Shipping,Shipsanity: Lunaloo,"SHIPSANITY,SHIPSANITY_FISH,GINGER_ISLAND",Stardew Valley Expanded 8054,Shipping,Shipsanity: Meteor Carp,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded 8055,Shipping,Shipsanity: Minnow,"SHIPSANITY,SHIPSANITY_FISH",Stardew Valley Expanded @@ -2774,7 +3115,7 @@ id,region,name,tags,mod_name 8072,Shipping,Shipsanity: Shrub Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8073,Shipping,Shipsanity: Slime Berry,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT,GINGER_ISLAND",Stardew Valley Expanded 8074,Shipping,Shipsanity: Slime Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded -8075,Shipping,Shipsanity: Smelly Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded +8075,Shipping,Shipsanity: Rafflesia,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",Stardew Valley Expanded 8076,Shipping,Shipsanity: Sports Drink,SHIPSANITY,Stardew Valley Expanded 8077,Shipping,Shipsanity: Stalk Seed,"SHIPSANITY,GINGER_ISLAND",Stardew Valley Expanded 8078,Shipping,Shipsanity: Stamina Capsule,SHIPSANITY,Stardew Valley Expanded @@ -2937,3 +3278,12 @@ id,region,name,tags,mod_name 8235,Shipping,Shipsanity: Pterodactyl Claw,SHIPSANITY,Boarding House and Bus Stop Extension 8236,Shipping,Shipsanity: Neanderthal Skull,SHIPSANITY,Boarding House and Bus Stop Extension 8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension +8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology +8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology +8240,Shipping,Shipsanity: Digging Like Worms,SHIPSANITY,Archaeology +8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology +8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology +8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology +8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology +8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill +8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py index 544bb92e6e55..b81c518a37c9 100644 --- a/worlds/stardew_valley/data/museum_data.py +++ b/worlds/stardew_valley/data/museum_data.py @@ -76,6 +76,8 @@ def create_mineral(name: str, difficulty += 1.0 / 26.0 * 100 if "Omni Geode" in geodes: difficulty += 31.0 / 2750.0 * 100 + if "Fishing Chest" in geodes: + difficulty += 4.3 mineral_item = MuseumItem.of(name, difficulty, locations, geodes, monsters) all_museum_minerals.append(mineral_item) @@ -95,7 +97,7 @@ class Artifact: geodes=Geode.artifact_trove) arrowhead = create_artifact("Arrowhead", 8.5, (Region.mountain, Region.forest, Region.bus_stop), geodes=Geode.artifact_trove) - ancient_doll = create_artifact("Ancient Doll", 13.1, (Region.mountain, Region.forest, Region.bus_stop), + ancient_doll = create_artifact(Artifact.ancient_doll, 13.1, (Region.mountain, Region.forest, Region.bus_stop), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) elvish_jewelry = create_artifact("Elvish Jewelry", 5.3, Region.forest, geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) @@ -103,8 +105,7 @@ class Artifact: geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town), geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) - dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.mountain, Region.skull_cavern), - geodes=WaterChest.fishing_chest, + dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern), monsters=Monster.pepper_rex) rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley, geodes=(Geode.artifact_trove, WaterChest.fishing_chest), @@ -170,18 +171,18 @@ class Artifact: class Mineral: - quartz = create_mineral(Mineral.quartz, Region.mines_floor_20) + quartz = create_mineral(Mineral.quartz, Region.mines_floor_20, difficulty=100.0 / 5.0) fire_quartz = create_mineral("Fire Quartz", Region.mines_floor_100, geodes=(Geode.magma, Geode.omni, WaterChest.fishing_chest), - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) frozen_tear = create_mineral("Frozen Tear", Region.mines_floor_60, geodes=(Geode.frozen, Geode.omni, WaterChest.fishing_chest), monsters=unlikely, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) earth_crystal = create_mineral("Earth Crystal", Region.mines_floor_20, geodes=(Geode.geode, Geode.omni, WaterChest.fishing_chest), monsters=Monster.duggy, - difficulty=1.0 / 12.0) + difficulty=100.0 / 5.0) emerald = create_mineral("Emerald", Region.mines_floor_100, geodes=WaterChest.fishing_chest) aquamarine = create_mineral("Aquamarine", Region.mines_floor_60, diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index 62dcd8709c64..b48246876271 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -7,15 +7,16 @@ from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish from ..strings.flower_names import Flower -from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable +from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient -from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal +from ..strings.food_names import Meal, SVEMeal, Beverage, DistantLandsMeal, BoardingHouseMeal, ArchaeologyMeal, TrashyMeal from ..strings.material_names import Material -from ..strings.metal_names import Fossil +from ..strings.metal_names import Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.region_names import Region, SVERegion from ..strings.season_names import Season -from ..strings.skill_names import Skill +from ..strings.seed_names import Seed +from ..strings.skill_names import Skill, ModSkill from ..strings.villager_names import NPC, ModNPC @@ -49,9 +50,9 @@ def friendship_and_shop_recipe(name: str, friend: str, hearts: int, region: str, return create_recipe(name, ingredients, source, mod_name) -def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int]) -> CookingRecipe: +def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: source = SkillSource(skill, level) - return create_recipe(name, ingredients, source) + return create_recipe(name, ingredients, source, mod_name) def shop_recipe(name: str, region: str, price: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CookingRecipe: @@ -116,7 +117,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, fried_calamari = friendship_recipe(Meal.fried_calamari, NPC.jodi, 3, {Fish.squid: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}) fried_eel = friendship_recipe(Meal.fried_eel, NPC.george, 3, {Fish.eel: 1, Ingredient.oil: 1}) fried_egg = starter_recipe(Meal.fried_egg, {AnimalProduct.chicken_egg: 1}) -fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Forageable.common_mushroom: 1, Forageable.morel: 1, Ingredient.oil: 1}) +fried_mushroom = friendship_recipe(Meal.fried_mushroom, NPC.demetrius, 3, {Mushroom.common: 1, Mushroom.morel: 1, Ingredient.oil: 1}) fruit_salad = queen_of_sauce_recipe(Meal.fruit_salad, 2, Season.fall, 7, {Fruit.blueberry: 1, Fruit.melon: 1, Fruit.apricot: 1}) ginger_ale = shop_recipe(Beverage.ginger_ale, Region.volcano_dwarf_shop, 1000, {Forageable.ginger: 3, Ingredient.sugar: 1}) glazed_yams = queen_of_sauce_recipe(Meal.glazed_yams, 1, Season.fall, 21, {Vegetable.yam: 1, Ingredient.sugar: 1}) @@ -130,6 +131,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, mango_sticky_rice = friendship_recipe(Meal.mango_sticky_rice, NPC.leo, 7, {Fruit.mango: 1, Forageable.coconut: 1, Ingredient.rice: 1}) maple_bar = queen_of_sauce_recipe(Meal.maple_bar, 2, Season.summer, 14, {ArtisanGood.maple_syrup: 1, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}) miners_treat = skill_recipe(Meal.miners_treat, Skill.mining, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.cow_milk: 1}) +moss_soup = skill_recipe(Meal.moss_soup, Skill.foraging, 3, {Material.moss: 20}) omelet = queen_of_sauce_recipe(Meal.omelet, 1, Season.spring, 28, {AnimalProduct.chicken_egg: 1, AnimalProduct.cow_milk: 1}) pale_broth = friendship_recipe(Meal.pale_broth, NPC.marnie, 3, {WaterItem.white_algae: 2}) pancakes = queen_of_sauce_recipe(Meal.pancakes, 1, Season.summer, 14, {Ingredient.wheat_flour: 1, AnimalProduct.chicken_egg: 1}) @@ -160,13 +162,14 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, spaghetti = friendship_recipe(Meal.spaghetti, NPC.lewis, 3, {Vegetable.tomato: 1, Ingredient.wheat_flour: 1}) spicy_eel = friendship_recipe(Meal.spicy_eel, NPC.george, 7, {Fish.eel: 1, Fruit.hot_pepper: 1}) squid_ink_ravioli = skill_recipe(Meal.squid_ink_ravioli, Skill.combat, 9, {AnimalProduct.squid_ink: 1, Ingredient.wheat_flour: 1, Vegetable.tomato: 1}) -stir_fry_ingredients = {Forageable.cave_carrot: 1, Forageable.common_mushroom: 1, Vegetable.kale: 1, Ingredient.sugar: 1} +stir_fry_ingredients = {Forageable.cave_carrot: 1, Mushroom.common: 1, Vegetable.kale: 1, Ingredient.sugar: 1} stir_fry_qos = queen_of_sauce_recipe(Meal.stir_fry, 1, Season.spring, 7, stir_fry_ingredients) strange_bun = friendship_recipe(Meal.strange_bun, NPC.shane, 7, {Ingredient.wheat_flour: 1, Fish.periwinkle: 1, ArtisanGood.void_mayonnaise: 1}) stuffing = friendship_recipe(Meal.stuffing, NPC.pam, 7, {Meal.bread: 1, Fruit.cranberries: 1, Forageable.hazelnut: 1}) super_meal = friendship_recipe(Meal.super_meal, NPC.kent, 7, {Vegetable.bok_choy: 1, Fruit.cranberries: 1, Vegetable.artichoke: 1}) -survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 2, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) -tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Forageable.common_mushroom: 1}) + +survival_burger = skill_recipe(Meal.survival_burger, Skill.foraging, 8, {Meal.bread: 1, Forageable.cave_carrot: 1, Vegetable.eggplant: 1}) +tom_kha_soup = friendship_recipe(Meal.tom_kha_soup, NPC.sandy, 7, {Forageable.coconut: 1, Fish.shrimp: 1, Mushroom.common: 1}) tortilla_ingredients = {Vegetable.corn: 1} tortilla_qos = queen_of_sauce_recipe(Meal.tortilla, 1, Season.fall, 7, tortilla_ingredients) tortilla_saloon = shop_recipe(Meal.tortilla, Region.saloon, 100, tortilla_ingredients) @@ -175,7 +178,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, trout_soup = queen_of_sauce_recipe(Meal.trout_soup, 1, Season.fall, 14, {Fish.rainbow_trout: 1, WaterItem.green_algae: 1}) vegetable_medley = friendship_recipe(Meal.vegetable_medley, NPC.caroline, 7, {Vegetable.tomato: 1, Vegetable.beet: 1}) -magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Forageable.purple_mushroom: 1}, ModNames.magic) +magic_elixir = shop_recipe(ModEdible.magic_elixir, Region.adventurer_guild, 3000, {Edible.life_elixir: 1, Mushroom.purple: 1}, ModNames.magic) baked_berry_oatmeal = shop_recipe(SVEMeal.baked_berry_oatmeal, SVERegion.bear_shop, 0, {Forageable.salmonberry: 15, Forageable.blackberry: 15, Ingredient.sugar: 1, Ingredient.wheat_flour: 2}, ModNames.sve) @@ -188,7 +191,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, glazed_butterfish = friendship_and_shop_recipe(SVEMeal.glazed_butterfish, NPC.gus, 10, Region.saloon, 4000, {SVEFish.butterfish: 1, Ingredient.wheat_flour: 1, Ingredient.oil: 1}, ModNames.sve) mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fruit.strawberry: 6, SVEFruit.salal_berry: 6, Forageable.blackberry: 6, - SVEForage.bearberrys: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, + SVEForage.bearberry: 6, Ingredient.sugar: 1, Ingredient.wheat_flour: 1}, ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) @@ -198,8 +201,8 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, {Fish.void_salmon: 1, ArtisanGood.void_mayonnaise: 1, WaterItem.seaweed: 3}, ModNames.sve) -mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Forageable.chanterelle: 1, Forageable.common_mushroom: 1, - Forageable.red_mushroom: 1, Material.wood: 1}, ModNames.distant_lands) +mushroom_kebab = friendship_recipe(DistantLandsMeal.mushroom_kebab, ModNPC.goblin, 2, {Mushroom.chanterelle: 1, Mushroom.common: 1, + Mushroom.red: 1, Material.wood: 1}, ModNames.distant_lands) void_mint_tea = friendship_recipe(DistantLandsMeal.void_mint_tea, ModNPC.goblin, 4, {DistantLandsCrop.void_mint: 1}, ModNames.distant_lands) crayfish_soup = friendship_recipe(DistantLandsMeal.crayfish_soup, ModNPC.goblin, 6, {Forageable.cave_carrot: 1, Fish.crayfish: 1, DistantLandsFish.purple_algae: 1, WaterItem.white_algae: 1}, ModNames.distant_lands) @@ -208,6 +211,11 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, special_pumpkin_soup = friendship_recipe(BoardingHouseMeal.special_pumpkin_soup, ModNPC.joel, 6, {Vegetable.pumpkin: 2, AnimalProduct.large_goat_milk: 1, Vegetable.garlic: 1}, ModNames.boarding_house) +diggers_delight = skill_recipe(ArchaeologyMeal.diggers_delight, ModSkill.archaeology, 3, {Forageable.cave_carrot: 2, Ingredient.sugar: 1, AnimalProduct.milk: 1}, ModNames.archaeology) +rocky_root = skill_recipe(ArchaeologyMeal.rocky_root, ModSkill.archaeology, 7, {Forageable.cave_carrot: 3, Seed.coffee: 1, Material.stone: 1}, ModNames.archaeology) +ancient_jello = skill_recipe(ArchaeologyMeal.ancient_jello, ModSkill.archaeology, 9, {WaterItem.cave_jelly: 6, Ingredient.sugar: 5, AnimalProduct.egg: 1, AnimalProduct.milk: 1, Artifact.chipped_amphora: 1}, ModNames.archaeology) +grilled_cheese = skill_recipe(TrashyMeal.grilled_cheese, ModSkill.binning, 1, {Meal.bread: 1, ArtisanGood.cheese: 1}, ModNames.binning_skill) +fish_casserole = skill_recipe(TrashyMeal.fish_casserole, ModSkill.binning, 8, {Fish.any: 1, AnimalProduct.milk: 1, Vegetable.carrot: 1}, ModNames.binning_skill) all_cooking_recipes_by_name = {recipe.meal: recipe for recipe in all_cooking_recipes} \ No newline at end of file diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 8dd622e926e7..24b03bf77bd4 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,16 @@ def __repr__(self): return f"SkillSource at level {self.level} {self.skill}" +class MasterySource(RecipeSource): + skill: str + + def __init__(self, skill: str): + self.skill = skill + + def __repr__(self): + return f"MasterySource at level {self.level} {self.skill}" + + class ShopSource(RecipeSource): region: str price: int diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py new file mode 100644 index 000000000000..7e9466630fc3 --- /dev/null +++ b/worlds/stardew_valley/data/requirement.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from .game_item import Requirement +from ..strings.tool_names import ToolMaterial + + +@dataclass(frozen=True) +class BookRequirement(Requirement): + book: str + + +@dataclass(frozen=True) +class ToolRequirement(Requirement): + tool: str + tier: str = ToolMaterial.basic + + +@dataclass(frozen=True) +class SkillRequirement(Requirement): + skill: str + level: int + + +@dataclass(frozen=True) +class SeasonRequirement(Requirement): + season: str + + +@dataclass(frozen=True) +class YearRequirement(Requirement): + year: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py new file mode 100644 index 000000000000..ca54d35e14f2 --- /dev/null +++ b/worlds/stardew_valley/data/shop.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from typing import Tuple, Optional + +from .game_item import ItemSource, kw_only, Requirement +from ..strings.season_names import Season + +ItemPrice = Tuple[int, str] + + +@dataclass(frozen=True, **kw_only) +class ShopSource(ItemSource): + shop_region: str + money_price: Optional[int] = None + items_price: Optional[Tuple[ItemPrice, ...]] = None + seasons: Tuple[str, ...] = Season.all + other_requirements: Tuple[Requirement, ...] = () + + def __post_init__(self): + assert self.money_price or self.items_price, "At least money price or items price need to be defined." + assert self.items_price is None or all(type(p) == tuple for p in self.items_price), "Items price should be a tuple." + + +@dataclass(frozen=True, **kw_only) +class MysteryBoxSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class ArtifactTroveSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class PrizeMachineSource(ItemSource): + amount: int + + +@dataclass(frozen=True, **kw_only) +class FishingTreasureChestSource(ItemSource): + amount: int diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py new file mode 100644 index 000000000000..d0674f34c0e1 --- /dev/null +++ b/worlds/stardew_valley/data/skill.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass, field + +from ..data.game_item import kw_only + + +@dataclass(frozen=True) +class Skill: + name: str + has_mastery: bool = field(**kw_only) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py index 718bce743b1c..70fb110ffbae 100644 --- a/worlds/stardew_valley/data/villagers_data.py +++ b/worlds/stardew_valley/data/villagers_data.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -from typing import List, Tuple, Optional, Dict, Callable, Set +from typing import Tuple, Optional from ..mods.mod_data import ModNames from ..strings.food_names import Beverage from ..strings.generic_names import Generic -from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion +from ..strings.region_names import Region, SVERegion, AlectoRegion, BoardingHouseRegion, LaceyRegion, LogicRegion from ..strings.season_names import Season from ..strings.villager_names import NPC, ModNPC @@ -36,7 +36,7 @@ def __repr__(self): alex_house = (Region.alex_house,) elliott_house = (Region.elliott_house,) ranch = (Region.ranch,) -mines_dwarf_shop = (Region.mines_dwarf_shop,) +mines_dwarf_shop = (LogicRegion.mines_dwarf_shop,) desert = (Region.desert,) oasis = (Region.oasis,) sewers = (Region.sewer,) @@ -355,28 +355,10 @@ def __repr__(self): susan_loves = pancakes + chocolate_cake + pink_cake + ice_cream + cookie + pumpkin_pie + rhubarb_pie + \ blueberry_tart + blackberry_cobbler + cranberry_candy + red_plate -all_villagers: List[Villager] = [] -villager_modifications_by_mod: Dict[str, Dict[str, Callable[[str, Villager], Villager]]] = {} - def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...], available: bool, mod_name: Optional[str] = None) -> Villager: - npc = Villager(name, bachelor, locations, birthday, gifts, available, mod_name) - all_villagers.append(npc) - return npc - - -def adapt_wizard_to_sve(mod_name: str, npc: Villager): - if npc.mod_name: - mod_name = npc.mod_name - # The wizard leaves his tower on sunday, for like 1 hour... Good enough to meet him! - return Villager(npc.name, True, npc.locations + forest, npc.birthday, npc.gifts, npc.available, mod_name) - - -def register_villager_modification(mod_name: str, npc: Villager, modification_function): - if mod_name not in villager_modifications_by_mod: - villager_modifications_by_mod[mod_name] = {} - villager_modifications_by_mod[mod_name][npc.name] = modification_function + return Villager(name, bachelor, locations, birthday, gifts, available, mod_name) josh = villager(NPC.alex, True, town + alex_house, Season.summer, universal_loves + complete_breakfast + salmon_dinner, True) @@ -385,18 +367,18 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu sam = villager(NPC.sam, True, town, Season.summer, universal_loves + sam_loves, True) sebastian = villager(NPC.sebastian, True, carpenter, Season.winter, universal_loves + sebastian_loves, True) shane = villager(NPC.shane, True, ranch, Season.spring, universal_loves + shane_loves, True) -best_girl = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) +abigail = villager(NPC.abigail, True, town, Season.fall, universal_loves + abigail_loves, True) emily = villager(NPC.emily, True, town, Season.spring, universal_loves + emily_loves, True) -hoe = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) +haley = villager(NPC.haley, True, town, Season.spring, universal_loves_no_prismatic_shard + haley_loves, True) leah = villager(NPC.leah, True, forest, Season.winter, universal_loves + leah_loves, True) -nerd = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) +maru = villager(NPC.maru, True, carpenter + hospital + town, Season.summer, universal_loves + maru_loves, True) penny = villager(NPC.penny, True, town, Season.fall, universal_loves_no_rabbit_foot + penny_loves, True) caroline = villager(NPC.caroline, False, town, Season.winter, universal_loves + caroline_loves, True) clint = villager(NPC.clint, False, town, Season.winter, universal_loves + clint_loves, True) demetrius = villager(NPC.demetrius, False, carpenter, Season.summer, universal_loves + demetrius_loves, True) dwarf = villager(NPC.dwarf, False, mines_dwarf_shop, Season.summer, universal_loves + dwarf_loves, False) -gilf = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) -boomer = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) +evelyn = villager(NPC.evelyn, False, town, Season.winter, universal_loves + evelyn_loves, True) +george = villager(NPC.george, False, town, Season.fall, universal_loves + george_loves, True) gus = villager(NPC.gus, False, town, Season.summer, universal_loves + gus_loves, True) jas = villager(NPC.jas, False, ranch, Season.summer, universal_loves + jas_loves, True) jodi = villager(NPC.jodi, False, town, Season.fall, universal_loves + jodi_loves, True) @@ -408,7 +390,7 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu marnie = villager(NPC.marnie, False, ranch, Season.fall, universal_loves + marnie_loves, True) pam = villager(NPC.pam, False, town, Season.spring, universal_loves + pam_loves, True) pierre = villager(NPC.pierre, False, town, Season.spring, universal_loves + pierre_loves, True) -milf = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) +robin = villager(NPC.robin, False, carpenter, Season.fall, universal_loves + robin_loves, True) sandy = villager(NPC.sandy, False, oasis, Season.fall, universal_loves + sandy_loves, False) vincent = villager(NPC.vincent, False, town, Season.spring, universal_loves + vincent_loves, True) willy = villager(NPC.willy, False, beach, Season.summer, universal_loves + willy_loves, True) @@ -443,54 +425,10 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu victor = villager(ModNPC.victor, True, town, Season.summer, universal_loves + victor_loves, True, ModNames.sve) andy = villager(ModNPC.andy, False, forest, Season.spring, universal_loves + andy_loves, True, ModNames.sve) apples = villager(ModNPC.apples, False, aurora + junimo, Generic.any, starfruit, False, ModNames.sve) -gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.jasper_sve) +gunther = villager(ModNPC.gunther, False, museum, Season.winter, universal_loves + gunther_loves, True, ModNames.sve) martin = villager(ModNPC.martin, False, town + jojamart, Season.summer, universal_loves + martin_loves, True, ModNames.sve) -marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.jasper_sve) +marlon = villager(ModNPC.marlon, False, adventurer, Season.winter, universal_loves + marlon_loves, False, ModNames.sve) morgan = villager(ModNPC.morgan, False, forest, Season.fall, universal_loves_no_rabbit_foot + morgan_loves, False, ModNames.sve) scarlett = villager(ModNPC.scarlett, False, bluemoon, Season.summer, universal_loves + scarlett_loves, False, ModNames.sve) susan = villager(ModNPC.susan, False, railroad, Season.fall, universal_loves + susan_loves, False, ModNames.sve) morris = villager(ModNPC.morris, False, jojamart, Season.spring, universal_loves + morris_loves, True, ModNames.sve) - -# Modified villagers; not included in all villagers - -register_villager_modification(ModNames.sve, wizard, adapt_wizard_to_sve) - -all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers} -all_villagers_by_mod: Dict[str, List[Villager]] = {} -all_villagers_by_mod_by_name: Dict[str, Dict[str, Villager]] = {} - -for npc in all_villagers: - mod = npc.mod_name - name = npc.name - if mod in all_villagers_by_mod: - all_villagers_by_mod[mod].append(npc) - all_villagers_by_mod_by_name[mod][name] = npc - else: - all_villagers_by_mod[mod] = [npc] - all_villagers_by_mod_by_name[mod] = {} - all_villagers_by_mod_by_name[mod][name] = npc - - -def villager_included_for_any_mod(npc: Villager, mods: Set[str]): - if not npc.mod_name: - return True - for mod in npc.mod_name.split(","): - if mod in mods: - return True - return False - - -def get_villagers_for_mods(mods: Set[str]) -> List[Villager]: - villagers_for_current_mods = [] - for npc in all_villagers: - if not villager_included_for_any_mod(npc, mods): - continue - modified_npc = npc - for active_mod in mods: - if (active_mod not in villager_modifications_by_mod or - npc.name not in villager_modifications_by_mod[active_mod]): - continue - modification = villager_modifications_by_mod[active_mod][npc.name] - modified_npc = modification(active_mod, modified_npc) - villagers_for_current_mods.append(modified_npc) - return villagers_for_current_mods diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index c29ae859e095..0ed693031b82 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -11,59 +11,62 @@ A vast number of objectives in Stardew Valley can be shuffled around the multiwo player can customize their experience in their YAML file. For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining -number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that +number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player. ## What is the goal of Stardew Valley? The player can choose from a number of goals, using their YAML options. + - Complete the [Community Center](https://stardewvalleywiki.com/Bundles) - Succeed [Grandpa's Evaluation](https://stardewvalleywiki.com/Grandpa) with 4 lit candles - Reach the bottom of the [Pelican Town Mineshaft](https://stardewvalleywiki.com/The_Mines) -- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on -floor 100 of the Skull Cavern +- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on + floor 100 of the Skull Cavern - Become a [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in your slot -- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and -minerals to the museum +- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and + minerals to the museum - Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids -- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires -finding all 130 golden walnuts on ginger island -- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by -completing all the monster slayer goals at the Adventure Guild +- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires + finding all 130 golden walnuts on ginger island +- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by + completing all the monster slayer goals at the Adventure Guild - Complete a [Full Shipment](https://stardewvalleywiki.com/Shipping#Collection) by shipping every item in your slot - Become a [Gourmet Chef](https://stardewvalleywiki.com/Cooking) by cooking every recipe in your slot - Become a [Craft Master](https://stardewvalleywiki.com/Crafting) by crafting every item -- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g -- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop +- Earn the title of [Legend](https://stardewvalleywiki.com/Gold) by earning 10 000 000g +- Solve the [Mystery of the Stardrops](https://stardewvalleywiki.com/Stardrop) by finding every stardrop - Finish 100% of your randomizer slot with Allsanity: Complete every check in your slot - Achieve [Perfection](https://stardewvalleywiki.com/Perfection) in your save file -The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt -to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" +The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt +to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal. ## What are location checks in Stardew Valley? Location checks in Stardew Valley always include: + - [Community Center Bundles](https://stardewvalleywiki.com/Bundles) - [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) - [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart) -- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), -[Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), -[Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc +- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), + [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), + [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: + - [Tools and Fishing Rod Upgrades](https://stardewvalleywiki.com/Tools) - [Carpenter Buildings](https://stardewvalleywiki.com/Carpenter%27s_Shop#Farm_Buildings) - [Backpack Upgrades](https://stardewvalleywiki.com/Tools#Other_Tools) - [Mine Elevator Levels](https://stardewvalleywiki.com/The_Mines#Staircases) -- [Skill Levels](https://stardewvalleywiki.com/Skills) +- [Skill Levels](https://stardewvalleywiki.com/Skills) and [Masteries](https://stardewvalleywiki.com/Mastery_Cave#Masteries) - Arcade Machines - [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests) - [Help Wanted Quests](https://stardewvalleywiki.com/Quests#Help_Wanted_Quests) - Participating in [Festivals](https://stardewvalleywiki.com/Festivals) -- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from -[Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) +- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from + [Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) - [Cropsanity](https://stardewvalleywiki.com/Crops): Growing and Harvesting individual crop types - [Fishsanity](https://stardewvalleywiki.com/Fish): Catching individual fish - [Museumsanity](https://stardewvalleywiki.com/Museum): Donating individual items, or reaching milestones for museum donations @@ -73,6 +76,8 @@ There also are a number of location checks that are optional, and individual pla - [Chefsanity](https://stardewvalleywiki.com/Cooking#Recipes): Learning cooking recipes - [Craftsanity](https://stardewvalleywiki.com/Crafting): Crafting individual items - [Shipsanity](https://stardewvalleywiki.com/Shipping): Shipping individual items +- [Booksanity](https://stardewvalleywiki.com/Books): Reading individual books +- [Walnutsanity](https://stardewvalleywiki.com/Golden_Walnut): Collecting Walnuts on Ginger Island ## Which items can be in another player's world? @@ -80,49 +85,57 @@ Every normal reward from the above locations can be in another player's world. For the locations which do not include a normal reward, Resource Packs and traps are instead added to the pool. Traps are optional. A player can enable some options that will add some items to the pool that are relevant to progression + - Seasons Randomizer: - - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. - - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. + - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. + - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only + choose from the seasons they have received. - Cropsanity: - - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check - - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each + seed and harvesting the resulting crop sends a location check + - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount + packs, not individually. - Museumsanity: - - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. - - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for + convenience. + - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the + player receives "Traveling Merchant Metal Detector" items. - TV Channels - Babies - Only if Friendsanity is enabled There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include + - [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings) - [Return Scepter](https://stardewvalleywiki.com/Return_Scepter) - [Qi Walnut Room QoL items](https://stardewvalleywiki.com/Qi%27s_Walnut_Room#Stock) And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include: + - Arcade Machine buffs (Only if the arcade machines are randomized) - - Journey of the Prairie King has drop rate increases, extra lives, and equipment - - Junimo Kart has extra lives. + - Journey of the Prairie King has drop rate increases, extra lives, and equipment + - Junimo Kart has extra lives. - Permanent Movement Speed Bonuses (customizable) -- Permanent Luck Bonuses (customizable) -- Traveling Merchant buffs +- Various Permanent Player Buffs (customizable) +- Traveling Merchant modifiers ## When the player receives an item, what happens? -Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received -while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received +while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. If an item is received while offline, it will be in the mailbox as soon as the player logs in. -Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. -In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. ## Mods Some Stardew Valley mods unrelated to Archipelago are officially "supported". -This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated -with the assumption that you will install and play with these mods. The multiworld will contain related items and locations +This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated +with the assumption that you will install and play with these mods. The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod [Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) @@ -131,17 +144,14 @@ List of supported mods: - General - [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753) - - [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571) - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963) - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845) - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401) - [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109) - Skills - - [Magic](https://www.nexusmods.com/stardewvalley/mods/2007) - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521) - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142) - - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793) - - [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522) + - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/22199) - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073) - NPCs - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427) @@ -149,20 +159,15 @@ List of supported mods: - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606) - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599) - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697) - - [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871) - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222) - - ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462) - - [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732) - - [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510) - - [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) - [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) - -Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found + +Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found [here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) ## Multiplayer You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. -You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game -Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew , or a player in another game that supports gifting, using +in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 74caf9b7daba..c672152543cf 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -2,14 +2,10 @@ ## Required Software -- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - - You need version 1.5.6. It is available in a public beta branch on Steam ![image](https://i.imgur.com/uKAUmF0.png). - - If your Stardew is not on Steam, you are responsible for finding a way to downgrade it. - - This measure is temporary. We are working hard to bring the mod to Stardew 1.6 as soon as possible. -- SMAPI 3.x.x ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) - - Same as Stardew Valley itself, SMAPI needs a slightly older version to be compatible with Stardew Valley 1.5.6 ![image](https://i.imgur.com/kzgObHy.png) -- [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases +- Stardew Valley 1.6 on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) +- SMAPI ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) +- [StardewArchipelago Mod Release 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) + - It is important to use a mod release of version 6.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software @@ -38,7 +34,7 @@ You can customize your options by visiting the [Stardew Valley Player Options Pa ### Installing the mod -- Install [SMAPI version 3.x.x](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page +- Install [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page - Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder - *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%` @@ -93,7 +89,7 @@ Stardew-exclusive commands. ### Playing with supported mods -See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) +See the [Supported mods documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) ### Multiplayer diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index 78170f29fee7..e1ad8cebfd4a 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -1,51 +1,69 @@ from random import Random -from .options import BuildingProgression, StardewValleyOptions, BackpackProgression, ExcludeGingerIsland, SeasonRandomization, SpecialOrderLocations, \ - Monstersanity, ToolProgression, SkillProgression, Cooksanity, Chefsanity +from . import options as stardew_options +from .strings.ap_names.ap_weapon_names import APWeapon +from .strings.ap_names.transport_names import Transportation +from .strings.building_names import Building +from .strings.region_names import Region +from .strings.season_names import Season +from .strings.tv_channel_names import Channel +from .strings.wallet_item_names import Wallet early_candidate_rate = 4 -always_early_candidates = ["Greenhouse", "Desert Obelisk", "Rusty Key"] -seasons = ["Spring", "Summer", "Fall", "Winter"] +always_early_candidates = [Region.greenhouse, Transportation.desert_obelisk, Wallet.rusty_key] +seasons = [Season.spring, Season.summer, Season.fall, Season.winter] -def setup_early_items(multiworld, options: StardewValleyOptions, player: int, random: Random): +def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, player: int, random: Random): early_forced = [] early_candidates = [] early_candidates.extend(always_early_candidates) add_seasonal_candidates(early_candidates, options) - if options.building_progression & BuildingProgression.option_progressive: - early_forced.append("Shipping Bin") - early_candidates.append("Progressive Coop") + if options.building_progression & stardew_options.BuildingProgression.option_progressive: + early_forced.append(Building.shipping_bin) + if options.farm_type != stardew_options.FarmType.option_meadowlands: + early_candidates.append("Progressive Coop") early_candidates.append("Progressive Barn") - if options.backpack_progression == BackpackProgression.option_early_progressive: + if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive: early_forced.append("Progressive Backpack") - if options.tool_progression & ToolProgression.option_progressive: - early_forced.append("Progressive Fishing Rod") + if options.tool_progression & stardew_options.ToolProgression.option_progressive: + if options.fishsanity != stardew_options.Fishsanity.option_none: + early_candidates.append("Progressive Fishing Rod") early_forced.append("Progressive Pickaxe") - if options.skill_progression == SkillProgression.option_progressive: + if options.skill_progression == stardew_options.SkillProgression.option_progressive: early_forced.append("Fishing Level") if options.quest_locations >= 0: - early_candidates.append("Magnifying Glass") + early_candidates.append(Wallet.magnifying_glass) - if options.special_order_locations != SpecialOrderLocations.option_disabled: + if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board: early_candidates.append("Special Order Board") - if options.cooksanity != Cooksanity.option_none | options.chefsanity & Chefsanity.option_queen_of_sauce: - early_candidates.append("The Queen of Sauce") + if options.cooksanity != stardew_options.Cooksanity.option_none or options.chefsanity & stardew_options.Chefsanity.option_queen_of_sauce: + early_candidates.append(Channel.queen_of_sauce) - if options.monstersanity == Monstersanity.option_none: - early_candidates.append("Progressive Weapon") + if options.craftsanity != stardew_options.Craftsanity.option_none: + early_candidates.append("Furnace Recipe") + + if options.monstersanity == stardew_options.Monstersanity.option_none: + early_candidates.append(APWeapon.weapon) else: - early_candidates.append("Progressive Sword") + early_candidates.append(APWeapon.sword) + + if options.exclude_ginger_island == stardew_options.ExcludeGingerIsland.option_false: + early_candidates.append(Transportation.island_obelisk) + + if options.walnutsanity.value: + early_candidates.append("Island North Turtle") + early_candidates.append("Island West Turtle") - if options.exclude_ginger_island == ExcludeGingerIsland.option_false: - early_candidates.append("Island Obelisk") + if options.museumsanity != stardew_options.Museumsanity.option_none or options.shipsanity >= stardew_options.Shipsanity.option_full_shipment: + early_candidates.append(Wallet.metal_detector) early_forced.extend(random.sample(early_candidates, len(early_candidates) // early_candidate_rate)) @@ -56,10 +74,10 @@ def setup_early_items(multiworld, options: StardewValleyOptions, player: int, ra def add_seasonal_candidates(early_candidates, options): - if options.season_randomization == SeasonRandomization.option_progressive: - early_candidates.extend(["Progressive Season"] * 3) + if options.season_randomization == stardew_options.SeasonRandomization.option_progressive: + early_candidates.extend([Season.progressive] * 3) return - if options.season_randomization == SeasonRandomization.option_disabled: + if options.season_randomization == stardew_options.SeasonRandomization.option_disabled: return early_candidates.extend(seasons) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index d0cb09bd9953..cb6102016942 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -8,18 +8,20 @@ from BaseClasses import Item, ItemClassification from . import data -from .data.villagers_data import get_villagers_for_mods +from .content.feature import friendsanity +from .content.game_content import StardewContent +from .data.game_item import ItemTag +from .logic.logic_event import all_events from .mods.mod_data import ModNames -from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, \ - Friendsanity, Museumsanity, \ - Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ - Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity +from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ + BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ + Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs +from .strings.ap_names.ap_option_names import OptionName from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.buff_names import Buff from .strings.ap_names.community_upgrade_names import CommunityUpgrade -from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem -from .strings.villager_names import NPC, ModNPC +from .strings.currency_names import Currency from .strings.wallet_item_names import Wallet ITEM_CODE_OFFSET = 717000 @@ -44,6 +46,7 @@ class Group(enum.Enum): WEAPON_SLINGSHOT = enum.auto() PROGRESSIVE_TOOLS = enum.auto() SKILL_LEVEL_UP = enum.auto() + SKILL_MASTERY = enum.auto() BUILDING = enum.auto() WIZARD_BUILDING = enum.auto() ARCADE_MACHINE_BUFFS = enum.auto() @@ -62,6 +65,7 @@ class Group(enum.Enum): FESTIVAL = enum.auto() RARECROW = enum.auto() TRAP = enum.auto() + BONUS = enum.auto() MAXIMUM_ONE = enum.auto() EXACTLY_TWO = enum.auto() DEPRECATED = enum.auto() @@ -80,6 +84,9 @@ class Group(enum.Enum): CHEFSANITY_FRIENDSHIP = enum.auto() CHEFSANITY_SKILL = enum.auto() CRAFTSANITY = enum.auto() + BOOK_POWER = enum.auto() + LOST_BOOK = enum.auto() + PLAYER_BUFF = enum.auto() # Mods MAGIC_SPELL = enum.auto() MOD_WARP = enum.auto() @@ -135,11 +142,8 @@ def load_item_csv(): events = [ - ItemData(None, Event.victory, ItemClassification.progression), - ItemData(None, Event.can_construct_buildings, ItemClassification.progression), - ItemData(None, Event.start_dark_talisman_quest, ItemClassification.progression), - ItemData(None, Event.can_ship_items, ItemClassification.progression), - ItemData(None, Event.can_shop_at_pierre, ItemClassification.progression), + ItemData(None, e, ItemClassification.progression) + for e in sorted(all_events) ] all_items: List[ItemData] = load_item_csv() + events @@ -168,9 +172,9 @@ def get_too_many_items_error_message(locations_count: int, items_count: int) -> def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item], - options: StardewValleyOptions, random: Random) -> List[Item]: + options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] - unique_items = create_unique_items(item_factory, options, random) + unique_items = create_unique_items(item_factory, options, content, random) remove_items(item_deleter, items_to_exclude, unique_items) @@ -213,11 +217,12 @@ def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_it remove_items(item_deleter, items_to_remove, unique_items) -def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]: +def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]: items = [] items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) items.append(item_factory(CommunityUpgrade.movie_theater)) # It is a community reward, but we need two of them + create_raccoons(item_factory, options, items) items.append(item_factory(Wallet.metal_detector)) # Always offer at least one metal detector create_backpack_items(item_factory, options, items) @@ -233,25 +238,30 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(CommunityUpgrade.mushroom_boxes)) items.append(item_factory("Beach Bridge")) create_tv_channels(item_factory, options, items) - create_special_quest_rewards(item_factory, options, items) - create_stardrops(item_factory, options, items) + create_quest_rewards(item_factory, options, items) + create_stardrops(item_factory, options, content, items) create_museum_items(item_factory, options, items) create_arcade_machine_items(item_factory, options, items) - create_player_buffs(item_factory, options, items) + create_movement_buffs(item_factory, options, items) create_traveling_merchant_items(item_factory, items) items.append(item_factory("Return Scepter")) create_seasons(item_factory, options, items) - create_seeds(item_factory, options, items) - create_friendsanity_items(item_factory, options, items, random) + create_seeds(item_factory, content, items) + create_friendsanity_items(item_factory, options, content, items, random) create_festival_rewards(item_factory, options, items) create_special_order_board_rewards(item_factory, options, items) create_special_order_qi_rewards(item_factory, options, items) + create_walnuts(item_factory, options, items) create_walnut_purchase_rewards(item_factory, options, items) create_crafting_recipes(item_factory, options, items) create_cooking_recipes(item_factory, options, items) create_shipsanity_items(item_factory, options, items) + create_booksanity_items(item_factory, content, items) create_goal_items(item_factory, options, items) items.append(item_factory("Golden Egg")) + items.append(item_factory(CommunityUpgrade.mr_qi_plane_ride)) + + create_sve_special_items(item_factory, options, items) create_magic_mod_spells(item_factory, options, items) create_deepwoods_pendants(item_factory, options, items) create_archaeology_items(item_factory, options, items) @@ -259,6 +269,14 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley return items +def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + number_progressive_raccoons = 9 + if options.quest_locations < 0: + number_progressive_raccoons = number_progressive_raccoons - 1 + + items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons) + + def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if (options.backpack_progression == BackpackProgression.option_progressive or options.backpack_progression == BackpackProgression.option_early_progressive): @@ -310,15 +328,28 @@ def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions items.append(item_factory(item_data, ItemClassification.useful)) else: items.extend([item_factory(item) for item in [item_data] * 4]) - items.append(item_factory("Golden Scythe")) + if options.skill_progression == SkillProgression.option_progressive_with_masteries: + items.append(item_factory("Progressive Scythe")) + items.append(item_factory("Progressive Fishing Rod")) + items.append(item_factory("Progressive Scythe")) def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.skill_progression == SkillProgression.option_progressive: - for item in items_by_group[Group.SKILL_LEVEL_UP]: - if item.mod_name not in options.mods and item.mod_name is not None: - continue - items.extend(item_factory(item) for item in [item.name] * 10) + if options.skill_progression == SkillProgression.option_vanilla: + return + + for item in items_by_group[Group.SKILL_LEVEL_UP]: + if item.mod_name not in options.mods and item.mod_name is not None: + continue + items.extend(item_factory(item) for item in [item.name] * 10) + + if options.skill_progression != SkillProgression.option_progressive_with_masteries: + return + + for item in items_by_group[Group.SKILL_MASTERY]: + if item.mod_name not in options.mods and item.mod_name is not None: + continue + items.append(item_factory(item)) def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -360,6 +391,13 @@ def create_carpenter_buildings(item_factory: StardewItemFactory, options: Starde items.append(item_factory("Tractor Garage")) +def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + create_special_quest_rewards(item_factory, options, items) + create_help_wanted_quest_rewards(item_factory, options, items) + + create_quest_rewards_sve(item_factory, options, items) + + def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.quest_locations < 0: return @@ -373,21 +411,28 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star items.append(item_factory(Wallet.iridium_snake_milk)) items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) - create_special_quest_rewards_sve(item_factory, options, items) - create_distant_lands_quest_rewards(item_factory, options, items) - create_boarding_house_quest_rewards(item_factory, options, items) -def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if options.quest_locations <= 0: + return + + number_help_wanted = options.quest_locations.value + quest_per_prize_ticket = 3 + number_prize_tickets = number_help_wanted // quest_per_prize_ticket + items.extend(item_factory(item) for item in [Currency.prize_ticket] * number_prize_tickets) + + +def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): stardrops_classification = get_stardrop_classification(options) items.append(item_factory("Stardrop", stardrops_classification)) # The Mines level 100 items.append(item_factory("Stardrop", stardrops_classification)) # Old Master Cannoli items.append(item_factory("Stardrop", stardrops_classification)) # Krobus Stardrop - if options.fishsanity != Fishsanity.option_none: + if content.features.fishsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Master Angler Stardrop if ModNames.deepwoods in options.mods: items.append(item_factory("Stardrop", stardrops_classification)) # Petting the Unicorn - if options.friendsanity != Friendsanity.option_none: + if content.features.friendsanity.is_enabled: items.append(item_factory("Stardrop", stardrops_classification)) # Spouse Stardrop @@ -403,39 +448,23 @@ def create_museum_items(item_factory: StardewItemFactory, options: StardewValley items.append(item_factory(Wallet.metal_detector)) -def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item], random: Random): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item], random: Random): + if not content.features.friendsanity.is_enabled: return + create_babies(item_factory, items, random) - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - mods = options.mods - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - classification = ItemClassification.progression - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - items.append(item_factory(f"{villager.name} <3", classification)) - if not exclude_non_bachelors: - need_pet = options.goal == Goal.option_grandpa_evaluation - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - items.append(item_factory(f"Pet <3", ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful)) + + for villager in content.villagers.values(): + item_name = friendsanity.to_item_name(villager.name) + + for _ in content.features.friendsanity.get_randomized_hearts(villager): + items.append(item_factory(item_name, ItemClassification.progression)) + + need_pet = options.goal == Goal.option_grandpa_evaluation + pet_item_classification = ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful + + for _ in content.features.friendsanity.get_pet_randomized_hearts(): + items.append(item_factory(friendsanity.pet_heart_item_name, pet_item_classification)) def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random): @@ -462,26 +491,14 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) -def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_movement_buffs(item_factory, options: StardewValleyOptions, items: List[Item]): movement_buffs: int = options.movement_buff_number.value - luck_buffs: int = options.luck_buff_number.value - need_all_buffs = options.special_order_locations == SpecialOrderLocations.option_board_qi - need_half_buffs = options.festival_locations == FestivalLocations.option_easy - create_player_buff(item_factory, Buff.movement, movement_buffs, need_all_buffs, need_half_buffs, items) - create_player_buff(item_factory, Buff.luck, luck_buffs, True, need_half_buffs, items) - - -def create_player_buff(item_factory, buff: str, amount: int, need_all_buffs: bool, need_half_buffs: bool, items: List[Item]): - progression_buffs = amount if need_all_buffs else (amount // 2 if need_half_buffs else 0) - useful_buffs = amount - progression_buffs - items.extend(item_factory(item) for item in [buff] * progression_buffs) - items.extend(item_factory(item, ItemClassification.useful) for item in [buff] * useful_buffs) + items.extend(item_factory(item) for item in [Buff.movement] * movement_buffs) def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]), - *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6), - *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)]) + *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)]) def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -495,14 +512,11 @@ def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptio items.extend([item_factory(item) for item in items_by_group[Group.SEASON]]) -def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.cropsanity == Cropsanity.option_disabled: +def create_seeds(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + if not content.features.cropsanity.is_enabled: return - base_seed_items = [item for item in items_by_group[Group.CROPSANITY]] - filtered_seed_items = remove_excluded_items(base_seed_items, options) - seed_items = [item_factory(item) for item in filtered_seed_items] - items.extend(seed_items) + items.extend(item_factory(item_table[seed.name]) for seed in content.find_tagged_items(ItemTag.CROPSANITY_SEED)) def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): @@ -514,6 +528,35 @@ def create_festival_rewards(item_factory: StardewItemFactory, options: StardewVa items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))]) +def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + walnutsanity = options.walnutsanity + if options.exclude_ginger_island == ExcludeGingerIsland.option_true or walnutsanity == Walnutsanity.preset_none: + return + + # Give baseline walnuts just to be nice + num_single_walnuts = 0 + num_triple_walnuts = 2 + num_penta_walnuts = 1 + # https://stardewvalleywiki.com/Golden_Walnut + # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts + if OptionName.walnutsanity_puzzles in walnutsanity: # 61 + num_single_walnuts += 6 # 6 + num_triple_walnuts += 5 # 15 + num_penta_walnuts += 8 # 40 + if OptionName.walnutsanity_bushes in walnutsanity: # 25 + num_single_walnuts += 16 # 16 + num_triple_walnuts += 3 # 9 + if OptionName.walnutsanity_dig_spots in walnutsanity: # 18 + num_single_walnuts += 18 # 18 + if OptionName.walnutsanity_repeatables in walnutsanity: # 33 + num_single_walnuts += 30 # 30 + num_triple_walnuts += 1 # 3 + + items.extend([item_factory(item) for item in ["Golden Walnut"] * num_single_walnuts]) + items.extend([item_factory(item) for item in ["3 Golden Walnuts"] * num_triple_walnuts]) + items.extend([item_factory(item) for item in ["5 Golden Walnuts"] * num_penta_walnuts]) + + def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if options.exclude_ginger_island == ExcludeGingerIsland.option_true: return @@ -526,12 +569,9 @@ def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: St def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return - - special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] - - items.extend([item_factory(item) for item in special_order_board_items]) + if options.special_order_locations & SpecialOrderLocations.option_board: + special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]] + items.extend([item_factory(item) for item in special_order_board_items]) def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification: @@ -554,7 +594,7 @@ def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: S qi_gem_rewards.append("15 Qi Gems") qi_gem_rewards.append("15 Qi Gems") - if options.special_order_locations == SpecialOrderLocations.option_board_qi: + if options.special_order_locations & SpecialOrderLocations.value_qi: qi_gem_rewards.extend(["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"]) @@ -607,6 +647,16 @@ def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewVa items.append(item_factory(Wallet.metal_detector)) +def create_booksanity_items(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + items.extend(item_factory(item_table[booksanity.to_item_name(book.name)]) for book in content.find_tagged_items(ItemTag.BOOK_POWER)) + progressive_lost_book = item_table[booksanity.progressive_lost_book] + items.extend(item_factory(progressive_lost_book) for _ in content.features.booksanity.get_randomized_lost_books()) + + def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): goal = options.goal if goal != Goal.option_perfection and goal != Goal.option_complete_collection: @@ -643,35 +693,29 @@ def create_deepwoods_pendants(item_factory: StardewItemFactory, options: Stardew items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]]) -def create_special_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): +def create_sve_special_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): if ModNames.sve not in options.mods: return items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve]) - if options.quest_locations < 0: - return - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) - if exclude_ginger_island: +def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if ModNames.sve not in options.mods: return - items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) + exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items]) + if not exclude_ginger_island: + items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island]) -def create_distant_lands_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.distant_lands not in options.mods: - return - items.append(item_factory("Crayfish Soup Recipe")) - if options.exclude_ginger_island == ExcludeGingerIsland.option_true: + if options.quest_locations < 0: return - items.append(item_factory("Ginger Tincture Recipe")) - -def create_boarding_house_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): - if options.quest_locations < 0 or ModNames.boarding_house not in options.mods: + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) + if exclude_ginger_island: return - items.append(item_factory("Special Pumpkin Soup Recipe")) + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island]) def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, @@ -699,18 +743,21 @@ def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options items_already_added_names = [item.name for item in items_already_added] useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL] if pack.name not in items_already_added_names] - trap_items = [pack for pack in items_by_group[Group.TRAP] - if pack.name not in items_already_added_names and - (pack.mod_name is None or pack.mod_name in options.mods)] + trap_items = [trap for trap in items_by_group[Group.TRAP] + if trap.name not in items_already_added_names and + (trap.mod_name is None or trap.mod_name in options.mods)] + player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs) priority_filler_items = [] priority_filler_items.extend(useful_resource_packs) + priority_filler_items.extend(player_buffs) if include_traps: priority_filler_items.extend(trap_items) exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options) + all_filler_packs.extend(player_buffs) priority_filler_items = remove_excluded_items(priority_filler_items, options) number_priority_items = len(priority_filler_items) @@ -776,7 +823,7 @@ def remove_limited_amount_packs(packs): return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups] -def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): +def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]: all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]] all_filler_items.extend(items_by_group[Group.TRASH]) if include_traps: @@ -785,6 +832,33 @@ def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool): return all_filler_items +def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]: + allowed_buffs = [] + if OptionName.buff_luck in buff_option: + allowed_buffs.append(item_table[Buff.luck]) + if OptionName.buff_damage in buff_option: + allowed_buffs.append(item_table[Buff.damage]) + if OptionName.buff_defense in buff_option: + allowed_buffs.append(item_table[Buff.defense]) + if OptionName.buff_immunity in buff_option: + allowed_buffs.append(item_table[Buff.immunity]) + if OptionName.buff_health in buff_option: + allowed_buffs.append(item_table[Buff.health]) + if OptionName.buff_energy in buff_option: + allowed_buffs.append(item_table[Buff.energy]) + if OptionName.buff_bite in buff_option: + allowed_buffs.append(item_table[Buff.bite_rate]) + if OptionName.buff_fish_trap in buff_option: + allowed_buffs.append(item_table[Buff.fish_trap]) + if OptionName.buff_fishing_bar in buff_option: + allowed_buffs.append(item_table[Buff.fishing_bar]) + if OptionName.buff_quality in buff_option: + allowed_buffs.append(item_table[Buff.quality]) + if OptionName.buff_glow in buff_option: + allowed_buffs.append(item_table[Buff.glow]) + return allowed_buffs + + def get_stardrop_classification(options) -> ItemClassification: return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 103b3bd96081..43246a94a356 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -6,17 +6,17 @@ from . import data from .bundles.bundle_room import BundleRoom -from .data.fish_data import special_fish, get_fish_for_mods +from .content.game_content import StardewContent +from .data.game_item import ItemTag from .data.museum_data import all_museum_items -from .data.villagers_data import get_villagers_for_mods from .mods.mod_data import ModNames -from .options import ExcludeGingerIsland, Friendsanity, ArcadeMachineLocations, SpecialOrderLocations, Cropsanity, Fishsanity, Museumsanity, FestivalLocations, \ - SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression +from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ + FestivalLocations, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity from .strings.goal_names import Goal -from .strings.quest_names import ModQuest -from .strings.region_names import Region -from .strings.villager_names import NPC, ModNPC +from .strings.quest_names import ModQuest, Quest +from .strings.region_names import Region, LogicRegion +from .strings.villager_names import NPC LOCATION_CODE_OFFSET = 717000 @@ -32,6 +32,7 @@ class LocationTags(enum.Enum): BULLETIN_BOARD_BUNDLE = enum.auto() VAULT_BUNDLE = enum.auto() COMMUNITY_CENTER_ROOM = enum.auto() + RACCOON_BUNDLES = enum.auto() BACKPACK = enum.auto() TOOL_UPGRADE = enum.auto() HOE_UPGRADE = enum.auto() @@ -40,6 +41,7 @@ class LocationTags(enum.Enum): WATERING_CAN_UPGRADE = enum.auto() TRASH_CAN_UPGRADE = enum.auto() FISHING_ROD_UPGRADE = enum.auto() + PAN_UPGRADE = enum.auto() THE_MINES_TREASURE = enum.auto() CROPSANITY = enum.auto() ELEVATOR = enum.auto() @@ -49,6 +51,7 @@ class LocationTags(enum.Enum): FORAGING_LEVEL = enum.auto() COMBAT_LEVEL = enum.auto() MINING_LEVEL = enum.auto() + MASTERY_LEVEL = enum.auto() BUILDING_BLUEPRINT = enum.auto() STORY_QUEST = enum.auto() ARCADE_MACHINE = enum.auto() @@ -63,11 +66,18 @@ class LocationTags(enum.Enum): FRIENDSANITY = enum.auto() FESTIVAL = enum.auto() FESTIVAL_HARD = enum.auto() + DESERT_FESTIVAL_CHEF = enum.auto() SPECIAL_ORDER_BOARD = enum.auto() SPECIAL_ORDER_QI = enum.auto() REQUIRES_QI_ORDERS = enum.auto() + REQUIRES_MASTERIES = enum.auto() GINGER_ISLAND = enum.auto() WALNUT_PURCHASE = enum.auto() + WALNUTSANITY = enum.auto() + WALNUTSANITY_PUZZLE = enum.auto() + WALNUTSANITY_BUSH = enum.auto() + WALNUTSANITY_DIG = enum.auto() + WALNUTSANITY_REPEATABLE = enum.auto() BABY = enum.auto() MONSTERSANITY = enum.auto() @@ -87,6 +97,10 @@ class LocationTags(enum.Enum): CHEFSANITY_SKILL = enum.auto() CHEFSANITY_STARTER = enum.auto() CRAFTSANITY = enum.auto() + BOOKSANITY = enum.auto() + BOOKSANITY_POWER = enum.auto() + BOOKSANITY_SKILL = enum.auto() + BOOKSANITY_LOST = enum.auto() # Mods # Skill Mods LUCK_LEVEL = enum.auto() @@ -143,10 +157,10 @@ def load_location_csv() -> List[LocationData]: LocationData(None, Region.farm_house, Goal.full_house), LocationData(None, Region.island_west, Goal.greatest_walnut_hunter), LocationData(None, Region.adventurer_guild, Goal.protector_of_the_valley), - LocationData(None, Region.shipping, Goal.full_shipment), - LocationData(None, Region.kitchen, Goal.gourmet_chef), + LocationData(None, LogicRegion.shipping, Goal.full_shipment), + LocationData(None, LogicRegion.kitchen, Goal.gourmet_chef), LocationData(None, Region.farm, Goal.craft_master), - LocationData(None, Region.shipping, Goal.legend), + LocationData(None, LogicRegion.shipping, Goal.legend), LocationData(None, Region.farm, Goal.mystery_of_the_stardrops), LocationData(None, Region.farm, Goal.allsanity), LocationData(None, Region.qi_walnut_room, Goal.perfection), @@ -168,13 +182,13 @@ def initialize_groups(): initialize_groups() -def extend_cropsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.cropsanity == Cropsanity.option_disabled: +def extend_cropsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + cropsanity = content.features.cropsanity + if not cropsanity.is_enabled: return - cropsanity_locations = [item for item in locations_by_tag[LocationTags.CROPSANITY] if not item.mod_name or item.mod_name in options.mods] - cropsanity_locations = filter_ginger_island(options, cropsanity_locations) - randomized_locations.extend(cropsanity_locations) + randomized_locations.extend(location_table[cropsanity.to_location_name(item.name)] + for item in content.find_tagged_items(ItemTag.CROPSANITY)) def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): @@ -199,32 +213,19 @@ def extend_quests_locations(randomized_locations: List[LocationData], options: S randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) -def extend_fishsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): - prefix = "Fishsanity: " - fishsanity = options.fishsanity - active_fish = get_fish_for_mods(options.mods.value) - if fishsanity == Fishsanity.option_none: +def extend_fishsanity_locations(randomized_locations: List[LocationData], content: StardewContent, random: Random): + fishsanity = content.features.fishsanity + if not fishsanity.is_enabled: return - elif fishsanity == Fishsanity.option_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_special: - randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif fishsanity == Fishsanity.option_randomized: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if random.random() < 0.4] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_all: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if not fish.legendary] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif fishsanity == Fishsanity.option_exclude_hard_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 80] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) - elif options.fishsanity == Fishsanity.option_only_easy_fish: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 50] - randomized_locations.extend(filter_disabled_locations(options, fish_locations)) + + for fish in content.fishes.values(): + if not fishsanity.is_included(fish): + continue + + if fishsanity.is_randomized and random.random() >= fishsanity.randomization_ratio: + continue + + randomized_locations.append(location_table[fishsanity.to_location_name(fish.name)]) def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): @@ -240,38 +241,20 @@ def extend_museumsanity_locations(randomized_locations: List[LocationData], opti randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"] for museum_item in all_museum_items) -def extend_friendsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - island_villagers = [NPC.leo, ModNPC.lance] - if options.friendsanity == Friendsanity.option_none: +def extend_friendsanity_locations(randomized_locations: List[LocationData], content: StardewContent): + friendsanity = content.features.friendsanity + if not friendsanity.is_enabled: return randomized_locations.append(location_table[f"Spouse Stardrop"]) extend_baby_locations(randomized_locations) - exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors - exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \ - options.friendsanity == Friendsanity.option_bachelors - include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage - heart_size = options.friendsanity_heart_size - for villager in get_villagers_for_mods(options.mods.value): - if not villager.available and exclude_locked_villagers: - continue - if not villager.bachelor and exclude_non_bachelors: - continue - if villager.name in island_villagers and exclude_ginger_island: - continue - heart_cap = 8 if villager.bachelor else 10 - if include_post_marriage_hearts and villager.bachelor: - heart_cap = 14 - for heart in range(1, 15): - if heart > heart_cap: - break - if heart % heart_size == 0 or heart == heart_cap: - randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) - if not exclude_non_bachelors: - for heart in range(1, 6): - if heart % heart_size == 0 or heart == 5: - randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) + + for villager in content.villagers.values(): + for heart in friendsanity.get_randomized_hearts(villager): + randomized_locations.append(location_table[friendsanity.to_location_name(villager.name, heart)]) + + for heart in friendsanity.get_pet_randomized_hearts(): + randomized_locations.append(location_table[friendsanity.to_location_name(NPC.pet, heart)]) def extend_baby_locations(randomized_locations: List[LocationData]): @@ -279,16 +262,17 @@ def extend_baby_locations(randomized_locations: List[LocationData]): randomized_locations.extend(baby_locations) -def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): +def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): if options.festival_locations == FestivalLocations.option_disabled: return festival_locations = locations_by_tag[LocationTags.FESTIVAL] randomized_locations.extend(festival_locations) extend_hard_festival_locations(randomized_locations, options) + extend_desert_festival_chef_locations(randomized_locations, options, random) -def extend_hard_festival_locations(randomized_locations, options: StardewValleyOptions): +def extend_hard_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): if options.festival_locations != FestivalLocations.option_hard: return @@ -296,14 +280,20 @@ def extend_hard_festival_locations(randomized_locations, options: StardewValleyO randomized_locations.extend(hard_festival_locations) +def extend_desert_festival_chef_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random): + festival_chef_locations = locations_by_tag[LocationTags.DESERT_FESTIVAL_CHEF] + number_to_add = 5 if options.festival_locations == FestivalLocations.option_easy else 10 + locations_to_add = random.sample(festival_chef_locations, number_to_add) + randomized_locations.extend(locations_to_add) + + def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): - if options.special_order_locations == SpecialOrderLocations.option_disabled: - return + if options.special_order_locations & SpecialOrderLocations.option_board: + board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) + randomized_locations.extend(board_locations) include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false - board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) - randomized_locations.extend(board_locations) - if options.special_order_locations == SpecialOrderLocations.option_board_qi and include_island: + if options.special_order_locations & SpecialOrderLocations.value_qi and include_island: include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags] @@ -440,13 +430,43 @@ def extend_craftsanity_locations(randomized_locations: List[LocationData], optio return craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]] - filtered_chefsanity_locations = filter_disabled_locations(options, craftsanity_locations) - randomized_locations.extend(filtered_chefsanity_locations) + filtered_craftsanity_locations = filter_disabled_locations(options, craftsanity_locations) + randomized_locations.extend(filtered_craftsanity_locations) + + +def extend_book_locations(randomized_locations: List[LocationData], content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + book_locations = [] + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + book_locations.append(location_table[booksanity.to_location_name(book.name)]) + + book_locations.extend(location_table[booksanity.to_location_name(book)] for book in booksanity.get_randomized_lost_books()) + + randomized_locations.extend(book_locations) + + +def extend_walnutsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): + if not options.walnutsanity: + return + + if "Puzzles" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_PUZZLE]) + if "Bushes" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_BUSH]) + if "Dig Spots" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_DIG]) + if "Repeatables" in options.walnutsanity: + randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_REPEATABLE]) def create_locations(location_collector: StardewLocationCollector, bundle_rooms: List[BundleRoom], options: StardewValleyOptions, + content: StardewContent, random: Random): randomized_locations = [] @@ -461,8 +481,11 @@ def create_locations(location_collector: StardewLocationCollector, if not options.skill_progression == SkillProgression.option_vanilla: for location in locations_by_tag[LocationTags.SKILL_LEVEL]: - if location.mod_name is None or location.mod_name in options.mods: - randomized_locations.append(location_table[location.name]) + if location.mod_name is not None and location.mod_name not in options.mods: + continue + if LocationTags.MASTERY_LEVEL in location.tags and options.skill_progression != SkillProgression.option_progressive_with_masteries: + continue + randomized_locations.append(location_table[location.name]) if options.building_progression & BuildingProgression.option_progressive: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: @@ -475,12 +498,12 @@ def create_locations(location_collector: StardewLocationCollector, if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling: randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) - extend_cropsanity_locations(randomized_locations, options) - extend_fishsanity_locations(randomized_locations, options, random) + extend_cropsanity_locations(randomized_locations, content) + extend_fishsanity_locations(randomized_locations, content, random) extend_museumsanity_locations(randomized_locations, options, random) - extend_friendsanity_locations(randomized_locations, options) + extend_friendsanity_locations(randomized_locations, content) - extend_festival_locations(randomized_locations, options) + extend_festival_locations(randomized_locations, options, random) extend_special_order_locations(randomized_locations, options) extend_walnut_purchase_locations(randomized_locations, options) @@ -490,28 +513,47 @@ def create_locations(location_collector: StardewLocationCollector, extend_chefsanity_locations(randomized_locations, options) extend_craftsanity_locations(randomized_locations, options) extend_quests_locations(randomized_locations, options) + extend_book_locations(randomized_locations, content) + extend_walnutsanity_locations(randomized_locations, options) + + # Mods extend_situational_quest_locations(randomized_locations, options) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) +def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: + # On Meadowlands, "Feeding Animals" replaces "Raising Animals" + if options.farm_type == FarmType.option_meadowlands: + return (location for location in locations if location.name != Quest.raising_animals) + else: + return (location for location in locations if location.name != Quest.feeding_animals) + + def filter_ginger_island(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false return (location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags) def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - include_qi_orders = options.special_order_locations == SpecialOrderLocations.option_board_qi + include_qi_orders = options.special_order_locations & SpecialOrderLocations.value_qi return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags) +def filter_masteries_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: + include_masteries = options.skill_progression == SkillProgression.option_progressive_with_masteries + return (location for location in locations if include_masteries or LocationTags.REQUIRES_MASTERIES not in location.tags) + + def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: return (location for location in locations if location.mod_name is None or location.mod_name in options.mods) def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: - locations_island_filter = filter_ginger_island(options, locations) + locations_farm_filter = filter_farm_type(options, locations) + locations_island_filter = filter_ginger_island(options, locations_farm_filter) locations_qi_filter = filter_qi_order_locations(options, locations_island_filter) - locations_mod_filter = filter_modded_locations(options, locations_qi_filter) + locations_masteries_filter = filter_masteries_locations(options, locations_qi_filter) + locations_mod_filter = filter_modded_locations(options, locations_masteries_filter) return locations_mod_filter diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index ae12ffee4742..add99a2c2e7e 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -1,6 +1,7 @@ from typing import Union from .base_logic import BaseLogicMixin, BaseLogic +from .cooking_logic import CookingLogicMixin from .mine_logic import MineLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin diff --git a/worlds/stardew_valley/logic/action_logic.py b/worlds/stardew_valley/logic/action_logic.py index 820ae4ead429..dc5deda427f3 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -5,10 +5,13 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin -from ..stardew_rule import StardewRule, True_, Or +from .tool_logic import ToolLogicMixin +from ..options import ToolProgression +from ..stardew_rule import StardewRule, True_ from ..strings.generic_names import Generic from ..strings.geode_names import Geode from ..strings.region_names import Region +from ..strings.tool_names import Tool class ActionLogicMixin(BaseLogicMixin): @@ -17,7 +20,7 @@ def __init__(self, *args, **kwargs): self.action = ActionLogic(*args, **kwargs) -class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): +class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, ToolLogicMixin]]): def can_watch(self, channel: str = None): tv_rule = True_() @@ -25,16 +28,13 @@ def can_watch(self, channel: str = None): return tv_rule return self.logic.received(channel) & tv_rule - def can_pan(self) -> StardewRule: - return self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) - - def can_pan_at(self, region: str) -> StardewRule: - return self.logic.region.can_reach(region) & self.logic.action.can_pan() + def can_pan_at(self, region: str, material: str) -> StardewRule: + return self.logic.region.can_reach(region) & self.logic.tool.has_tool(Tool.pan, material) @cache_self1 def can_open_geode(self, geode: str) -> StardewRule: blacksmith_access = self.logic.region.can_reach(Region.blacksmith) geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni] if geode == Generic.any: - return blacksmith_access & Or(*(self.logic.has(geode_type) for geode_type in geodes)) + return blacksmith_access & self.logic.or_(*(self.logic.has(geode_type) for geode_type in geodes)) return blacksmith_access & self.logic.has(geode) diff --git a/worlds/stardew_valley/logic/artisan_logic.py b/worlds/stardew_valley/logic/artisan_logic.py index cdc2186d807a..23f0ae03b790 100644 --- a/worlds/stardew_valley/logic/artisan_logic.py +++ b/worlds/stardew_valley/logic/artisan_logic.py @@ -3,8 +3,13 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from .time_logic import TimeLogicMixin +from ..data.artisan import MachineSource +from ..data.game_item import ItemTag from ..stardew_rule import StardewRule -from ..strings.crop_names import all_vegetables, all_fruits, Vegetable, Fruit +from ..strings.artisan_good_names import ArtisanGood +from ..strings.crop_names import Vegetable, Fruit +from ..strings.fish_names import Fish, all_fish +from ..strings.forageable_names import Mushroom from ..strings.generic_names import Generic from ..strings.machine_names import Machine @@ -16,6 +21,10 @@ def __init__(self, *args, **kwargs): class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]): + def initialize_rules(self): + # TODO remove this one too once fish are converted to sources + self.registry.artisan_good_rules.update({ArtisanGood.specific_smoked_fish(fish): self.can_smoke(fish) for fish in all_fish}) + self.registry.artisan_good_rules.update({ArtisanGood.specific_bait(fish): self.can_bait(fish) for fish in all_fish}) def has_jelly(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Fruit.any) @@ -23,31 +32,62 @@ def has_jelly(self) -> StardewRule: def has_pickle(self) -> StardewRule: return self.logic.artisan.can_preserves_jar(Vegetable.any) + def has_smoked_fish(self) -> StardewRule: + return self.logic.artisan.can_smoke(Fish.any) + + def has_targeted_bait(self) -> StardewRule: + return self.logic.artisan.can_bait(Fish.any) + + def has_dried_fruits(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.any) + + def has_dried_mushrooms(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Mushroom.any_edible) + + def has_raisins(self) -> StardewRule: + return self.logic.artisan.can_dehydrate(Fruit.grape) + + def can_produce_from(self, source: MachineSource) -> StardewRule: + return self.logic.has(source.item) & self.logic.has(source.machine) + def can_preserves_jar(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.preserves_jar) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) - def has_wine(self) -> StardewRule: - return self.logic.artisan.can_keg(Fruit.any) - - def has_juice(self) -> StardewRule: - return self.logic.artisan.can_keg(Vegetable.any) - def can_keg(self, item: str) -> StardewRule: machine_rule = self.logic.has(Machine.keg) if item == Generic.any: return machine_rule if item == Fruit.any: - return machine_rule & self.logic.has_any(*all_fruits) + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT))) if item == Vegetable.any: - return machine_rule & self.logic.has_any(*all_vegetables) + return machine_rule & self.logic.has_any(*(vege.name for vege in self.content.find_tagged_items(ItemTag.VEGETABLE))) return machine_rule & self.logic.has(item) def can_mayonnaise(self, item: str) -> StardewRule: return self.logic.has(Machine.mayonnaise_machine) & self.logic.has(item) + + def can_smoke(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.fish_smoker) + return machine_rule & self.logic.has(item) + + def can_bait(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.bait_maker) + return machine_rule & self.logic.has(item) + + def can_dehydrate(self, item: str) -> StardewRule: + machine_rule = self.logic.has(Machine.dehydrator) + if item == Generic.any: + return machine_rule + if item == Fruit.any: + # Grapes make raisins + return machine_rule & self.logic.has_any(*(fruit.name for fruit in self.content.find_tagged_items(ItemTag.FRUIT) if fruit.name != Fruit.grape)) + if item == Mushroom.any_edible: + return machine_rule & self.logic.has_any(*(mushroom.name for mushroom in self.content.find_tagged_items(ItemTag.EDIBLE_MUSHROOM))) + return machine_rule & self.logic.has(item) diff --git a/worlds/stardew_valley/logic/base_logic.py b/worlds/stardew_valley/logic/base_logic.py index 9cfd089ea4f6..7b377fce1fcc 100644 --- a/worlds/stardew_valley/logic/base_logic.py +++ b/worlds/stardew_valley/logic/base_logic.py @@ -2,6 +2,7 @@ from typing import TypeVar, Generic, Dict, Collection +from ..content.game_content import StardewContent from ..options import StardewValleyOptions from ..stardew_rule import StardewRule @@ -10,12 +11,11 @@ class LogicRegistry: def __init__(self): self.item_rules: Dict[str, StardewRule] = {} - self.sapling_rules: Dict[str, StardewRule] = {} - self.tree_fruit_rules: Dict[str, StardewRule] = {} self.seed_rules: Dict[str, StardewRule] = {} self.cooking_rules: Dict[str, StardewRule] = {} self.crafting_rules: Dict[str, StardewRule] = {} self.crop_rules: Dict[str, StardewRule] = {} + self.artisan_good_rules: Dict[str, StardewRule] = {} self.fish_rules: Dict[str, StardewRule] = {} self.museum_rules: Dict[str, StardewRule] = {} self.festival_rules: Dict[str, StardewRule] = {} @@ -38,13 +38,15 @@ class BaseLogic(BaseLogicMixin, Generic[T]): player: int registry: LogicRegistry options: StardewValleyOptions + content: StardewContent regions: Collection[str] logic: T - def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, regions: Collection[str], logic: T): - super().__init__(player, registry, options, regions, logic) + def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, content: StardewContent, regions: Collection[str], logic: T): + super().__init__(player, registry, options, content, regions, logic) self.player = player self.registry = registry self.options = options + self.content = content self.regions = regions self.logic = logic diff --git a/worlds/stardew_valley/logic/book_logic.py b/worlds/stardew_valley/logic/book_logic.py new file mode 100644 index 000000000000..464056ee06ba --- /dev/null +++ b/worlds/stardew_valley/logic/book_logic.py @@ -0,0 +1,24 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from ..stardew_rule import StardewRule + + +class BookLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.book = BookLogic(*args, **kwargs) + + +class BookLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_book_power(self, book: str) -> StardewRule: + booksanity = self.content.features.booksanity + if booksanity.is_included(self.content.game_items[book]): + return self.logic.received(booksanity.to_item_name(book)) + else: + return self.logic.has(book) diff --git a/worlds/stardew_valley/logic/buff_logic.py b/worlds/stardew_valley/logic/buff_logic.py deleted file mode 100644 index fee9c9fc4d25..000000000000 --- a/worlds/stardew_valley/logic/buff_logic.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Union - -from .base_logic import BaseLogicMixin, BaseLogic -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule -from ..strings.ap_names.buff_names import Buff - - -class BuffLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.buff = BuffLogic(*args, **kwargs) - - -class BuffLogic(BaseLogic[Union[ReceivedLogicMixin]]): - def has_max_buffs(self) -> StardewRule: - return self.has_max_speed() & self.has_max_luck() - - def has_max_speed(self) -> StardewRule: - return self.logic.received(Buff.movement, self.options.movement_buff_number.value) - - def has_max_luck(self) -> StardewRule: - return self.logic.received(Buff.luck, self.options.luck_buff_number.value) diff --git a/worlds/stardew_valley/logic/building_logic.py b/worlds/stardew_valley/logic/building_logic.py index 7be3d19ec33b..4611eba37d64 100644 --- a/worlds/stardew_valley/logic/building_logic.py +++ b/worlds/stardew_valley/logic/building_logic.py @@ -15,6 +15,8 @@ from ..strings.material_names import Material from ..strings.metal_names import MetalBar +has_group = "building" + class BuildingLogicMixin(BaseLogicMixin): def __init__(self, *args, **kwargs): @@ -42,7 +44,7 @@ def initialize_rules(self): Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone), Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood), Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0), - Building.kids_room: self.logic.money.can_spend(50000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1), + Building.kids_room: self.logic.money.can_spend(65000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1), Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2), # @formatter:on }) @@ -60,7 +62,7 @@ def has_building(self, building: str) -> StardewRule: carpenter_rule = self.logic.received(Event.can_construct_buildings) if not self.options.building_progression & BuildingProgression.option_progressive: - return Has(building, self.registry.building_rules) & carpenter_rule + return Has(building, self.registry.building_rules, has_group) & carpenter_rule count = 1 if building in [Building.coop, Building.barn, Building.shed]: @@ -86,10 +88,10 @@ def has_house(self, upgrade_level: int) -> StardewRule: return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level) if upgrade_level == 1: - return carpenter_rule & Has(Building.kitchen, self.registry.building_rules) + return carpenter_rule & Has(Building.kitchen, self.registry.building_rules, has_group) if upgrade_level == 2: - return carpenter_rule & Has(Building.kids_room, self.registry.building_rules) + return carpenter_rule & Has(Building.kids_room, self.registry.building_rules, has_group) # if upgrade_level == 3: - return carpenter_rule & Has(Building.cellar, self.registry.building_rules) + return carpenter_rule & Has(Building.cellar, self.registry.building_rules, has_group) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 1ae07cf2ed82..4ca5fd81fc76 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -2,17 +2,22 @@ from typing import Union, List from .base_logic import BaseLogicMixin, BaseLogic -from .farming_logic import FarmingLogicMixin from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin from .money_logic import MoneyLogicMixin +from .quality_logic import QualityLogicMixin +from .quest_logic import QuestLogicMixin +from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin from ..bundles.bundle import Bundle -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.currency_names import Currency from ..strings.machine_names import Machine from ..strings.quality_names import CropQuality, ForageQuality, FishQuality, ArtisanQuality +from ..strings.quest_names import Quest from ..strings.region_names import Region @@ -22,21 +27,26 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, FarmingLogicMixin, FishingLogicMixin, SkillLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, +QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] qualities = [] + time_to_grind = 0 can_speak_junimo = self.logic.region.can_reach(Region.wizard_tower) for bundle_item in bundle.items: - if Currency.is_currency(bundle_item.item_name): - return can_speak_junimo & self.logic.money.can_trade(bundle_item.item_name, bundle_item.amount) + if Currency.is_currency(bundle_item.get_item()): + return can_speak_junimo & self.logic.money.can_trade(bundle_item.get_item(), bundle_item.amount) - item_rules.append(bundle_item.item_name) + item_rules.append(bundle_item.get_item()) + if bundle_item.amount > 50: + time_to_grind = bundle_item.amount // 50 qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - return can_speak_junimo & item_rules & quality_rules + time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: crop_quality = CropQuality.get_highest(qualities) @@ -45,7 +55,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: artisan_quality = ArtisanQuality.get_highest(qualities) quality_rules = [] if crop_quality != CropQuality.basic: - quality_rules.append(self.logic.farming.can_grow_crop_quality(crop_quality)) + quality_rules.append(self.logic.quality.can_grow_crop_quality(crop_quality)) if fish_quality != FishQuality.basic: quality_rules.append(self.logic.fishing.can_catch_quality_fish(fish_quality)) if forage_quality != ForageQuality.basic: @@ -54,7 +64,7 @@ def get_quality_rules(self, qualities: List[str]) -> StardewRule: quality_rules.append(self.logic.has(Machine.cask)) if not quality_rules: return True_() - return And(*quality_rules) + return self.logic.and_(*quality_rules) @cached_property def can_complete_community_center(self) -> StardewRule: @@ -64,3 +74,11 @@ def can_complete_community_center(self) -> StardewRule: self.logic.region.can_reach_location("Complete Bulletin Board") & self.logic.region.can_reach_location("Complete Vault") & self.logic.region.can_reach_location("Complete Boiler Room")) + + def can_access_raccoon_bundles(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + return self.logic.received(CommunityUpgrade.raccoon, 2) diff --git a/worlds/stardew_valley/logic/combat_logic.py b/worlds/stardew_valley/logic/combat_logic.py index ba825192a99e..849bf14b2203 100644 --- a/worlds/stardew_valley/logic/combat_logic.py +++ b/worlds/stardew_valley/logic/combat_logic.py @@ -3,10 +3,11 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin -from ..stardew_rule import StardewRule, Or, False_ +from ..stardew_rule import StardewRule, False_ from ..strings.ap_names.ap_weapon_names import APWeapon from ..strings.performance_names import Performance @@ -19,7 +20,7 @@ def __init__(self, *args, **kwargs): self.combat = CombatLogic(*args, **kwargs) -class CombatLogic(BaseLogic[Union[CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): +class CombatLogic(BaseLogic[Union[HasLogicMixin, CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]): @cache_self1 def can_fight_at_level(self, level: str) -> StardewRule: if level == Performance.basic: @@ -42,16 +43,20 @@ def has_any_weapon(self) -> StardewRule: @cached_property def has_decent_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 2) for weapon in valid_weapons)) @cached_property def has_good_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 3) for weapon in valid_weapons)) @cached_property def has_great_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 4) for weapon in valid_weapons)) @cached_property def has_galaxy_weapon(self) -> StardewRule: - return Or(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + return self.logic.or_(*(self.logic.received(weapon, 5) for weapon in valid_weapons)) + + @cached_property + def has_slingshot(self) -> StardewRule: + return self.logic.received(APWeapon.slingshot) diff --git a/worlds/stardew_valley/logic/cooking_logic.py b/worlds/stardew_valley/logic/cooking_logic.py index 51cc74d0517a..46f3bdc93f2f 100644 --- a/worlds/stardew_valley/logic/cooking_logic.py +++ b/worlds/stardew_valley/logic/cooking_logic.py @@ -19,8 +19,8 @@ from ..locations import locations_by_tag, LocationTags from ..options import Chefsanity from ..options import ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And -from ..strings.region_names import Region +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.region_names import LogicRegion from ..strings.skill_names import Skill from ..strings.tv_channel_names import Channel @@ -39,7 +39,7 @@ def can_cook_in_kitchen(self) -> StardewRule: # Should be cached def can_cook(self, recipe: CookingRecipe = None) -> StardewRule: - cook_rule = self.logic.region.can_reach(Region.kitchen) + cook_rule = self.logic.region.can_reach(LogicRegion.kitchen) if recipe is None: return cook_rule @@ -65,7 +65,7 @@ def knows_recipe(self, source: RecipeSource, meal_name: str) -> StardewRule: return self.logic.cooking.received_recipe(meal_name) if isinstance(source, QueenOfSauceSource) and self.options.chefsanity & Chefsanity.option_queen_of_sauce: return self.logic.cooking.received_recipe(meal_name) - if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship: + if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_purchases: return self.logic.cooking.received_recipe(meal_name) return self.logic.cooking.can_learn_recipe(source) @@ -105,4 +105,4 @@ def can_cook_everything(self) -> StardewRule: continue all_recipes_names.append(location.name[len(cooksanity_prefix):]) all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index 8c267b7d1090..e346e4ba238b 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -13,12 +13,11 @@ from .special_order_logic import SpecialOrderLogicMixin from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name -from ..data.recipe_data import StarterSource, ShopSource, SkillSource, FriendshipSource from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource from ..locations import locations_by_tag, LocationTags -from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_, And +from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression +from ..stardew_rule import StardewRule, True_, False_ from ..strings.region_names import Region @@ -58,7 +57,7 @@ def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule: if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance( recipe.source, ShopSource): return self.logic.crafting.received_recipe(recipe.item) - if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations != SpecialOrderLocations.option_disabled: + if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) return self.logic.crafting.can_learn_recipe(recipe) @@ -74,6 +73,8 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) + if isinstance(recipe.source, MasterySource): + return self.logic.skill.has_mastery(recipe.source.skill) if isinstance(recipe.source, CutsceneSource): return self.logic.region.can_reach(recipe.source.region) & self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts) if isinstance(recipe.source, FriendshipSource): @@ -81,9 +82,9 @@ def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule: if isinstance(recipe.source, QuestSource): return self.logic.quest.can_complete_quest(recipe.source.quest) if isinstance(recipe.source, SpecialOrderSource): - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.logic.special_order.can_complete_special_order(recipe.source.special_order) - return self.logic.crafting.received_recipe(recipe.item) + if self.options.special_order_locations & SpecialOrderLocations.option_board: + return self.logic.crafting.received_recipe(recipe.item) + return self.logic.special_order.can_complete_special_order(recipe.source.special_order) if isinstance(recipe.source, LogicSource): if recipe.source.logic_rule == "Cellar": return self.logic.region.can_reach(Region.cellar) @@ -99,13 +100,16 @@ def can_craft_everything(self) -> StardewRule: craftsanity_prefix = "Craft " all_recipes_names = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true + exclude_masteries = self.options.skill_progression != SkillProgression.option_progressive_with_masteries for location in locations_by_tag[LocationTags.CRAFTSANITY]: if not location.name.startswith(craftsanity_prefix): continue if exclude_island and LocationTags.GINGER_ISLAND in location.tags: continue + if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags: + continue if location.mod_name and location.mod_name not in self.options.mods: continue all_recipes_names.append(location.name[len(craftsanity_prefix):]) all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names] - return And(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) + return self.logic.and_(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes)) diff --git a/worlds/stardew_valley/logic/crop_logic.py b/worlds/stardew_valley/logic/crop_logic.py deleted file mode 100644 index 8c107ba6a5df..000000000000 --- a/worlds/stardew_valley/logic/crop_logic.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Union, Iterable - -from Utils import cache_self1 -from .base_logic import BaseLogicMixin, BaseLogic -from .has_logic import HasLogicMixin -from .money_logic import MoneyLogicMixin -from .received_logic import ReceivedLogicMixin -from .region_logic import RegionLogicMixin -from .season_logic import SeasonLogicMixin -from .tool_logic import ToolLogicMixin -from .traveling_merchant_logic import TravelingMerchantLogicMixin -from ..data import CropItem, SeedItem -from ..options import Cropsanity, ExcludeGingerIsland -from ..stardew_rule import StardewRule, True_, False_ -from ..strings.craftable_names import Craftable -from ..strings.forageable_names import Forageable -from ..strings.machine_names import Machine -from ..strings.metal_names import Fossil -from ..strings.region_names import Region -from ..strings.seed_names import Seed -from ..strings.tool_names import Tool - - -class CropLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.crop = CropLogic(*args, **kwargs) - - -class CropLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, SeasonLogicMixin, MoneyLogicMixin, - ToolLogicMixin, CropLogicMixin]]): - @cache_self1 - def can_grow(self, crop: CropItem) -> StardewRule: - season_rule = self.logic.season.has_any(crop.farm_growth_seasons) - seed_rule = self.logic.has(crop.seed.name) - farm_rule = self.logic.region.can_reach(Region.farm) & season_rule - tool_rule = self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.has_tool(Tool.watering_can) - region_rule = farm_rule | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - if crop.name == Forageable.cactus_fruit: - region_rule = self.logic.region.can_reach(Region.greenhouse) | self.logic.has(Craftable.garden_pot) - return seed_rule & region_rule & tool_rule - - def can_plant_and_grow_item(self, seasons: Union[str, Iterable[str]]) -> StardewRule: - if isinstance(seasons, str): - seasons = [seasons] - season_rule = self.logic.season.has_any(seasons) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - farm_rule = self.logic.region.can_reach(Region.farm) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm() - return season_rule & farm_rule - - def has_island_farm(self) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_false: - return self.logic.region.can_reach(Region.island_west) - return False_() - - @cache_self1 - def can_buy_seed(self, seed: SeedItem) -> StardewRule: - if seed.requires_island and self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if self.options.cropsanity == Cropsanity.option_disabled or seed.name == Seed.qi_bean: - item_rule = True_() - else: - item_rule = self.logic.received(seed.name) - if seed.name == Seed.coffee: - item_rule = item_rule & self.logic.traveling_merchant.has_days(3) - season_rule = self.logic.season.has_any(seed.seasons) - region_rule = self.logic.region.can_reach_all(seed.regions) - currency_rule = self.logic.money.can_spend(1000) - if seed.name == Seed.pineapple: - currency_rule = self.logic.has(Forageable.magma_cap) - if seed.name == Seed.taro: - currency_rule = self.logic.has(Fossil.bone_fragment) - return season_rule & region_rule & item_rule & currency_rule diff --git a/worlds/stardew_valley/logic/farming_logic.py b/worlds/stardew_valley/logic/farming_logic.py index b255aa27f785..88523bb85d8e 100644 --- a/worlds/stardew_valley/logic/farming_logic.py +++ b/worlds/stardew_valley/logic/farming_logic.py @@ -1,11 +1,27 @@ -from typing import Union +from functools import cached_property +from typing import Union, Tuple +from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .has_logic import HasLogicMixin -from .skill_logic import SkillLogicMixin -from ..stardew_rule import StardewRule, True_, False_ +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .tool_logic import ToolLogicMixin +from .. import options +from ..stardew_rule import StardewRule, True_, false_ +from ..strings.ap_names.event_names import Event from ..strings.fertilizer_names import Fertilizer -from ..strings.quality_names import CropQuality +from ..strings.region_names import Region +from ..strings.season_names import Season +from ..strings.tool_names import Tool + +farming_event_by_season = { + Season.spring: Event.spring_farming, + Season.summer: Event.summer_farming, + Season.fall: Event.fall_farming, + Season.winter: Event.winter_farming, +} class FarmingLogicMixin(BaseLogicMixin): @@ -14,7 +30,12 @@ def __init__(self, *args, **kwargs): self.farming = FarmingLogic(*args, **kwargs) -class FarmingLogic(BaseLogic[Union[HasLogicMixin, SkillLogicMixin, FarmingLogicMixin]]): +class FarmingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, FarmingLogicMixin]]): + + @cached_property + def has_farming_tools(self) -> StardewRule: + return self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.can_water(0) + def has_fertilizer(self, tier: int) -> StardewRule: if tier <= 0: return True_() @@ -25,17 +46,17 @@ def has_fertilizer(self, tier: int) -> StardewRule: if tier >= 3: return self.logic.has(Fertilizer.deluxe) - def can_grow_crop_quality(self, quality: str) -> StardewRule: - if quality == CropQuality.basic: - return True_() - if quality == CropQuality.silver: - return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) - if quality == CropQuality.gold: - return self.logic.skill.has_farming_level(10) | ( - self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( - self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( - self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) - if quality == CropQuality.iridium: - return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) - return False_() + @cache_self1 + def can_plant_and_grow_item(self, seasons: Union[str, Tuple[str]]) -> StardewRule: + if seasons == (): # indoor farming + return (self.logic.region.can_reach(Region.greenhouse) | self.logic.farming.has_island_farm()) & self.logic.farming.has_farming_tools + + if isinstance(seasons, str): + seasons = (seasons,) + + return self.logic.or_(*(self.logic.received(farming_event_by_season[season]) for season in seasons)) + + def has_island_farm(self) -> StardewRule: + if self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_false: + return self.logic.region.can_reach(Region.island_west) + return false_ diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index a7399a65d99c..539385232fd2 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -1,18 +1,21 @@ -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin -from ..data import FishItem, fish_data -from ..locations import LocationTags, locations_by_tag -from ..options import ExcludeGingerIsland, Fishsanity +from ..data import fish_data +from ..data.fish_data import FishItem +from ..options import ExcludeGingerIsland from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, False_, And +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.fish_names import SVEFish +from ..strings.machine_names import Machine from ..strings.quality_names import FishQuality from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -24,17 +27,16 @@ def __init__(self, *args, **kwargs): self.fishing = FishingLogic(*args, **kwargs) -class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class FishingLogic(BaseLogic[Union[HasLogicMixin, FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, + SkillLogicMixin]]): def can_fish_in_freshwater(self) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain)) def has_max_fishing(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 10) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 10) def can_fish_chests(self) -> StardewRule: - skill_rule = self.logic.skill.has_level(Skill.fishing, 6) - return self.logic.tool.has_fishing_rod(4) & skill_rule + return self.logic.tool.has_fishing_rod(4) & self.logic.skill.has_level(Skill.fishing, 6) def can_fish_at(self, region: str) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach(region) @@ -51,17 +53,23 @@ def can_catch_fish(self, fish: FishItem) -> StardewRule: else: difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty)) if fish.name == SVEFish.kittyfish: - item_rule = self.logic.received("Kittyfish Spell") + item_rule = self.logic.received(SVEQuestItem.kittyfish_spell) else: item_rule = True_() return quest_rule & region_rule & season_rule & difficulty_rule & item_rule + def can_catch_fish_for_fishsanity(self, fish: FishItem) -> StardewRule: + """ Rule could be different from the basic `can_catch_fish`. Imagine a fishsanity setting where you need to catch every fish with gold quality. + """ + return self.logic.fishing.can_catch_fish(fish) + def can_start_extended_family_quest(self) -> StardewRule: if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: return False_() - if self.options.special_order_locations != SpecialOrderLocations.option_board_qi: + if not self.options.special_order_locations & SpecialOrderLocations.value_qi: return False_() - return self.logic.region.can_reach(Region.qi_walnut_room) & And(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.legendary_fish)) + return (self.logic.region.can_reach(Region.qi_walnut_room) & + self.logic.and_(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.vanilla_legendary_fish))) def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: if fish_quality == FishQuality.basic: @@ -78,24 +86,27 @@ def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: def can_catch_every_fish(self) -> StardewRule: rules = [self.has_max_fishing()] - exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_extended_family = self.options.special_order_locations != SpecialOrderLocations.option_board_qi - for fish in fish_data.get_fish_for_mods(self.options.mods.value): - if exclude_island and fish in fish_data.island_fish: - continue - if exclude_extended_family and fish in fish_data.extended_family: - continue - rules.append(self.logic.fishing.can_catch_fish(fish)) - return And(*rules) - - def can_catch_every_fish_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule: - if self.options.fishsanity == Fishsanity.option_none: + + rules.extend( + self.logic.fishing.can_catch_fish(fish) + for fish in self.content.fishes.values() + ) + + return self.logic.and_(*rules) + + def can_catch_every_fish_for_fishsanity(self) -> StardewRule: + if not self.content.features.fishsanity.is_enabled: return self.can_catch_every_fish() rules = [self.has_max_fishing()] - for fishsanity_location in locations_by_tag[LocationTags.FISHSANITY]: - if fishsanity_location.name not in all_location_names_in_slot: - continue - rules.append(self.logic.region.can_reach_location(fishsanity_location.name)) - return And(*rules) + rules.extend( + self.logic.fishing.can_catch_fish_for_fishsanity(fish) + for fish in self.content.fishes.values() + if self.content.features.fishsanity.is_included(fish) + ) + + return self.logic.and_(*rules) + + def has_specific_bait(self, fish: FishItem) -> StardewRule: + return self.can_catch_fish(fish) & self.logic.has(Machine.bait_maker) diff --git a/worlds/stardew_valley/logic/grind_logic.py b/worlds/stardew_valley/logic/grind_logic.py new file mode 100644 index 000000000000..ccd8c5daccfb --- /dev/null +++ b/worlds/stardew_valley/logic/grind_logic.py @@ -0,0 +1,74 @@ +from typing import Union, TYPE_CHECKING + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +from .book_logic import BookLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .time_logic import TimeLogicMixin +from ..options import Booksanity +from ..stardew_rule import StardewRule, HasProgressionPercent +from ..strings.book_names import Book +from ..strings.craftable_names import Consumable +from ..strings.currency_names import Currency +from ..strings.fish_names import WaterChest +from ..strings.geode_names import Geode +from ..strings.tool_names import Tool + +if TYPE_CHECKING: + from .tool_logic import ToolLogicMixin +else: + ToolLogicMixin = object + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class GrindLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.grind = GrindLogic(*args, **kwargs) + + +class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMixin, BookLogicMixin, TimeLogicMixin, ToolLogicMixin]]): + + def can_grind_mystery_boxes(self, quantity: int) -> StardewRule: + mystery_box_rule = self.logic.has(Consumable.mystery_box) + book_of_mysteries_rule = self.logic.true_ \ + if self.options.booksanity == Booksanity.option_none \ + else self.logic.book.has_book_power(Book.book_of_mysteries) + # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. + time_rule = self.logic.time.has_lived_months(quantity // 14) + return self.logic.and_(mystery_box_rule, + book_of_mysteries_rule, + time_rule) + + def can_grind_artifact_troves(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(Geode.artifact_trove), + # Assuming one per month if the player does not grind it. + self.logic.time.has_lived_months(quantity)) + + def can_grind_prize_tickets(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(Currency.prize_ticket), + # Assuming two per month if the player does not grind it. + self.logic.time.has_lived_months(quantity // 2)) + + def can_grind_fishing_treasure_chests(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.has(WaterChest.fishing_chest), + # Assuming one per week if the player does not grind it. + self.logic.time.has_lived_months(quantity // 4)) + + def can_grind_artifact_spots(self, quantity: int) -> StardewRule: + return self.logic.and_(self.logic.tool.has_tool(Tool.hoe), + # Assuming twelve per month if the player does not grind it. + self.logic.time.has_lived_months(quantity // 12)) + + @cache_self1 + def can_grind_item(self, quantity: int) -> StardewRule: + if quantity <= MIN_ITEMS: + return self.logic.true_ + + quantity = min(quantity, MAX_ITEMS) + price = max(1, quantity * PERCENT_REQUIRED_FOR_MAX_ITEM // MAX_ITEMS) + return HasProgressionPercent(self.player, price) diff --git a/worlds/stardew_valley/logic/harvesting_logic.py b/worlds/stardew_valley/logic/harvesting_logic.py new file mode 100644 index 000000000000..3b4d41953ccd --- /dev/null +++ b/worlds/stardew_valley/logic/harvesting_logic.py @@ -0,0 +1,56 @@ +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ..stardew_rule import StardewRule +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade +from ..strings.region_names import Region + + +class HarvestingLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.harvesting = HarvestingLogic(*args, **kwargs) + + +class HarvestingLogic(BaseLogic[Union[HarvestingLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, +FarmingLogicMixin, TimeLogicMixin]]): + + @cached_property + def can_harvest_from_fruit_bats(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.fruit_bats) + + @cached_property + def can_harvest_from_mushroom_cave(self) -> StardewRule: + return self.logic.region.can_reach(Region.farm_cave) & self.logic.received(CommunityUpgrade.mushroom_boxes) + + @cache_self1 + def can_forage_from(self, source: ForagingSource) -> StardewRule: + seasons_rule = self.logic.season.has_any(source.seasons) + regions_rule = self.logic.region.can_reach_any(source.regions) + return seasons_rule & regions_rule + + @cache_self1 + def can_harvest_tree_from(self, source: HarvestFruitTreeSource) -> StardewRule: + # FIXME tool not required for this + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + sapling_rule = self.logic.has(source.sapling) + # Because it takes 1 month to grow the sapling + time_rule = self.logic.time.has_lived_months(1) + + return region_to_grow_rule & sapling_rule & time_rule + + @cache_self1 + def can_harvest_crop_from(self, source: HarvestCropSource) -> StardewRule: + region_to_grow_rule = self.logic.farming.can_plant_and_grow_item(source.seasons) + seed_rule = self.logic.has(source.seed) + return region_to_grow_rule & seed_rule diff --git a/worlds/stardew_valley/logic/has_logic.py b/worlds/stardew_valley/logic/has_logic.py index d92d4224d7d2..4331780dc01d 100644 --- a/worlds/stardew_valley/logic/has_logic.py +++ b/worlds/stardew_valley/logic/has_logic.py @@ -1,8 +1,11 @@ from .base_logic import BaseLogic -from ..stardew_rule import StardewRule, And, Or, Has, Count +from ..stardew_rule import StardewRule, And, Or, Has, Count, true_, false_ class HasLogicMixin(BaseLogic[None]): + true_ = true_ + false_ = false_ + # Should be cached def has(self, item: str) -> StardewRule: return Has(item, self.registry.item_rules) @@ -10,12 +13,12 @@ def has(self, item: str) -> StardewRule: def has_all(self, *items: str): assert items, "Can't have all of no items." - return And(*(self.has(item) for item in items)) + return self.logic.and_(*(self.has(item) for item in items)) def has_any(self, *items: str): assert items, "Can't have any of no items." - return Or(*(self.has(item) for item in items)) + return self.logic.or_(*(self.has(item) for item in items)) def has_n(self, *items: str, count: int): return self.count(count, *(self.has(item) for item in items)) @@ -24,6 +27,16 @@ def has_n(self, *items: str, count: int): def count(count: int, *rules: StardewRule) -> StardewRule: assert rules, "Can't create a Count conditions without rules" assert len(rules) >= count, "Count need at least as many rules as the count" + assert count > 0, "Count can't be negative" + + count -= sum(r is true_ for r in rules) + rules = list(r for r in rules if r is not true_) + + if count <= 0: + return true_ + + if len(rules) == 1: + return rules[0] if count == 1: return Or(*rules) @@ -31,4 +44,22 @@ def count(count: int, *rules: StardewRule) -> StardewRule: if count == len(rules): return And(*rules) - return Count(list(rules), count) + return Count(rules, count) + + @staticmethod + def and_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a And conditions without rules" + + if len(rules) == 1: + return rules[0] + + return And(*rules) + + @staticmethod + def or_(*rules: StardewRule) -> StardewRule: + assert rules, "Can't create a Or conditions without rules" + + if len(rules) == 1: + return rules[0] + + return Or(*rules) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index a7fcec922838..74cdaf2374e1 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,8 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import Collection +import logging +from functools import cached_property +from typing import Collection, Callable from .ability_logic import AbilityLogicMixin from .action_logic import ActionLogicMixin @@ -9,50 +10,53 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import LogicRegistry -from .buff_logic import BuffLogicMixin +from .book_logic import BookLogicMixin from .building_logic import BuildingLogicMixin from .bundle_logic import BundleLogicMixin from .combat_logic import CombatLogicMixin from .cooking_logic import CookingLogicMixin from .crafting_logic import CraftingLogicMixin -from .crop_logic import CropLogicMixin from .farming_logic import FarmingLogicMixin from .fishing_logic import FishingLogicMixin from .gift_logic import GiftLogicMixin +from .grind_logic import GrindLogicMixin +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin +from .logic_event import all_logic_events from .mine_logic import MineLogicMixin from .money_logic import MoneyLogicMixin from .monster_logic import MonsterLogicMixin from .museum_logic import MuseumLogicMixin from .pet_logic import PetLogicMixin +from .quality_logic import QualityLogicMixin from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .relationship_logic import RelationshipLogicMixin +from .requirement_logic import RequirementLogicMixin from .season_logic import SeasonLogicMixin from .shipping_logic import ShippingLogicMixin from .skill_logic import SkillLogicMixin +from .source_logic import SourceLogicMixin from .special_order_logic import SpecialOrderLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin -from ..data import all_purchasable_seeds, all_crops +from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes -from ..data.crops_data import crops_by_name -from ..data.fish_data import get_fish_for_mods from ..data.museum_data import all_museum_items from ..data.recipe_data import all_cooking_recipes from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import Cropsanity, SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, Fishsanity, Friendsanity, StardewValleyOptions -from ..stardew_rule import False_, Or, True_, And, StardewRule +from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_weapon_names import APWeapon -from ..strings.ap_names.buff_names import Buff +from ..strings.ap_names.ap_option_names import OptionName from ..strings.ap_names.community_upgrade_names import CommunityUpgrade +from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds @@ -72,10 +76,10 @@ from ..strings.ingredient_names import Ingredient from ..strings.machine_names import Machine from ..strings.material_names import Material -from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil +from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil, Artifact from ..strings.monster_drop_names import Loot from ..strings.monster_names import Monster -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion from ..strings.season_names import Season from ..strings.seed_names import Seed, TreeSeed from ..strings.skill_names import Skill @@ -83,23 +87,26 @@ from ..strings.villager_names import NPC from ..strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) -@dataclass(frozen=False, repr=False) -class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, + +class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, GiftLogicMixin, BuildingLogicMixin, ShippingLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, WalletLogicMixin, AnimalLogicMixin, - CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, CropLogicMixin, + CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, - SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin): + SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): player: int options: StardewValleyOptions + content: StardewContent regions: Collection[str] - def __init__(self, player: int, options: StardewValleyOptions, regions: Collection[str]): + def __init__(self, player: int, options: StardewValleyOptions, content: StardewContent, regions: Collection[str]): self.registry = LogicRegistry() - super().__init__(player, self.registry, options, regions, self) + super().__init__(player, self.registry, options, content, regions, self) - self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value)}) + self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in content.fishes.values()}) self.registry.museum_rules.update({donation.item_name: self.museum.can_find_museum_item(donation) for donation in all_museum_items}) for recipe in all_cooking_recipes: @@ -118,37 +125,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti can_craft_rule = can_craft_rule | self.registry.crafting_rules[recipe.item] self.registry.crafting_rules[recipe.item] = can_craft_rule - self.registry.sapling_rules.update({ - Sapling.apple: self.can_buy_sapling(Fruit.apple), - Sapling.apricot: self.can_buy_sapling(Fruit.apricot), - Sapling.cherry: self.can_buy_sapling(Fruit.cherry), - Sapling.orange: self.can_buy_sapling(Fruit.orange), - Sapling.peach: self.can_buy_sapling(Fruit.peach), - Sapling.pomegranate: self.can_buy_sapling(Fruit.pomegranate), - Sapling.banana: self.can_buy_sapling(Fruit.banana), - Sapling.mango: self.can_buy_sapling(Fruit.mango), - }) - - self.registry.tree_fruit_rules.update({ - Fruit.apple: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.apricot: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.cherry: self.crop.can_plant_and_grow_item(Season.spring), - Fruit.orange: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.peach: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.pomegranate: self.crop.can_plant_and_grow_item(Season.fall), - Fruit.banana: self.crop.can_plant_and_grow_item(Season.summer), - Fruit.mango: self.crop.can_plant_and_grow_item(Season.summer), - }) - - for tree_fruit in self.registry.tree_fruit_rules: - existing_rules = self.registry.tree_fruit_rules[tree_fruit] - sapling = f"{tree_fruit} Sapling" - self.registry.tree_fruit_rules[tree_fruit] = existing_rules & self.has(sapling) & self.time.has_lived_months(1) - - self.registry.seed_rules.update({seed.name: self.crop.can_buy_seed(seed) for seed in all_purchasable_seeds}) - self.registry.crop_rules.update({crop.name: self.crop.can_grow(crop) for crop in all_crops}) self.registry.crop_rules.update({ - Seed.coffee: (self.season.has(Season.spring) | self.season.has(Season.summer)) & self.crop.can_buy_seed(crops_by_name[Seed.coffee].seed), Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) & self.region.can_reach(Region.greenhouse) & self.has(Machine.seed_maker), }) @@ -157,6 +134,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti self.registry.item_rules.update({ "Energy Tonic": self.money.can_spend_at(Region.hospital, 1000), WaterChest.fishing_chest: self.fishing.can_fish_chests(), + WaterChest.golden_fishing_chest: self.fishing.can_fish_chests() & self.skill.has_mastery(Skill.fishing), WaterChest.treasure: self.fishing.can_fish_chests(), Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10), "Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40), @@ -197,7 +175,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat), AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow), AnimalProduct.milk: self.animal.has_animal(Animal.cow), - AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True), + AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5), AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit), AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond), AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)), @@ -218,29 +196,35 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti ArtisanGood.dinosaur_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.dinosaur_egg), ArtisanGood.duck_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.duck_egg), ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press), - ArtisanGood.green_tea: self.artisan.can_keg(Vegetable.tea_leaves), ArtisanGood.honey: self.money.can_spend_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.season.has_any_not_winter()), - ArtisanGood.jelly: self.artisan.has_jelly(), - ArtisanGood.juice: self.artisan.has_juice(), ArtisanGood.maple_syrup: self.has(Machine.tapper), ArtisanGood.mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.chicken_egg), - ArtisanGood.mead: self.artisan.can_keg(ArtisanGood.honey), + ArtisanGood.mystic_syrup: self.has(Machine.tapper) & self.has(TreeSeed.mystic), ArtisanGood.oak_resin: self.has(Machine.tapper), - ArtisanGood.pale_ale: self.artisan.can_keg(Vegetable.hops), - ArtisanGood.pickles: self.artisan.has_pickle(), ArtisanGood.pine_tar: self.has(Machine.tapper), + ArtisanGood.smoked_fish: self.artisan.has_smoked_fish(), + ArtisanGood.targeted_bait: self.artisan.has_targeted_bait(), + ArtisanGood.stardrop_tea: self.has(WaterChest.golden_fishing_chest), ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker), ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)), - ArtisanGood.wine: self.artisan.has_wine(), - Beverage.beer: self.artisan.can_keg(Vegetable.wheat) | self.money.can_spend_at(Region.saloon, 400), - Beverage.coffee: self.artisan.can_keg(Seed.coffee) | self.has(Machine.coffee_maker) | (self.money.can_spend_at(Region.saloon, 300)) | self.has("Hot Java Ring"), Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600), Beverage.triple_shot_espresso: self.has("Hot Java Ring"), + Consumable.butterfly_powder: self.money.can_spend_at(Region.sewer, 20000), + Consumable.far_away_stone: self.region.can_reach(Region.mines_floor_100) & self.has(Artifact.ancient_doll), + Consumable.fireworks_red: self.region.can_reach(Region.casino), + Consumable.fireworks_purple: self.region.can_reach(Region.casino), + Consumable.fireworks_green: self.region.can_reach(Region.casino), + Consumable.golden_animal_cracker: self.skill.has_mastery(Skill.farming), + Consumable.mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride), + Consumable.gold_mystery_box: self.received(CommunityUpgrade.mr_qi_plane_ride) & self.skill.has_mastery(Skill.foraging), + Currency.calico_egg: self.region.can_reach(LogicRegion.desert_festival), + Currency.golden_tag: self.region.can_reach(LogicRegion.trout_derby), + Currency.prize_ticket: self.time.has_lived_months(2), # Time to do a few help wanted quests Decoration.rotten_plant: self.has(Lighting.jack_o_lantern) & self.season.has(Season.winter), Fertilizer.basic: self.money.can_spend_at(Region.pierre_store, 100), Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone), - Fish.any: Or(*(self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value))), + Fish.any: self.logic.or_(*(self.fishing.can_catch_fish(fish) for fish in content.fishes.values())), Fish.crab: self.skill.can_crab_pot_at(Region.beach), Fish.crayfish: self.skill.can_crab_pot_at(Region.town), Fish.lobster: self.skill.can_crab_pot_at(Region.beach), @@ -252,44 +236,15 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Fish.snail: self.skill.can_crab_pot_at(Region.town), Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]), Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200), - Forageable.blackberry: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.cactus_fruit: self.tool.can_forage(Generic.any, Region.desert), - Forageable.cave_carrot: self.tool.can_forage(Generic.any, Region.mines_floor_10, True), - Forageable.chanterelle: self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.coconut: self.tool.can_forage(Generic.any, Region.desert), - Forageable.common_mushroom: self.tool.can_forage(Season.fall) | (self.tool.can_forage(Season.spring, Region.secret_woods)) | self.has_mushroom_cave(), - Forageable.crocus: self.tool.can_forage(Season.winter), - Forageable.crystal_fruit: self.tool.can_forage(Season.winter), - Forageable.daffodil: self.tool.can_forage(Season.spring), - Forageable.dandelion: self.tool.can_forage(Season.spring), - Forageable.dragon_tooth: self.tool.can_forage(Generic.any, Region.volcano_floor_10), - Forageable.fiddlehead_fern: self.tool.can_forage(Season.summer, Region.secret_woods), - Forageable.ginger: self.tool.can_forage(Generic.any, Region.island_west, True), - Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), - Forageable.hazelnut: self.tool.can_forage(Season.fall), - Forageable.holly: self.tool.can_forage(Season.winter), - Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.leek: self.tool.can_forage(Season.spring), - Forageable.magma_cap: self.tool.can_forage(Generic.any, Region.volcano_floor_5), - Forageable.morel: self.tool.can_forage(Season.spring, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.purple_mushroom: self.tool.can_forage(Generic.any, Region.mines_floor_95) | self.tool.can_forage(Generic.any, Region.skull_cavern_25) | self.has_mushroom_cave(), - Forageable.rainbow_shell: self.tool.can_forage(Season.summer, Region.beach), - Forageable.red_mushroom: self.tool.can_forage(Season.summer, Region.secret_woods) | self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(), - Forageable.salmonberry: self.tool.can_forage(Season.spring) | self.has_fruit_bats(), - Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), - Forageable.snow_yam: self.tool.can_forage(Season.winter, Region.beach, True), - Forageable.spice_berry: self.tool.can_forage(Season.summer) | self.has_fruit_bats(), - Forageable.spring_onion: self.tool.can_forage(Season.spring), - Forageable.sweet_pea: self.tool.can_forage(Season.summer), - Forageable.wild_horseradish: self.tool.can_forage(Season.spring), - Forageable.wild_plum: self.tool.can_forage(Season.fall) | self.has_fruit_bats(), - Forageable.winter_root: self.tool.can_forage(Season.winter, Region.forest, True), + Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe), # + Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),# + Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()), # Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton), Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe), Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe), Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut), Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site), - Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site), + Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site, ToolMaterial.copper), Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10), Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe), Fossil.snake_skull: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.hoe), @@ -299,10 +254,10 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Geode.geode: self.mine.can_mine_in_the_mines_floor_1_40(), Geode.golden_coconut: self.region.can_reach(Region.island_north), Geode.magma: self.mine.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.building.has_building(Building.fish_pond)), - Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.action.can_pan() | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10), - Gift.bouquet: self.relationship.has_hearts(Generic.bachelor, 8) & self.money.can_spend_at(Region.pierre_store, 100), + Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10), + Gift.bouquet: self.relationship.has_hearts_with_any_bachelor(8) & self.money.can_spend_at(Region.pierre_store, 100), Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove), - Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts(Generic.bachelor, 10) & self.building.has_house(1) & self.has(Consumable.rain_totem), + Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts_with_any_bachelor(10) & self.building.has_house(1) & self.has(Consumable.rain_totem), Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000), Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove), Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months, @@ -312,45 +267,27 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti Ingredient.qi_seasoning: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 10), Ingredient.rice: self.money.can_spend_at(Region.pierre_store, 200) | (self.building.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)), Ingredient.sugar: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.beet)), - Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200), + Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200) | self.artisan.can_keg(Ingredient.rice), Ingredient.wheat_flour: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.wheat)), Loot.bat_wing: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.bug_meat: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.slime: self.mine.can_mine_in_the_mines_floor_1_40(), Loot.solar_essence: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(), Loot.void_essence: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern(), - Machine.bee_house: self.skill.has_farming_level(3) & self.has(MetalBar.iron) & self.has(ArtisanGood.maple_syrup) & self.has(Material.coal) & self.has(Material.wood), - Machine.cask: self.building.has_house(3) & self.region.can_reach(Region.cellar) & self.has(Material.wood) & self.has(Material.hardwood), - Machine.cheese_press: self.skill.has_farming_level(6) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.hardwood) & self.has(MetalBar.copper), Machine.coffee_maker: self.received(Machine.coffee_maker), - Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & (self.money.can_spend_at(Region.fish_shop, 1500) | (self.has(MetalBar.iron) & self.has(Material.wood))), - Machine.furnace: self.has(Material.stone) & self.has(Ore.copper), - Machine.keg: self.skill.has_farming_level(8) & self.has(Material.wood) & self.has(MetalBar.iron) & self.has(MetalBar.copper) & self.has(ArtisanGood.oak_resin), - Machine.lightning_rod: self.skill.has_level(Skill.foraging, 6) & self.has(MetalBar.iron) & self.has(MetalBar.quartz) & self.has(Loot.bat_wing), - Machine.loom: self.skill.has_farming_level(7) & self.has(Material.wood) & self.has(Material.fiber) & self.has(ArtisanGood.pine_tar), - Machine.mayonnaise_machine: self.skill.has_farming_level(2) & self.has(Material.wood) & self.has(Material.stone) & self.has("Earth Crystal") & self.has(MetalBar.copper), - Machine.ostrich_incubator: self.received("Ostrich Incubator Recipe") & self.has(Fossil.bone_fragment) & self.has(Material.hardwood) & self.has(Material.cinder_shard), - Machine.preserves_jar: self.skill.has_farming_level(4) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.coal), - Machine.recycling_machine: self.skill.has_level(Skill.fishing, 4) & self.has(Material.wood) & self.has(Material.stone) & self.has(MetalBar.iron), - Machine.seed_maker: self.skill.has_farming_level(9) & self.has(Material.wood) & self.has(MetalBar.gold) & self.has(Material.coal), - Machine.solar_panel: self.received("Solar Panel Recipe") & self.has(MetalBar.quartz) & self.has(MetalBar.iron) & self.has(MetalBar.gold), - Machine.tapper: self.skill.has_level(Skill.foraging, 3) & self.has(Material.wood) & self.has(MetalBar.copper), - Machine.worm_bin: self.skill.has_level(Skill.fishing, 8) & self.has(Material.hardwood) & self.has(MetalBar.gold) & self.has(MetalBar.iron) & self.has(Material.fiber), + Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & self.money.can_spend_at(Region.fish_shop, 1500), Machine.enricher: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Machine.pressure_nozzle: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20), Material.cinder_shard: self.region.can_reach(Region.volcano_floor_5), Material.clay: self.region.can_reach_any((Region.farm, Region.beach, Region.quarry)) & self.tool.has_tool(Tool.hoe), - Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.action.can_pan(), + Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.tool.has_tool(Tool.pan), Material.fiber: True_(), Material.hardwood: self.tool.has_tool(Tool.axe, ToolMaterial.copper) & (self.region.can_reach(Region.secret_woods) | self.region.can_reach(Region.island_west)), + Material.moss: True_(), Material.sap: self.ability.can_chop_trees(), Material.stone: self.tool.has_tool(Tool.pickaxe), Material.wood: self.tool.has_tool(Tool.axe), - Meal.bread: self.money.can_spend_at(Region.saloon, 120), Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240), - Meal.pizza: self.money.can_spend_at(Region.saloon, 600), - Meal.salad: self.money.can_spend_at(Region.saloon, 220), - Meal.spaghetti: self.money.can_spend_at(Region.saloon, 240), Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise), MetalBar.copper: self.can_smelt(Ore.copper), MetalBar.gold: self.can_smelt(Ore.gold), @@ -358,15 +295,14 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti MetalBar.iron: self.can_smelt(Ore.iron), MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))), MetalBar.radioactive: self.can_smelt(Ore.radioactive), - Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), - Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber), - Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(), + Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), + Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.iron), + Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber) | self.tool.has_tool(Tool.pan, ToolMaterial.gold), + Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.tool.has_tool(Tool.pan, ToolMaterial.copper), Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room), RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100), RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood), - Seed.mixed: self.tool.has_tool(Tool.scythe) & self.region.can_reach_all((Region.farm, Region.forest, Region.town)), SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100), SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150), Trash.broken_cd: self.skill.can_crab_pot, @@ -380,24 +316,33 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), TreeSeed.mushroom: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 5), TreeSeed.pine: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(), - Vegetable.tea_leaves: self.has(Sapling.tea) & self.time.has_lived_months(2) & self.season.has_any_not_winter(), + TreeSeed.mossy: self.ability.can_chop_trees() & self.season.has(Season.summer), Fish.clam: self.tool.can_forage(Generic.any, Region.beach), Fish.cockle: self.tool.can_forage(Generic.any, Region.beach), - WaterItem.coral: self.tool.can_forage(Generic.any, Region.tide_pools) | self.tool.can_forage(Season.summer, Region.beach), WaterItem.green_algae: self.fishing.can_fish_in_freshwater(), - WaterItem.nautilus_shell: self.tool.can_forage(Season.winter, Region.beach), - WaterItem.sea_urchin: self.tool.can_forage(Generic.any, Region.tide_pools), + WaterItem.cave_jelly: self.fishing.can_fish_at(Region.mines_floor_100) & self.tool.has_fishing_rod(2), + WaterItem.river_jelly: self.fishing.can_fish_at(Region.town) & self.tool.has_fishing_rod(2), + WaterItem.sea_jelly: self.fishing.can_fish_at(Region.beach) & self.tool.has_fishing_rod(2), WaterItem.seaweed: self.skill.can_fish(Region.tide_pools), WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20), WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100), }) # @formatter:on + + content_rules = { + item_name: self.source.has_access_to_item(game_item) + for item_name, game_item in self.content.game_items.items() + } + + for item in set(content_rules.keys()).intersection(self.registry.item_rules.keys()): + logger.warning(f"Rule for {item} already exists in the registry, overwriting it.") + + self.registry.item_rules.update(content_rules) self.registry.item_rules.update(self.registry.fish_rules) self.registry.item_rules.update(self.registry.museum_rules) - self.registry.item_rules.update(self.registry.sapling_rules) - self.registry.item_rules.update(self.registry.tree_fruit_rules) - self.registry.item_rules.update(self.registry.seed_rules) self.registry.item_rules.update(self.registry.crop_rules) + self.artisan.initialize_rules() + self.registry.item_rules.update(self.registry.artisan_good_rules) self.registry.item_rules.update(self.mod.item.get_modded_item_rules()) self.mod.item.modify_vanilla_item_rules_with_mod_additions(self.registry.item_rules) # New regions and content means new ways to obtain old items @@ -423,7 +368,7 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti self.registry.festival_rules.update({ FestivalCheck.egg_hunt: self.can_win_egg_hunt(), FestivalCheck.strawberry_seeds: self.money.can_spend(1000), - FestivalCheck.dance: self.relationship.has_hearts(Generic.bachelor, 4), + FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4), FestivalCheck.tub_o_flowers: self.money.can_spend(2000), FestivalCheck.rarecrow_5: self.money.can_spend(2500), FestivalCheck.luau_soup: self.can_succeed_luau_soup(), @@ -457,43 +402,90 @@ def __init__(self, player: int, options: StardewValleyOptions, regions: Collecti FestivalCheck.legend_of_the_winter_star: True_(), FestivalCheck.rarecrow_3: True_(), FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(), + FestivalCheck.calico_race: True_(), + FestivalCheck.mummy_mask: True_(), + FestivalCheck.calico_statue: True_(), + FestivalCheck.emily_outfit_service: True_(), + FestivalCheck.earthy_mousse: True_(), + FestivalCheck.sweet_bean_cake: True_(), + FestivalCheck.skull_cave_casserole: True_(), + FestivalCheck.spicy_tacos: True_(), + FestivalCheck.mountain_chili: True_(), + FestivalCheck.crystal_cake: True_(), + FestivalCheck.cave_kebab: True_(), + FestivalCheck.hot_log: True_(), + FestivalCheck.sour_salad: True_(), + FestivalCheck.superfood_cake: True_(), + FestivalCheck.warrior_smoothie: True_(), + FestivalCheck.rumpled_fruit_skin: True_(), + FestivalCheck.calico_pizza: True_(), + FestivalCheck.stuffed_mushrooms: True_(), + FestivalCheck.elf_quesadilla: True_(), + FestivalCheck.nachos_of_the_desert: True_(), + FestivalCheck.cloppino: True_(), + FestivalCheck.rainforest_shrimp: True_(), + FestivalCheck.shrimp_donut: True_(), + FestivalCheck.smell_of_the_sea: True_(), + FestivalCheck.desert_gumbo: True_(), + FestivalCheck.free_cactis: True_(), + FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent), + FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50), + FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25), + FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25), + FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100), + FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]), + FestivalCheck.desert_scholar: True_(), + FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), + FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & + self.fishing.has_specific_bait(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]), + FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait), + FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait), + FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & + self.fishing.has_specific_bait(content.fishes[Fish.squid]), }) + for i in range(1, 11): + self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout]) self.special_order.initialize_rules() self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules()) - def can_buy_sapling(self, fruit: str) -> StardewRule: - sapling_prices = {Fruit.apple: 4000, Fruit.apricot: 2000, Fruit.cherry: 3400, Fruit.orange: 4000, - Fruit.peach: 6000, - Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0} - received_sapling = self.received(f"{fruit} Sapling") - if self.options.cropsanity == Cropsanity.option_disabled: - allowed_buy_sapling = True_() - else: - allowed_buy_sapling = received_sapling - can_buy_sapling = self.money.can_spend_at(Region.pierre_store, sapling_prices[fruit]) - if fruit == Fruit.banana: - can_buy_sapling = self.has_island_trader() & self.has(Forageable.dragon_tooth) - elif fruit == Fruit.mango: - can_buy_sapling = self.has_island_trader() & self.has(Fish.mussel_node) - - return allowed_buy_sapling & can_buy_sapling + def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) -> None: + for logic_event in all_logic_events: + rule = self.registry.item_rules[logic_event.item] + register_event(logic_event.name, logic_event.region, rule) + self.registry.item_rules[logic_event.item] = self.received(logic_event.name) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - def can_complete_field_office(self) -> StardewRule: + @cached_property + def can_start_field_office(self) -> StardewRule: field_office = self.region.can_reach(Region.field_office) professor_snail = self.received("Open Professor Snail Cave") - tools = self.tool.has_tool(Tool.pickaxe) & self.tool.has_tool(Tool.hoe) & self.tool.has_tool(Tool.scythe) - leg_and_snake_skull = self.has_all(Fossil.fossilized_leg, Fossil.snake_skull) - ribs_and_spine = self.has_all(Fossil.fossilized_ribs, Fossil.fossilized_spine) - skull = self.has(Fossil.fossilized_skull) - tail = self.has(Fossil.fossilized_tail) - frog = self.has(Fossil.mummified_frog) - bat = self.has(Fossil.mummified_bat) - snake_vertebrae = self.has(Fossil.snake_vertebrae) - return field_office & professor_snail & tools & leg_and_snake_skull & ribs_and_spine & skull & tail & frog & bat & snake_vertebrae + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa @@ -511,9 +503,9 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: # Catching every fish not expected # Shipping every item not expected self.relationship.can_get_married() & self.building.has_house(2), - self.relationship.has_hearts("5", 8), # 5 Friends - self.relationship.has_hearts("10", 8), # 10 friends - self.pet.has_hearts(5), # Max Pet + self.relationship.has_hearts_with_n(5, 8), # 5 Friends + self.relationship.has_hearts_with_n(10, 8), # 10 friends + self.pet.has_pet_hearts(5), # Max Pet self.bundle.can_complete_community_center, # Community Center Completion self.bundle.can_complete_community_center, # CC Ceremony first point self.bundle.can_complete_community_center, # CC Ceremony second point @@ -523,41 +515,44 @@ def can_finish_grandpa_evaluation(self) -> StardewRule: return self.count(12, *rules_worth_a_point) def can_win_egg_hunt(self) -> StardewRule: - number_of_movement_buffs = self.options.movement_buff_number - if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2: - return True_() - return self.received(Buff.movement, number_of_movement_buffs // 2) + return True_() def can_succeed_luau_soup(self) -> StardewRule: if self.options.festival_locations != FestivalLocations.option_hard: return True_() - eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, Fish.mutant_carp, - Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"] - fish_rule = self.has_any(*eligible_fish) - eligible_kegables = [Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, + eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, + Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber) + fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray + eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon, Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry, Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum, - Vegetable.hops, Vegetable.wheat] - keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables] - aged_rule = self.has(Machine.cask) & Or(*keg_rules) + Vegetable.hops, Vegetable.wheat) + keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items] + aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules) # There are a few other valid items, but I don't feel like coding them all return fish_rule | aged_rule def can_succeed_grange_display(self) -> StardewRule: if self.options.festival_locations != FestivalLocations.option_hard: return True_() - + animal_rule = self.animal.has_animal(Generic.any) artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any) cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough fish_rule = self.skill.can_fish(difficulty=50) forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough - good_fruits = [Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, - Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit, ] + good_fruits = (fruit + for fruit in + (Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, + Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit) + if fruit in self.content.game_items) fruit_rule = self.has_any(*good_fruits) - good_vegetables = [Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, - Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin] + good_vegetables = (vegeteable + for vegeteable in + (Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, + Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin) + if vegeteable in self.content.game_items) vegetable_rule = self.has_any(*good_vegetables) return animal_rule & artisan_rule & cooking_rule & fish_rule & \ @@ -576,6 +571,44 @@ def has_walnut(self, number: int) -> StardewRule: return False_() if number <= 0: return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if OptionName.walnutsanity_bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations reach_south = self.region.can_reach(Region.island_south) reach_north = self.region.can_reach(Region.island_north) @@ -584,28 +617,28 @@ def has_walnut(self, number: int) -> StardewRule: reach_southeast = self.region.can_reach(Region.island_south_east) reach_field_office = self.region.can_reach(Region.field_office) reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) reach_volcano_regions = [self.region.can_reach(Region.volcano), self.region.can_reach(Region.volcano_secret_beach), self.region.can_reach(Region.volcano_floor_5), self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = Or(*reach_volcano_regions) - reach_all_volcano = And(*reach_volcano_regions) + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = And(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.received(APWeapon.slingshot)) - reach_entire_island = And(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + reach_caves = self.logic.and_(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), + self.region.can_reach(Region.gourmand_frog_cave), + self.region.can_reach(Region.colored_crystals_cave), + self.region.can_reach(Region.shipwreck), self.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) if number <= 5: - return Or(reach_south, reach_north, reach_west, reach_volcano) + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) if number <= 10: return self.count(2, *reach_walnut_regions) if number <= 15: return self.count(3, *reach_walnut_regions) if number <= 20: - return And(*reach_walnut_regions) + return self.logic.and_(*reach_walnut_regions) if number <= 50: return reach_entire_island gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) @@ -621,20 +654,22 @@ def has_all_stardrops(self) -> StardewRule: number_of_stardrops_to_receive += 1 # Museum Stardrop number_of_stardrops_to_receive += 1 # Krobus Stardrop - if self.options.fishsanity == Fishsanity.option_none: # Master Angler Stardrop - other_rules.append(self.fishing.can_catch_every_fish()) - else: + # Master Angler Stardrop + if self.content.features.fishsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.fishing.can_catch_every_fish()) if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop other_rules.append(self.season.has(Season.fall)) else: number_of_stardrops_to_receive += 1 - if self.options.friendsanity == Friendsanity.option_none: # Spouse Stardrop - other_rules.append(self.relationship.has_hearts(Generic.bachelor, 13)) - else: + # Spouse Stardrop + if self.content.features.friendsanity.is_enabled: number_of_stardrops_to_receive += 1 + else: + other_rules.append(self.relationship.has_hearts_with_any_bachelor(13)) if ModNames.deepwoods in self.options.mods: # Petting the Unicorn number_of_stardrops_to_receive += 1 @@ -642,18 +677,13 @@ def has_all_stardrops(self) -> StardewRule: if not other_rules: return self.received("Stardrop", number_of_stardrops_to_receive) - return self.received("Stardrop", number_of_stardrops_to_receive) & And(*other_rules) - - def has_prismatic_jelly_reward_access(self) -> StardewRule: - if self.options.special_order_locations == SpecialOrderLocations.option_disabled: - return self.special_order.can_complete_special_order("Prismatic Jelly") - return self.received("Monster Musk Recipe") + return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules) def has_all_rarecrows(self) -> StardewRule: rules = [] for rarecrow_number in range(1, 9): rules.append(self.received(f"Rarecrow #{rarecrow_number}")) - return And(*rules) + return self.logic.and_(*rules) def has_abandoned_jojamart(self) -> StardewRule: return self.received(CommunityUpgrade.movie_theater, 1) @@ -664,11 +694,5 @@ def has_movie_theater(self) -> StardewRule: def can_use_obelisk(self, obelisk: str) -> StardewRule: return self.region.can_reach(Region.farm) & self.received(obelisk) - def has_fruit_bats(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.fruit_bats) - - def has_mushroom_cave(self) -> StardewRule: - return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.mushroom_boxes) - def can_fish_pond(self, fish: str) -> StardewRule: return self.building.has_building(Building.fish_pond) & self.has(fish) diff --git a/worlds/stardew_valley/logic/logic_and_mods_design.md b/worlds/stardew_valley/logic/logic_and_mods_design.md index 14716e1af0e1..87631175b391 100644 --- a/worlds/stardew_valley/logic/logic_and_mods_design.md +++ b/worlds/stardew_valley/logic/logic_and_mods_design.md @@ -55,4 +55,21 @@ dependencies. Vanilla would always be first, then anything that depends only on 3. In `create_items`, AP items are unpacked, and randomized. 4. In `set_rules`, the rules are applied to the AP entrances and locations. Each content pack have to apply the proper rules for their entrances and locations. - (idea) To begin this step, sphere 0 could be simplified instantly as sphere 0 regions and items are already known. -5. Nothing to do in `generate_basic`. +5. Nothing to do in `generate_basic`. + +## Item Sources + +Instead of containing rules directly, items would contain sources that would then be transformed into rules. Using a single dispatch mechanism, the sources will +be associated to their actual logic. + +This system is extensible and easily maintainable in the ways that it decouple the rule and the actual items. Any "type" of item could be used with any "type" +of source (Monster drop and fish can have foraging sources). + +- Mods requiring special rules can remove sources from vanilla content or wrap them to add their own logic (Magic add sources for some items), or change the + rules for monster drop sources. +- (idea) A certain difficulty level (or maybe tags) could be added to the source, to enable or disable them given settings chosen by the player. Someone with a + high grinding tolerance can enable "hard" or "grindy" sources. Some source that are pushed back in further spheres can be replaced by less forgiving sources + if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood). + +Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An +item with a foraging source is a forageable. Items can fit in multiple categories. diff --git a/worlds/stardew_valley/logic/logic_event.py b/worlds/stardew_valley/logic/logic_event.py new file mode 100644 index 000000000000..9af1d622578f --- /dev/null +++ b/worlds/stardew_valley/logic/logic_event.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass + +from ..strings.ap_names import event_names +from ..strings.metal_names import MetalBar, Ore +from ..strings.region_names import Region + +all_events = event_names.all_events.copy() +all_logic_events = list() + + +@dataclass(frozen=True) +class LogicEvent: + name: str + region: str + + +@dataclass(frozen=True) +class LogicItemEvent(LogicEvent): + item: str + + def __init__(self, item: str, region: str): + super().__init__(f"{item} (Logic event)", region) + super().__setattr__("item", item) + + +def register_item_event(item: str, region: str = Region.farm): + event = LogicItemEvent(item, region) + all_logic_events.append(event) + all_events.add(event.name) + + +for i in (MetalBar.copper, MetalBar.iron, MetalBar.gold, MetalBar.iridium, Ore.copper, Ore.iron, Ore.gold, Ore.iridium): + register_item_event(i) diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py index 2c2eaabfd8ee..61eba41ffe07 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -3,13 +3,15 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .cooking_logic import CookingLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin from .. import options from ..options import ToolProgression -from ..stardew_rule import StardewRule, And, True_ +from ..stardew_rule import StardewRule, True_ from ..strings.performance_names import Performance from ..strings.region_names import Region from ..strings.skill_names import Skill @@ -22,7 +24,8 @@ def __init__(self, *args, **kwargs): self.mine = MineLogic(*args, **kwargs) -class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, SkillLogicMixin]]): +class MineLogic(BaseLogic[Union[HasLogicMixin, MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, +SkillLogicMixin, CookingLogicMixin]]): # Regions def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: return self.logic.region.can_reach(Region.mines_floor_5) @@ -57,11 +60,13 @@ def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: rules.append(weapon_rule) if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: skill_tier = min(10, max(0, tier * 2)) rules.append(self.logic.skill.has_level(Skill.combat, skill_tier)) rules.append(self.logic.skill.has_level(Skill.mining, skill_tier)) - return And(*rules) + if tier >= 4: + rules.append(self.logic.cooking.can_cook()) + return self.logic.and_(*rules) @cache_self1 def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: @@ -79,8 +84,8 @@ def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule rules.append(weapon_rule) if self.options.tool_progression & ToolProgression.option_progressive: rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: skill_tier = min(10, max(0, tier * 2 + 6)) rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier), self.logic.skill.has_level(Skill.mining, skill_tier)}) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/money_logic.py b/worlds/stardew_valley/logic/money_logic.py index 92945a3636a8..73c5291af082 100644 --- a/worlds/stardew_valley/logic/money_logic.py +++ b/worlds/stardew_valley/logic/money_logic.py @@ -2,16 +2,18 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin +from .grind_logic import GrindLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin +from ..data.shop import ShopSource from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_ +from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_ from ..strings.ap_names.event_names import Event from ..strings.currency_names import Currency -from ..strings.region_names import Region +from ..strings.region_names import Region, LogicRegion qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems", "20 Qi Gems", "15 Qi Gems", "10 Qi Gems") @@ -23,7 +25,8 @@ def __init__(self, *args, **kwargs): self.money = MoneyLogic(*args, **kwargs) -class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, BuffLogicMixin]]): +class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin, +GrindLogicMixin]]): @cache_self1 def can_have_earned_total(self, amount: int) -> StardewRule: @@ -31,7 +34,7 @@ def can_have_earned_total(self, amount: int) -> StardewRule: return True_() pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest)) - willy_rule = self.logic.region.can_reach_all((Region.fish_shop, Region.fishing)) + willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing)) clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5)) robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods)) shipping_rule = self.logic.received(Event.can_ship_items) @@ -64,6 +67,20 @@ def can_spend(self, amount: int) -> StardewRule: def can_spend_at(self, region: str, amount: int) -> StardewRule: return self.logic.region.can_reach(region) & self.logic.money.can_spend(amount) + @cache_self1 + def can_shop_from(self, source: ShopSource) -> StardewRule: + season_rule = self.logic.season.has_any(source.seasons) + money_rule = self.logic.money.can_spend(source.money_price) if source.money_price is not None else true_ + + item_rules = [] + if source.items_price is not None: + for price, item in source.items_price: + item_rules.append(self.logic.has(item) & self.logic.grind.can_grind_item(price)) + + region_rule = self.logic.region.can_reach(source.shop_region) + + return self.logic.and_(season_rule, money_rule, *item_rules, region_rule) + # Should be cached def can_trade(self, currency: str, amount: int) -> StardewRule: if amount == 0: @@ -71,11 +88,11 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.money: return self.can_spend(amount) if currency == Currency.star_token: - return self.logic.region.can_reach(Region.fair) + return self.logic.region.can_reach(LogicRegion.fair) if currency == Currency.qi_coin: - return self.logic.region.can_reach(Region.casino) & self.logic.buff.has_max_luck() + return self.logic.region.can_reach(Region.casino) & self.logic.time.has_lived_months(amount // 1000) if currency == Currency.qi_gem: - if self.options.special_order_locations == SpecialOrderLocations.option_board_qi: + if self.options.special_order_locations & SpecialOrderLocations.value_qi: number_rewards = min(len(qi_gem_rewards), max(1, (amount // 10))) return self.logic.received_n(*qi_gem_rewards, count=number_rewards) number_rewards = 2 @@ -84,7 +101,7 @@ def can_trade(self, currency: str, amount: int) -> StardewRule: if currency == Currency.golden_walnut: return self.can_spend_walnut(amount) - return self.logic.has(currency) & self.logic.time.has_lived_months(amount) + return self.logic.has(currency) & self.logic.grind.can_grind_item(amount) # Should be cached def can_trade_at(self, region: str, currency: str, amount: int) -> StardewRule: diff --git a/worlds/stardew_valley/logic/monster_logic.py b/worlds/stardew_valley/logic/monster_logic.py index 790f492347e6..7e6d786972ac 100644 --- a/worlds/stardew_valley/logic/monster_logic.py +++ b/worlds/stardew_valley/logic/monster_logic.py @@ -4,11 +4,13 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin, MAX_MONTHS from .. import options from ..data import monster_data -from ..stardew_rule import StardewRule, Or, And +from ..stardew_rule import StardewRule +from ..strings.generic_names import Generic from ..strings.region_names import Region @@ -18,7 +20,7 @@ def __init__(self, *args, **kwargs): self.monster = MonsterLogic(*args, **kwargs) -class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): +class MonsterLogic(BaseLogic[Union[HasLogicMixin, MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]): @cached_property def all_monsters_by_name(self): @@ -29,13 +31,18 @@ def all_monsters_by_category(self): return monster_data.all_monsters_by_category_given_mods(self.options.mods.value) def can_kill(self, monster: Union[str, monster_data.StardewMonster], amount_tier: int = 0) -> StardewRule: + if amount_tier <= 0: + amount_tier = 0 + time_rule = self.logic.time.has_lived_months(amount_tier) + if isinstance(monster, str): + if monster == Generic.any: + return self.logic.monster.can_kill_any(self.all_monsters_by_name.values()) & time_rule + monster = self.all_monsters_by_name[monster] region_rule = self.logic.region.can_reach_any(monster.locations) combat_rule = self.logic.combat.can_fight_at_level(monster.difficulty) - if amount_tier <= 0: - amount_tier = 0 - time_rule = self.logic.time.has_lived_months(amount_tier) + return region_rule & combat_rule & time_rule @cache_self1 @@ -48,13 +55,11 @@ def can_kill_max(self, monster: monster_data.StardewMonster) -> StardewRule: # Should be cached def can_kill_any(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return Or(*rules) + return self.logic.or_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) # Should be cached def can_kill_all(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule: - rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters] - return And(*rules) + return self.logic.and_(*(self.logic.monster.can_kill(monster, amount_tier) for monster in monsters)) def can_complete_all_monster_slaying_goals(self) -> StardewRule: rules = [self.logic.time.has_lived_max_months] @@ -66,4 +71,4 @@ def can_complete_all_monster_slaying_goals(self) -> StardewRule: continue rules.append(self.logic.monster.can_kill_any(self.all_monsters_by_category[category])) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 59ef0f6499c1..4ba5364f5524 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -6,10 +6,14 @@ from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin from .. import options from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals -from ..stardew_rule import StardewRule, And, False_ +from ..stardew_rule import StardewRule, False_ +from ..strings.metal_names import Mineral from ..strings.region_names import Region +from ..strings.tool_names import Tool, ToolMaterial class MuseumLogicMixin(BaseLogicMixin): @@ -18,7 +22,7 @@ def __init__(self, *args, **kwargs): self.museum = MuseumLogic(*args, **kwargs) -class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, ActionLogicMixin, MuseumLogicMixin]]): +class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, ActionLogicMixin, ToolLogicMixin, MuseumLogicMixin]]): def can_donate_museum_items(self, number: int) -> StardewRule: return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number) @@ -33,15 +37,16 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: region_rule = False_() if item.geodes: - geodes_rule = And(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) + geodes_rule = self.logic.and_(*(self.logic.action.can_open_geode(geode) for geode in item.geodes)) else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - # extra_rule = True_() + time_needed_to_grind = (20 - item.difficulty) / 2 + time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() - if item.item_name == "Earth Crystal" or item.item_name == "Fire Quartz" or item.item_name == "Frozen Tear": - pan_rule = self.logic.action.can_pan() - return pan_rule | region_rule | geodes_rule # & monster_rule & extra_rule + if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: + pan_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) + return (pan_rule | region_rule | geodes_rule) & time_rule # & monster_rule & extra_rule def can_find_museum_artifacts(self, number: int) -> StardewRule: rules = [] @@ -74,7 +79,7 @@ def can_complete_museum(self) -> StardewRule: for donation in all_museum_items: rules.append(self.logic.museum.can_find_museum_item(donation)) - return And(*rules) & self.logic.region.can_reach(Region.museum) + return self.logic.and_(*rules) & self.logic.region.can_reach(Region.museum) def can_donate(self, item: str) -> StardewRule: return self.logic.has(item) & self.logic.region.can_reach(Region.museum) diff --git a/worlds/stardew_valley/logic/pet_logic.py b/worlds/stardew_valley/logic/pet_logic.py index 5d7d79a358ca..0438940a6633 100644 --- a/worlds/stardew_valley/logic/pet_logic.py +++ b/worlds/stardew_valley/logic/pet_logic.py @@ -6,11 +6,9 @@ from .region_logic import RegionLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..data.villagers_data import Villager -from ..options import Friendsanity +from ..content.feature.friendsanity import pet_heart_item_name from ..stardew_rule import StardewRule, True_ from ..strings.region_names import Region -from ..strings.villager_names import NPC class PetLogicMixin(BaseLogicMixin): @@ -20,21 +18,25 @@ def __init__(self, *args, **kwargs): class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]): - def has_hearts(self, hearts: int = 1) -> StardewRule: - if hearts <= 0: + def has_pet_hearts(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none or self.options.friendsanity == Friendsanity.option_bachelors: - return self.can_befriend_pet(hearts) - return self.received_hearts(NPC.pet, hearts) - def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule: - if isinstance(npc, Villager): - return self.received_hearts(npc.name, hearts) - return self.logic.received(self.heart(npc), math.ceil(hearts / self.options.friendsanity_heart_size)) + if self.content.features.friendsanity.is_pet_randomized: + return self.received_pet_hearts(hearts) + + return self.can_befriend_pet(hearts) + + def received_pet_hearts(self, hearts: int) -> StardewRule: + return self.logic.received(pet_heart_item_name, + math.ceil(hearts / self.content.features.friendsanity.heart_size)) def can_befriend_pet(self, hearts: int) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, "You can't have negative hearts with a pet." + if hearts == 0: return True_() + points = hearts * 200 points_per_month = 12 * 14 points_per_water_month = 18 * 14 @@ -43,8 +45,3 @@ def can_befriend_pet(self, hearts: int) -> StardewRule: time_without_water_rule = self.logic.time.has_lived_months(points // points_per_month) time_rule = time_with_water_rule | time_without_water_rule return farm_rule & time_rule - - def heart(self, npc: Union[str, Villager]) -> str: - if isinstance(npc, str): - return f"{npc} <3" - return self.heart(npc.name) diff --git a/worlds/stardew_valley/logic/quality_logic.py b/worlds/stardew_valley/logic/quality_logic.py new file mode 100644 index 000000000000..54e2d242654b --- /dev/null +++ b/worlds/stardew_valley/logic/quality_logic.py @@ -0,0 +1,33 @@ +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogicMixin, BaseLogic +from .farming_logic import FarmingLogicMixin +from .skill_logic import SkillLogicMixin +from ..stardew_rule import StardewRule, True_, False_ +from ..strings.quality_names import CropQuality + + +class QualityLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.quality = QualityLogic(*args, **kwargs) + + +class QualityLogic(BaseLogic[Union[SkillLogicMixin, FarmingLogicMixin]]): + + @cache_self1 + def can_grow_crop_quality(self, quality: str) -> StardewRule: + if quality == CropQuality.basic: + return True_() + if quality == CropQuality.silver: + return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3) + if quality == CropQuality.gold: + return self.logic.skill.has_farming_level(10) | ( + self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | ( + self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | ( + self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2)) + if quality == CropQuality.iridium: + return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4) + return False_() diff --git a/worlds/stardew_valley/logic/quest_logic.py b/worlds/stardew_valley/logic/quest_logic.py index bc1f731429c6..42f401b96025 100644 --- a/worlds/stardew_valley/logic/quest_logic.py +++ b/worlds/stardew_valley/logic/quest_logic.py @@ -17,6 +17,7 @@ from .tool_logic import ToolLogicMixin from .wallet_logic import WalletLogicMixin from ..stardew_rule import StardewRule, Has, True_ +from ..strings.ap_names.community_upgrade_names import CommunityUpgrade from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building from ..strings.craftable_names import Craftable @@ -43,7 +44,8 @@ def __init__(self, *args, **kwargs): class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMixin, MineLogicMixin, RegionLogicMixin, RelationshipLogicMixin, ToolLogicMixin, -FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, BuildingLogicMixin, TimeLogicMixin]]): + FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, + BuildingLogicMixin, TimeLogicMixin]]): def initialize_rules(self): self.update_rules({ @@ -52,6 +54,7 @@ def initialize_rules(self): Quest.getting_started: self.logic.has(Vegetable.parsnip), Quest.to_the_beach: self.logic.region.can_reach(Region.beach), Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop), + Quest.feeding_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.silo), Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow), Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(), Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)), @@ -63,7 +66,8 @@ def initialize_rules(self): Quest.jodis_request: self.logic.season.has(Season.spring) & self.logic.has(Vegetable.cauliflower) & self.logic.relationship.can_meet(NPC.jodi), Quest.mayors_shorts: self.logic.season.has(Season.summer) & self.logic.relationship.has_hearts(NPC.marnie, 2) & self.logic.relationship.can_meet(NPC.lewis), - Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus), + Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus) & self.logic.region.can_reach( + Region.tunnel_entrance), Quest.marnies_request: self.logic.relationship.has_hearts(NPC.marnie, 3) & self.logic.has(Forageable.cave_carrot), Quest.pam_is_thirsty: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.pale_ale) & self.logic.relationship.can_meet(NPC.pam), Quest.a_dark_reagent: self.logic.season.has(Season.winter) & self.logic.has(Loot.void_essence) & self.logic.relationship.can_meet(NPC.wizard), @@ -104,13 +108,14 @@ def initialize_rules(self): Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) & self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) & self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy), + Quest.giant_stump: self.logic.has(Material.hardwood) }) def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.quest_rules.update(new_rules) def can_complete_quest(self, quest: str) -> StardewRule: - return Has(quest, self.registry.quest_rules) + return Has(quest, self.registry.quest_rules, "quest") def has_club_card(self) -> StardewRule: if self.options.quest_locations < 0: @@ -126,3 +131,12 @@ def has_dark_talisman(self) -> StardewRule: if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(Quest.dark_talisman) return self.logic.received(Wallet.dark_talisman) + + def has_raccoon_shop(self) -> StardewRule: + if self.options.quest_locations < 0: + return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump) + + # 1 - Break the tree + # 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off + # 3 - Raccoon's wife opens the shop + return self.logic.received(CommunityUpgrade.raccoon, 3) diff --git a/worlds/stardew_valley/logic/received_logic.py b/worlds/stardew_valley/logic/received_logic.py index 66dc078ad46f..f5c5c9f7a206 100644 --- a/worlds/stardew_valley/logic/received_logic.py +++ b/worlds/stardew_valley/logic/received_logic.py @@ -1,26 +1,32 @@ from typing import Optional +from BaseClasses import ItemClassification from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin -from ..stardew_rule import StardewRule, Received, And, Or, TotalReceived +from .logic_event import all_events +from ..items import item_table +from ..stardew_rule import StardewRule, Received, TotalReceived class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin): - # Should be cached def received(self, item: str, count: Optional[int] = 1) -> StardewRule: assert count >= 0, "Can't receive a negative amount of item." + if item in all_events: + return Received(item, self.player, count, event=True) + + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" return Received(item, self.player, count) def received_all(self, *items: str): assert items, "Can't receive all of no items." - return And(*(self.received(item) for item in items)) + return self.logic.and_(*(self.received(item) for item in items)) def received_any(self, *items: str): assert items, "Can't receive any of no items." - return Or(*(self.received(item) for item in items)) + return self.logic.or_(*(self.received(item) for item in items)) def received_once(self, *items: str, count: int): assert items, "Can't receive once of no items." @@ -32,4 +38,7 @@ def received_n(self, *items: str, count: int): assert items, "Can't receive n of no items." assert count >= 0, "Can't receive a negative amount of item." + for item in items: + assert item_table[item].classification & ItemClassification.progression, f"Item [{item_table[item].name}] has to be progression to be used in logic" + return TotalReceived(count, items, self.player) diff --git a/worlds/stardew_valley/logic/region_logic.py b/worlds/stardew_valley/logic/region_logic.py index 81dabf45aac5..69afa624f22c 100644 --- a/worlds/stardew_valley/logic/region_logic.py +++ b/worlds/stardew_valley/logic/region_logic.py @@ -4,7 +4,7 @@ from .base_logic import BaseLogic, BaseLogicMixin from .has_logic import HasLogicMixin from ..options import EntranceRandomization -from ..stardew_rule import StardewRule, And, Or, Reach, false_, true_ +from ..stardew_rule import StardewRule, Reach, false_, true_ from ..strings.region_names import Region main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest, @@ -18,6 +18,7 @@ always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er, EntranceRandomization.option_pelican_town: always_accessible_regions_without_er, EntranceRandomization.option_non_progression: always_accessible_regions_without_er, + EntranceRandomization.option_buildings_without_house: main_outside_area, EntranceRandomization.option_buildings: main_outside_area, EntranceRandomization.option_chaos: always_accessible_regions_without_er} @@ -42,11 +43,14 @@ def can_reach(self, region_name: str) -> StardewRule: @cache_self1 def can_reach_any(self, region_names: Tuple[str, ...]) -> StardewRule: - return Or(*(self.logic.region.can_reach(spot) for spot in region_names)) + if any(r in always_regions_by_setting[self.options.entrance_randomization] for r in region_names): + return true_ + + return self.logic.or_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all(self, region_names: Tuple[str, ...]) -> StardewRule: - return And(*(self.logic.region.can_reach(spot) for spot in region_names)) + return self.logic.and_(*(self.logic.region.can_reach(spot) for spot in region_names)) @cache_self1 def can_reach_all_except_one(self, region_names: Tuple[str, ...]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/relationship_logic.py b/worlds/stardew_valley/logic/relationship_logic.py index fb0267bddb1a..61e63a90c83a 100644 --- a/worlds/stardew_valley/logic/relationship_logic.py +++ b/worlds/stardew_valley/logic/relationship_logic.py @@ -1,6 +1,5 @@ import math -from functools import cached_property -from typing import Union, List +from typing import Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin @@ -11,9 +10,9 @@ from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from .time_logic import TimeLogicMixin -from ..data.villagers_data import all_villagers_by_name, Villager, get_villagers_for_mods -from ..options import Friendsanity -from ..stardew_rule import StardewRule, True_, And, Or +from ..content.feature import friendsanity +from ..data.villagers_data import Villager +from ..stardew_rule import StardewRule, True_, false_, true_ from ..strings.ap_names.mods.mod_items import SVEQuestItem from ..strings.crop_names import Fruit from ..strings.generic_names import Generic @@ -38,12 +37,8 @@ def __init__(self, *args, **kwargs): self.relationship = RelationshipLogic(*args, **kwargs) -class RelationshipLogic(BaseLogic[Union[ - RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]): - - @cached_property - def all_villagers_given_mods(self) -> List[Villager]: - return get_villagers_for_mods(self.options.mods.value) +class RelationshipLogic(BaseLogic[Union[RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, +ReceivedLogicMixin, HasLogicMixin]]): def can_date(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet) @@ -52,134 +47,160 @@ def can_marry(self, npc: str) -> StardewRule: return self.logic.relationship.has_hearts(npc, 10) & self.logic.has(Gift.mermaid_pendant) def can_get_married(self) -> StardewRule: - return self.logic.relationship.has_hearts(Generic.bachelor, 10) & self.logic.has(Gift.mermaid_pendant) + return self.logic.relationship.has_hearts_with_any_bachelor(10) & self.logic.has(Gift.mermaid_pendant) def has_children(self, number_children: int) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: + + if not self.content.features.friendsanity.is_enabled: return self.logic.relationship.can_reproduce(number_children) + return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2) def can_reproduce(self, number_children: int = 1) -> StardewRule: - if number_children <= 0: + assert number_children >= 0, "Can't have a negative amount of children." + if number_children == 0: return True_() - baby_rules = [self.logic.relationship.can_get_married(), self.logic.building.has_house(2), self.logic.relationship.has_hearts(Generic.bachelor, 12), + + baby_rules = [self.logic.relationship.can_get_married(), + self.logic.building.has_house(2), + self.logic.relationship.has_hearts_with_any_bachelor(12), self.logic.relationship.has_children(number_children - 1)] - return And(*baby_rules) - # Should be cached - def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: - if hearts <= 0: + return self.logic.and_(*baby_rules) + + @cache_self1 + def has_hearts_with_any_bachelor(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any bachelor." + if hearts == 0: return True_() - if self.options.friendsanity == Friendsanity.option_none: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if npc not in all_villagers_by_name: - if npc == Generic.any or npc == Generic.bachelor: - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - if npc == Generic.any or all_villagers_by_name[name].bachelor: - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return Or(*possible_friends) - if npc == Generic.all: - mandatory_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - mandatory_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return And(*mandatory_friends) - if npc.isnumeric(): - possible_friends = [] - for name in all_villagers_by_name: - if not self.npc_is_in_current_slot(name): - continue - possible_friends.append(self.logic.relationship.has_hearts(name, hearts)) - return self.logic.count(int(npc), *possible_friends) - return self.can_earn_relationship(npc, hearts) - - if not self.npc_is_in_current_slot(npc): + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items() + if villager.bachelor)) + + @cache_self1 + def has_hearts_with_any(self, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + if hearts == 0: return True_() - villager = all_villagers_by_name[npc] - if self.options.friendsanity == Friendsanity.option_bachelors and not villager.bachelor: - return self.logic.relationship.can_earn_relationship(npc, hearts) - if self.options.friendsanity == Friendsanity.option_starting_npcs and not villager.available: + + return self.logic.or_(*(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + def has_hearts_with_n(self, amount: int, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with any npc." + assert amount >= 0, f"Can't have a negative amount of npc." + if hearts == 0 or amount == 0: + return True_() + + return self.logic.count(amount, *(self.logic.relationship.has_hearts(name, hearts) + for name, villager in self.content.villagers.items())) + + # Should be cached + def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: + return true_ + + heart_steps = self.content.features.friendsanity.get_randomized_hearts(villager) + if not heart_steps or hearts > heart_steps[-1]: # Hearts are sorted, bigger is the last one. return self.logic.relationship.can_earn_relationship(npc, hearts) - is_capped_at_8 = villager.bachelor and self.options.friendsanity != Friendsanity.option_all_with_marriage - if is_capped_at_8 and hearts > 8: - return self.logic.relationship.received_hearts(villager.name, 8) & self.logic.relationship.can_earn_relationship(npc, hearts) - return self.logic.relationship.received_hearts(villager.name, hearts) + + return self.logic.relationship.received_hearts(villager, hearts) # Should be cached - def received_hearts(self, npc: str, hearts: int) -> StardewRule: - heart_item = heart_item_name(npc) - number_required = math.ceil(hearts / self.options.friendsanity_heart_size) - return self.logic.received(heart_item, number_required) + def received_hearts(self, villager: Villager, hearts: int) -> StardewRule: + heart_item = friendsanity.to_item_name(villager.name) + + number_required = math.ceil(hearts / self.content.features.friendsanity.heart_size) + return self.logic.received(heart_item, number_required) & self.can_meet(villager.name) @cache_self1 def can_meet(self, npc: str) -> StardewRule: - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return True_() - villager = all_villagers_by_name[npc] + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + rules = [self.logic.region.can_reach_any(villager.locations)] + if npc == NPC.kent: rules.append(self.logic.time.has_year_two) + elif npc == NPC.leo: - rules.append(self.logic.received("Island West Turtle")) + rules.append(self.logic.received("Island North Turtle")) + elif npc == ModNPC.lance: rules.append(self.logic.region.can_reach(Region.volcano_floor_10)) + elif npc == ModNPC.apples: rules.append(self.logic.has(Fruit.starfruit)) + elif npc == ModNPC.scarlett: scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer) scarlett_spring = self.logic.season.has(Season.spring) & self.can_meet(ModNPC.andy) scarlett_summer = self.logic.season.has(Season.summer) & self.can_meet(ModNPC.susan) scarlett_fall = self.logic.season.has(Season.fall) & self.can_meet(ModNPC.sophia) rules.append(scarlett_job & (scarlett_spring | scarlett_summer | scarlett_fall)) + elif npc == ModNPC.morgan: rules.append(self.logic.received(SVEQuestItem.morgan_schooling)) + elif npc == ModNPC.goblin: rules.append(self.logic.region.can_reach_all((Region.witch_hut, Region.wizard_tower))) - return And(*rules) + return self.logic.and_(*rules) def can_give_loved_gifts_to_everyone(self) -> StardewRule: rules = [] - for npc in all_villagers_by_name: - if not self.npc_is_in_current_slot(npc): - continue + + for npc in self.content.villagers: meet_rule = self.logic.relationship.can_meet(npc) rules.append(meet_rule) + rules.append(self.logic.gifts.has_any_universal_love) - return And(*rules) + + return self.logic.and_(*rules) # Should be cached def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: - if hearts <= 0: + assert hearts >= 0, f"Can't have a negative hearts with {npc}." + + villager = self.content.villagers.get(npc) + if villager is None: + return false_ + + if hearts == 0: return True_() - previous_heart = hearts - self.options.friendsanity_heart_size - previous_heart_rule = self.logic.relationship.has_hearts(npc, previous_heart) + rules = [self.logic.relationship.can_meet(npc)] - if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc): - return previous_heart_rule + heart_size = self.content.features.friendsanity.heart_size + max_randomized_hearts = self.content.features.friendsanity.get_randomized_hearts(villager) + if max_randomized_hearts: + if hearts > max_randomized_hearts[-1]: + rules.append(self.logic.relationship.has_hearts(npc, hearts - 1)) + else: + previous_heart = max(hearts - heart_size, 0) + rules.append(self.logic.relationship.has_hearts(npc, previous_heart)) - rules = [previous_heart_rule, self.logic.relationship.can_meet(npc)] - villager = all_villagers_by_name[npc] - if hearts > 2 or hearts > self.options.friendsanity_heart_size: + if hearts > 2 or hearts > heart_size: rules.append(self.logic.season.has(villager.birthday)) + if villager.birthday == Generic.any: rules.append(self.logic.season.has_all() | self.logic.time.has_year_three) # push logic back for any birthday-less villager + if villager.bachelor: - if hearts > 8: - rules.append(self.logic.relationship.can_date(npc)) if hearts > 10: rules.append(self.logic.relationship.can_marry(npc)) + elif hearts > 8: + rules.append(self.logic.relationship.can_date(npc)) - return And(*rules) - - @cache_self1 - def npc_is_in_current_slot(self, name: str) -> bool: - npc = all_villagers_by_name[name] - return npc in self.all_villagers_given_mods + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py new file mode 100644 index 000000000000..87d9ee021524 --- /dev/null +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -0,0 +1,52 @@ +import functools +from typing import Union, Iterable + +from .base_logic import BaseLogicMixin, BaseLogic +from .book_logic import BookLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .season_logic import SeasonLogicMixin +from .skill_logic import SkillLogicMixin +from .time_logic import TimeLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.game_item import Requirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement + + +class RequirementLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.requirement = RequirementLogic(*args, **kwargs) + + +class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, +SeasonLogicMixin, TimeLogicMixin]]): + + def meet_all_requirements(self, requirements: Iterable[Requirement]): + if not requirements: + return self.logic.true_ + return self.logic.and_(*(self.logic.requirement.meet_requirement(requirement) for requirement in requirements)) + + @functools.singledispatchmethod + def meet_requirement(self, requirement: Requirement): + raise ValueError(f"Requirements of type{type(requirement)} have no rule registered.") + + @meet_requirement.register + def _(self, requirement: ToolRequirement): + return self.logic.tool.has_tool(requirement.tool, requirement.tier) + + @meet_requirement.register + def _(self, requirement: SkillRequirement): + return self.logic.skill.has_level(requirement.skill, requirement.level) + + @meet_requirement.register + def _(self, requirement: BookRequirement): + return self.logic.book.has_book_power(requirement.book) + + @meet_requirement.register + def _(self, requirement: SeasonRequirement): + return self.logic.season.has(requirement.season) + + @meet_requirement.register + def _(self, requirement: YearRequirement): + return self.logic.time.has_year(requirement.year) diff --git a/worlds/stardew_valley/logic/season_logic.py b/worlds/stardew_valley/logic/season_logic.py index 1953502099b4..6df315c0db94 100644 --- a/worlds/stardew_valley/logic/season_logic.py +++ b/worlds/stardew_valley/logic/season_logic.py @@ -1,11 +1,13 @@ +from functools import cached_property from typing import Iterable, Union from Utils import cache_self1 from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .time_logic import TimeLogicMixin from ..options import SeasonRandomization -from ..stardew_rule import StardewRule, True_, Or, And +from ..stardew_rule import StardewRule, True_, true_ from ..strings.generic_names import Generic from ..strings.season_names import Season @@ -16,7 +18,23 @@ def __init__(self, *args, **kwargs): self.season = SeasonLogic(*args, **kwargs) -class SeasonLogic(BaseLogic[Union[SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): +class SeasonLogic(BaseLogic[Union[HasLogicMixin, SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]): + + @cached_property + def has_spring(self) -> StardewRule: + return self.logic.season.has(Season.spring) + + @cached_property + def has_summer(self) -> StardewRule: + return self.logic.season.has(Season.summer) + + @cached_property + def has_fall(self) -> StardewRule: + return self.logic.season.has(Season.fall) + + @cached_property + def has_winter(self) -> StardewRule: + return self.logic.season.has(Season.winter) @cache_self1 def has(self, season: str) -> StardewRule: @@ -32,13 +50,16 @@ def has(self, season: str) -> StardewRule: return self.logic.received(season) def has_any(self, seasons: Iterable[str]): + if seasons == Season.all: + return true_ if not seasons: + # That should be false, but I'm scared. return True_() - return Or(*(self.logic.season.has(season) for season in seasons)) + return self.logic.or_(*(self.logic.season.has(season) for season in seasons)) def has_any_not_winter(self): return self.logic.season.has_any([Season.spring, Season.summer, Season.fall]) def has_all(self): seasons = [Season.spring, Season.summer, Season.fall, Season.winter] - return And(*(self.logic.season.has(season) for season in seasons)) + return self.logic.and_(*(self.logic.season.has(season) for season in seasons)) diff --git a/worlds/stardew_valley/logic/shipping_logic.py b/worlds/stardew_valley/logic/shipping_logic.py index 52c97561b326..8d545e219627 100644 --- a/worlds/stardew_valley/logic/shipping_logic.py +++ b/worlds/stardew_valley/logic/shipping_logic.py @@ -10,7 +10,7 @@ from ..locations import LocationTags, locations_by_tag from ..options import ExcludeGingerIsland, Shipsanity from ..options import SpecialOrderLocations -from ..stardew_rule import StardewRule, And +from ..stardew_rule import StardewRule from ..strings.ap_names.event_names import Event from ..strings.building_names import Building @@ -35,7 +35,7 @@ def can_ship_everything(self) -> StardewRule: shipsanity_prefix = "Shipsanity: " all_items_to_ship = [] exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true - exclude_qi = self.options.special_order_locations != SpecialOrderLocations.option_board_qi + exclude_qi = not (self.options.special_order_locations & SpecialOrderLocations.value_qi) mod_list = self.options.mods.value for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]: if exclude_island and LocationTags.GINGER_ISLAND in location.tags: @@ -57,4 +57,4 @@ def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> if shipsanity_location.name not in all_location_names_in_slot: continue rules.append(self.logic.region.can_reach_location(shipsanity_location.name)) - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 35946a0a4d36..4d5567302afe 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -4,7 +4,7 @@ from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic from .combat_logic import CombatLogicMixin -from .crop_logic import CropLogicMixin +from .harvesting_logic import HarvestingLogicMixin from .has_logic import HasLogicMixin from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin @@ -12,10 +12,10 @@ from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .. import options -from ..data import all_crops +from ..data.harvest import HarvestCropSource from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_skills_levels import get_mod_skill_levels -from ..stardew_rule import StardewRule, True_, Or, False_ +from ..stardew_rule import StardewRule, True_, False_, true_, And from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance @@ -23,8 +23,10 @@ from ..strings.region_names import Region from ..strings.skill_names import Skill, all_mod_skills from ..strings.tool_names import ToolMaterial, Tool +from ..strings.wallet_item_names import Wallet fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west) +vanilla_skill_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") class SkillLogicMixin(BaseLogicMixin): @@ -34,7 +36,8 @@ def __init__(self, *args, **kwargs): class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin, -CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): +CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): + # Should be cached def can_earn_level(self, skill: str, level: int) -> StardewRule: if level <= 0: @@ -48,14 +51,15 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: if self.options.skill_progression != options.SkillProgression.option_vanilla: previous_level_rule = self.logic.skill.has_level(skill, level - 1) else: - previous_level_rule = True_() + previous_level_rule = true_ if skill == Skill.fishing: - xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1)) + xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: - xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) + xp_rule = self.can_get_farming_xp & self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) elif skill == Skill.foraging: - xp_rule = self.logic.tool.has_tool(Tool.axe, tool_material) | self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) + xp_rule = (self.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) elif skill == Skill.mining: xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \ self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level) @@ -66,7 +70,7 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5) elif skill in all_mod_skills: # Ideal solution would be to add a logic registry, but I'm too lazy. - return self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") @@ -77,10 +81,10 @@ def has_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() - if self.options.skill_progression == options.SkillProgression.option_progressive: - return self.logic.received(f"{skill} Level", level) + if self.options.skill_progression == options.SkillProgression.option_vanilla: + return self.logic.skill.can_earn_level(skill, level) - return self.logic.skill.can_earn_level(skill, level) + return self.logic.received(f"{skill} Level", level) @cache_self1 def has_farming_level(self, level: int) -> StardewRule: @@ -91,8 +95,8 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star if level <= 0: return True_() - if self.options.skill_progression == options.SkillProgression.option_progressive: - skills_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level") + if self.options.skill_progression >= options.SkillProgression.option_progressive: + skills_items = vanilla_skill_items if allow_modded_skills: skills_items += get_mod_skill_levels(self.options.mods) return self.logic.received_n(*skills_items, count=level) @@ -104,12 +108,26 @@ def has_total_level(self, level: int, allow_modded_skills: bool = False) -> Star return rule_with_fishing return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing + def has_all_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_vanilla: + return self.has_total_level(50) + skills_items = vanilla_skill_items + if included_modded_skills: + skills_items += get_mod_skill_levels(self.options.mods) + return And(*[self.logic.received(skill, 10) for skill in skills_items]) + + def can_enter_mastery_cave(self) -> StardewRule: + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(Wallet.mastery_of_the_five_ways) + return self.has_all_skills_maxed() + @cached_property def can_get_farming_xp(self) -> StardewRule: + sources = self.content.find_sources_of_type(HarvestCropSource) crop_rules = [] - for crop in all_crops: - crop_rules.append(self.logic.crop.can_grow(crop)) - return Or(*crop_rules) + for crop_source in sources: + crop_rules.append(self.logic.harvesting.can_harvest_crop_from(crop_source)) + return self.logic.or_(*crop_rules) @cached_property def can_get_foraging_xp(self) -> StardewRule: @@ -132,7 +150,7 @@ def can_get_combat_xp(self) -> StardewRule: @cached_property def can_get_fishing_xp(self) -> StardewRule: - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot return self.logic.skill.can_fish() @@ -162,7 +180,7 @@ def can_crab_pot_at(self, region: str) -> StardewRule: @cached_property def can_crab_pot(self) -> StardewRule: crab_pot_rule = self.logic.has(Fishing.bait) - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot) else: crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp @@ -178,3 +196,14 @@ def can_forage_quality(self, quality: str) -> StardewRule: if quality == ForageQuality.gold: return self.has_level(Skill.foraging, 9) return False_() + + @cached_property + def can_earn_mastery_experience(self) -> StardewRule: + if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: + return self.has_all_skills_maxed() & self.logic.time.has_lived_max_months + return self.logic.time.has_lived_max_months + + def has_mastery(self, skill: str) -> StardewRule: + if self.options.skill_progression != options.SkillProgression.option_progressive_with_masteries: + return self.can_earn_mastery_experience and self.logic.region.can_reach(Region.mastery_cave) + return self.logic.received(f"{skill} Mastery") diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py new file mode 100644 index 000000000000..0e9b8e976f5b --- /dev/null +++ b/worlds/stardew_valley/logic/source_logic.py @@ -0,0 +1,106 @@ +import functools +from typing import Union, Any, Iterable + +from .artisan_logic import ArtisanLogicMixin +from .base_logic import BaseLogicMixin, BaseLogic +from .grind_logic import GrindLogicMixin +from .harvesting_logic import HarvestingLogicMixin +from .has_logic import HasLogicMixin +from .money_logic import MoneyLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from .requirement_logic import RequirementLogicMixin +from .tool_logic import ToolLogicMixin +from ..data.artisan import MachineSource +from ..data.game_item import GenericSource, ItemSource, GameItem, CustomRuleSource +from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ + HarvestCropSource, HarvestFruitTreeSource, ArtifactSpotSource +from ..data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource + + +class SourceLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.source = SourceLogic(*args, **kwargs) + + +class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin, +ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]): + + def has_access_to_item(self, item: GameItem): + rules = [] + + if self.content.features.cropsanity.is_included(item): + rules.append(self.logic.received(item.name)) + + rules.append(self.logic.source.has_access_to_any(item.sources)) + return self.logic.and_(*rules) + + def has_access_to_any(self, sources: Iterable[ItemSource]): + return self.logic.or_(*(self.logic.source.has_access_to(source) & self.logic.requirement.meet_all_requirements(source.other_requirements) + for source in sources)) + + @functools.singledispatchmethod + def has_access_to(self, source: Any): + raise ValueError(f"Sources of type{type(source)} have no rule registered.") + + @has_access_to.register + def _(self, source: GenericSource): + return self.logic.region.can_reach_any(source.regions) if source.regions else self.logic.true_ + + @has_access_to.register + def _(self, source: CustomRuleSource): + return source.create_rule(self.logic) + + @has_access_to.register + def _(self, source: ForagingSource): + return self.logic.harvesting.can_forage_from(source) + + @has_access_to.register + def _(self, source: SeasonalForagingSource): + # Implementation could be different with some kind of "calendar shuffle" + return self.logic.harvesting.can_forage_from(source.as_foraging_source()) + + @has_access_to.register + def _(self, _: FruitBatsSource): + return self.logic.harvesting.can_harvest_from_fruit_bats + + @has_access_to.register + def _(self, _: MushroomCaveSource): + return self.logic.harvesting.can_harvest_from_mushroom_cave + + @has_access_to.register + def _(self, source: ShopSource): + return self.logic.money.can_shop_from(source) + + @has_access_to.register + def _(self, source: HarvestFruitTreeSource): + return self.logic.harvesting.can_harvest_tree_from(source) + + @has_access_to.register + def _(self, source: HarvestCropSource): + return self.logic.harvesting.can_harvest_crop_from(source) + + @has_access_to.register + def _(self, source: MachineSource): + return self.logic.artisan.can_produce_from(source) + + @has_access_to.register + def _(self, source: MysteryBoxSource): + return self.logic.grind.can_grind_mystery_boxes(source.amount) + + @has_access_to.register + def _(self, source: ArtifactTroveSource): + return self.logic.grind.can_grind_artifact_troves(source.amount) + + @has_access_to.register + def _(self, source: PrizeMachineSource): + return self.logic.grind.can_grind_prize_tickets(source.amount) + + @has_access_to.register + def _(self, source: FishingTreasureChestSource): + return self.logic.grind.can_grind_fishing_treasure_chests(source.amount) + + @has_access_to.register + def _(self, source: ArtifactSpotSource): + return self.logic.grind.can_grind_artifact_spots(source.amount) diff --git a/worlds/stardew_valley/logic/special_order_logic.py b/worlds/stardew_valley/logic/special_order_logic.py index e0b1a7e2fb27..65497df477b8 100644 --- a/worlds/stardew_valley/logic/special_order_logic.py +++ b/worlds/stardew_valley/logic/special_order_logic.py @@ -4,7 +4,6 @@ from .arcade_logic import ArcadeLogicMixin from .artisan_logic import ArtisanLogicMixin from .base_logic import BaseLogicMixin, BaseLogic -from .buff_logic import BuffLogicMixin from .cooking_logic import CookingLogicMixin from .has_logic import HasLogicMixin from .mine_logic import MineLogicMixin @@ -18,7 +17,9 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin -from ..stardew_rule import StardewRule, Has +from ..content.vanilla.ginger_island import ginger_island_content_pack +from ..content.vanilla.qi_board import qi_board_content_pack +from ..stardew_rule import StardewRule, Has, false_ from ..strings.animal_product_names import AnimalProduct from ..strings.ap_names.event_names import Event from ..strings.ap_names.transport_names import Transportation @@ -35,7 +36,6 @@ from ..strings.region_names import Region from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder -from ..strings.tool_names import Tool from ..strings.villager_names import NPC @@ -47,14 +47,11 @@ def __init__(self, *args, **kwargs): class SpecialOrderLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, MoneyLogicMixin, ShippingLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, RelationshipLogicMixin, ToolLogicMixin, SkillLogicMixin, -MineLogicMixin, CookingLogicMixin, BuffLogicMixin, +MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]): def initialize_rules(self): self.update_rules({ - SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & - self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & - self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), SpecialOrder.cave_patrol: self.logic.relationship.can_meet(NPC.clint), SpecialOrder.aquatic_overpopulation: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), SpecialOrder.biome_balance: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(), @@ -66,46 +63,63 @@ def initialize_rules(self): SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg), SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items), SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot, - SpecialOrder.the_strong_stuff: self.logic.artisan.can_keg(Vegetable.potato), + SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)), SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(), SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Material.hardwood), SpecialOrder.robins_resource_rush: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() & self.logic.has(Fertilizer.tree) & self.logic.ability.can_mine_perfectly(), SpecialOrder.juicy_bugs_wanted: self.logic.has(Loot.bug_meat), - SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & - self.logic.special_order.has_island_transport() & - self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), SpecialOrder.a_curious_substance: self.logic.region.can_reach(Region.wizard_tower), SpecialOrder.prismatic_jelly: self.logic.region.can_reach(Region.wizard_tower), - SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & - self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & - self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), - SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), - SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & - self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), - SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & - (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), - SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), - SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & - self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), - SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), - SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(), - SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat - self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti - self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore - self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay - self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads - self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola - self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive... + }) + if ginger_island_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() & + self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) & + self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger), + SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") & + self.logic.special_order.has_island_transport() & + self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish), + }) + else: + self.update_rules({ + SpecialOrder.island_ingredients: false_, + SpecialOrder.tropical_fish: false_, + }) + + if qi_board_content_pack.name in self.content.registered_packs: + self.update_rules({ + SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) & + self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) & + self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items), + SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(), + SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") & + self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) & + (self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)), + SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(), + SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) & + self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend), + SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120), + SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(), + SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat + self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti + self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore + self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay + self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads + self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola + self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive...) + }) + def update_rules(self, new_rules: Dict[str, StardewRule]): self.registry.special_order_rules.update(new_rules) def can_complete_special_order(self, special_order: str) -> StardewRule: - return Has(special_order, self.registry.special_order_rules) + return Has(special_order, self.registry.special_order_rules, "special order") def has_island_transport(self) -> StardewRule: return self.logic.received(Transportation.island_obelisk) | self.logic.received(Transportation.boat_repair) diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 9dcebfe82a4f..94e0e277c86c 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -1,38 +1,52 @@ -from functools import cached_property -from typing import Union - -from Utils import cache_self1 -from .base_logic import BaseLogic, BaseLogicMixin -from .received_logic import ReceivedLogicMixin -from ..stardew_rule import StardewRule, HasProgressionPercent, True_ - -MAX_MONTHS = 12 -MONTH_COEFFICIENT = 24 // MAX_MONTHS - - -class TimeLogicMixin(BaseLogicMixin): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.time = TimeLogic(*args, **kwargs) - - -class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]): - - @cache_self1 - def has_lived_months(self, number: int) -> StardewRule: - if number <= 0: - return True_() - number = min(number, MAX_MONTHS) - return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) - - @cached_property - def has_lived_max_months(self) -> StardewRule: - return self.logic.time.has_lived_months(MAX_MONTHS) - - @cached_property - def has_year_two(self) -> StardewRule: - return self.logic.time.has_lived_months(4) - - @cached_property - def has_year_three(self) -> StardewRule: - return self.logic.time.has_lived_months(8) +from functools import cached_property +from typing import Union + +from Utils import cache_self1 +from .base_logic import BaseLogic, BaseLogicMixin +from .has_logic import HasLogicMixin +from ..stardew_rule import StardewRule, HasProgressionPercent + +ONE_YEAR = 4 +MAX_MONTHS = 3 * ONE_YEAR +PERCENT_REQUIRED_FOR_MAX_MONTHS = 48 +MONTH_COEFFICIENT = PERCENT_REQUIRED_FOR_MAX_MONTHS // MAX_MONTHS + +MIN_ITEMS = 10 +MAX_ITEMS = 999 +PERCENT_REQUIRED_FOR_MAX_ITEM = 24 + + +class TimeLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.time = TimeLogic(*args, **kwargs) + + +class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): + + @cache_self1 + def has_lived_months(self, number: int) -> StardewRule: + if number <= 0: + return self.logic.true_ + number = min(number, MAX_MONTHS) + return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) + + @cached_property + def has_lived_max_months(self) -> StardewRule: + return self.logic.time.has_lived_months(MAX_MONTHS) + + @cache_self1 + def has_lived_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_months(number * ONE_YEAR) + + @cache_self1 + def has_year(self, number: int) -> StardewRule: + return self.logic.time.has_lived_year(number - 1) + + @cached_property + def has_year_two(self) -> StardewRule: + return self.logic.time.has_year(2) + + @cached_property + def has_year_three(self) -> StardewRule: + return self.logic.time.has_year(3) diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 1b1dc2a52120..ba593c085ae4 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -1,4 +1,4 @@ -from typing import Union, Iterable +from typing import Union, Iterable, Tuple from Utils import cache_self1 from .base_logic import BaseLogicMixin, BaseLogic @@ -42,9 +42,17 @@ def __init__(self, *args, **kwargs): class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]): + + def has_all_tools(self, tools: Iterable[Tuple[str, str]]): + return self.logic.and_(*(self.logic.tool.has_tool(tool, material) for tool, material in tools)) + # Should be cached def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule: - assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`." + if tool == Tool.fishing_rod: + return self.logic.tool.has_fishing_rod(tool_materials[material]) + + if tool == Tool.pan and material == ToolMaterial.basic: + material = ToolMaterial.copper # The first Pan is the copper one, so the basic one does not exist if material == ToolMaterial.basic or tool == Tool.scythe: return True_() @@ -52,7 +60,14 @@ def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {tool}", tool_materials[material]) - return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) + if tool == Tool.pan: + has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) + if material == ToolMaterial.copper: + return has_base_pan + return has_base_pan & can_upgrade_rule + + return can_upgrade_rule def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: return self.has_tool(tool, material) & self.logic.region.can_reach(region) diff --git a/worlds/stardew_valley/mods/logic/deepwoods_logic.py b/worlds/stardew_valley/mods/logic/deepwoods_logic.py index 7699521542a7..26704eb7d11b 100644 --- a/worlds/stardew_valley/mods/logic/deepwoods_logic.py +++ b/worlds/stardew_valley/mods/logic/deepwoods_logic.py @@ -10,13 +10,13 @@ from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames from ...options import ElevatorProgression -from ...stardew_rule import StardewRule, True_, And, true_ -from ...strings.ap_names.mods.mod_items import DeepWoodsItem, SkillLevel +from ...stardew_rule import StardewRule, True_, true_ +from ...strings.ap_names.mods.mod_items import DeepWoodsItem from ...strings.ap_names.transport_names import ModTransportation from ...strings.craftable_names import Bomb from ...strings.food_names import Meal from ...strings.performance_names import Performance -from ...strings.skill_names import Skill +from ...strings.skill_names import Skill, ModSkill from ...strings.tool_names import Tool, ToolMaterial @@ -45,11 +45,11 @@ def can_reach_woods_depth(self, depth: int) -> StardewRule: self.logic.received(ModTransportation.woods_obelisk)) tier = int(depth / 25) + 1 - if self.options.skill_progression == options.SkillProgression.option_progressive: + if self.options.skill_progression >= options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier + 5)) rules.append(self.logic.skill.has_level(Skill.combat, combat_tier)) - return And(*rules) + return self.logic.and_(*rules) def has_woods_rune_to_depth(self, floor: int) -> StardewRule: if self.options.elevator_progression == ElevatorProgression.option_vanilla: @@ -66,8 +66,8 @@ def can_pull_sword(self) -> StardewRule: self.logic.received(DeepWoodsItem.pendant_elder), self.logic.skill.has_total_level(40)] if ModNames.luck_skill in self.options.mods: - rules.append(self.logic.received(SkillLevel.luck, 7)) + rules.append(self.logic.skill.has_level(ModSkill.luck, 7)) else: rules.append( self.logic.has(Meal.magic_rock_candy)) # You need more luck than this, but it'll push the logic down a ways; you can get the rest there. - return And(*rules) + return self.logic.and_(*rules) diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index 8f5e676d8c2d..cfafc88e83f5 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -7,7 +7,7 @@ from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin +from ...logic.farming_logic import FarmingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.money_logic import MoneyLogicMixin @@ -24,11 +24,10 @@ from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop, Fruit -from ...strings.fish_names import WaterItem -from ...strings.flower_names import Flower +from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop +from ...strings.fish_names import ModTrash, SVEFish from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable, Forageable +from ...strings.forageable_names import SVEForage, DistantLandsForageable from ...strings.gift_names import SVEGift from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material @@ -53,8 +52,9 @@ def __init__(self, *args, **kwargs): self.item = ModItemLogic(*args, **kwargs) -class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CropLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, -RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin]]): +class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, CookingLogicMixin, FishingLogicMixin, HasLogicMixin, MoneyLogicMixin, +RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, ToolLogicMixin, CraftingLogicMixin, SkillLogicMixin, TimeLogicMixin, QuestLogicMixin, +FarmingLogicMixin]]): def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() @@ -78,53 +78,53 @@ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, Star def get_sve_item_rules(self): return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, + SVESeed.fungus: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk_seed), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus_seed), - SVEForage.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & + SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk), + SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus), + ModLoot.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime_seed), - SVESeed.slime_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk_seed: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVEForage.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void_seed), - SVESeed.void_seed: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - SVEForage.void_soul: self.logic.region.can_reach( + SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime), + SVESeed.slime: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, + SVESeed.stalk: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, + ModLoot.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, + SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void), + SVESeed.void: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, + ModLoot.void_soul: self.logic.region.can_reach( SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberrys: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), + SVEForage.bearberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.crop.can_plant_and_grow_item([Season.spring, Season.summer]) & self.logic.has(SVESeed.shrub_seed), + SVESeed.shrub: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), + SVEFruit.salal_berry: self.logic.farming.can_plant_and_grow_item((Season.spring, Season.summer)) & self.logic.has(SVESeed.shrub), ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_ferns_seed: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.crop.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_ferns_seed), - SVEForage.big_conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), + SVESeed.ancient_fern: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), + SVEVegetable.ancient_fiber: self.logic.farming.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_fern), + SVEForage.conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.dried_sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), + SVEForage.sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & + self.logic.season.has_any([Season.summer, Season.fall])), SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.lucky_four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), + SVEForage.four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & + self.logic.season.has_any([Season.spring, Season.summer]), SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & self.logic.season.has(Season.fall), SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.smelly_rafflesia: self.logic.region.can_reach(Region.secret_woods), + SVEForage.rafflesia: self.logic.region.can_reach(Region.secret_woods), SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - SVEForage.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, + ModLoot.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three } @@ -135,49 +135,17 @@ def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( SVERegion.crimson_badlands), Loot.solar_essence: items[Loot.solar_essence] | self.logic.region.can_reach(SVERegion.crimson_badlands), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.tool.can_forage(Season.spring, SVERegion.sprite_spring), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.summer, SVERegion.sprite_spring), - Flower.sunflower: items[Flower.sunflower] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.sprite_spring), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.tool.can_forage(Season.fall, SVERegion.sprite_spring), - Fruit.ancient_fruit: items[Fruit.ancient_fruit] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three) | self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Fruit.sweet_gem_berry: items[Fruit.sweet_gem_berry] | ( - self.logic.tool.can_forage((Season.spring, Season.summer, Season.fall), SVERegion.sprite_spring) & - self.logic.time.has_year_three), - WaterItem.coral: items[WaterItem.coral] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.rainbow_shell: items[Forageable.rainbow_shell] | self.logic.region.can_reach(SVERegion.fable_reef), - WaterItem.sea_urchin: items[WaterItem.sea_urchin] | self.logic.region.can_reach(SVERegion.fable_reef), - Forageable.red_mushroom: items[Forageable.red_mushroom] | self.logic.tool.can_forage((Season.summer, Season.fall), SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.purple_mushroom: items[Forageable.purple_mushroom] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), - Forageable.morel: items[Forageable.morel] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west), - Forageable.chanterelle: items[Forageable.chanterelle] | self.logic.tool.can_forage(Season.fall, SVERegion.forest_west) | - self.logic.region.can_reach(SVERegion.sprite_spring_cave), Ore.copper: items[Ore.copper] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iron: items[Ore.iron] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.highlands_cavern) & self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), + SVEFish.dulse_seaweed: self.logic.fishing.can_fish_at(Region.beach) & self.logic.season.has_any([Season.spring, Season.summer, Season.winter]) } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): options_to_update = { - Fruit.apple: items[Fruit.apple] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), # Deep enough to have seen such a tree at least once - Fruit.apricot: items[Fruit.apricot] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.cherry: items[Fruit.cherry] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.orange: items[Fruit.orange] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.peach: items[Fruit.peach] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.pomegranate: items[Fruit.pomegranate] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Fruit.mango: items[Fruit.mango] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.tulip: items[Flower.tulip] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.blue_jazz: items[Flower.blue_jazz] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), - Flower.summer_spangle: items[Flower.summer_spangle] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.poppy: items[Flower.poppy] | self.logic.tool.can_forage(Season.not_winter, DeepWoodsRegion.floor_10), - Flower.fairy_rose: items[Flower.fairy_rose] | self.logic.region.can_reach(DeepWoodsRegion.floor_10), Material.hardwood: items[Material.hardwood] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iron, DeepWoodsRegion.floor_10), Ingredient.sugar: items[Ingredient.sugar] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.gold, DeepWoodsRegion.floor_50), # Gingerbread House @@ -207,6 +175,7 @@ def get_archaeology_item_rules(self): archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule else: archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule + archaeology_item_rules[ModTrash.rusty_scrap] = self.logic.has(ModMachine.grinder) & self.logic.has_any(*all_artifacts) return archaeology_item_rules def get_distant_lands_item_rules(self): diff --git a/worlds/stardew_valley/mods/logic/mod_skills_levels.py b/worlds/stardew_valley/mods/logic/mod_skills_levels.py index 18402283857b..32b3368a8c8b 100644 --- a/worlds/stardew_valley/mods/logic/mod_skills_levels.py +++ b/worlds/stardew_valley/mods/logic/mod_skills_levels.py @@ -2,20 +2,21 @@ from ...mods.mod_data import ModNames from ...options import Mods +from ...strings.ap_names.mods.mod_items import SkillLevel def get_mod_skill_levels(mods: Mods) -> Tuple[str]: skills_items = [] if ModNames.luck_skill in mods: - skills_items.append("Luck Level") + skills_items.append(SkillLevel.luck) if ModNames.socializing_skill in mods: - skills_items.append("Socializing Level") + skills_items.append(SkillLevel.socializing) if ModNames.magic in mods: - skills_items.append("Magic Level") + skills_items.append(SkillLevel.magic) if ModNames.archaeology in mods: - skills_items.append("Archaeology Level") + skills_items.append(SkillLevel.archaeology) if ModNames.binning_skill in mods: - skills_items.append("Binning Level") + skills_items.append(SkillLevel.binning) if ModNames.cooking_skill in mods: - skills_items.append("Cooking Level") + skills_items.append(SkillLevel.cooking) return tuple(skills_items) diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py index 40b5545ee39f..1aa71404ae51 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -19,7 +19,7 @@ from ...strings.forageable_names import SVEForage from ...strings.material_names import Material from ...strings.metal_names import Ore, MetalBar -from ...strings.monster_drop_names import Loot +from ...strings.monster_drop_names import Loot, ModLoot from ...strings.monster_names import Monster from ...strings.quest_names import Quest, ModQuest from ...strings.region_names import Region, SVERegion, BoardingHouseRegion @@ -86,7 +86,7 @@ def _get_sve_quest_rules(self): self.logic.relationship.can_meet(ModNPC.lance) & self.logic.region.can_reach(SVERegion.guild_summit), ModQuest.AuroraVineyard: self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(SVERegion.aurora_vineyard), ModQuest.MonsterCrops: self.logic.has_all(*(SVEVegetable.monster_mushroom, SVEFruit.slime_berry, SVEFruit.monster_fruit, SVEVegetable.void_root)), - ModQuest.VoidSoul: self.logic.has(SVEForage.void_soul) & self.logic.region.can_reach(Region.farm) & + ModQuest.VoidSoul: self.logic.has(ModLoot.void_soul) & self.logic.region.can_reach(Region.farm) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.badlands_entrance) & self.logic.relationship.has_hearts(NPC.krobus, 10) & self.logic.quest.can_complete_quest(ModQuest.MonsterCrops) & self.logic.monster.can_kill_any((Monster.shadow_brute, Monster.shadow_shaman, Monster.shadow_sniper)), diff --git a/worlds/stardew_valley/mods/logic/skills_logic.py b/worlds/stardew_valley/mods/logic/skills_logic.py index ce8bebbffef5..cb12274dc651 100644 --- a/worlds/stardew_valley/mods/logic/skills_logic.py +++ b/worlds/stardew_valley/mods/logic/skills_logic.py @@ -1,11 +1,11 @@ from typing import Union from .magic_logic import MagicLogicMixin -from ...data.villagers_data import all_villagers from ...logic.action_logic import ActionLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.building_logic import BuildingLogicMixin from ...logic.cooking_logic import CookingLogicMixin +from ...logic.crafting_logic import CraftingLogicMixin from ...logic.fishing_logic import FishingLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin @@ -14,10 +14,9 @@ from ...logic.tool_logic import ToolLogicMixin from ...mods.mod_data import ModNames from ...options import SkillProgression -from ...stardew_rule import StardewRule, False_, True_ -from ...strings.ap_names.mods.mod_items import SkillLevel -from ...strings.craftable_names import ModCraftable, ModMachine +from ...stardew_rule import StardewRule, False_, True_, And from ...strings.building_names import Building +from ...strings.craftable_names import ModCraftable, ModMachine from ...strings.geode_names import Geode from ...strings.machine_names import Machine from ...strings.region_names import Region @@ -33,7 +32,7 @@ def __init__(self, *args, **kwargs): class ModSkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, ActionLogicMixin, RelationshipLogicMixin, BuildingLogicMixin, -ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): +ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicLogicMixin]]): def has_mod_level(self, skill: str, level: int) -> StardewRule: if level <= 0: return True_() @@ -77,9 +76,10 @@ def can_earn_magic_skill_level(self, level: int) -> StardewRule: def can_earn_socializing_skill_level(self, level: int) -> StardewRule: villager_count = [] - for villager in all_villagers: - if villager.mod_name in self.options.mods or villager.mod_name is None: - villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + + for villager in self.content.villagers.values(): + villager_count.append(self.logic.relationship.can_earn_relationship(villager.name, level)) + return self.logic.count(level * 2, *villager_count) def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: @@ -89,12 +89,12 @@ def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: shifter_rule = self.logic.has(ModCraftable.water_shifter) preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) if level >= 8: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule + return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule if level >= 5: - return (self.logic.action.can_pan() & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule + return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule if level >= 3: - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) - return self.logic.action.can_pan() | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) + return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) def can_earn_cooking_skill_level(self, level: int) -> StardewRule: if level >= 6: @@ -104,7 +104,13 @@ def can_earn_cooking_skill_level(self, level: int) -> StardewRule: return self.logic.cooking.can_cook() def can_earn_binning_skill_level(self, level: int) -> StardewRule: - if level >= 6: - return self.logic.has(Machine.recycling_machine) - else: - return True_() # You can always earn levels 1-5 with trash cans + if level <= 2: + return True_() + binning_rule = [self.logic.has(ModMachine.trash_bin) & self.logic.has(Machine.recycling_machine)] + if level > 4: + binning_rule.append(self.logic.has(ModMachine.composter)) + if level > 7: + binning_rule.append(self.logic.has(ModMachine.recycling_bin)) + if level > 9: + binning_rule.append(self.logic.has(ModMachine.advanced_recycling_machine)) + return And(*binning_rule) diff --git a/worlds/stardew_valley/mods/logic/special_orders_logic.py b/worlds/stardew_valley/mods/logic/special_orders_logic.py index e51a23d50254..1a0934282e09 100644 --- a/worlds/stardew_valley/mods/logic/special_orders_logic.py +++ b/worlds/stardew_valley/mods/logic/special_orders_logic.py @@ -1,12 +1,11 @@ from typing import Union -from ...data.craftable_data import all_crafting_recipes_by_name from ..mod_data import ModNames +from ...data.craftable_data import all_crafting_recipes_by_name from ...logic.action_logic import ActionLogicMixin from ...logic.artisan_logic import ArtisanLogicMixin from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.crafting_logic import CraftingLogicMixin -from ...logic.crop_logic import CropLogicMixin from ...logic.has_logic import HasLogicMixin from ...logic.received_logic import ReceivedLogicMixin from ...logic.region_logic import RegionLogicMixin @@ -34,7 +33,7 @@ def __init__(self, *args, **kwargs): self.special_order = ModSpecialOrderLogic(*args, **kwargs) -class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, CropLogicMixin, HasLogicMixin, RegionLogicMixin, +class ModSpecialOrderLogic(BaseLogic[Union[ActionLogicMixin, ArtisanLogicMixin, CraftingLogicMixin, HasLogicMixin, RegionLogicMixin, ReceivedLogicMixin, RelationshipLogicMixin, SeasonLogicMixin, WalletLogicMixin]]): def get_modded_special_orders_rules(self): special_orders = {} @@ -54,7 +53,7 @@ def get_modded_special_orders_rules(self): self.logic.region.can_reach(SVERegion.fairhaven_farm), ModSpecialOrder.a_mysterious_venture: self.logic.has(Bomb.cherry_bomb) & self.logic.has(Bomb.bomb) & self.logic.has(Bomb.mega_bomb) & self.logic.region.can_reach(Region.adventurer_guild), - ModSpecialOrder.an_elegant_reception: self.logic.artisan.can_keg(Fruit.starfruit) & self.logic.has(ArtisanGood.cheese) & + ModSpecialOrder.an_elegant_reception: self.logic.has(ArtisanGood.specific_wine(Fruit.starfruit)) & self.logic.has(ArtisanGood.cheese) & self.logic.has(ArtisanGood.goat_cheese) & self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.jenkins_cellar), ModSpecialOrder.fairy_garden: self.logic.has(Consumable.fairy_dust) & diff --git a/worlds/stardew_valley/mods/logic/sve_logic.py b/worlds/stardew_valley/mods/logic/sve_logic.py index 1254338fe2fc..fc093554d8e6 100644 --- a/worlds/stardew_valley/mods/logic/sve_logic.py +++ b/worlds/stardew_valley/mods/logic/sve_logic.py @@ -14,12 +14,11 @@ from ...logic.time_logic import TimeLogicMixin from ...logic.tool_logic import ToolLogicMixin from ...strings.ap_names.mods.mod_items import SVELocation, SVERunes, SVEQuestItem +from ...strings.quest_names import ModQuest from ...strings.quest_names import Quest from ...strings.region_names import Region from ...strings.tool_names import Tool, ToolMaterial from ...strings.wallet_item_names import Wallet -from ...stardew_rule import Or -from ...strings.quest_names import ModQuest class SVELogicMixin(BaseLogicMixin): @@ -29,7 +28,7 @@ def __init__(self, *args, **kwargs): class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixin, RegionLogicMixin, RelationshipLogicMixin, TimeLogicMixin, ToolLogicMixin, - CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin, QuestLogicMixin]]): + CookingLogicMixin, MoneyLogicMixin, CombatLogicMixin, SeasonLogicMixin]]): def initialize_rules(self): self.registry.sve_location_rules.update({ SVELocation.tempered_galaxy_sword: self.logic.money.can_spend_at(SVERegion.alesia_shop, 350000), @@ -39,17 +38,31 @@ def initialize_rules(self): def has_any_rune(self): rune_list = SVERunes.nexus_items - return Or(*(self.logic.received(rune) for rune in rune_list)) + return self.logic.or_(*(self.logic.received(rune) for rune in rune_list)) def has_iridium_bomb(self): if self.options.quest_locations < 0: return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder) return self.logic.received(SVEQuestItem.iridium_bomb) + def has_marlon_boat(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat) + return self.logic.received(SVEQuestItem.marlon_boat_paddle) + + def has_grandpa_shed_repaired(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed) + return self.logic.received(SVEQuestItem.grandpa_shed) + + def has_bear_knowledge(self): + if self.options.quest_locations < 0: + return self.logic.quest.can_complete_quest(Quest.strange_note) + return self.logic.received(Wallet.bears_knowledge) + def can_buy_bear_recipe(self): access_rule = (self.logic.quest.can_complete_quest(Quest.strange_note) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.basic) & self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.basic)) forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods, Region.mountain)) - knowledge_rule = self.logic.received(Wallet.bears_knowledge) + knowledge_rule = self.has_bear_knowledge() return access_rule & forage_rule & knowledge_rule - diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index a4d3b9828aa6..54408fb2c571 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -26,14 +26,3 @@ class ModNames: distant_lands = "Distant Lands - Witch Swamp Overhaul" lacey = "Hat Mouse Lacey" boarding_house = "Boarding House and Bus Stop Extension" - - jasper_sve = jasper + "," + sve - - -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, ModNames.sve, ModNames.alecto, - ModNames.distant_lands, ModNames.lacey, ModNames.boarding_house}) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index df0a12f6ef18..c075bd4d106f 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -1,11 +1,11 @@ from typing import Dict, List +from .mod_data import ModNames +from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData from ..strings.entrance_names import Entrance, DeepWoodsEntrance, EugeneEntrance, LaceyEntrance, BoardingHouseEntrance, \ JasperEntrance, AlecEntrance, YobaEntrance, JunaEntrance, MagicEntrance, AyeishaEntrance, RileyEntrance, SVEEntrance, AlectoEntrance from ..strings.region_names import Region, DeepWoodsRegion, EugeneRegion, JasperRegion, BoardingHouseRegion, \ AlecRegion, YobaRegion, JunaRegion, MagicRegion, AyeishaRegion, RileyRegion, SVERegion, AlectoRegion, LaceyRegion -from ..region_classes import RegionData, ConnectionData, ModificationFlag, RandomizationFlag, ModRegionData -from .mod_data import ModNames deep_woods_regions = [ RegionData(Region.farm, [DeepWoodsEntrance.use_woods_obelisk]), @@ -179,15 +179,15 @@ RegionData(SVERegion.grampleton_suburbs, [SVEEntrance.grampleton_suburbs_to_scarlett_house]), RegionData(SVERegion.scarlett_house), RegionData(Region.wizard_basement, [SVEEntrance.wizard_to_fable_reef]), - RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild]), - RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway]), - RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room]), - RegionData(SVERegion.first_slash_spare_room), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave]), - RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison]), - RegionData(SVERegion.dwarf_prison), - RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder]), - RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands]), + RegionData(SVERegion.fable_reef, [SVEEntrance.fable_reef_to_guild], is_ginger_island=True), + RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), + RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), + RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave], is_ginger_island=True), + RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), + RegionData(SVERegion.dwarf_prison, is_ginger_island=True), + RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), + RegionData(SVERegion.lances_ladder, [SVEEntrance.lance_ladder_to_highlands], is_ginger_island=True), RegionData(SVERegion.forest_west, [SVEEntrance.forest_west_to_spring, SVEEntrance.west_to_aurora, SVEEntrance.use_bear_shop]), RegionData(SVERegion.aurora_vineyard, [SVEEntrance.to_aurora_basement]), @@ -217,7 +217,8 @@ ConnectionData(SVEEntrance.town_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.plot_to_bridge, SVERegion.shearwater), ConnectionData(SVEEntrance.bus_stop_to_shed, SVERegion.grandpas_shed), - ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(SVEEntrance.grandpa_shed_to_interior, SVERegion.grandpas_shed_interior, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(SVEEntrance.grandpa_interior_to_upstairs, SVERegion.grandpas_shed_upstairs, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.grandpa_shed_to_town, Region.town), ConnectionData(SVEEntrance.bmv_to_sophia, SVERegion.sophias_house, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), @@ -270,8 +271,9 @@ ConnectionData(SVEEntrance.grampleton_station_to_grampleton_suburbs, SVERegion.grampleton_suburbs), ConnectionData(SVEEntrance.grampleton_suburbs_to_scarlett_house, SVERegion.scarlett_house, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.first_slash_guild_to_hallway, SVERegion.first_slash_hallway, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.first_slash_hallway_to_room, SVERegion.first_slash_spare_room, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), + ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), ] @@ -332,7 +334,8 @@ flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(BoardingHouseEntrance.boarding_house_plateau_to_abandoned_mines_entrance, BoardingHouseRegion.abandoned_mines_entrance, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, flag=RandomizationFlag.BUILDINGS), + ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_the_lost_valley, BoardingHouseRegion.lost_valley_minecart, + flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_entrance_to_abandoned_mines_1a, BoardingHouseRegion.abandoned_mines_1a, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.abandoned_mines_1a_to_abandoned_mines_1b, BoardingHouseRegion.abandoned_mines_1b, flag=RandomizationFlag.BUILDINGS), @@ -347,16 +350,15 @@ ConnectionData(BoardingHouseEntrance.the_lost_valley_to_lost_valley_ruins, BoardingHouseRegion.lost_valley_ruins, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_1, BoardingHouseRegion.lost_valley_house_1, flag=RandomizationFlag.BUILDINGS), ConnectionData(BoardingHouseEntrance.lost_valley_ruins_to_lost_valley_house_2, BoardingHouseRegion.lost_valley_house_2, flag=RandomizationFlag.BUILDINGS) - - ] vanilla_connections_to_remove_by_mod: Dict[str, List[ConnectionData]] = { - ModNames.sve: [ConnectionData(Entrance.mountain_to_the_mines, Region.mines, - flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, - flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), - ] + ModNames.sve: [ + ConnectionData(Entrance.mountain_to_the_mines, Region.mines, + flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, + flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ] } ModDataList = { diff --git a/worlds/stardew_valley/option_groups.py b/worlds/stardew_valley/option_groups.py index 50709c10fd49..d0f052348a7e 100644 --- a/worlds/stardew_valley/option_groups.py +++ b/worlds/stardew_valley/option_groups.py @@ -1,65 +1,76 @@ -from Options import OptionGroup, DeathLink, ProgressionBalancing, Accessibility +import logging + +from Options import DeathLink, ProgressionBalancing, Accessibility from .options import (Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, - NumberOfMovementBuffs, NumberOfLuckBuffs, ExcludeGingerIsland, TrapItems, + NumberOfMovementBuffs, EnabledFillerBuffs, ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, Gifting, FarmType, - Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods) + Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Mods, Booksanity, Walnutsanity, BundlePlando) -sv_option_groups = [ - OptionGroup("General", [ - Goal, - FarmType, - BundleRandomization, - BundlePrice, - EntranceRandomization, - ExcludeGingerIsland, - ]), - OptionGroup("Major Unlocks", [ - SeasonRandomization, - Cropsanity, - BackpackProgression, - ToolProgression, - ElevatorProgression, - SkillProgression, - BuildingProgression, - ]), - OptionGroup("Extra Shuffling", [ - FestivalLocations, - ArcadeMachineLocations, - SpecialOrderLocations, - QuestLocations, - Fishsanity, - Museumsanity, - Friendsanity, - FriendsanityHeartSize, - Monstersanity, - Shipsanity, - Cooksanity, - Chefsanity, - Craftsanity, - ]), - OptionGroup("Multipliers and Buffs", [ - StartingMoney, - ProfitMargin, - ExperienceMultiplier, - FriendshipMultiplier, - DebrisMultiplier, - NumberOfMovementBuffs, - NumberOfLuckBuffs, - TrapItems, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - QuickStart, - ]), - OptionGroup("Advanced Options", [ - Gifting, - DeathLink, - Mods, - ProgressionBalancing, - Accessibility, - ]), -] +sv_option_groups = [] +try: + from Options import OptionGroup +except: + logging.warning("Old AP Version, OptionGroup not available.") +else: + sv_option_groups = [ + OptionGroup("General", [ + Goal, + FarmType, + BundleRandomization, + BundlePrice, + EntranceRandomization, + ExcludeGingerIsland, + ]), + OptionGroup("Major Unlocks", [ + SeasonRandomization, + Cropsanity, + BackpackProgression, + ToolProgression, + ElevatorProgression, + SkillProgression, + BuildingProgression, + ]), + OptionGroup("Extra Shuffling", [ + FestivalLocations, + ArcadeMachineLocations, + SpecialOrderLocations, + QuestLocations, + Fishsanity, + Museumsanity, + Friendsanity, + FriendsanityHeartSize, + Monstersanity, + Shipsanity, + Cooksanity, + Chefsanity, + Craftsanity, + Booksanity, + Walnutsanity, + ]), + OptionGroup("Multipliers and Buffs", [ + StartingMoney, + ProfitMargin, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + NumberOfMovementBuffs, + EnabledFillerBuffs, + TrapItems, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + QuickStart, + ]), + OptionGroup("Advanced Options", [ + Gifting, + DeathLink, + Mods, + BundlePlando, + ProgressionBalancing, + Accessibility, + ]), + ] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index ba1ebfb9c177..5369e88a2dcb 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,8 +1,12 @@ +import sys +import typing from dataclasses import dataclass from typing import Protocol, ClassVar -from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink +from Options import Range, NamedRange, Toggle, Choice, OptionSet, PerGameCommonOptions, DeathLink, OptionList, Visibility from .mods.mod_data import ModNames +from .strings.ap_names.ap_option_names import OptionName +from .strings.bundle_names import all_cc_bundle_names class StardewValleyOption(Protocol): @@ -18,7 +22,7 @@ class Goal(Choice): Master Angler: Catch every fish. Adapts to Fishsanity Complete Collection: Complete the museum collection Full House: Get married and have 2 children - Greatest Walnut Hunter: Find 130 Golden Walnuts + Greatest Walnut Hunter: Find 130 Golden Walnuts. Pairs well with Walnutsanity Protector of the Valley: Complete the monster slayer goals. Adapts to Monstersanity Full Shipment: Ship every item. Adapts to Shipsanity Gourmet Chef: Cook every recipe. Adapts to Cooksanity @@ -73,6 +77,7 @@ class FarmType(Choice): option_wilderness = 4 option_four_corners = 5 option_beach = 6 + option_meadowlands = 7 class StartingMoney(NamedRange): @@ -118,14 +123,16 @@ class BundleRandomization(Choice): Vanilla: Standard bundles from the vanilla game Thematic: Every bundle will require random items compatible with their original theme Remixed: Picks bundles at random from thematic, vanilla remixed and new custom ones + Remixed Anywhere: Remixed, but bundles are not locked to specific rooms. Shuffled: Every bundle will require random items and follow no particular structure""" internal_name = "bundle_randomization" display_name = "Bundle Randomization" - default = 2 option_vanilla = 0 option_thematic = 1 - option_remixed = 2 - option_shuffled = 3 + option_remixed = 3 + option_remixed_anywhere = 4 + option_shuffled = 6 + default = option_remixed class BundlePrice(Choice): @@ -155,6 +162,7 @@ class EntranceRandomization(Choice): Pelican Town: Only doors in the main town area are randomized with each other Non Progression: Only entrances that are always available are randomized with each other Buildings: All entrances that allow you to enter a building are randomized with each other + Buildings Without House: Buildings, but excluding the farmhouse Chaos: Same as "Buildings", but the entrances get reshuffled every single day! """ # Everything: All buildings and areas are randomized with each other @@ -169,9 +177,10 @@ class EntranceRandomization(Choice): option_disabled = 0 option_pelican_town = 1 option_non_progression = 2 - option_buildings = 3 - # option_everything = 4 - option_chaos = 5 + option_buildings_without_house = 3 + option_buildings = 4 + # option_everything = 10 + option_chaos = 12 # option_buildings_one_way = 6 # option_everything_one_way = 7 # option_chaos_one_way = 8 @@ -255,12 +264,14 @@ class ElevatorProgression(Choice): class SkillProgression(Choice): """Shuffle skill levels? Vanilla: Leveling up skills is normal - Progressive: Skill levels are unlocked randomly, and earning xp sends checks""" + Progressive: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are excluded + With Masteries: Skill levels are unlocked randomly, and earning xp sends checks. Masteries are included""" internal_name = "skill_progression" display_name = "Skill Progression" - default = 1 + default = 2 option_vanilla = 0 option_progressive = 1 + option_progressive_with_masteries = 2 class BuildingProgression(Choice): @@ -319,13 +330,26 @@ class SpecialOrderLocations(Choice): Disabled: The special orders are not included in the Archipelago shuffling. Board Only: The Special Orders on the board in town are location checks Board and Qi: The Special Orders from Mr Qi's walnut room are checks, in addition to the board in town + Short: All Special Order requirements are reduced by 40% + Very Short: All Special Order requirements are reduced by 80% """ internal_name = "special_order_locations" display_name = "Special Order Locations" - default = 1 - option_disabled = 0 - option_board_only = 1 - option_board_qi = 2 + option_vanilla = 0b0000 # 0 + option_board = 0b0001 # 1 + value_qi = 0b0010 # 2 + value_short = 0b0100 # 4 + value_very_short = 0b1000 # 8 + option_board_qi = option_board | value_qi # 3 + option_vanilla_short = value_short # 4 + option_board_short = option_board | value_short # 5 + option_board_qi_short = option_board_qi | value_short # 7 + option_vanilla_very_short = value_very_short # 8 + option_board_very_short = option_board | value_very_short # 9 + option_board_qi_very_short = option_board_qi | value_very_short # 11 + alias_disabled = option_vanilla + alias_board_only = option_board + default = option_board_short class QuestLocations(NamedRange): @@ -533,6 +557,46 @@ class FriendsanityHeartSize(Range): # step = 1 +class Booksanity(Choice): + """Shuffle Books? + None: All books behave like vanilla + Power: Power books are turned into checks + Power and Skill: Power and skill books are turned into checks. + All: Lost books are also included in the shuffling + """ + internal_name = "booksanity" + display_name = "Booksanity" + default = 2 + option_none = 0 + option_power = 1 + option_power_skill = 2 + option_all = 3 + + +class Walnutsanity(OptionSet): + """Shuffle walnuts? + Puzzles: Walnuts obtained from solving a special puzzle or winning a minigame + Bushes: Walnuts that are in a bush and can be collected by clicking it + Dig spots: Walnuts that are underground and must be digged up. Includes Journal scrap walnuts + Repeatables: Random chance walnuts from normal actions (fishing, farming, combat, etc) + """ + internal_name = "walnutsanity" + display_name = "Walnutsanity" + valid_keys = frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes, OptionName.walnutsanity_dig_spots, + OptionName.walnutsanity_repeatables, }) + preset_none = frozenset() + preset_all = valid_keys + default = preset_none + + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, OptionSet): + return set(self.value) == other.value + if isinstance(other, OptionList): + return set(self.value) == set(other.value) + else: + return typing.cast(bool, self.value == other) + + class NumberOfMovementBuffs(Range): """Number of movement speed buffs to the player that exist as items in the pool. Each movement speed buff is a +25% multiplier that stacks additively""" @@ -544,15 +608,26 @@ class NumberOfMovementBuffs(Range): # step = 1 -class NumberOfLuckBuffs(Range): - """Number of luck buffs to the player that exist as items in the pool. - Each luck buff is a bonus to daily luck of 0.025""" - internal_name = "luck_buff_number" - display_name = "Number of Luck Buffs" - range_start = 0 - range_end = 12 - default = 4 - # step = 1 +class EnabledFillerBuffs(OptionSet): + """Enable various permanent player buffs to roll as filler items + Luck: Increase daily luck + Damage: Increased Damage % + Defense: Increased Defense + Immunity: Increased Immunity + Health: Increased Max Health + Energy: Increased Max Energy + Bite Rate: Shorter delay to get a bite when fishing + Fish Trap: Effect similar to the Trap Bobber, but weaker + Fishing Bar Size: Increased Fishing Bar Size + """ + internal_name = "enabled_filler_buffs" + display_name = "Enabled Filler Buffs" + valid_keys = frozenset({OptionName.buff_luck, OptionName.buff_damage, OptionName.buff_defense, OptionName.buff_immunity, OptionName.buff_health, + OptionName.buff_energy, OptionName.buff_bite, OptionName.buff_fish_trap, OptionName.buff_fishing_bar}) + # OptionName.buff_quality, OptionName.buff_glow}) # Disabled these two buffs because they are too hard to make on the mod side + preset_none = frozenset() + preset_all = valid_keys + default = frozenset({OptionName.buff_luck, OptionName.buff_defense, OptionName.buff_bite}) class ExcludeGingerIsland(Toggle): @@ -678,19 +753,40 @@ class Gifting(Toggle): default = 1 +# These mods have been disabled because either they are not updated for the current supported version of Stardew Valley, +# or we didn't find the time to validate that they work or fix compatibility issues if they do. +# Once a mod is validated to be functional, it can simply be removed from this list +disabled_mods = {ModNames.deepwoods, ModNames.magic, + ModNames.cooking_skill, + ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.shiko, ModNames.delores, ModNames.riley, + ModNames.boarding_house} + + +if 'unittest' in sys.modules.keys() or 'pytest' in sys.modules.keys(): + disabled_mods = {} + + class Mods(OptionSet): """List of mods that will be included in the shuffling.""" internal_name = "mods" display_name = "Mods" - valid_keys = { - 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, ModNames.sve, ModNames.distant_lands, - ModNames.alecto, ModNames.lacey, ModNames.boarding_house - } + valid_keys = {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, ModNames.sve, ModNames.distant_lands, + ModNames.alecto, ModNames.lacey, ModNames.boarding_house}.difference(disabled_mods) + + +class BundlePlando(OptionSet): + """If using Remixed bundles, this guarantees some of them will show up in your community center. + If more bundles are specified than what fits in their parent room, that room will randomly pick from only the plando ones""" + internal_name = "bundle_plando" + display_name = "Bundle Plando" + visibility = Visibility.template | Visibility.spoiler + valid_keys = set(all_cc_bundle_names) @dataclass @@ -720,6 +816,8 @@ class StardewValleyOptions(PerGameCommonOptions): craftsanity: Craftsanity friendsanity: Friendsanity friendsanity_heart_size: FriendsanityHeartSize + booksanity: Booksanity + walnutsanity: Walnutsanity exclude_ginger_island: ExcludeGingerIsland quick_start: QuickStart starting_money: StartingMoney @@ -728,10 +826,11 @@ class StardewValleyOptions(PerGameCommonOptions): friendship_multiplier: FriendshipMultiplier debris_multiplier: DebrisMultiplier movement_buff_number: NumberOfMovementBuffs - luck_buff_number: NumberOfLuckBuffs + enabled_filler_buffs: EnabledFillerBuffs trap_items: TrapItems multiple_day_sleep_enabled: MultipleDaySleepEnabled multiple_day_sleep_cost: MultipleDaySleepCost gifting: Gifting mods: Mods + bundle_plando: BundlePlando death_link: DeathLink diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index e75eb5c5fcde..e663241ac6af 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -3,9 +3,12 @@ from Options import Accessibility, ProgressionBalancing, DeathLink from .options import Goal, StartingMoney, ProfitMargin, BundleRandomization, BundlePrice, EntranceRandomization, SeasonRandomization, Cropsanity, \ BackpackProgression, ToolProgression, ElevatorProgression, SkillProgression, BuildingProgression, FestivalLocations, ArcadeMachineLocations, \ - SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, NumberOfLuckBuffs, \ - ExcludeGingerIsland, TrapItems, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ - Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity + SpecialOrderLocations, QuestLocations, Fishsanity, Museumsanity, Friendsanity, FriendsanityHeartSize, NumberOfMovementBuffs, ExcludeGingerIsland, TrapItems, \ + MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, FriendshipMultiplier, DebrisMultiplier, QuickStart, \ + Gifting, FarmType, Monstersanity, Shipsanity, Cooksanity, Chefsanity, Craftsanity, Booksanity, Walnutsanity, EnabledFillerBuffs + +# @formatter:off +from .strings.ap_names.ap_option_names import OptionName all_random_settings = { "progression_balancing": "random", @@ -37,8 +40,10 @@ Craftsanity.internal_name: "random", Friendsanity.internal_name: "random", FriendsanityHeartSize.internal_name: "random", + Booksanity.internal_name: "random", + Walnutsanity.internal_name: "random", NumberOfMovementBuffs.internal_name: "random", - NumberOfLuckBuffs.internal_name: "random", + EnabledFillerBuffs.internal_name: "random", ExcludeGingerIsland.internal_name: "random", TrapItems.internal_name: "random", MultipleDaySleepEnabled.internal_name: "random", @@ -70,7 +75,7 @@ BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, FestivalLocations.internal_name: FestivalLocations.option_easy, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "minimum", Fishsanity.internal_name: Fishsanity.option_only_easy_fish, Museumsanity.internal_name: Museumsanity.option_milestones, @@ -81,8 +86,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: 8, - NumberOfLuckBuffs.internal_name: 8, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_easy, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -109,12 +116,12 @@ Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories_easy, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_short, QuestLocations.internal_name: "normal", Fishsanity.internal_name: Fishsanity.option_exclude_legendaries, Museumsanity.internal_name: Museumsanity.option_milestones, @@ -125,8 +132,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_starting_npcs, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_power_skill, + Walnutsanity.internal_name: [OptionName.walnutsanity_puzzles], NumberOfMovementBuffs.internal_name: 6, - NumberOfLuckBuffs.internal_name: 6, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_medium, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -148,17 +157,17 @@ ProfitMargin.internal_name: "normal", BundleRandomization.internal_name: BundleRandomization.option_remixed, BundlePrice.internal_name: BundlePrice.option_expensive, - EntranceRandomization.internal_name: EntranceRandomization.option_buildings, + EntranceRandomization.internal_name: EntranceRandomization.option_buildings_without_house, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi_short, QuestLocations.internal_name: "lots", Fishsanity.internal_name: Fishsanity.option_all, Museumsanity.internal_name: Museumsanity.option_all, @@ -169,8 +178,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_all, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 4, - NumberOfLuckBuffs.internal_name: 4, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.option_hard, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -198,7 +209,7 @@ BackpackProgression.internal_name: BackpackProgression.option_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, @@ -213,8 +224,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_all_with_marriage, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 2, - NumberOfLuckBuffs.internal_name: 2, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_none, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.option_hell, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -241,12 +254,12 @@ Cropsanity.internal_name: Cropsanity.option_disabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, FestivalLocations.internal_name: FestivalLocations.option_disabled, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "none", Fishsanity.internal_name: Fishsanity.option_none, Museumsanity.internal_name: Museumsanity.option_none, @@ -257,8 +270,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: 4, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: 10, - NumberOfLuckBuffs.internal_name: 10, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.option_easy, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.option_true, @@ -290,7 +305,7 @@ BuildingProgression.internal_name: BuildingProgression.option_vanilla, FestivalLocations.internal_name: FestivalLocations.option_disabled, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla_very_short, QuestLocations.internal_name: "none", Fishsanity.internal_name: Fishsanity.option_none, Museumsanity.internal_name: Museumsanity.option_none, @@ -301,8 +316,10 @@ Craftsanity.internal_name: Craftsanity.option_none, Friendsanity.internal_name: Friendsanity.option_none, FriendsanityHeartSize.internal_name: FriendsanityHeartSize.default, + Booksanity.internal_name: Booksanity.option_none, + Walnutsanity.internal_name: Walnutsanity.preset_none, NumberOfMovementBuffs.internal_name: NumberOfMovementBuffs.default, - NumberOfLuckBuffs.internal_name: NumberOfLuckBuffs.default, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.default, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, TrapItems.internal_name: TrapItems.default, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, @@ -330,7 +347,7 @@ BackpackProgression.internal_name: BackpackProgression.option_early_progressive, ToolProgression.internal_name: ToolProgression.option_progressive, ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, BuildingProgression.internal_name: BuildingProgression.option_progressive, FestivalLocations.internal_name: FestivalLocations.option_hard, ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, @@ -345,8 +362,10 @@ Craftsanity.internal_name: Craftsanity.option_all, Friendsanity.internal_name: Friendsanity.option_all, FriendsanityHeartSize.internal_name: 1, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, + EnabledFillerBuffs.internal_name: EnabledFillerBuffs.preset_all, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, TrapItems.internal_name: TrapItems.default, MultipleDaySleepEnabled.internal_name: MultipleDaySleepEnabled.default, @@ -358,6 +377,8 @@ Gifting.internal_name: Gifting.default, "death_link": DeathLink.default, } +# @formatter:on + sv_options_presets: Dict[str, Dict[str, Any]] = { "All random": all_random_settings, diff --git a/worlds/stardew_valley/region_classes.py b/worlds/stardew_valley/region_classes.py index eaabcfa5fd36..bd64518ea153 100644 --- a/worlds/stardew_valley/region_classes.py +++ b/worlds/stardew_valley/region_classes.py @@ -1,6 +1,7 @@ -from enum import IntFlag -from typing import Optional, List +from copy import deepcopy from dataclasses import dataclass, field +from enum import IntFlag +from typing import Optional, List, Set connector_keyword = " to " @@ -9,15 +10,16 @@ class ModificationFlag(IntFlag): NOT_MODIFIED = 0 MODIFIED = 1 + class RandomizationFlag(IntFlag): NOT_RANDOMIZED = 0b0 - PELICAN_TOWN = 0b11111 - NON_PROGRESSION = 0b11110 - BUILDINGS = 0b11100 - EVERYTHING = 0b11000 - CHAOS = 0b10000 - GINGER_ISLAND = 0b0100000 - LEAD_TO_OPEN_AREA = 0b1000000 + PELICAN_TOWN = 0b00011111 + NON_PROGRESSION = 0b00011110 + BUILDINGS = 0b00011100 + EVERYTHING = 0b00011000 + GINGER_ISLAND = 0b00100000 + LEAD_TO_OPEN_AREA = 0b01000000 + MASTERIES = 0b10000000 @dataclass(frozen=True) @@ -25,6 +27,7 @@ class RegionData: name: str exits: List[str] = field(default_factory=list) flag: ModificationFlag = ModificationFlag.NOT_MODIFIED + is_ginger_island: bool = False def get_merged_with(self, exits: List[str]): merged_exits = [] @@ -32,14 +35,14 @@ def get_merged_with(self, exits: List[str]): if exits is not None: merged_exits.extend(exits) merged_exits = list(set(merged_exits)) - return RegionData(self.name, merged_exits) + return RegionData(self.name, merged_exits, is_ginger_island=self.is_ginger_island) - def get_without_exit(self, exit_to_remove: str): - exits = [exit for exit in self.exits if exit != exit_to_remove] - return RegionData(self.name, exits) + def get_without_exits(self, exits_to_remove: Set[str]): + exits = [exit_ for exit_ in self.exits if exit_ not in exits_to_remove] + return RegionData(self.name, exits, is_ginger_island=self.is_ginger_island) def get_clone(self): - return self.get_merged_with(None) + return deepcopy(self) @dataclass(frozen=True) @@ -62,6 +65,3 @@ class ModRegionData: mod_name: str regions: List[RegionData] connections: List[ConnectionData] - - - diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 4284b438f806..2aca2d3f4d3e 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,11 @@ from typing import Iterable, Dict, Protocol, List, Tuple, Set from BaseClasses import Region, Entrance -from .options import EntranceRandomization, ExcludeGingerIsland, Museumsanity, StardewValleyOptions -from .strings.entrance_names import Entrance -from .strings.region_names import Region -from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod +from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions, SkillProgression +from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag +from .strings.entrance_names import Entrance, LogicEntrance +from .strings.region_names import Region, LogicRegion class RegionFactory(Protocol): @@ -17,78 +17,57 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: vanilla_regions = [ RegionData(Region.menu, [Entrance.to_stardew_valley]), RegionData(Region.stardew_valley, [Entrance.to_farmhouse]), - RegionData(Region.farm_house, [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, Entrance.farmhouse_cooking, Entrance.watch_queen_of_sauce]), + RegionData(Region.farm_house, + [Entrance.farmhouse_to_farm, Entrance.downstairs_to_cellar, LogicEntrance.farmhouse_cooking, LogicEntrance.watch_queen_of_sauce]), RegionData(Region.cellar), - RegionData(Region.kitchen), - RegionData(Region.queen_of_sauce), RegionData(Region.farm, - [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, - Entrance.farm_to_farmcave, Entrance.enter_greenhouse, - Entrance.enter_coop, Entrance.enter_barn, - Entrance.enter_shed, Entrance.enter_slime_hutch, - Entrance.farming, Entrance.shipping]), - RegionData(Region.farming), - RegionData(Region.shipping), + [Entrance.farm_to_backwoods, Entrance.farm_to_bus_stop, Entrance.farm_to_forest, Entrance.farm_to_farmcave, Entrance.enter_greenhouse, + Entrance.enter_coop, Entrance.enter_barn, Entrance.enter_shed, Entrance.enter_slime_hutch, LogicEntrance.grow_spring_crops, + LogicEntrance.grow_summer_crops, LogicEntrance.grow_fall_crops, LogicEntrance.grow_winter_crops, LogicEntrance.shipping]), RegionData(Region.backwoods, [Entrance.backwoods_to_mountain]), RegionData(Region.bus_stop, [Entrance.bus_stop_to_town, Entrance.take_bus_to_desert, Entrance.bus_stop_to_tunnel_entrance]), RegionData(Region.forest, - [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, - Entrance.forest_to_marnie_ranch, - Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, - Entrance.buy_from_traveling_merchant, - Entrance.attend_flower_dance, Entrance.attend_festival_of_ice]), - RegionData(Region.traveling_cart, [Entrance.buy_from_traveling_merchant_sunday, - Entrance.buy_from_traveling_merchant_monday, - Entrance.buy_from_traveling_merchant_tuesday, - Entrance.buy_from_traveling_merchant_wednesday, - Entrance.buy_from_traveling_merchant_thursday, - Entrance.buy_from_traveling_merchant_friday, - Entrance.buy_from_traveling_merchant_saturday]), - RegionData(Region.traveling_cart_sunday), - RegionData(Region.traveling_cart_monday), - RegionData(Region.traveling_cart_tuesday), - RegionData(Region.traveling_cart_wednesday), - RegionData(Region.traveling_cart_thursday), - RegionData(Region.traveling_cart_friday), - RegionData(Region.traveling_cart_saturday), + [Entrance.forest_to_town, Entrance.enter_secret_woods, Entrance.forest_to_wizard_tower, Entrance.forest_to_marnie_ranch, + Entrance.forest_to_leah_cottage, Entrance.forest_to_sewer, Entrance.forest_to_mastery_cave, LogicEntrance.buy_from_traveling_merchant, + LogicEntrance.complete_raccoon_requests, LogicEntrance.fish_in_waterfall, LogicEntrance.attend_flower_dance, LogicEntrance.attend_trout_derby, + LogicEntrance.attend_festival_of_ice]), + RegionData(LogicRegion.forest_waterfall), RegionData(Region.farm_cave), - RegionData(Region.greenhouse), + RegionData(Region.greenhouse, + [LogicEntrance.grow_spring_crops_in_greenhouse, LogicEntrance.grow_summer_crops_in_greenhouse, LogicEntrance.grow_fall_crops_in_greenhouse, + LogicEntrance.grow_winter_crops_in_greenhouse, LogicEntrance.grow_indoor_crops_in_greenhouse]), RegionData(Region.mountain, [Entrance.mountain_to_railroad, Entrance.mountain_to_tent, Entrance.mountain_to_carpenter_shop, Entrance.mountain_to_the_mines, Entrance.enter_quarry, Entrance.mountain_to_adventurer_guild, Entrance.mountain_to_town, Entrance.mountain_to_maru_room, Entrance.mountain_to_leo_treehouse]), - RegionData(Region.leo_treehouse), + RegionData(Region.leo_treehouse, is_ginger_island=True), RegionData(Region.maru_room), RegionData(Region.tunnel_entrance, [Entrance.tunnel_entrance_to_bus_tunnel]), RegionData(Region.bus_tunnel), RegionData(Region.town, - [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, - Entrance.town_to_pierre_general_store, Entrance.town_to_saloon, Entrance.town_to_alex_house, - Entrance.town_to_trailer, - Entrance.town_to_mayor_manor, - Entrance.town_to_sam_house, Entrance.town_to_haley_house, Entrance.town_to_sewer, - Entrance.town_to_clint_blacksmith, - Entrance.town_to_museum, - Entrance.town_to_jojamart, Entrance.purchase_movie_ticket, - Entrance.attend_egg_festival, Entrance.attend_fair, Entrance.attend_spirit_eve, Entrance.attend_winter_star]), - RegionData(Region.beach, [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, - Entrance.fishing, - Entrance.attend_luau, Entrance.attend_moonlight_jellies, Entrance.attend_night_market]), - RegionData(Region.fishing), + [Entrance.town_to_community_center, Entrance.town_to_beach, Entrance.town_to_hospital, Entrance.town_to_pierre_general_store, + Entrance.town_to_saloon, Entrance.town_to_alex_house, Entrance.town_to_trailer, Entrance.town_to_mayor_manor, Entrance.town_to_sam_house, + Entrance.town_to_haley_house, Entrance.town_to_sewer, Entrance.town_to_clint_blacksmith, Entrance.town_to_museum, Entrance.town_to_jojamart, + Entrance.purchase_movie_ticket, LogicEntrance.buy_experience_books, LogicEntrance.attend_egg_festival, LogicEntrance.attend_fair, + LogicEntrance.attend_spirit_eve, LogicEntrance.attend_winter_star]), + RegionData(Region.beach, + [Entrance.beach_to_willy_fish_shop, Entrance.enter_elliott_house, Entrance.enter_tide_pools, LogicEntrance.fishing, LogicEntrance.attend_luau, + LogicEntrance.attend_moonlight_jellies, LogicEntrance.attend_night_market, LogicEntrance.attend_squidfest]), RegionData(Region.railroad, [Entrance.enter_bathhouse_entrance, Entrance.enter_witch_warp_cave]), RegionData(Region.ranch), RegionData(Region.leah_house), + RegionData(Region.mastery_cave), RegionData(Region.sewer, [Entrance.enter_mutant_bug_lair]), RegionData(Region.mutant_bug_lair), - RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, - Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), + RegionData(Region.wizard_tower, [Entrance.enter_wizard_basement, Entrance.use_desert_obelisk, Entrance.use_island_obelisk]), RegionData(Region.wizard_basement), RegionData(Region.tent), RegionData(Region.carpenter, [Entrance.enter_sebastian_room]), RegionData(Region.sebastian_room), - RegionData(Region.adventurer_guild), + RegionData(Region.adventurer_guild, [Entrance.adventurer_guild_to_bedroom]), + RegionData(Region.adventurer_guild_bedroom), RegionData(Region.community_center, [Entrance.access_crafts_room, Entrance.access_pantry, Entrance.access_fish_tank, Entrance.access_boiler_room, Entrance.access_bulletin_board, Entrance.access_vault]), @@ -114,18 +93,14 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.mayor_house), RegionData(Region.sam_house), RegionData(Region.haley_house), - RegionData(Region.blacksmith, [Entrance.blacksmith_copper]), - RegionData(Region.blacksmith_copper, [Entrance.blacksmith_iron]), - RegionData(Region.blacksmith_iron, [Entrance.blacksmith_gold]), - RegionData(Region.blacksmith_gold, [Entrance.blacksmith_iridium]), - RegionData(Region.blacksmith_iridium), + RegionData(Region.blacksmith, [LogicEntrance.blacksmith_copper]), RegionData(Region.museum), RegionData(Region.jojamart, [Entrance.enter_abandoned_jojamart]), RegionData(Region.abandoned_jojamart, [Entrance.enter_movie_theater]), RegionData(Region.movie_ticket_stand), RegionData(Region.movie_theater), RegionData(Region.fish_shop, [Entrance.fish_shop_to_boat_tunnel]), - RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island]), + RegionData(Region.boat_tunnel, [Entrance.boat_to_ginger_island], is_ginger_island=True), RegionData(Region.elliott_house), RegionData(Region.tide_pools), RegionData(Region.bathhouse_entrance, [Entrance.enter_locker_room]), @@ -138,7 +113,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.quarry_mine_entrance, [Entrance.enter_quarry_mine]), RegionData(Region.quarry_mine), RegionData(Region.secret_woods), - RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis]), + RegionData(Region.desert, [Entrance.enter_skull_cavern_entrance, Entrance.enter_oasis, LogicEntrance.attend_desert_festival]), RegionData(Region.oasis, [Entrance.enter_casino]), RegionData(Region.casino), RegionData(Region.skull_cavern_entrance, [Entrance.enter_skull_cavern]), @@ -151,49 +126,52 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.skull_cavern_150, [Entrance.mine_to_skull_cavern_floor_175]), RegionData(Region.skull_cavern_175, [Entrance.mine_to_skull_cavern_floor_200]), RegionData(Region.skull_cavern_200, [Entrance.enter_dangerous_skull_cavern]), - RegionData(Region.dangerous_skull_cavern), - RegionData(Region.island_south, [Entrance.island_south_to_west, Entrance.island_south_to_north, - Entrance.island_south_to_east, Entrance.island_south_to_southeast, - Entrance.use_island_resort, - Entrance.parrot_express_docks_to_volcano, - Entrance.parrot_express_docks_to_dig_site, - Entrance.parrot_express_docks_to_jungle]), - RegionData(Region.island_resort), + RegionData(Region.dangerous_skull_cavern, is_ginger_island=True), + RegionData(Region.island_south, + [Entrance.island_south_to_west, Entrance.island_south_to_north, Entrance.island_south_to_east, Entrance.island_south_to_southeast, + Entrance.use_island_resort, Entrance.parrot_express_docks_to_volcano, Entrance.parrot_express_docks_to_dig_site, + Entrance.parrot_express_docks_to_jungle], + is_ginger_island=True), + RegionData(Region.island_resort, is_ginger_island=True), RegionData(Region.island_west, - [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, - Entrance.island_west_to_crystals_cave, Entrance.island_west_to_shipwreck, - Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, - Entrance.parrot_express_jungle_to_docks, Entrance.parrot_express_jungle_to_dig_site, - Entrance.parrot_express_jungle_to_volcano]), - RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine]), - RegionData(Region.island_shrine), - RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove]), - RegionData(Region.island_north, [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, - Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, - Entrance.parrot_express_volcano_to_dig_site, - Entrance.parrot_express_volcano_to_jungle, - Entrance.parrot_express_volcano_to_docks]), - RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach]), - RegionData(Region.volcano_secret_beach), - RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10]), - RegionData(Region.volcano_dwarf_shop), - RegionData(Region.volcano_floor_10), - RegionData(Region.island_trader), - RegionData(Region.island_farmhouse, [Entrance.island_cooking]), - RegionData(Region.gourmand_frog_cave), - RegionData(Region.colored_crystals_cave), - RegionData(Region.shipwreck), - RegionData(Region.qi_walnut_room), - RegionData(Region.leo_hut), - RegionData(Region.pirate_cove), - RegionData(Region.field_office), + [Entrance.island_west_to_islandfarmhouse, Entrance.island_west_to_gourmand_cave, Entrance.island_west_to_crystals_cave, + Entrance.island_west_to_shipwreck, Entrance.island_west_to_qi_walnut_room, Entrance.use_farm_obelisk, Entrance.parrot_express_jungle_to_docks, + Entrance.parrot_express_jungle_to_dig_site, Entrance.parrot_express_jungle_to_volcano, LogicEntrance.grow_spring_crops_on_island, + LogicEntrance.grow_summer_crops_on_island, LogicEntrance.grow_fall_crops_on_island, LogicEntrance.grow_winter_crops_on_island, LogicEntrance.grow_indoor_crops_on_island], + is_ginger_island=True), + RegionData(Region.island_east, [Entrance.island_east_to_leo_hut, Entrance.island_east_to_island_shrine], is_ginger_island=True), + RegionData(Region.island_shrine, is_ginger_island=True), + RegionData(Region.island_south_east, [Entrance.island_southeast_to_pirate_cove], is_ginger_island=True), + RegionData(Region.island_north, + [Entrance.talk_to_island_trader, Entrance.island_north_to_field_office, Entrance.island_north_to_dig_site, Entrance.island_north_to_volcano, + Entrance.parrot_express_volcano_to_dig_site, Entrance.parrot_express_volcano_to_jungle, Entrance.parrot_express_volcano_to_docks], + is_ginger_island=True), + RegionData(Region.volcano, [Entrance.climb_to_volcano_5, Entrance.volcano_to_secret_beach], is_ginger_island=True), + RegionData(Region.volcano_secret_beach, is_ginger_island=True), + RegionData(Region.volcano_floor_5, [Entrance.talk_to_volcano_dwarf, Entrance.climb_to_volcano_10], is_ginger_island=True), + RegionData(Region.volcano_dwarf_shop, is_ginger_island=True), + RegionData(Region.volcano_floor_10, is_ginger_island=True), + RegionData(Region.island_trader, is_ginger_island=True), + RegionData(Region.island_farmhouse, [LogicEntrance.island_cooking], is_ginger_island=True), + RegionData(Region.gourmand_frog_cave, is_ginger_island=True), + RegionData(Region.colored_crystals_cave, is_ginger_island=True), + RegionData(Region.shipwreck, is_ginger_island=True), + RegionData(Region.qi_walnut_room, is_ginger_island=True), + RegionData(Region.leo_hut, is_ginger_island=True), + RegionData(Region.pirate_cove, is_ginger_island=True), + RegionData(Region.field_office, is_ginger_island=True), RegionData(Region.dig_site, [Entrance.dig_site_to_professor_snail_cave, Entrance.parrot_express_dig_site_to_volcano, - Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle]), - RegionData(Region.professor_snail_cave), - RegionData(Region.mines, [Entrance.talk_to_mines_dwarf, + Entrance.parrot_express_dig_site_to_docks, Entrance.parrot_express_dig_site_to_jungle], + is_ginger_island=True), + RegionData(Region.professor_snail_cave, is_ginger_island=True), + RegionData(Region.coop), + RegionData(Region.barn), + RegionData(Region.shed), + RegionData(Region.slime_hutch), + + RegionData(Region.mines, [LogicEntrance.talk_to_mines_dwarf, Entrance.dig_to_mines_floor_5]), - RegionData(Region.mines_dwarf_shop), RegionData(Region.mines_floor_5, [Entrance.dig_to_mines_floor_10]), RegionData(Region.mines_floor_10, [Entrance.dig_to_mines_floor_15]), RegionData(Region.mines_floor_15, [Entrance.dig_to_mines_floor_20]), @@ -218,22 +196,59 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: RegionData(Region.mines_floor_110, [Entrance.dig_to_mines_floor_115]), RegionData(Region.mines_floor_115, [Entrance.dig_to_mines_floor_120]), RegionData(Region.mines_floor_120, [Entrance.dig_to_dangerous_mines_20, Entrance.dig_to_dangerous_mines_60, Entrance.dig_to_dangerous_mines_100]), - RegionData(Region.dangerous_mines_20), - RegionData(Region.dangerous_mines_60), - RegionData(Region.dangerous_mines_100), - RegionData(Region.coop), - RegionData(Region.barn), - RegionData(Region.shed), - RegionData(Region.slime_hutch), - RegionData(Region.egg_festival), - RegionData(Region.flower_dance), - RegionData(Region.luau), - RegionData(Region.moonlight_jellies), - RegionData(Region.fair), - RegionData(Region.spirit_eve), - RegionData(Region.festival_of_ice), - RegionData(Region.night_market), - RegionData(Region.winter_star), + RegionData(Region.dangerous_mines_20, is_ginger_island=True), + RegionData(Region.dangerous_mines_60, is_ginger_island=True), + RegionData(Region.dangerous_mines_100, is_ginger_island=True), + + RegionData(LogicRegion.mines_dwarf_shop), + RegionData(LogicRegion.blacksmith_copper, [LogicEntrance.blacksmith_iron]), + RegionData(LogicRegion.blacksmith_iron, [LogicEntrance.blacksmith_gold]), + RegionData(LogicRegion.blacksmith_gold, [LogicEntrance.blacksmith_iridium]), + RegionData(LogicRegion.blacksmith_iridium), + RegionData(LogicRegion.kitchen), + RegionData(LogicRegion.queen_of_sauce), + RegionData(LogicRegion.fishing), + + RegionData(LogicRegion.spring_farming), + RegionData(LogicRegion.summer_farming, [LogicEntrance.grow_summer_fall_crops_in_summer]), + RegionData(LogicRegion.fall_farming, [LogicEntrance.grow_summer_fall_crops_in_fall]), + RegionData(LogicRegion.winter_farming), + RegionData(LogicRegion.summer_or_fall_farming), + RegionData(LogicRegion.indoor_farming), + + RegionData(LogicRegion.shipping), + RegionData(LogicRegion.traveling_cart, [LogicEntrance.buy_from_traveling_merchant_sunday, + LogicEntrance.buy_from_traveling_merchant_monday, + LogicEntrance.buy_from_traveling_merchant_tuesday, + LogicEntrance.buy_from_traveling_merchant_wednesday, + LogicEntrance.buy_from_traveling_merchant_thursday, + LogicEntrance.buy_from_traveling_merchant_friday, + LogicEntrance.buy_from_traveling_merchant_saturday]), + RegionData(LogicRegion.traveling_cart_sunday), + RegionData(LogicRegion.traveling_cart_monday), + RegionData(LogicRegion.traveling_cart_tuesday), + RegionData(LogicRegion.traveling_cart_wednesday), + RegionData(LogicRegion.traveling_cart_thursday), + RegionData(LogicRegion.traveling_cart_friday), + RegionData(LogicRegion.traveling_cart_saturday), + RegionData(LogicRegion.raccoon_daddy, [LogicEntrance.buy_from_raccoon]), + RegionData(LogicRegion.raccoon_shop), + + RegionData(LogicRegion.egg_festival), + RegionData(LogicRegion.desert_festival), + RegionData(LogicRegion.flower_dance), + RegionData(LogicRegion.luau), + RegionData(LogicRegion.trout_derby), + RegionData(LogicRegion.moonlight_jellies), + RegionData(LogicRegion.fair), + RegionData(LogicRegion.spirit_eve), + RegionData(LogicRegion.festival_of_ice), + RegionData(LogicRegion.night_market), + RegionData(LogicRegion.winter_star), + RegionData(LogicRegion.squidfest), + RegionData(LogicRegion.bookseller_1, [LogicEntrance.buy_year1_books]), + RegionData(LogicRegion.bookseller_2, [LogicEntrance.buy_year3_books]), + RegionData(LogicRegion.bookseller_3), ] # Exists and where they lead @@ -242,19 +257,15 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.to_farmhouse, Region.farm_house), ConnectionData(Entrance.farmhouse_to_farm, Region.farm), ConnectionData(Entrance.downstairs_to_cellar, Region.cellar), - ConnectionData(Entrance.farmhouse_cooking, Region.kitchen), - ConnectionData(Entrance.watch_queen_of_sauce, Region.queen_of_sauce), ConnectionData(Entrance.farm_to_backwoods, Region.backwoods), ConnectionData(Entrance.farm_to_bus_stop, Region.bus_stop), ConnectionData(Entrance.farm_to_forest, Region.forest), ConnectionData(Entrance.farm_to_farmcave, Region.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData(Entrance.farming, Region.farming), ConnectionData(Entrance.enter_greenhouse, Region.greenhouse), ConnectionData(Entrance.enter_coop, Region.coop), ConnectionData(Entrance.enter_barn, Region.barn), ConnectionData(Entrance.enter_shed, Region.shed), ConnectionData(Entrance.enter_slime_hutch, Region.slime_hutch), - ConnectionData(Entrance.shipping, Region.shipping), ConnectionData(Entrance.use_desert_obelisk, Region.desert), ConnectionData(Entrance.use_island_obelisk, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.use_farm_obelisk, Region.farm), @@ -273,14 +284,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.enter_secret_woods, Region.secret_woods), ConnectionData(Entrance.forest_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), - ConnectionData(Entrance.buy_from_traveling_merchant, Region.traveling_cart), - ConnectionData(Entrance.buy_from_traveling_merchant_sunday, Region.traveling_cart_sunday), - ConnectionData(Entrance.buy_from_traveling_merchant_monday, Region.traveling_cart_monday), - ConnectionData(Entrance.buy_from_traveling_merchant_tuesday, Region.traveling_cart_tuesday), - ConnectionData(Entrance.buy_from_traveling_merchant_wednesday, Region.traveling_cart_wednesday), - ConnectionData(Entrance.buy_from_traveling_merchant_thursday, Region.traveling_cart_thursday), - ConnectionData(Entrance.buy_from_traveling_merchant_friday, Region.traveling_cart_friday), - ConnectionData(Entrance.buy_from_traveling_merchant_saturday, Region.traveling_cart_saturday), + ConnectionData(Entrance.forest_to_mastery_cave, Region.mastery_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.MASTERIES), ConnectionData(Entrance.town_to_sewer, Region.sewer, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.enter_mutant_bug_lair, Region.mutant_bug_lair, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_railroad, Region.railroad), @@ -295,6 +299,7 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sebastian_room, Region.sebastian_room, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.mountain_to_adventurer_guild, Region.adventurer_guild, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.LEAD_TO_OPEN_AREA), + ConnectionData(Entrance.adventurer_guild_to_bedroom, Region.adventurer_guild_bedroom), ConnectionData(Entrance.enter_quarry, Region.quarry), ConnectionData(Entrance.enter_quarry_mine_entrance, Region.quarry_mine_entrance, flag=RandomizationFlag.BUILDINGS), @@ -316,10 +321,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.enter_sunroom, Region.sunroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(Entrance.town_to_clint_blacksmith, Region.blacksmith, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.blacksmith_copper, Region.blacksmith_copper), - ConnectionData(Entrance.blacksmith_iron, Region.blacksmith_iron), - ConnectionData(Entrance.blacksmith_gold, Region.blacksmith_gold), - ConnectionData(Entrance.blacksmith_iridium, Region.blacksmith_iridium), ConnectionData(Entrance.town_to_saloon, Region.saloon, flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA), ConnectionData(Entrance.play_journey_of_the_prairie_king, Region.jotpk_world_1), @@ -354,10 +355,8 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.boat_to_ginger_island, Region.island_south, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.enter_tide_pools, Region.tide_pools), - ConnectionData(Entrance.fishing, Region.fishing), ConnectionData(Entrance.mountain_to_the_mines, Region.mines, flag=RandomizationFlag.NON_PROGRESSION | RandomizationFlag.LEAD_TO_OPEN_AREA), - ConnectionData(Entrance.talk_to_mines_dwarf, Region.mines_dwarf_shop), ConnectionData(Entrance.dig_to_mines_floor_5, Region.mines_floor_5), ConnectionData(Entrance.dig_to_mines_floor_10, Region.mines_floor_10), ConnectionData(Entrance.dig_to_mines_floor_15, Region.mines_floor_15), @@ -416,7 +415,6 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.use_island_resort, Region.island_resort, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_islandfarmhouse, Region.island_farmhouse, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.island_cooking, Region.kitchen), ConnectionData(Entrance.island_west_to_gourmand_cave, Region.gourmand_frog_cave, flag=RandomizationFlag.BUILDINGS | RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.island_west_to_crystals_cave, Region.colored_crystals_cave, @@ -454,15 +452,62 @@ def __call__(self, name: str, regions: Iterable[str]) -> Region: ConnectionData(Entrance.parrot_express_dig_site_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_docks_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), ConnectionData(Entrance.parrot_express_jungle_to_volcano, Region.island_north, flag=RandomizationFlag.GINGER_ISLAND), - ConnectionData(Entrance.attend_egg_festival, Region.egg_festival), - ConnectionData(Entrance.attend_flower_dance, Region.flower_dance), - ConnectionData(Entrance.attend_luau, Region.luau), - ConnectionData(Entrance.attend_moonlight_jellies, Region.moonlight_jellies), - ConnectionData(Entrance.attend_fair, Region.fair), - ConnectionData(Entrance.attend_spirit_eve, Region.spirit_eve), - ConnectionData(Entrance.attend_festival_of_ice, Region.festival_of_ice), - ConnectionData(Entrance.attend_night_market, Region.night_market), - ConnectionData(Entrance.attend_winter_star, Region.winter_star), + + ConnectionData(LogicEntrance.talk_to_mines_dwarf, LogicRegion.mines_dwarf_shop), + + ConnectionData(LogicEntrance.buy_from_traveling_merchant, LogicRegion.traveling_cart), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_sunday, LogicRegion.traveling_cart_sunday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_monday, LogicRegion.traveling_cart_monday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_tuesday, LogicRegion.traveling_cart_tuesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_wednesday, LogicRegion.traveling_cart_wednesday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_thursday, LogicRegion.traveling_cart_thursday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_friday, LogicRegion.traveling_cart_friday), + ConnectionData(LogicEntrance.buy_from_traveling_merchant_saturday, LogicRegion.traveling_cart_saturday), + ConnectionData(LogicEntrance.complete_raccoon_requests, LogicRegion.raccoon_daddy), + ConnectionData(LogicEntrance.fish_in_waterfall, LogicRegion.forest_waterfall), + ConnectionData(LogicEntrance.buy_from_raccoon, LogicRegion.raccoon_shop), + ConnectionData(LogicEntrance.farmhouse_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.watch_queen_of_sauce, LogicRegion.queen_of_sauce), + + ConnectionData(LogicEntrance.grow_spring_crops, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_spring_crops_in_greenhouse, LogicRegion.spring_farming), + ConnectionData(LogicEntrance.grow_summer_crops_in_greenhouse, LogicRegion.summer_farming), + ConnectionData(LogicEntrance.grow_fall_crops_in_greenhouse, LogicRegion.fall_farming), + ConnectionData(LogicEntrance.grow_winter_crops_in_greenhouse, LogicRegion.winter_farming), + ConnectionData(LogicEntrance.grow_indoor_crops_in_greenhouse, LogicRegion.indoor_farming), + ConnectionData(LogicEntrance.grow_spring_crops_on_island, LogicRegion.spring_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_crops_on_island, LogicRegion.summer_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_fall_crops_on_island, LogicRegion.fall_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_winter_crops_on_island, LogicRegion.winter_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_indoor_crops_on_island, LogicRegion.indoor_farming, flag=RandomizationFlag.GINGER_ISLAND), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_summer, LogicRegion.summer_or_fall_farming), + ConnectionData(LogicEntrance.grow_summer_fall_crops_in_fall, LogicRegion.summer_or_fall_farming), + + ConnectionData(LogicEntrance.shipping, LogicRegion.shipping), + ConnectionData(LogicEntrance.blacksmith_copper, LogicRegion.blacksmith_copper), + ConnectionData(LogicEntrance.blacksmith_iron, LogicRegion.blacksmith_iron), + ConnectionData(LogicEntrance.blacksmith_gold, LogicRegion.blacksmith_gold), + ConnectionData(LogicEntrance.blacksmith_iridium, LogicRegion.blacksmith_iridium), + ConnectionData(LogicEntrance.fishing, LogicRegion.fishing), + ConnectionData(LogicEntrance.island_cooking, LogicRegion.kitchen), + ConnectionData(LogicEntrance.attend_egg_festival, LogicRegion.egg_festival), + ConnectionData(LogicEntrance.attend_desert_festival, LogicRegion.desert_festival), + ConnectionData(LogicEntrance.attend_flower_dance, LogicRegion.flower_dance), + ConnectionData(LogicEntrance.attend_luau, LogicRegion.luau), + ConnectionData(LogicEntrance.attend_trout_derby, LogicRegion.trout_derby), + ConnectionData(LogicEntrance.attend_moonlight_jellies, LogicRegion.moonlight_jellies), + ConnectionData(LogicEntrance.attend_fair, LogicRegion.fair), + ConnectionData(LogicEntrance.attend_spirit_eve, LogicRegion.spirit_eve), + ConnectionData(LogicEntrance.attend_festival_of_ice, LogicRegion.festival_of_ice), + ConnectionData(LogicEntrance.attend_night_market, LogicRegion.night_market), + ConnectionData(LogicEntrance.attend_winter_star, LogicRegion.winter_star), + ConnectionData(LogicEntrance.attend_squidfest, LogicRegion.squidfest), + ConnectionData(LogicEntrance.buy_experience_books, LogicRegion.bookseller_1), + ConnectionData(LogicEntrance.buy_year1_books, LogicRegion.bookseller_2), + ConnectionData(LogicEntrance.buy_year3_books, LogicRegion.bookseller_3), ] @@ -499,12 +544,22 @@ def create_final_connections_and_regions(world_options) -> Tuple[Dict[str, Conne def remove_ginger_island_regions_and_connections(regions_by_name: Dict[str, RegionData], connections: Dict[str, ConnectionData], include_island: bool): if include_island: return connections, regions_by_name - for connection_name in list(connections): + + removed_connections = set() + + for connection_name in tuple(connections): connection = connections[connection_name] if connection.flag & RandomizationFlag.GINGER_ISLAND: - regions_by_name.pop(connection.destination, None) connections.pop(connection_name) - regions_by_name = {name: regions_by_name[name].get_without_exit(connection_name) for name in regions_by_name} + removed_connections.add(connection_name) + + for region_name in tuple(regions_by_name): + region = regions_by_name[region_name] + if region.is_ginger_island: + regions_by_name.pop(region_name) + else: + regions_by_name[region_name] = region.get_without_exits(removed_connections) + return connections, regions_by_name @@ -522,7 +577,6 @@ def modify_connections_for_mods(connections: Dict[str, ConnectionData], mods) -> def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionData) -> RegionData: - updated_region = existing_region region_exits = updated_region.exits modified_exits = modified_region.exits @@ -532,12 +586,16 @@ def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionD return updated_region -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) -> Tuple[ - Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) \ + -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: entrances_data, regions_data = create_final_connections_and_regions(world_options) regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data} - entrances_by_name: Dict[str: Entrance] = {entrance.name: entrance for region in regions_by_name.values() for entrance in region.exits - if entrance.name in entrances_data} + entrances_by_name: Dict[str: Entrance] = { + entrance.name: entrance + for region in regions_by_name.values() + for entrance in region.exits + if entrance.name in entrances_data + } connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data) @@ -556,7 +614,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r elif world_options.entrance_randomization == EntranceRandomization.option_non_progression: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.NON_PROGRESSION in connections_by_name[connection].flag] - elif world_options.entrance_randomization == EntranceRandomization.option_buildings: + elif world_options.entrance_randomization == EntranceRandomization.option_buildings or world_options.entrance_randomization == EntranceRandomization.option_buildings_without_house: connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] elif world_options.entrance_randomization == EntranceRandomization.option_chaos: @@ -590,6 +648,9 @@ def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], wo exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true if exclude_island: connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] + exclude_masteries = world_options.skill_progression != SkillProgression.option_progressive_with_masteries + if exclude_masteries: + connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag] return connections_to_randomize @@ -685,7 +746,7 @@ def swap_one_random_connection(regions_by_name, connections_by_name, randomized_ for connection in randomized_connections if connection != randomized_connections[connection]} unreachable_regions_names_leading_somewhere = tuple([region for region in unreachable_regions - if len(regions_by_name[region].exits) > 0]) + if len(regions_by_name[region].exits) > 0]) unreachable_regions_leading_somewhere = [regions_by_name[region_name] for region_name in unreachable_regions_names_leading_somewhere] unreachable_regions_exits_names = [exit_name for region in unreachable_regions_leading_somewhere for exit_name in region.exits] unreachable_connections = [connections_by_name[exit_name] for exit_name in unreachable_regions_exits_names] diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt index b0922176e43b..65e922a64483 100644 --- a/worlds/stardew_valley/requirements.txt +++ b/worlds/stardew_valley/requirements.txt @@ -1 +1,2 @@ importlib_resources; python_version <= '3.8' +graphlib_backport; python_version <= '3.8' diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 8002031ac792..c30d04c8a6f2 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,11 +1,16 @@ import itertools +import logging from typing import List, Dict, Set -from BaseClasses import MultiWorld +from BaseClasses import MultiWorld, CollectionState from worlds.generic import Rules as MultiWorldRules from . import locations from .bundles.bundle_room import BundleRoom +from .content import StardewContent +from .content.feature import friendsanity from .data.craftable_data import all_crafting_recipes_by_name +from .data.game_item import ItemTag +from .data.harvest import HarvestCropSource, HarvestFruitTreeSource from .data.museum_data import all_museum_items, dwarf_scrolls, skeleton_front, skeleton_middle, skeleton_back, all_museum_items_by_name, all_museum_minerals, \ all_museum_artifacts, Artifact from .data.recipe_data import all_cooking_recipes_by_name @@ -14,11 +19,14 @@ from .logic.time_logic import MAX_MONTHS from .logic.tool_logic import tool_upgrade_prices from .mods.mod_data import ModNames -from .options import StardewValleyOptions, Friendsanity +from .options import StardewValleyOptions, Walnutsanity from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ - Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, Cropsanity, SkillProgression -from .stardew_rule import And, StardewRule + Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, SkillProgression +from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection +from .stardew_rule.rule_explain import explain +from .strings.ap_names.ap_option_names import OptionName +from .strings.ap_names.community_upgrade_names import CommunityUpgrade from .strings.ap_names.event_names import Event from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes from .strings.ap_names.transport_names import Transportation @@ -26,13 +34,15 @@ from .strings.building_names import Building from .strings.bundle_names import CCRoom from .strings.calendar_names import Weekday -from .strings.craftable_names import Bomb -from .strings.crop_names import Fruit +from .strings.craftable_names import Bomb, Furniture +from .strings.crop_names import Fruit, Vegetable from .strings.entrance_names import dig_to_mines_floor, dig_to_skull_floor, Entrance, move_to_woods_depth, DeepWoodsEntrance, AlecEntrance, \ - SVEEntrance, LaceyEntrance, BoardingHouseEntrance -from .strings.generic_names import Generic + SVEEntrance, LaceyEntrance, BoardingHouseEntrance, LogicEntrance +from .strings.forageable_names import Forageable +from .strings.geode_names import Geode from .strings.material_names import Material -from .strings.metal_names import MetalBar +from .strings.metal_names import MetalBar, Mineral +from .strings.monster_names import Monster from .strings.performance_names import Performance from .strings.quest_names import Quest from .strings.region_names import Region @@ -43,10 +53,13 @@ from .strings.villager_names import NPC, ModNPC from .strings.wallet_item_names import Wallet +logger = logging.getLogger(__name__) + def set_rules(world): multiworld = world.multiworld world_options = world.options + world_content = world.content player = world.player logic = world.logic bundle_rooms: List[BundleRoom] = world.modified_bundles @@ -58,16 +71,16 @@ def set_rules(world): set_tool_rules(logic, multiworld, player, world_options) set_skills_rules(logic, multiworld, player, world_options) - set_bundle_rules(bundle_rooms, logic, multiworld, player) + set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options) set_building_rules(logic, multiworld, player, world_options) - set_cropsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_cropsanity_rules(logic, multiworld, player, world_content) set_story_quests_rules(all_location_names, logic, multiworld, player, world_options) set_special_order_rules(all_location_names, logic, multiworld, player, world_options) set_help_wanted_quests_rules(logic, multiworld, player, world_options) set_fishsanity_rules(all_location_names, logic, multiworld, player) set_museumsanity_rules(all_location_names, logic, multiworld, player, world_options) - set_friendsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_friendsanity_rules(logic, multiworld, player, world_content) set_backpack_rules(logic, multiworld, player, world_options) set_festival_rules(all_location_names, logic, multiworld, player) set_monstersanity_rules(all_location_names, logic, multiworld, player, world_options) @@ -75,6 +88,7 @@ def set_rules(world): set_cooksanity_rules(all_location_names, logic, multiworld, player, world_options) set_chefsanity_rules(all_location_names, logic, multiworld, player, world_options) set_craftsanity_rules(all_location_names, logic, multiworld, player, world_options) + set_booksanity_rules(logic, multiworld, player, world_content) set_isolated_locations_rules(logic, multiworld, player) set_traveling_merchant_day_rules(logic, multiworld, player) set_arcade_machine_rules(logic, multiworld, player, world_options) @@ -93,6 +107,8 @@ def set_isolated_locations_rules(logic: StardewLogic, multiworld, player): logic.money.can_spend(20000)) MultiWorldRules.add_rule(multiworld.get_location("Demetrius's Breakthrough", player), logic.money.can_have_earned_total(25000)) + MultiWorldRules.add_rule(multiworld.get_location("Pot Of Gold", player), + logic.season.has(Season.spring)) def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -104,8 +120,10 @@ def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: Stard MultiWorldRules.add_rule(multiworld.get_location("Purchase Iridium Rod", player), (logic.skill.has_level(Skill.fishing, 6) & logic.money.can_spend(7500))) + MultiWorldRules.add_rule(multiworld.get_location("Copper Pan Cutscene", player), logic.received("Glittering Boulder Removed")) + 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.trash_can, Tool.pan] for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): if previous is None: continue @@ -124,15 +142,23 @@ def set_building_rules(logic: StardewLogic, multiworld, player, world_options: S logic.registry.building_rules[building.name.replace(" Blueprint", "")]) -def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player): +def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): for bundle_room in bundle_rooms: room_rules = [] for bundle in bundle_room.bundles: location = multiworld.get_location(bundle.name, player) bundle_rules = logic.bundle.can_complete_bundle(bundle) + if bundle_room.name == CCRoom.raccoon_requests: + num = int(bundle.name[-1]) + extra_raccoons = 1 if world_options.quest_locations >= 0 else 0 + extra_raccoons = extra_raccoons + num + bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules + if num > 1: + previous_bundle_name = f"Raccoon Request {num-1}" + bundle_rules = bundle_rules & logic.region.can_reach_location(previous_bundle_name) room_rules.append(bundle_rules) MultiWorldRules.set_rule(location, bundle_rules) - if bundle_room.name == CCRoom.abandoned_joja_mart: + if bundle_room.name == CCRoom.abandoned_joja_mart or bundle_room.name == CCRoom.raccoon_requests: continue room_location = f"Complete {bundle_room.name}" MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules)) @@ -145,6 +171,10 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta for i in range(1, 11): set_vanilla_skill_rule_for_level(logic, multiworld, player, i) set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) + if world_options.skill_progression != SkillProgression.option_progressive_with_masteries: + return + for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery_experience) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -181,7 +211,7 @@ def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int): - rule = logic.mod.skill.can_earn_mod_skill_level(skill, level) + rule = logic.skill.can_earn_level(skill, level) MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule) @@ -189,7 +219,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_mines_floor_entrance_rules(logic, multiworld, player) set_skull_cavern_floor_entrance_rules(logic, multiworld, player) set_blacksmith_entrance_rules(logic, multiworld, player) - set_skill_entrance_rules(logic, multiworld, player) + set_skill_entrance_rules(logic, multiworld, player, world_options) set_traveling_merchant_day_rules(logic, multiworld, player) set_dangerous_mine_rules(logic, multiworld, player, world_options) @@ -204,8 +234,12 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_entrance_rule(multiworld, player, Entrance.purchase_movie_ticket, movie_theater_rule) set_entrance_rule(multiworld, player, Entrance.take_bus_to_desert, logic.received("Bus Repair")) set_entrance_rule(multiworld, player, Entrance.enter_skull_cavern, logic.received(Wallet.skull_key)) - set_entrance_rule(multiworld, player, Entrance.talk_to_mines_dwarf, logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) - set_entrance_rule(multiworld, player, Entrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.talk_to_mines_dwarf, + logic.wallet.can_speak_dwarf() & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_traveling_merchant, logic.traveling_merchant.has_days()) + set_entrance_rule(multiworld, player, LogicEntrance.buy_from_raccoon, logic.quest.has_raccoon_shop()) + set_entrance_rule(multiworld, player, LogicEntrance.fish_in_waterfall, + logic.skill.has_level(Skill.fishing, 5) & logic.tool.has_fishing_rod(2)) set_farm_buildings_entrance_rules(logic, multiworld, player) @@ -218,10 +252,15 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S set_bedroom_entrance_rules(logic, multiworld, player, world_options) set_festival_entrance_rules(logic, multiworld, player) - set_island_entrance_rule(multiworld, player, Entrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) - set_entrance_rule(multiworld, player, Entrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) - set_entrance_rule(multiworld, player, Entrance.shipping, logic.shipping.can_use_shipping_bin) - set_entrance_rule(multiworld, player, Entrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) + set_island_entrance_rule(multiworld, player, LogicEntrance.island_cooking, logic.cooking.can_cook_in_kitchen, world_options) + set_entrance_rule(multiworld, player, LogicEntrance.farmhouse_cooking, logic.cooking.can_cook_in_kitchen) + set_entrance_rule(multiworld, player, LogicEntrance.shipping, logic.shipping.can_use_shipping_bin) + set_entrance_rule(multiworld, player, LogicEntrance.watch_queen_of_sauce, logic.action.can_watch(Channel.queen_of_sauce)) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, Entrance.forest_to_mastery_cave, logic.skill.can_enter_mastery_cave()) + set_entrance_rule(multiworld, player, LogicEntrance.buy_experience_books, logic.time.has_lived_months(2)) + set_entrance_rule(multiworld, player, LogicEntrance.buy_year1_books, logic.time.has_year_two) + set_entrance_rule(multiworld, player, LogicEntrance.buy_year3_books, logic.time.has_year_three) def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewValleyOptions): @@ -236,6 +275,7 @@ def set_dangerous_mine_rules(logic, multiworld, player, world_options: StardewVa def set_farm_buildings_entrance_rules(logic, multiworld, player): + set_entrance_rule(multiworld, player, Entrance.downstairs_to_cellar, logic.building.has_house(3)) set_entrance_rule(multiworld, player, Entrance.use_desert_obelisk, logic.can_use_obelisk(Transportation.desert_obelisk)) set_entrance_rule(multiworld, player, Entrance.enter_greenhouse, logic.received("Greenhouse")) set_entrance_rule(multiworld, player, Entrance.enter_coop, logic.building.has_building(Building.coop)) @@ -277,15 +317,28 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player): def set_blacksmith_entrance_rules(logic, multiworld, player): - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) - set_blacksmith_upgrade_rule(logic, multiworld, player, Entrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) - - -def set_skill_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.farming, logic.skill.can_get_farming_xp) - set_entrance_rule(multiworld, player, Entrance.fishing, logic.skill.can_get_fishing_xp) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold) + set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium) + + +def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions): + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops, logic.farming.has_farming_tools & logic.season.has_fall) + set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_in_greenhouse, logic.farming.has_farming_tools) + set_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_in_greenhouse, logic.farming.has_farming_tools) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_fall_crops_on_island, logic.farming.has_farming_tools, world_options) + set_island_entrance_rule(multiworld, player, LogicEntrance.grow_indoor_crops_on_island, logic.farming.has_farming_tools, world_options) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_summer, true_) + set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_fall_crops_in_fall, true_) + + set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp) def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str): @@ -295,18 +348,21 @@ def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, i def set_festival_entrance_rules(logic, multiworld, player): - set_entrance_rule(multiworld, player, Entrance.attend_egg_festival, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_flower_dance, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_egg_festival, logic.season.has(Season.spring)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_desert_festival, logic.season.has(Season.spring) & logic.received("Bus Repair")) + set_entrance_rule(multiworld, player, LogicEntrance.attend_flower_dance, logic.season.has(Season.spring)) - set_entrance_rule(multiworld, player, Entrance.attend_luau, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_moonlight_jellies, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_luau, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_trout_derby, logic.season.has(Season.summer)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_moonlight_jellies, logic.season.has(Season.summer)) - set_entrance_rule(multiworld, player, Entrance.attend_fair, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_spirit_eve, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_fair, logic.season.has(Season.fall)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_spirit_eve, logic.season.has(Season.fall)) - set_entrance_rule(multiworld, player, Entrance.attend_festival_of_ice, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_night_market, logic.season.has(Season.winter)) - set_entrance_rule(multiworld, player, Entrance.attend_winter_star, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_festival_of_ice, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_squidfest, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_night_market, logic.season.has(Season.winter)) + set_entrance_rule(multiworld, player, LogicEntrance.attend_winter_star, logic.season.has(Season.winter)) def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -320,6 +376,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), logic.can_complete_field_office()) + set_walnut_rules(logic, multiworld, player, world_options) def set_boat_repair_rules(logic: StardewLogic, multiworld, player): @@ -374,10 +431,11 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): - has_walnut = logic.has_walnut(1) - has_5_walnut = logic.has_walnut(5) - has_10_walnut = logic.has_walnut(10) - has_20_walnut = logic.has_walnut(20) + # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" + has_walnut = logic.has_walnut(5) + has_5_walnut = logic.has_walnut(15) + has_10_walnut = logic.has_walnut(40) + has_20_walnut = logic.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -403,17 +461,82 @@ def set_island_parrot_rules(logic: StardewLogic, multiworld, player): has_10_walnut) -def set_cropsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.cropsanity == Cropsanity.option_disabled: +def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): + if world_options.walnutsanity == Walnutsanity.preset_none: + return + + set_walnut_puzzle_rules(logic, multiworld, player, world_options) + set_walnut_bushes_rules(logic, multiworld, player, world_options) + set_walnut_dig_spot_rules(logic, multiworld, player, world_options) + set_walnut_repeatable_rules(logic, multiworld, player, world_options) + + +def set_walnut_puzzle_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: + return + + MultiWorldRules.add_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut)) + MultiWorldRules.add_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana)) + MultiWorldRules.add_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe)) + MultiWorldRules.add_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) & + logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & + logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) + MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) + + +def set_walnut_bushes_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_bushes not in world_options.walnutsanity: + return + # I don't think any of the bushes require something special, but that might change with ER + return + + +def set_walnut_dig_spot_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_dig_spots not in world_options.walnutsanity: return - harvest_prefix = "Harvest " - harvest_prefix_length = len(harvest_prefix) - for harvest_location in locations.locations_by_tag[LocationTags.CROPSANITY]: - if harvest_location.name in all_location_names and (harvest_location.mod_name is None or harvest_location.mod_name in world_options.mods): - crop_name = harvest_location.name[harvest_prefix_length:] - MultiWorldRules.set_rule(multiworld.get_location(harvest_location.name, player), - logic.has(crop_name)) + for dig_spot_walnut in locations.locations_by_tag[LocationTags.WALNUTSANITY_DIG]: + rule = logic.tool.has_tool(Tool.hoe) + if "Journal Scrap" in dig_spot_walnut.name: + rule = rule & logic.has(Forageable.journal_scrap) + if "Starfish Diamond" in dig_spot_walnut.name: + rule = rule & logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) + MultiWorldRules.set_rule(multiworld.get_location(dig_spot_walnut.name, player), rule) + + +def set_walnut_repeatable_rules(logic, multiworld, player, world_options): + if OptionName.walnutsanity_repeatables not in world_options.walnutsanity: + return + for i in range(1, 6): + MultiWorldRules.set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1)) + MultiWorldRules.set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp) + MultiWorldRules.set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe)) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon) + MultiWorldRules.set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime)) + + +def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent): + if not world_content.features.cropsanity.is_enabled: + return + + for item in world_content.find_tagged_items(ItemTag.CROPSANITY): + location = world_content.features.cropsanity.to_location_name(item.name) + harvest_sources = (source for source in item.sources if isinstance(source, (HarvestFruitTreeSource, HarvestCropSource))) + MultiWorldRules.set_rule(multiworld.get_location(location, player), logic.source.has_access_to_any(harvest_sources)) def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): @@ -427,23 +550,21 @@ def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, mu def set_special_order_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if world_options.special_order_locations == SpecialOrderLocations.option_disabled: - return - board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) - for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_order.name in all_location_names: - order_rule = board_rule & logic.registry.special_order_rules[board_order.name] - MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.option_board: + board_rule = logic.received("Special Order Board") & logic.time.has_lived_months(4) + for board_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_order.name in all_location_names: + order_rule = board_rule & logic.registry.special_order_rules[board_order.name] + MultiWorldRules.set_rule(multiworld.get_location(board_order.name, player), order_rule) if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - if world_options.special_order_locations == SpecialOrderLocations.option_board_only: - return - qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) - for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_order.name in all_location_names: - order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] - MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) + if world_options.special_order_locations & SpecialOrderLocations.value_qi: + qi_rule = logic.region.can_reach(Region.qi_walnut_room) & logic.time.has_lived_months(8) + for qi_order in locations.locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_order.name in all_location_names: + order_rule = qi_rule & logic.registry.special_order_rules[qi_order.name] + MultiWorldRules.set_rule(multiworld.get_location(qi_order.name, player), order_rule) help_wanted_prefix = "Help Wanted:" @@ -730,6 +851,21 @@ def set_craftsanity_rules(all_location_names: Set[str], logic: StardewLogic, mul MultiWorldRules.set_rule(multiworld.get_location(location.name, player), craft_rule) +def set_booksanity_rules(logic: StardewLogic, multiworld, player, content: StardewContent): + booksanity = content.features.booksanity + if not booksanity.is_enabled: + return + + for book in content.find_tagged_items(ItemTag.BOOK): + if booksanity.is_included(book): + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book.name), player), logic.has(book.name)) + + for i, book in enumerate(booksanity.get_randomized_lost_books()): + if i <= 0: + continue + MultiWorldRules.set_rule(multiworld.get_location(booksanity.to_location_name(book), player), logic.received(booksanity.progressive_lost_book, i)) + + def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld, player: int): for day in Weekday.all_days: item_for_day = f"Traveling Merchant: {day}" @@ -761,28 +897,26 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player logic.has("JotPK Max Buff")) -def set_friendsanity_rules(all_location_names: Set[str], logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): - if world_options.friendsanity == Friendsanity.option_none: +def set_friendsanity_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent): + if not content.features.friendsanity.is_enabled: return MultiWorldRules.add_rule(multiworld.get_location("Spouse Stardrop", player), - logic.relationship.has_hearts(Generic.bachelor, 13)) + logic.relationship.has_hearts_with_any_bachelor(13)) MultiWorldRules.add_rule(multiworld.get_location("Have a Baby", player), logic.relationship.can_reproduce(1)) MultiWorldRules.add_rule(multiworld.get_location("Have Another Baby", player), logic.relationship.can_reproduce(2)) - friend_prefix = "Friendsanity: " - friend_suffix = " <3" - for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: - if not friend_location.name in all_location_names: - continue - friend_location_without_prefix = friend_location.name[len(friend_prefix):] - friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] - split_index = friend_location_trimmed.rindex(" ") - friend_name = friend_location_trimmed[:split_index] - num_hearts = int(friend_location_trimmed[split_index + 1:]) - MultiWorldRules.set_rule(multiworld.get_location(friend_location.name, player), - logic.relationship.can_earn_relationship(friend_name, num_hearts)) + for villager in content.villagers.values(): + for heart in content.features.friendsanity.get_randomized_hearts(villager): + rule = logic.relationship.can_earn_relationship(villager.name, heart) + location_name = friendsanity.to_location_name(villager.name, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) + + for heart in content.features.friendsanity.get_pet_randomized_hearts(): + rule = logic.pet.can_befriend_pet(heart) + location_name = friendsanity.to_location_name(NPC.pet, heart) + MultiWorldRules.set_rule(multiworld.get_location(location_name, player), rule) def set_deepwoods_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): @@ -876,7 +1010,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl set_entrance_rule(multiworld, player, SVEEntrance.outpost_warp_to_outpost, logic.received(SVERunes.nexus_outpost)) set_entrance_rule(multiworld, player, SVEEntrance.wizard_warp_to_wizard, logic.received(SVERunes.nexus_wizard)) set_entrance_rule(multiworld, player, SVEEntrance.use_purple_junimo, logic.relationship.has_hearts(ModNPC.apples, 10)) - set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.received(SVEQuestItem.grandpa_shed)) + set_entrance_rule(multiworld, player, SVEEntrance.grandpa_interior_to_upstairs, logic.mod.sve.has_grandpa_shed_repaired()) set_entrance_rule(multiworld, player, SVEEntrance.use_bear_shop, (logic.mod.sve.can_buy_bear_recipe())) set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer)) set_entrance_rule(multiworld, player, SVEEntrance.museum_to_gunther_bedroom, logic.relationship.has_hearts(ModNPC.gunther, 2)) @@ -891,7 +1025,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): if world_options.exclude_ginger_island == ExcludeGingerIsland.option_true: return - set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.received(SVEQuestItem.marlon_boat_paddle)) + set_entrance_rule(multiworld, player, SVEEntrance.summit_to_highlands, logic.mod.sve.has_marlon_boat()) set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) @@ -904,12 +1038,16 @@ def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule): - potentially_required_regions = look_for_indirect_connection(rule) - if potentially_required_regions: - for region in potentially_required_regions: - multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) - - MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + try: + potentially_required_regions = look_for_indirect_connection(rule) + if potentially_required_regions: + for region in potentially_required_regions: + multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player)) + + MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule) + except KeyError as ex: + logger.error(f"""Failed to evaluate indirect connection in: {explain(rule, CollectionState(multiworld))}""") + raise ex def set_island_entrance_rule(multiworld, player, entrance: str, rule: StardewRule, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py index 1dc60f79b14b..c181faec7b94 100644 --- a/worlds/stardew_valley/scripts/export_locations.py +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -16,11 +16,17 @@ if __name__ == "__main__": with open("output/stardew_valley_location_table.json", "w+") as f: locations = { + "Cheat Console": + {"code": -1, "region": "Archipelago"}, + "Server": + {"code": -2, "region": "Archipelago"} + } + locations.update({ location.name: { "code": location.code, "region": location.region, } for location in location_table.values() if location.code is not None - } + }) json.dump({"locations": locations}, f) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 007d2b64dc41..576cd36851fb 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -1,7 +1,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections import deque +from collections import deque, Counter +from dataclasses import dataclass, field from functools import cached_property from itertools import chain from threading import Lock @@ -295,7 +296,10 @@ def __eq__(self, other): self.simplification_state.original_simplifiable_rules == self.simplification_state.original_simplifiable_rules) def __hash__(self): - return hash((id(self.combinable_rules), self.simplification_state.original_simplifiable_rules)) + if len(self.combinable_rules) + len(self.simplification_state.original_simplifiable_rules) > 5: + return id(self) + + return hash((*self.combinable_rules.values(), self.simplification_state.original_simplifiable_rules)) class Or(AggregatingStardewRule): @@ -323,9 +327,6 @@ def __or__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return min(left, right, key=lambda x: x.value) - def get_difficulty(self): - return min(rule.get_difficulty() for rule in self.original_rules) - class And(AggregatingStardewRule): identity = true_ @@ -352,19 +353,34 @@ def __and__(self, other): def combine(left: CombinableStardewRule, right: CombinableStardewRule) -> CombinableStardewRule: return max(left, right, key=lambda x: x.value) - def get_difficulty(self): - return max(rule.get_difficulty() for rule in self.original_rules) - class Count(BaseStardewRule): count: int rules: List[StardewRule] + counter: Counter[StardewRule] + evaluate: Callable[[CollectionState], bool] + + total: Optional[int] + rule_mapping: Optional[Dict[StardewRule, StardewRule]] def __init__(self, rules: List[StardewRule], count: int): - self.rules = rules self.count = count + self.counter = Counter(rules) + + if len(self.counter) / len(rules) < .66: + # Checking if it's worth using the count operation with shortcircuit or not. Value should be fine-tuned when Count has more usage. + self.total = sum(self.counter.values()) + self.rules = sorted(self.counter.keys(), key=lambda x: self.counter[x], reverse=True) + self.rule_mapping = {} + self.evaluate = self.evaluate_with_shortcircuit + else: + self.rules = rules + self.evaluate = self.evaluate_without_shortcircuit - def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + def __call__(self, state: CollectionState) -> bool: + return self.evaluate(state) + + def evaluate_without_shortcircuit(self, state: CollectionState) -> bool: c = 0 for i in range(self.rules_count): self.rules[i], value = self.rules[i].evaluate_while_simplifying(state) @@ -372,37 +388,58 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul c += 1 if c >= self.count: - return self, True + return True if c + self.rules_count - i < self.count: break - return self, False + return False - def __call__(self, state: CollectionState) -> bool: - return self.evaluate_while_simplifying(state)[1] + def evaluate_with_shortcircuit(self, state: CollectionState) -> bool: + c = 0 + t = self.total + + for rule in self.rules: + evaluation_value = self.call_evaluate_while_simplifying_cached(rule, state) + rule_value = self.counter[rule] + + if evaluation_value: + c += rule_value + else: + t -= rule_value + + if c >= self.count: + return True + elif t < self.count: + break + + return False + + def call_evaluate_while_simplifying_cached(self, rule: StardewRule, state: CollectionState) -> bool: + try: + # A mapping table with the original rule is used here because two rules could resolve to the same rule. + # This would require to change the counter to merge both rules, and quickly become complicated. + return self.rule_mapping[rule](state) + except KeyError: + self.rule_mapping[rule], value = rule.evaluate_while_simplifying(state) + return value + + def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + return self, self(state) @cached_property def rules_count(self): return len(self.rules) - def get_difficulty(self): - self.rules = sorted(self.rules, key=lambda x: x.get_difficulty()) - # In an optimal situation, all the simplest rules will be true. Since the rules are sorted, we know that the most difficult rule we might have to do - # is the one at the "self.count". - return self.rules[self.count - 1].get_difficulty() - def __repr__(self): return f"Received {self.count} {repr(self.rules)}" +@dataclass(frozen=True) class Has(BaseStardewRule): item: str # For sure there is a better way than just passing all the rules everytime - other_rules: Dict[str, StardewRule] - - def __init__(self, item: str, other_rules: Dict[str, StardewRule]): - self.item = item - self.other_rules = other_rules + other_rules: Dict[str, StardewRule] = field(repr=False, hash=False, compare=False) + group: str = "item" def __call__(self, state: CollectionState) -> bool: return self.evaluate_while_simplifying(state)[1] @@ -410,21 +447,15 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self.other_rules[self.item].evaluate_while_simplifying(state) - def get_difficulty(self): - return self.other_rules[self.item].get_difficulty() + 1 - def __str__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item}" + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group})" def __repr__(self): if self.item not in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item} -> {repr(self.other_rules[self.item])}" - - def __hash__(self): - return hash(self.item) + return f"Has {self.item} ({self.group}) -> {MISSING_ITEM}" + return f"Has {self.item} ({self.group}) -> {repr(self.other_rules[self.item])}" class RepeatableChain(Iterable, Sized): diff --git a/worlds/stardew_valley/stardew_rule/indirect_connection.py b/worlds/stardew_valley/stardew_rule/indirect_connection.py index 2bbddb16818f..17433f7df4a8 100644 --- a/worlds/stardew_valley/stardew_rule/indirect_connection.py +++ b/worlds/stardew_valley/stardew_rule/indirect_connection.py @@ -6,34 +6,38 @@ def look_for_indirect_connection(rule: StardewRule) -> Set[str]: required_regions = set() - _find(rule, required_regions) + _find(rule, required_regions, depth=0) return required_regions @singledispatch -def _find(rule: StardewRule, regions: Set[str]): +def _find(rule: StardewRule, regions: Set[str], depth: int): ... @_find.register -def _(rule: AggregatingStardewRule, regions: Set[str]): +def _(rule: AggregatingStardewRule, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.original_rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Count, regions: Set[str]): +def _(rule: Count, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" for r in rule.rules: - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Has, regions: Set[str]): +def _(rule: Has, regions: Set[str], depth: int): + assert depth < 50, f"Recursion depth exceeded on {rule.item}" r = rule.other_rules[rule.item] - _find(r, regions) + _find(r, regions, depth + 1) @_find.register -def _(rule: Reach, regions: Set[str]): +def _(rule: Reach, regions: Set[str], depth: int): + assert depth < 50, "Recursion depth exceeded" if rule.resolution_hint == "Region": regions.add(rule.spot) diff --git a/worlds/stardew_valley/stardew_rule/literal.py b/worlds/stardew_valley/stardew_rule/literal.py index 58f7bae047fa..93a8503e8739 100644 --- a/worlds/stardew_valley/stardew_rule/literal.py +++ b/worlds/stardew_valley/stardew_rule/literal.py @@ -33,9 +33,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return other - def get_difficulty(self): - return 0 - class False_(LiteralStardewRule): # noqa value = False @@ -52,9 +49,6 @@ def __or__(self, other) -> StardewRule: def __and__(self, other) -> StardewRule: return self - def get_difficulty(self): - return 999999999 - false_ = False_() true_ = True_() diff --git a/worlds/stardew_valley/stardew_rule/protocol.py b/worlds/stardew_valley/stardew_rule/protocol.py index c20394d5b826..f69a3663c63a 100644 --- a/worlds/stardew_valley/stardew_rule/protocol.py +++ b/worlds/stardew_valley/stardew_rule/protocol.py @@ -24,7 +24,3 @@ def __or__(self, other: StardewRule): @abstractmethod def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: ... - - @abstractmethod - def get_difficulty(self): - ... diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py new file mode 100644 index 000000000000..61a88ceb6996 --- /dev/null +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import cached_property, singledispatch +from typing import Iterable, Set, Tuple, List, Optional + +from BaseClasses import CollectionState +from worlds.generic.Rules import CollectionRule +from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_ + + +@dataclass +class RuleExplanation: + rule: StardewRule + state: CollectionState + expected: bool + sub_rules: Iterable[StardewRule] = field(default_factory=list) + explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set) + current_rule_explored: bool = False + + def __post_init__(self): + checkpoint = _rule_key(self.rule) + if checkpoint is not None and checkpoint in self.explored_rules_key: + self.current_rule_explored = True + self.sub_rules = [] + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + def __str__(self, depth=0): + if not self.sub_rules: + return self.summary(depth) + + return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) + if i.result is not self.expected else i.summary(depth + 1) + for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) + + def __repr__(self, depth=0): + if not self.sub_rules: + return self.summary(depth) + + return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) + for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) + + @cached_property + def result(self) -> bool: + try: + return self.rule(self.state) + except KeyError: + return False + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + rule_key = _rule_key(self.rule) + if rule_key is not None: + self.explored_rules_key.add(rule_key) + + return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] + + +def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: + if isinstance(rule, StardewRule): + return _explain(rule, state, expected, explored_spots=set()) + else: + return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa + + +@singledispatch +def _explain(rule: StardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, rule.original_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Has, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + try: + return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]], explored_rules_key=explored_spots) + except KeyError: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: TotalReceived, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items], explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.resolution_hint == 'Location': + spot = state.multiworld.get_location(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + + elif rule.resolution_hint == 'Entrance': + spot = state.multiworld.get_entrance(rule.spot, rule.player) + + if isinstance(spot.access_rule, StardewRule): + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + + else: + spot = state.multiworld.get_region(rule.spot, rule.player) + access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@_explain.register +def _(rule: Received, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: + access_rules = None + if rule.event: + try: + spot = state.multiworld.get_location(rule.item, rule.player) + if spot.access_rule is true_: + access_rules = [Reach(spot.parent_region.name, "Region", rule.player)] + else: + access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] + except KeyError: + pass + + if not access_rules: + return RuleExplanation(rule, state, expected, explored_rules_key=explored_spots) + + return RuleExplanation(rule, state, expected, access_rules, explored_rules_key=explored_spots) + + +@singledispatch +def _rule_key(_: StardewRule) -> Optional[Tuple[str, str]]: + return None + + +@_rule_key.register +def _(rule: Reach) -> Tuple[str, str]: + return rule.spot, rule.resolution_hint + + +@_rule_key.register +def _(rule: Received) -> Optional[Tuple[str, str]]: + if not rule.event: + return None + + return rule.item, "Logic Event" diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index a0fce7c7c19e..cf0996a63bbc 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -1,10 +1,9 @@ from dataclasses import dataclass from typing import Iterable, Union, List, Tuple, Hashable -from BaseClasses import ItemClassification, CollectionState +from BaseClasses import CollectionState from .base import BaseStardewRule, CombinableStardewRule from .protocol import StardewRule -from ..items import item_table class TotalReceived(BaseStardewRule): @@ -20,11 +19,6 @@ def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): else: items_list = [items] - assert items_list, "Can't create a Total Received conditions without items" - for item in items_list: - assert item_table[item].classification & ItemClassification.progression, \ - f"Item [{item_table[item].name}] has to be progression to be used in logic" - self.player = player self.items = items_list self.count = count @@ -40,9 +34,6 @@ def __call__(self, state: CollectionState) -> bool: def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) - def get_difficulty(self): - return self.count - def __repr__(self): return f"Received {self.count} {self.items}" @@ -52,10 +43,8 @@ class Received(CombinableStardewRule): item: str player: int count: int - - def __post_init__(self): - assert item_table[self.item].classification & ItemClassification.progression, \ - f"Item [{item_table[self.item].name}] has to be progression to be used in logic" + event: bool = False + """Helps `explain` to know it can dig into a location with the same name.""" @property def combination_key(self) -> Hashable: @@ -73,11 +62,8 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): if self.count == 1: - return f"Received {self.item}" - return f"Received {self.count} {self.item}" - - def get_difficulty(self): - return self.count + return f"Received {'event ' if self.event else ''}{self.item}" + return f"Received {'event ' if self.event else ''}{self.count} {self.item}" @dataclass(frozen=True) @@ -97,9 +83,6 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul def __repr__(self): return f"Reach {self.resolution_hint} {self.spot}" - def get_difficulty(self): - return 1 - @dataclass(frozen=True) class HasProgressionPercent(CombinableStardewRule): @@ -122,19 +105,21 @@ def __call__(self, state: CollectionState) -> bool: stardew_world = state.multiworld.worlds[self.player] total_count = stardew_world.total_progression_items needed_count = (total_count * self.percent) // 100 + player_state = state.prog_items[self.player] + + if needed_count <= len(player_state): + return True + total_count = 0 - for item in state.prog_items[self.player]: - item_count = state.prog_items[self.player][item] + for item, item_count in player_state.items(): total_count += item_count if total_count >= needed_count: return True + return False def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: return self, self(state) def __repr__(self): - return f"HasProgressionPercent {self.percent}" - - def get_difficulty(self): - return self.percent + return f"Received {self.percent}% progression items." diff --git a/worlds/stardew_valley/strings/ap_names/ap_option_names.py b/worlds/stardew_valley/strings/ap_names/ap_option_names.py new file mode 100644 index 000000000000..a5cc10f7d7b8 --- /dev/null +++ b/worlds/stardew_valley/strings/ap_names/ap_option_names.py @@ -0,0 +1,16 @@ +class OptionName: + walnutsanity_puzzles = "Puzzles" + walnutsanity_bushes = "Bushes" + walnutsanity_dig_spots = "Dig Spots" + walnutsanity_repeatables = "Repeatables" + buff_luck = "Luck" + buff_damage = "Damage" + buff_defense = "Defense" + buff_immunity = "Immunity" + buff_health = "Health" + buff_energy = "Energy" + buff_bite = "Bite Rate" + buff_fish_trap = "Fish Trap" + buff_fishing_bar = "Fishing Bar Size" + buff_quality = "Quality" + buff_glow = "Glow" diff --git a/worlds/stardew_valley/strings/ap_names/buff_names.py b/worlds/stardew_valley/strings/ap_names/buff_names.py index 4ddd6fb5034f..0f311869aa9a 100644 --- a/worlds/stardew_valley/strings/ap_names/buff_names.py +++ b/worlds/stardew_valley/strings/ap_names/buff_names.py @@ -1,3 +1,13 @@ class Buff: movement = "Movement Speed Bonus" - luck = "Luck Bonus" \ No newline at end of file + luck = "Luck Bonus" + damage = "Damage Bonus" + defense = "Defense Bonus" + immunity = "Immunity Bonus" + health = "Health Bonus" + energy = "Energy Bonus" + bite_rate = "Bite Rate Bonus" + fish_trap = "Fish Trap Bonus" + fishing_bar = "Fishing Bar Size Bonus" + quality = "Quality Bonus" + glow = "Glow Bonus" diff --git a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py index 68dad8e75287..6826b9234a30 100644 --- a/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py +++ b/worlds/stardew_valley/strings/ap_names/community_upgrade_names.py @@ -1,4 +1,6 @@ class CommunityUpgrade: + raccoon = "Progressive Raccoon" fruit_bats = "Fruit Bats" mushroom_boxes = "Mushroom Boxes" movie_theater = "Progressive Movie Theater" + mr_qi_plane_ride = "Mr Qi's Plane Ride" diff --git a/worlds/stardew_valley/strings/ap_names/event_names.py b/worlds/stardew_valley/strings/ap_names/event_names.py index 08b9d8f8131c..88f9715abc65 100644 --- a/worlds/stardew_valley/strings/ap_names/event_names.py +++ b/worlds/stardew_valley/strings/ap_names/event_names.py @@ -1,6 +1,20 @@ +all_events = set() + + +def event(name: str): + all_events.add(name) + return name + + class Event: - victory = "Victory" - can_construct_buildings = "Can Construct Buildings" - start_dark_talisman_quest = "Start Dark Talisman Quest" - can_ship_items = "Can Ship Items" - can_shop_at_pierre = "Can Shop At Pierre's" + victory = event("Victory") + can_construct_buildings = event("Can Construct Buildings") + start_dark_talisman_quest = event("Start Dark Talisman Quest") + can_ship_items = event("Can Ship Items") + can_shop_at_pierre = event("Can Shop At Pierre's") + spring_farming = event("Spring Farming") + summer_farming = event("Summer Farming") + fall_farming = event("Fall Farming") + winter_farming = event("Winter Farming") + + received_walnuts = event("Received Walnuts") diff --git a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py index ccc2765544a6..58371aebe7ed 100644 --- a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py +++ b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py @@ -9,6 +9,10 @@ class DeepWoodsItem: class SkillLevel: + cooking = "Cooking Level" + binning = "Binning Level" + magic = "Magic Level" + socializing = "Socializing Level" luck = "Luck Level" archaeology = "Archaeology Level" @@ -25,8 +29,10 @@ class SVEQuestItem: fable_reef_portal = "Fable Reef Portal" grandpa_shed = "Grandpa's Shed" - sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, kittyfish_spell, scarlett_job_offer, morgan_schooling, grandpa_shed] - sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle, fable_reef_portal] + sve_always_quest_items: List[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling] + sve_always_quest_items_ginger_island: List[str] = [fable_reef_portal] + sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, grandpa_shed] + sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle] class SVELocation: diff --git a/worlds/stardew_valley/strings/artisan_good_names.py b/worlds/stardew_valley/strings/artisan_good_names.py index a017cff1f9dd..366189568cf7 100644 --- a/worlds/stardew_valley/strings/artisan_good_names.py +++ b/worlds/stardew_valley/strings/artisan_good_names.py @@ -21,6 +21,45 @@ class ArtisanGood: caviar = "Caviar" green_tea = "Green Tea" mead = "Mead" + mystic_syrup = "Mystic Syrup" + dried_fruit = "Dried Fruit" + dried_mushroom = "Dried Mushrooms" + raisins = "Raisins" + stardrop_tea = "Stardrop Tea" + smoked_fish = "Smoked Fish" + targeted_bait = "Targeted Bait" + + @classmethod + def specific_wine(cls, fruit: str) -> str: + return f"{cls.wine} [{fruit}]" + + @classmethod + def specific_juice(cls, vegetable: str) -> str: + return f"{cls.juice} [{vegetable}]" + + @classmethod + def specific_jelly(cls, fruit: str) -> str: + return f"{cls.jelly} [{fruit}]" + + @classmethod + def specific_pickles(cls, vegetable: str) -> str: + return f"{cls.pickles} [{vegetable}]" + + @classmethod + def specific_dried_fruit(cls, food: str) -> str: + return f"{cls.dried_fruit} [{food}]" + + @classmethod + def specific_dried_mushroom(cls, food: str) -> str: + return f"{cls.dried_mushroom} [{food}]" + + @classmethod + def specific_smoked_fish(cls, fish: str) -> str: + return f"{cls.smoked_fish} [{fish}]" + + @classmethod + def specific_bait(cls, fish: str) -> str: + return f"{cls.targeted_bait} [{fish}]" class ModArtisanGood: diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py new file mode 100644 index 000000000000..3c32cd81b326 --- /dev/null +++ b/worlds/stardew_valley/strings/book_names.py @@ -0,0 +1,65 @@ +class Book: + animal_catalogue = "Animal Catalogue" + book_of_mysteries = "Book of Mysteries" + book_of_stars = "Book Of Stars" + stardew_valley_almanac = "Stardew Valley Almanac" + bait_and_bobber = "Bait And Bobber" + mining_monthly = "Mining Monthly" + combat_quarterly = "Combat Quarterly" + woodcutters_weekly = "Woodcutter's Weekly" + the_alleyway_buffet = "The Alleyway Buffet" + the_art_o_crabbing = "The Art O' Crabbing" + dwarvish_safety_manual = "Dwarvish Safety Manual" + jewels_of_the_sea = "Jewels Of The Sea" + raccoon_journal = "Raccoon Journal" + woodys_secret = "Woody's Secret" + jack_be_nimble_jack_be_thick = "Jack Be Nimble, Jack Be Thick" + friendship_101 = "Friendship 101" + monster_compendium = "Monster Compendium" + mapping_cave_systems = "Mapping Cave Systems" + treasure_appraisal_guide = "Treasure Appraisal Guide" + way_of_the_wind_pt_1 = "Way Of The Wind pt. 1" + way_of_the_wind_pt_2 = "Way Of The Wind pt. 2" + horse_the_book = "Horse: The Book" + ol_slitherlegs = "Ol' Slitherlegs" + queen_of_sauce_cookbook = "Queen Of Sauce Cookbook" + price_catalogue = "Price Catalogue" + the_diamond_hunter = "The Diamond Hunter" + + +class ModBook: + digging_like_worms = "Digging Like Worms" + + +ordered_lost_books = [] +all_lost_books = set() + + +def lost_book(book_name: str): + ordered_lost_books.append(book_name) + all_lost_books.add(book_name) + return book_name + + +class LostBook: + tips_on_farming = lost_book("Tips on Farming") + this_is_a_book_by_marnie = lost_book("This is a book by Marnie") + on_foraging = lost_book("On Foraging") + the_fisherman_act_1 = lost_book("The Fisherman, Act 1") + how_deep_do_the_mines_go = lost_book("How Deep do the mines go?") + an_old_farmers_journal = lost_book("An Old Farmer's Journal") + scarecrows = lost_book("Scarecrows") + the_secret_of_the_stardrop = lost_book("The Secret of the Stardrop") + journey_of_the_prairie_king_the_smash_hit_video_game = lost_book("Journey of the Prairie King -- The Smash Hit Video Game!") + a_study_on_diamond_yields = lost_book("A Study on Diamond Yields") + brewmasters_guide = lost_book("Brewmaster's Guide") + mysteries_of_the_dwarves = lost_book("Mysteries of the Dwarves") + highlights_from_the_book_of_yoba = lost_book("Highlights From The Book of Yoba") + marriage_guide_for_farmers = lost_book("Marriage Guide for Farmers") + the_fisherman_act_ii = lost_book("The Fisherman, Act II") + technology_report = lost_book("Technology Report!") + secrets_of_the_legendary_fish = lost_book("Secrets of the Legendary Fish") + gunther_tunnel_notice = lost_book("Gunther Tunnel Notice") + note_from_gunther = lost_book("Note From Gunther") + goblins_by_m_jasper = lost_book("Goblins by M. Jasper") + secret_statues_acrostics = lost_book("Secret Statues Acrostics") diff --git a/worlds/stardew_valley/strings/bundle_names.py b/worlds/stardew_valley/strings/bundle_names.py index de8d8af3877f..5f560a545434 100644 --- a/worlds/stardew_valley/strings/bundle_names.py +++ b/worlds/stardew_valley/strings/bundle_names.py @@ -6,75 +6,103 @@ class CCRoom: vault = "Vault" boiler_room = "Boiler Room" abandoned_joja_mart = "Abandoned Joja Mart" + raccoon_requests = "Raccoon Requests" + + +all_cc_bundle_names = [] + + +def cc_bundle(name: str) -> str: + all_cc_bundle_names.append(name) + return name class BundleName: - spring_foraging = "Spring Foraging Bundle" - summer_foraging = "Summer Foraging Bundle" - fall_foraging = "Fall Foraging Bundle" - winter_foraging = "Winter Foraging Bundle" - construction = "Construction Bundle" - exotic_foraging = "Exotic Foraging Bundle" - beach_foraging = "Beach Foraging Bundle" - mines_foraging = "Mines Foraging Bundle" - desert_foraging = "Desert Foraging Bundle" - island_foraging = "Island Foraging Bundle" - sticky = "Sticky Bundle" - wild_medicine = "Wild Medicine Bundle" - quality_foraging = "Quality Foraging Bundle" - spring_crops = "Spring Crops Bundle" - summer_crops = "Summer Crops Bundle" - fall_crops = "Fall Crops Bundle" - quality_crops = "Quality Crops Bundle" - animal = "Animal Bundle" - artisan = "Artisan Bundle" - rare_crops = "Rare Crops Bundle" - fish_farmer = "Fish Farmer's Bundle" - garden = "Garden Bundle" - brewer = "Brewer's Bundle" - orchard = "Orchard Bundle" - island_crops = "Island Crops Bundle" - agronomist = "Agronomist's Bundle" - slime_farmer = "Slime Farmer Bundle" - river_fish = "River Fish Bundle" - lake_fish = "Lake Fish Bundle" - ocean_fish = "Ocean Fish Bundle" - night_fish = "Night Fishing Bundle" - crab_pot = "Crab Pot Bundle" - trash = "Trash Bundle" - recycling = "Recycling Bundle" - specialty_fish = "Specialty Fish Bundle" - spring_fish = "Spring Fishing Bundle" - summer_fish = "Summer Fishing Bundle" - fall_fish = "Fall Fishing Bundle" - winter_fish = "Winter Fishing Bundle" - rain_fish = "Rain Fishing Bundle" - quality_fish = "Quality Fish Bundle" - master_fisher = "Master Fisher's Bundle" - legendary_fish = "Legendary Fish Bundle" - island_fish = "Island Fish Bundle" - deep_fishing = "Deep Fishing Bundle" - tackle = "Tackle Bundle" - bait = "Master Baiter Bundle" - blacksmith = "Blacksmith's Bundle" - geologist = "Geologist's Bundle" - adventurer = "Adventurer's Bundle" - treasure_hunter = "Treasure Hunter's Bundle" - engineer = "Engineer's Bundle" - demolition = "Demolition Bundle" - paleontologist = "Paleontologist's Bundle" - archaeologist = "Archaeologist's Bundle" - chef = "Chef's Bundle" - dye = "Dye Bundle" - field_research = "Field Research Bundle" - fodder = "Fodder Bundle" - enchanter = "Enchanter's Bundle" - children = "Children's Bundle" - forager = "Forager's Bundle" - home_cook = "Home Cook's Bundle" - bartender = "Bartender's Bundle" - gambler = "Gambler's Bundle" - carnival = "Carnival Bundle" - walnut_hunter = "Walnut Hunter Bundle" - qi_helper = "Qi's Helper Bundle" + spring_foraging = cc_bundle("Spring Foraging Bundle") + summer_foraging = cc_bundle("Summer Foraging Bundle") + fall_foraging = cc_bundle("Fall Foraging Bundle") + winter_foraging = cc_bundle("Winter Foraging Bundle") + construction = cc_bundle("Construction Bundle") + exotic_foraging = cc_bundle("Exotic Foraging Bundle") + beach_foraging = cc_bundle("Beach Foraging Bundle") + mines_foraging = cc_bundle("Mines Foraging Bundle") + desert_foraging = cc_bundle("Desert Foraging Bundle") + island_foraging = cc_bundle("Island Foraging Bundle") + sticky = cc_bundle("Sticky Bundle") + forest = cc_bundle("Forest Bundle") + green_rain = cc_bundle("Green Rain Bundle") + wild_medicine = cc_bundle("Wild Medicine Bundle") + quality_foraging = cc_bundle("Quality Foraging Bundle") + spring_crops = cc_bundle("Spring Crops Bundle") + summer_crops = cc_bundle("Summer Crops Bundle") + fall_crops = cc_bundle("Fall Crops Bundle") + quality_crops = cc_bundle("Quality Crops Bundle") + animal = cc_bundle("Animal Bundle") + artisan = cc_bundle("Artisan Bundle") + rare_crops = cc_bundle("Rare Crops Bundle") + fish_farmer = cc_bundle("Fish Farmer's Bundle") + garden = cc_bundle("Garden Bundle") + brewer = cc_bundle("Brewer's Bundle") + orchard = cc_bundle("Orchard Bundle") + island_crops = cc_bundle("Island Crops Bundle") + agronomist = cc_bundle("Agronomist's Bundle") + slime_farmer = cc_bundle("Slime Farmer Bundle") + sommelier = cc_bundle("Sommelier Bundle") + dry = cc_bundle("Dry Bundle") + river_fish = cc_bundle("River Fish Bundle") + lake_fish = cc_bundle("Lake Fish Bundle") + ocean_fish = cc_bundle("Ocean Fish Bundle") + night_fish = cc_bundle("Night Fishing Bundle") + crab_pot = cc_bundle("Crab Pot Bundle") + trash = cc_bundle("Trash Bundle") + recycling = cc_bundle("Recycling Bundle") + specialty_fish = cc_bundle("Specialty Fish Bundle") + spring_fish = cc_bundle("Spring Fishing Bundle") + summer_fish = cc_bundle("Summer Fishing Bundle") + fall_fish = cc_bundle("Fall Fishing Bundle") + winter_fish = cc_bundle("Winter Fishing Bundle") + rain_fish = cc_bundle("Rain Fishing Bundle") + quality_fish = cc_bundle("Quality Fish Bundle") + master_fisher = cc_bundle("Master Fisher's Bundle") + legendary_fish = cc_bundle("Legendary Fish Bundle") + island_fish = cc_bundle("Island Fish Bundle") + deep_fishing = cc_bundle("Deep Fishing Bundle") + tackle = cc_bundle("Tackle Bundle") + bait = cc_bundle("Master Baiter Bundle") + specific_bait = cc_bundle("Specific Fishing Bundle") + fish_smoker = cc_bundle("Fish Smoker Bundle") + blacksmith = cc_bundle("Blacksmith's Bundle") + geologist = cc_bundle("Geologist's Bundle") + adventurer = cc_bundle("Adventurer's Bundle") + treasure_hunter = cc_bundle("Treasure Hunter's Bundle") + engineer = cc_bundle("Engineer's Bundle") + demolition = cc_bundle("Demolition Bundle") + paleontologist = cc_bundle("Paleontologist's Bundle") + archaeologist = cc_bundle("Archaeologist's Bundle") + chef = cc_bundle("Chef's Bundle") + dye = cc_bundle("Dye Bundle") + field_research = cc_bundle("Field Research Bundle") + fodder = cc_bundle("Fodder Bundle") + enchanter = cc_bundle("Enchanter's Bundle") + children = cc_bundle("Children's Bundle") + forager = cc_bundle("Forager's Bundle") + home_cook = cc_bundle("Home Cook's Bundle") + helper = cc_bundle("Helper's Bundle") + spirit_eve = cc_bundle("Spirit's Eve Bundle") + winter_star = cc_bundle("Winter Star Bundle") + bartender = cc_bundle("Bartender's Bundle") + calico = cc_bundle("Calico Bundle") + raccoon = cc_bundle("Raccoon Bundle") + money_2500 = cc_bundle("2,500g Bundle") + money_5000 = cc_bundle("5,000g Bundle") + money_10000 = cc_bundle("10,000g Bundle") + money_25000 = cc_bundle("25,000g Bundle") + gambler = cc_bundle("Gambler's Bundle") + carnival = cc_bundle("Carnival Bundle") + walnut_hunter = cc_bundle("Walnut Hunter Bundle") + qi_helper = cc_bundle("Qi's Helper Bundle") missing_bundle = "The Missing Bundle" + raccoon_fish = "Raccoon Fish" + raccoon_artisan = "Raccoon Artisan" + raccoon_food = "Raccoon Food" + raccoon_foraging = "Raccoon Foraging" diff --git a/worlds/stardew_valley/strings/craftable_names.py b/worlds/stardew_valley/strings/craftable_names.py index 74a77a8e9467..83445c702c32 100644 --- a/worlds/stardew_valley/strings/craftable_names.py +++ b/worlds/stardew_valley/strings/craftable_names.py @@ -25,6 +25,7 @@ class WildSeeds: winter = "Winter Seeds" ancient = "Ancient Seeds" grass_starter = "Grass Starter" + blue_grass_starter = "Blue Grass Starter" tea_sapling = "Tea Sapling" fiber = "Fiber Seeds" @@ -48,6 +49,7 @@ class Floor: class Fishing: spinner = "Spinner" trap_bobber = "Trap Bobber" + sonar_bobber = "Sonar Bobber" cork_bobber = "Cork Bobber" quality_bobber = "Quality Bobber" treasure_hunter = "Treasure Hunter" @@ -59,6 +61,8 @@ class Fishing: magic_bait = "Magic Bait" lead_bobber = "Lead Bobber" curiosity_lure = "Curiosity Lure" + deluxe_bait = "Deluxe Bait" + challenge_bait = "Challenge Bait" class Ring: @@ -70,6 +74,7 @@ class Ring: glowstone_ring = "Glowstone Ring" iridium_band = "Iridium Band" wedding_ring = "Wedding Ring" + lucky_ring = "Lucky Ring" class Edible: @@ -88,6 +93,15 @@ class Consumable: warp_totem_desert = "Warp Totem: Desert" warp_totem_island = "Warp Totem: Island" rain_totem = "Rain Totem" + mystery_box = "Mystery Box" + gold_mystery_box = "Golden Mystery Box" + treasure_totem = "Treasure Totem" + fireworks_red = "Fireworks (Red)" + fireworks_purple = "Fireworks (Purple)" + fireworks_green = "Fireworks (Green)" + far_away_stone = "Far Away Stone" + golden_animal_cracker = "Golden Animal Cracker" + butterfly_powder = "Butterfly Powder" class Lighting: @@ -116,12 +130,20 @@ class Furniture: class Storage: chest = "Chest" stone_chest = "Stone Chest" + big_chest = "Big Chest" + big_stone_chest = "Big Stone Chest" class Sign: wood = "Wood Sign" stone = "Stone Sign" dark = "Dark Sign" + text = "Text Sign" + + +class Statue: + blessings = "Statue Of Blessings" + dwarf_king = "Statue Of The Dwarf King" class Craftable: @@ -137,6 +159,7 @@ class Craftable: farm_computer = "Farm Computer" hopper = "Hopper" cookout_kit = "Cookout Kit" + tent_kit = "Tent Kit" class ModEdible: @@ -152,9 +175,11 @@ class ModEdible: class ModCraftable: travel_core = "Travel Core" - glass_bazier = "Glass Bazier" + glass_brazier = "Glass Brazier" water_shifter = "Water Shifter" + rusty_brazier = "Rusty Brazier" glass_fence = "Glass Fence" + bone_fence = "Bone Fence" wooden_display = "Wooden Display" hardwood_display = "Hardwood Display" neanderthal_skeleton = "Neanderthal Skeleton" @@ -171,11 +196,17 @@ class ModMachine: hardwood_preservation_chamber = "Hardwood Preservation Chamber" grinder = "Grinder" ancient_battery = "Ancient Battery Production Station" + restoration_table = "Restoration Table" + trash_bin = "Trash Bin" + composter = "Composter" + recycling_bin = "Recycling Bin" + advanced_recycling_machine = "Advanced Recycling Machine" class ModFloor: glass_path = "Glass Path" bone_path = "Bone Path" + rusty_path = "Rusty Path" class ModConsumable: diff --git a/worlds/stardew_valley/strings/crop_names.py b/worlds/stardew_valley/strings/crop_names.py index 295e40005f75..fa7a77c834fc 100644 --- a/worlds/stardew_valley/strings/crop_names.py +++ b/worlds/stardew_valley/strings/crop_names.py @@ -1,64 +1,55 @@ -all_fruits = [] -all_vegetables = [] - - -def veggie(name: str) -> str: - all_vegetables.append(name) - return name - - -def fruity(name: str) -> str: - all_fruits.append(name) - return name - - class Fruit: - sweet_gem_berry = fruity("Sweet Gem Berry") + sweet_gem_berry = "Sweet Gem Berry" any = "Any Fruit" - blueberry = fruity("Blueberry") - melon = fruity("Melon") - apple = fruity("Apple") - apricot = fruity("Apricot") - cherry = fruity("Cherry") - orange = fruity("Orange") - peach = fruity("Peach") - pomegranate = fruity("Pomegranate") - banana = fruity("Banana") - mango = fruity("Mango") - pineapple = fruity("Pineapple") - ancient_fruit = fruity("Ancient Fruit") - strawberry = fruity("Strawberry") - starfruit = fruity("Starfruit") - rhubarb = fruity("Rhubarb") - grape = fruity("Grape") - cranberries = fruity("Cranberries") - hot_pepper = fruity("Hot Pepper") + blueberry = "Blueberry" + melon = "Melon" + apple = "Apple" + apricot = "Apricot" + cherry = "Cherry" + orange = "Orange" + peach = "Peach" + pomegranate = "Pomegranate" + banana = "Banana" + mango = "Mango" + pineapple = "Pineapple" + ancient_fruit = "Ancient Fruit" + strawberry = "Strawberry" + starfruit = "Starfruit" + rhubarb = "Rhubarb" + grape = "Grape" + cranberries = "Cranberries" + hot_pepper = "Hot Pepper" + powdermelon = "Powdermelon" + qi_fruit = "Qi Fruit" class Vegetable: any = "Any Vegetable" - parsnip = veggie("Parsnip") - garlic = veggie("Garlic") + parsnip = "Parsnip" + garlic = "Garlic" bok_choy = "Bok Choy" wheat = "Wheat" - potato = veggie("Potato") - corn = veggie("Corn") - tomato = veggie("Tomato") - pumpkin = veggie("Pumpkin") - unmilled_rice = veggie("Unmilled Rice") - beet = veggie("Beet") + potato = "Potato" + corn = "Corn" + tomato = "Tomato" + pumpkin = "Pumpkin" + unmilled_rice = "Unmilled Rice" + beet = "Beet" hops = "Hops" - cauliflower = veggie("Cauliflower") - amaranth = veggie("Amaranth") - kale = veggie("Kale") - artichoke = veggie("Artichoke") + cauliflower = "Cauliflower" + amaranth = "Amaranth" + kale = "Kale" + artichoke = "Artichoke" tea_leaves = "Tea Leaves" - eggplant = veggie("Eggplant") - green_bean = veggie("Green Bean") - red_cabbage = veggie("Red Cabbage") - yam = veggie("Yam") - radish = veggie("Radish") - taro_root = veggie("Taro Root") + eggplant = "Eggplant" + green_bean = "Green Bean" + red_cabbage = "Red Cabbage" + yam = "Yam" + radish = "Radish" + taro_root = "Taro Root" + carrot = "Carrot" + summer_squash = "Summer Squash" + broccoli = "Broccoli" class SVEFruit: @@ -76,7 +67,3 @@ class SVEVegetable: class DistantLandsCrop: void_mint = "Void Mint Leaves" vile_ancient_fruit = "Vile Ancient Fruit" - - -all_vegetables = tuple(all_vegetables) -all_fruits = tuple(all_fruits) diff --git a/worlds/stardew_valley/strings/currency_names.py b/worlds/stardew_valley/strings/currency_names.py index 5192466c9ca7..21ccb5b55c58 100644 --- a/worlds/stardew_valley/strings/currency_names.py +++ b/worlds/stardew_valley/strings/currency_names.py @@ -5,6 +5,9 @@ class Currency: star_token = "Star Token" money = "Money" cinder_shard = "Cinder Shard" + prize_ticket = "Prize Ticket" + calico_egg = "Calico Egg" + golden_tag = "Golden Tag" @staticmethod def is_currency(item: str) -> bool: diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 00823d62ea07..9b651f42760a 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -42,14 +42,7 @@ class Entrance: forest_to_marnie_ranch = "Forest to Marnie's Ranch" forest_to_leah_cottage = "Forest to Leah's Cottage" forest_to_sewer = "Forest to Sewer" - buy_from_traveling_merchant = "Buy from Traveling Merchant" - buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" - buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" - buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" - buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" - buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" - buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" - buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" + forest_to_mastery_cave = "Forest to Mastery Cave" mountain_to_railroad = "Mountain to Railroad" mountain_to_tent = "Mountain to Tent" mountain_to_carpenter_shop = "Mountain to Carpenter Shop" @@ -57,6 +50,7 @@ class Entrance: mountain_to_the_mines = "Mountain to The Mines" enter_quarry = "Mountain to Quarry" mountain_to_adventurer_guild = "Mountain to Adventurer's Guild" + adventurer_guild_to_bedroom = "Adventurer's Guild to Marlon's Bedroom" mountain_to_town = "Mountain to Town" town_to_community_center = "Town to Community Center" access_crafts_room = "Access Crafts Room" @@ -120,7 +114,6 @@ class Entrance: mine_to_skull_cavern_floor_175 = dig_to_skull_floor(175) mine_to_skull_cavern_floor_200 = dig_to_skull_floor(200) enter_dangerous_skull_cavern = "Enter the Dangerous Skull Cavern" - talk_to_mines_dwarf = "Talk to Mines Dwarf" dig_to_mines_floor_5 = dig_to_mines_floor(5) dig_to_mines_floor_10 = dig_to_mines_floor(10) dig_to_mines_floor_15 = dig_to_mines_floor(15) @@ -183,6 +176,19 @@ class Entrance: parrot_express_jungle_to_docks = "Parrot Express Jungle to Docks" parrot_express_dig_site_to_docks = "Parrot Express Dig Site to Docks" parrot_express_volcano_to_docks = "Parrot Express Volcano to Docks" + + +class LogicEntrance: + talk_to_mines_dwarf = "Talk to Mines Dwarf" + + buy_from_traveling_merchant = "Buy from Traveling Merchant" + buy_from_traveling_merchant_sunday = "Buy from Traveling Merchant Sunday" + buy_from_traveling_merchant_monday = "Buy from Traveling Merchant Monday" + buy_from_traveling_merchant_tuesday = "Buy from Traveling Merchant Tuesday" + buy_from_traveling_merchant_wednesday = "Buy from Traveling Merchant Wednesday" + buy_from_traveling_merchant_thursday = "Buy from Traveling Merchant Thursday" + buy_from_traveling_merchant_friday = "Buy from Traveling Merchant Friday" + buy_from_traveling_merchant_saturday = "Buy from Traveling Merchant Saturday" farmhouse_cooking = "Farmhouse Cooking" island_cooking = "Island Cooking" shipping = "Use Shipping Bin" @@ -191,17 +197,43 @@ class Entrance: blacksmith_iron = "Upgrade Iron Tools" blacksmith_gold = "Upgrade Gold Tools" blacksmith_iridium = "Upgrade Iridium Tools" - farming = "Start Farming" + + grow_spring_crops = "Grow Spring Crops" + grow_summer_crops = "Grow Summer Crops" + grow_fall_crops = "Grow Fall Crops" + grow_winter_crops = "Grow Winter Crops" + grow_spring_crops_in_greenhouse = "Grow Spring Crops in Greenhouse" + grow_summer_crops_in_greenhouse = "Grow Summer Crops in Greenhouse" + grow_fall_crops_in_greenhouse = "Grow Fall Crops in Greenhouse" + grow_winter_crops_in_greenhouse = "Grow Winter Crops in Greenhouse" + grow_indoor_crops_in_greenhouse = "Grow Indoor Crops in Greenhouse" + grow_spring_crops_on_island = "Grow Spring Crops on Island" + grow_summer_crops_on_island = "Grow Summer Crops on Island" + grow_fall_crops_on_island = "Grow Fall Crops on Island" + grow_winter_crops_on_island = "Grow Winter Crops on Island" + grow_indoor_crops_on_island = "Grow Indoor Crops on Island" + grow_summer_fall_crops_in_summer = "Grow Summer Fall Crops in Summer" + grow_summer_fall_crops_in_fall = "Grow Summer Fall Crops in Fall" + fishing = "Start Fishing" attend_egg_festival = "Attend Egg Festival" + attend_desert_festival = "Attend Desert Festival" attend_flower_dance = "Attend Flower Dance" attend_luau = "Attend Luau" + attend_trout_derby = "Attend Trout Derby" attend_moonlight_jellies = "Attend Dance of the Moonlight Jellies" attend_fair = "Attend Stardew Valley Fair" attend_spirit_eve = "Attend Spirit's Eve" attend_festival_of_ice = "Attend Festival of Ice" attend_night_market = "Attend Night Market" attend_winter_star = "Attend Feast of the Winter Star" + attend_squidfest = "Attend SquidFest" + buy_experience_books = "Buy Experience Books from the bookseller" + buy_year1_books = "Buy Year 1 Books from the Bookseller" + buy_year3_books = "Buy Year 3 Books from the Bookseller" + complete_raccoon_requests = "Complete Raccoon Requests" + buy_from_raccoon = "Buy From Raccoon" + fish_in_waterfall = "Fish In Waterfall" # Skull Cavern Elevator @@ -356,4 +388,3 @@ class BoardingHouseEntrance: lost_valley_ruins_to_lost_valley_house_1 = "Lost Valley Ruins to Lost Valley Ruins - First House" lost_valley_ruins_to_lost_valley_house_2 = "Lost Valley Ruins to Lost Valley Ruins - Second House" boarding_house_plateau_to_buffalo_ranch = "Boarding House Outside to Buffalo's Ranch" - diff --git a/worlds/stardew_valley/strings/festival_check_names.py b/worlds/stardew_valley/strings/festival_check_names.py index 73a9d3978eab..b59b3cd03f17 100644 --- a/worlds/stardew_valley/strings/festival_check_names.py +++ b/worlds/stardew_valley/strings/festival_check_names.py @@ -35,3 +35,46 @@ class FestivalCheck: jack_o_lantern = "Jack-O-Lantern Recipe" moonlight_jellies_banner = "Moonlight Jellies Banner" starport_decal = "Starport Decal" + calico_race = "Calico Race" + mummy_mask = "Mummy Mask" + calico_statue = "Calico Statue" + emily_outfit_service = "Emily's Outfit Services" + earthy_mousse = "Earthy Mousse" + sweet_bean_cake = "Sweet Bean Cake" + skull_cave_casserole = "Skull Cave Casserole" + spicy_tacos = "Spicy Tacos" + mountain_chili = "Mountain Chili" + crystal_cake = "Crystal Cake" + cave_kebab = "Cave Kebab" + hot_log = "Hot Log" + sour_salad = "Sour Salad" + superfood_cake = "Superfood Cake" + warrior_smoothie = "Warrior Smoothie" + rumpled_fruit_skin = "Rumpled Fruit Skin" + calico_pizza = "Calico Pizza" + stuffed_mushrooms = "Stuffed Mushrooms" + elf_quesadilla = "Elf Quesadilla" + nachos_of_the_desert = "Nachos Of The Desert" + cloppino = "Cloppino" + rainforest_shrimp = "Rainforest Shrimp" + shrimp_donut = "Shrimp Donut" + smell_of_the_sea = "Smell Of The Sea" + desert_gumbo = "Desert Gumbo" + free_cactis = "Free Cactis" + monster_hunt = "Monster Hunt" + deep_dive = "Deep Dive" + treasure_hunt = "Treasure Hunt" + touch_calico_statue = "Touch A Calico Statue" + real_calico_egg_hunter = "Real Calico Egg Hunter" + willy_challenge = "Willy's Challenge" + desert_scholar = "Desert Scholar" + trout_derby_reward_pattern = "Trout Derby Reward " + squidfest_day_1_copper = "SquidFest Day 1 Copper" + squidfest_day_1_iron = "SquidFest Day 1 Iron" + squidfest_day_1_gold = "SquidFest Day 1 Gold" + squidfest_day_1_iridium = "SquidFest Day 1 Iridium" + squidfest_day_2_copper = "SquidFest Day 2 Copper" + squidfest_day_2_iron = "SquidFest Day 2 Iron" + squidfest_day_2_gold = "SquidFest Day 2 Gold" + squidfest_day_2_iridium = "SquidFest Day 2 Iridium" + diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index cd59d749ee01..d94f9e2fd403 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -1,81 +1,92 @@ +all_fish = [] + + +def fish(fish_name: str) -> str: + all_fish.append(fish_name) + return fish_name + + class Fish: - albacore = "Albacore" - anchovy = "Anchovy" - angler = "Angler" - any = "Any Fish" - blob_fish = "Blobfish" - blobfish = "Blobfish" - blue_discus = "Blue Discus" - bream = "Bream" - bullhead = "Bullhead" - carp = "Carp" - catfish = "Catfish" - chub = "Chub" - clam = "Clam" - cockle = "Cockle" - crab = "Crab" - crayfish = "Crayfish" - crimsonfish = "Crimsonfish" - dorado = "Dorado" - eel = "Eel" - flounder = "Flounder" - ghostfish = "Ghostfish" - glacierfish = "Glacierfish" - glacierfish_jr = "Glacierfish Jr." - halibut = "Halibut" - herring = "Herring" - ice_pip = "Ice Pip" - largemouth_bass = "Largemouth Bass" - lava_eel = "Lava Eel" - legend = "Legend" - legend_ii = "Legend II" - lingcod = "Lingcod" - lionfish = "Lionfish" - lobster = "Lobster" - midnight_carp = "Midnight Carp" - midnight_squid = "Midnight Squid" - ms_angler = "Ms. Angler" - mussel = "Mussel" - mussel_node = "Mussel Node" - mutant_carp = "Mutant Carp" - octopus = "Octopus" - oyster = "Oyster" - perch = "Perch" - periwinkle = "Periwinkle" - pike = "Pike" - pufferfish = "Pufferfish" - radioactive_carp = "Radioactive Carp" - rainbow_trout = "Rainbow Trout" - red_mullet = "Red Mullet" - red_snapper = "Red Snapper" - salmon = "Salmon" - sandfish = "Sandfish" - sardine = "Sardine" - scorpion_carp = "Scorpion Carp" - sea_cucumber = "Sea Cucumber" - shad = "Shad" - shrimp = "Shrimp" - slimejack = "Slimejack" - smallmouth_bass = "Smallmouth Bass" - snail = "Snail" - son_of_crimsonfish = "Son of Crimsonfish" - spook_fish = "Spook Fish" - spookfish = "Spook Fish" - squid = "Squid" - stingray = "Stingray" - stonefish = "Stonefish" - sturgeon = "Sturgeon" - sunfish = "Sunfish" - super_cucumber = "Super Cucumber" - tiger_trout = "Tiger Trout" - tilapia = "Tilapia" - tuna = "Tuna" - void_salmon = "Void Salmon" - walleye = "Walleye" - woodskip = "Woodskip" + albacore = fish("Albacore") + anchovy = fish("Anchovy") + angler = fish("Angler") + any = fish("Any Fish") + blobfish = fish("Blobfish") + blue_discus = fish("Blue Discus") + bream = fish("Bream") + bullhead = fish("Bullhead") + carp = fish("Carp") + catfish = fish("Catfish") + chub = fish("Chub") + clam = fish("Clam") + cockle = fish("Cockle") + crab = fish("Crab") + crayfish = fish("Crayfish") + crimsonfish = fish("Crimsonfish") + dorado = fish("Dorado") + eel = fish("Eel") + flounder = fish("Flounder") + ghostfish = fish("Ghostfish") + goby = fish("Goby") + glacierfish = fish("Glacierfish") + glacierfish_jr = fish("Glacierfish Jr.") + halibut = fish("Halibut") + herring = fish("Herring") + ice_pip = fish("Ice Pip") + largemouth_bass = fish("Largemouth Bass") + lava_eel = fish("Lava Eel") + legend = fish("Legend") + legend_ii = fish("Legend II") + lingcod = fish("Lingcod") + lionfish = fish("Lionfish") + lobster = fish("Lobster") + midnight_carp = fish("Midnight Carp") + midnight_squid = fish("Midnight Squid") + ms_angler = fish("Ms. Angler") + mussel = fish("Mussel") + mussel_node = fish("Mussel Node") + mutant_carp = fish("Mutant Carp") + octopus = fish("Octopus") + oyster = fish("Oyster") + perch = fish("Perch") + periwinkle = fish("Periwinkle") + pike = fish("Pike") + pufferfish = fish("Pufferfish") + radioactive_carp = fish("Radioactive Carp") + rainbow_trout = fish("Rainbow Trout") + red_mullet = fish("Red Mullet") + red_snapper = fish("Red Snapper") + salmon = fish("Salmon") + sandfish = fish("Sandfish") + sardine = fish("Sardine") + scorpion_carp = fish("Scorpion Carp") + sea_cucumber = fish("Sea Cucumber") + shad = fish("Shad") + shrimp = fish("Shrimp") + slimejack = fish("Slimejack") + smallmouth_bass = fish("Smallmouth Bass") + snail = fish("Snail") + son_of_crimsonfish = fish("Son of Crimsonfish") + spook_fish = fish("Spook Fish") + spookfish = fish("Spook Fish") + squid = fish("Squid") + stingray = fish("Stingray") + stonefish = fish("Stonefish") + sturgeon = fish("Sturgeon") + sunfish = fish("Sunfish") + super_cucumber = fish("Super Cucumber") + tiger_trout = fish("Tiger Trout") + tilapia = fish("Tilapia") + tuna = fish("Tuna") + void_salmon = fish("Void Salmon") + walleye = fish("Walleye") + woodskip = fish("Woodskip") class WaterItem: + sea_jelly = "Sea Jelly" + river_jelly = "River Jelly" + cave_jelly = "Cave Jelly" seaweed = "Seaweed" green_algae = "Green Algae" white_algae = "White Algae" @@ -95,6 +106,7 @@ class Trash: class WaterChest: fishing_chest = "Fishing Chest" + golden_fishing_chest = "Golden Fishing Chest" treasure = "Treasure Chest" @@ -134,3 +146,9 @@ class DistantLandsFish: purple_algae = "Purple Algae" giant_horsehoe_crab = "Giant Horsehoe Crab" + +class ModTrash: + rusty_scrap = "Scrap Rust" + + +all_fish = tuple(all_fish) \ No newline at end of file diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 6e2f98fd581b..5555316f8314 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -42,6 +42,7 @@ class Meal: maki_roll = "Maki Roll" maple_bar = "Maple Bar" miners_treat = "Miner's Treat" + moss_soup = "Moss Soup" omelet = "Omelet" pale_broth = "Pale Broth" pancakes = "Pancakes" @@ -103,6 +104,17 @@ class SVEMeal: grampleton_orange_chicken = "Grampleton Orange Chicken" +class TrashyMeal: + grilled_cheese = "Grilled Cheese" + fish_casserole = "Fish Casserole" + + +class ArchaeologyMeal: + diggers_delight = "Digger's Delight" + rocky_root = "Rocky Root Coffee" + ancient_jello = "Ancient Jello" + + class SVEBeverage: sports_drink = "Sports Drink" diff --git a/worlds/stardew_valley/strings/forageable_names.py b/worlds/stardew_valley/strings/forageable_names.py index 24127beb9838..c7dae8af3ce0 100644 --- a/worlds/stardew_valley/strings/forageable_names.py +++ b/worlds/stardew_valley/strings/forageable_names.py @@ -1,10 +1,26 @@ +all_edible_mushrooms = [] + + +def mushroom(name: str) -> str: + all_edible_mushrooms.append(name) + return name + + +class Mushroom: + any_edible = "Any Edible Mushroom" + chanterelle = mushroom("Chanterelle") + common = mushroom("Common Mushroom") + morel = mushroom("Morel") + purple = mushroom("Purple Mushroom") + red = "Red Mushroom" # Not in all mushrooms, as it can't be dried + magma_cap = mushroom("Magma Cap") + + class Forageable: blackberry = "Blackberry" cactus_fruit = "Cactus Fruit" cave_carrot = "Cave Carrot" - chanterelle = "Chanterelle" coconut = "Coconut" - common_mushroom = "Common Mushroom" crocus = "Crocus" crystal_fruit = "Crystal Fruit" daffodil = "Daffodil" @@ -16,8 +32,6 @@ class Forageable: holly = "Holly" journal_scrap = "Journal Scrap" leek = "Leek" - magma_cap = "Magma Cap" - morel = "Morel" secret_note = "Secret Note" spice_berry = "Spice Berry" sweet_pea = "Sweet Pea" @@ -25,8 +39,6 @@ class Forageable: wild_plum = "Wild Plum" winter_root = "Winter Root" dragon_tooth = "Dragon Tooth" - red_mushroom = "Red Mushroom" - purple_mushroom = "Purple Mushroom" rainbow_shell = "Rainbow Shell" salmonberry = "Salmonberry" snow_yam = "Snow Yam" @@ -34,28 +46,26 @@ class Forageable: class SVEForage: - ornate_treasure_chest = "Ornate Treasure Chest" - swirl_stone = "Swirl Stone" - void_pebble = "Void Pebble" - void_soul = "Void Soul" ferngill_primrose = "Ferngill Primrose" goldenrod = "Goldenrod" winter_star_rose = "Winter Star Rose" - bearberrys = "Bearberrys" + bearberry = "Bearberry" poison_mushroom = "Poison Mushroom" red_baneberry = "Red Baneberry" - big_conch = "Big Conch" + conch = "Conch" dewdrop_berry = "Dewdrop Berry" - dried_sand_dollar = "Dried Sand Dollar" + sand_dollar = "Sand Dollar" golden_ocean_flower = "Golden Ocean Flower" - lucky_four_leaf_clover = "Lucky Four Leaf Clover" + four_leaf_clover = "Four Leaf Clover" mushroom_colony = "Mushroom Colony" - poison_mushroom = "Poison Mushroom" rusty_blade = "Rusty Blade" - smelly_rafflesia = "Smelly Rafflesia" + rafflesia = "Rafflesia" thistle = "Thistle" class DistantLandsForageable: brown_amanita = "Brown Amanita" swamp_herb = "Swamp Herb" + + +all_edible_mushrooms = tuple(all_edible_mushrooms) diff --git a/worlds/stardew_valley/strings/machine_names.py b/worlds/stardew_valley/strings/machine_names.py index f9be78c41a03..d9e249a33594 100644 --- a/worlds/stardew_valley/strings/machine_names.py +++ b/worlds/stardew_valley/strings/machine_names.py @@ -1,4 +1,6 @@ class Machine: + dehydrator = "Dehydrator" + fish_smoker = "Fish Smoker" bee_house = "Bee House" bone_mill = "Bone Mill" cask = "Cask" @@ -10,6 +12,7 @@ class Machine: enricher = "Enricher" furnace = "Furnace" geode_crusher = "Geode Crusher" + mushroom_log = "Mushroom Log" heavy_tapper = "Heavy Tapper" keg = "Keg" lightning_rod = "Lightning Rod" @@ -26,4 +29,9 @@ class Machine: solar_panel = "Solar Panel" tapper = "Tapper" worm_bin = "Worm Bin" + deluxe_worm_bin = "Deluxe Worm Bin" + heavy_furnace = "Heavy Furnace" + anvil = "Anvil" + mini_forge = "Mini-Forge" + bait_maker = "Bait Maker" diff --git a/worlds/stardew_valley/strings/material_names.py b/worlds/stardew_valley/strings/material_names.py index 16511a5bcb97..797a42b73756 100644 --- a/worlds/stardew_valley/strings/material_names.py +++ b/worlds/stardew_valley/strings/material_names.py @@ -1,4 +1,5 @@ class Material: + moss = "Moss" coal = "Coal" fiber = "Fiber" hardwood = "Hardwood" diff --git a/worlds/stardew_valley/strings/metal_names.py b/worlds/stardew_valley/strings/metal_names.py index bf15b9d01c8e..7798c06defeb 100644 --- a/worlds/stardew_valley/strings/metal_names.py +++ b/worlds/stardew_valley/strings/metal_names.py @@ -44,6 +44,7 @@ class Mineral: ruby = "Ruby" emerald = "Emerald" amethyst = "Amethyst" + tigerseye = "Tigerseye" class Artifact: diff --git a/worlds/stardew_valley/strings/monster_drop_names.py b/worlds/stardew_valley/strings/monster_drop_names.py index c42e7ad5ede0..df2cacf0c6aa 100644 --- a/worlds/stardew_valley/strings/monster_drop_names.py +++ b/worlds/stardew_valley/strings/monster_drop_names.py @@ -14,4 +14,8 @@ class Loot: class ModLoot: void_shard = "Void Shard" green_mushroom = "Green Mushroom" + ornate_treasure_chest = "Ornate Treasure Chest" + swirl_stone = "Swirl Stone" + void_pebble = "Void Pebble" + void_soul = "Void Soul" diff --git a/worlds/stardew_valley/strings/quest_names.py b/worlds/stardew_valley/strings/quest_names.py index 2c02381609ec..6370b8b56875 100644 --- a/worlds/stardew_valley/strings/quest_names.py +++ b/worlds/stardew_valley/strings/quest_names.py @@ -4,6 +4,7 @@ class Quest: getting_started = "Getting Started" to_the_beach = "To The Beach" raising_animals = "Raising Animals" + feeding_animals = "Feeding Animals" advancement = "Advancement" archaeology = "Archaeology" rat_problem = "Rat Problem" @@ -49,12 +50,13 @@ class Quest: dark_talisman = "Dark Talisman" goblin_problem = "Goblin Problem" magic_ink = "Magic Ink" + giant_stump = "The Giant Stump" class ModQuest: MrGinger = "Mr.Ginger's request" AyeishaEnvelope = "Missing Envelope" - AyeishaRing = "Lost Emerald Ring" + AyeishaRing = "Ayeisha's Lost Ring" JunaCola = "Juna's Drink Request" JunaSpaghetti = "Juna's BFF Request" RailroadBoulder = "The Railroad Boulder" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 0fdab64fef68..9cedb6b8ef32 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -14,6 +14,7 @@ class Region: forest = "Forest" bus_stop = "Bus Stop" backwoods = "Backwoods" + tunnel_entrance = "Tunnel Entrance" bus_tunnel = "Bus Tunnel" railroad = "Railroad" secret_woods = "Secret Woods" @@ -28,7 +29,6 @@ class Region: oasis = "Oasis" casino = "Casino" mines = "The Mines" - mines_dwarf_shop = "Mines Dwarf Shop" skull_cavern_entrance = "Skull Cavern Entrance" skull_cavern = "Skull Cavern" sewer = "Sewer" @@ -73,17 +73,9 @@ class Region: alex_house = "Alex's House" elliott_house = "Elliott's House" ranch = "Marnie's Ranch" - traveling_cart = "Traveling Cart" - traveling_cart_sunday = "Traveling Cart Sunday" - traveling_cart_monday = "Traveling Cart Monday" - traveling_cart_tuesday = "Traveling Cart Tuesday" - traveling_cart_wednesday = "Traveling Cart Wednesday" - traveling_cart_thursday = "Traveling Cart Thursday" - traveling_cart_friday = "Traveling Cart Friday" - traveling_cart_saturday = "Traveling Cart Saturday" + mastery_cave = "Mastery Cave" farm_cave = "Farmcave" greenhouse = "Greenhouse" - tunnel_entrance = "Tunnel Entrance" leah_house = "Leah's Cottage" wizard_tower = "Wizard Tower" wizard_basement = "Wizard Basement" @@ -91,6 +83,7 @@ class Region: maru_room = "Maru's Room" sebastian_room = "Sebastian's Room" adventurer_guild = "Adventurer's Guild" + adventurer_guild_bedroom = "Marlon's bedroom" quarry = "Quarry" quarry_mine_entrance = "Quarry Mine Entrance" quarry_mine = "Quarry Mine" @@ -148,6 +141,20 @@ class Region: dangerous_mines_20 = "Dangerous Mines - Floor 20" dangerous_mines_60 = "Dangerous Mines - Floor 60" dangerous_mines_100 = "Dangerous Mines - Floor 100" + + +class LogicRegion: + mines_dwarf_shop = "Mines Dwarf Shop" + + traveling_cart = "Traveling Cart" + traveling_cart_sunday = "Traveling Cart Sunday" + traveling_cart_monday = "Traveling Cart Monday" + traveling_cart_tuesday = "Traveling Cart Tuesday" + traveling_cart_wednesday = "Traveling Cart Wednesday" + traveling_cart_thursday = "Traveling Cart Thursday" + traveling_cart_friday = "Traveling Cart Friday" + traveling_cart_saturday = "Traveling Cart Saturday" + kitchen = "Kitchen" shipping = "Shipping" queen_of_sauce = "The Queen of Sauce" @@ -155,9 +162,18 @@ class Region: blacksmith_iron = "Blacksmith Iron Upgrades" blacksmith_gold = "Blacksmith Gold Upgrades" blacksmith_iridium = "Blacksmith Iridium Upgrades" - farming = "Farming" + + spring_farming = "Spring Farming" + summer_farming = "Summer Farming" + fall_farming = "Fall Farming" + winter_farming = "Winter Farming" + indoor_farming = "Indoor Farming" + summer_or_fall_farming = "Summer or Fall Farming" + fishing = "Fishing" egg_festival = "Egg Festival" + desert_festival = "Desert Festival" + trout_derby = "Trout Derby" flower_dance = "Flower Dance" luau = "Luau" moonlight_jellies = "Dance of the Moonlight Jellies" @@ -166,6 +182,13 @@ class Region: festival_of_ice = "Festival of Ice" night_market = "Night Market" winter_star = "Feast of the Winter Star" + squidfest = "SquidFest" + raccoon_daddy = "Raccoon Bundles" + raccoon_shop = "Raccoon Shop" + bookseller_1 = "Bookseller Experience Books" + bookseller_2 = "Bookseller Year 1 Books" + bookseller_3 = "Bookseller Year 3 Books" + forest_waterfall = "Waterfall" class DeepWoodsRegion: @@ -302,5 +325,3 @@ class BoardingHouseRegion: lost_valley_house_1 = "Lost Valley Ruins - First House" lost_valley_house_2 = "Lost Valley Ruins - Second House" buffalo_ranch = "Buffalo's Ranch" - - diff --git a/worlds/stardew_valley/strings/season_names.py b/worlds/stardew_valley/strings/season_names.py index f3659bc87fe0..1c4971c3f802 100644 --- a/worlds/stardew_valley/strings/season_names.py +++ b/worlds/stardew_valley/strings/season_names.py @@ -5,4 +5,5 @@ class Season: winter = "Winter" progressive = "Progressive Season" + all = (spring, summer, fall, winter) not_winter = (spring, summer, fall,) diff --git a/worlds/stardew_valley/strings/seed_names.py b/worlds/stardew_valley/strings/seed_names.py index 398b370f2745..f2799d4e449f 100644 --- a/worlds/stardew_valley/strings/seed_names.py +++ b/worlds/stardew_valley/strings/seed_names.py @@ -1,35 +1,72 @@ class Seed: + amaranth = "Amaranth Seeds" + artichoke = "Artichoke Seeds" + bean = "Bean Starter" + beet = "Beet Seeds" + blueberry = "Blueberry Seeds" + bok_choy = "Bok Choy Seeds" + broccoli = "Broccoli Seeds" + cactus = "Cactus Seeds" + carrot = "Carrot Seeds" + cauliflower = "Cauliflower Seeds" + coffee_starter = "Coffee Bean (Starter)" + """This item does not really exist and should never end up being displayed. + It's there to patch the loop in logic because "Coffee Bean" is both the seed and the crop.""" coffee = "Coffee Bean" + corn = "Corn Seeds" + cranberry = "Cranberry Seeds" + eggplant = "Eggplant Seeds" + fairy = "Fairy Seeds" garlic = "Garlic Seeds" + grape = "Grape Starter" + hops = "Hops Starter" jazz = "Jazz Seeds" + kale = "Kale Seeds" melon = "Melon Seeds" mixed = "Mixed Seeds" + mixed_flower = "Mixed Flower Seeds" + parsnip = "Parsnip Seeds" + pepper = "Pepper Seeds" pineapple = "Pineapple Seeds" poppy = "Poppy Seeds" + potato = "Potato Seeds" + powdermelon = "Powdermelon Seeds" + pumpkin = "Pumpkin Seeds" qi_bean = "Qi Bean" + radish = "Radish Seeds" + rare_seed = "Rare Seed" + red_cabbage = "Red Cabbage Seeds" + rhubarb = "Rhubarb Seeds" + rice = "Rice Shoot" spangle = "Spangle Seeds" + starfruit = "Starfruit Seeds" + strawberry = "Strawberry Seeds" + summer_squash = "Summer Squash Seeds" sunflower = "Sunflower Seeds" taro = "Taro Tuber" tomato = "Tomato Seeds" tulip = "Tulip Bulb" wheat = "Wheat Seeds" + yam = "Yam Seeds" class TreeSeed: acorn = "Acorn" maple = "Maple Seed" + mossy = "Mossy Seed" + mystic = "Mystic Tree Seed" pine = "Pine Cone" mahogany = "Mahogany Seed" mushroom = "Mushroom Tree Seed" class SVESeed: - stalk_seed = "Stalk Seed" - fungus_seed = "Fungus Seed" - slime_seed = "Slime Seed" - void_seed = "Void Seed" - shrub_seed = "Shrub Seed" - ancient_ferns_seed = "Ancient Ferns Seed" + stalk = "Stalk Seed" + fungus = "Fungus Seed" + slime = "Slime Seed" + void = "Void Seed" + shrub = "Shrub Seed" + ancient_fern = "Ancient Fern Seed" class DistantLandsSeed: diff --git a/worlds/stardew_valley/strings/skill_names.py b/worlds/stardew_valley/strings/skill_names.py index bae4c26fd716..7f3a61f2dfcd 100644 --- a/worlds/stardew_valley/strings/skill_names.py +++ b/worlds/stardew_valley/strings/skill_names.py @@ -15,4 +15,6 @@ class ModSkill: socializing = "Socializing" +all_vanilla_skills = {Skill.farming, Skill.foraging, Skill.fishing, Skill.mining, Skill.combat} all_mod_skills = {ModSkill.luck, ModSkill.binning, ModSkill.archaeology, ModSkill.cooking, ModSkill.magic, ModSkill.socializing} +all_skills = {*all_vanilla_skills, *all_mod_skills} diff --git a/worlds/stardew_valley/strings/tool_names.py b/worlds/stardew_valley/strings/tool_names.py index ea8c00b9bfd2..761f50e0a9bb 100644 --- a/worlds/stardew_valley/strings/tool_names.py +++ b/worlds/stardew_valley/strings/tool_names.py @@ -4,6 +4,7 @@ class Tool: hoe = "Hoe" watering_can = "Watering Can" trash_can = "Trash Can" + pan = "Pan" fishing_rod = "Fishing Rod" scythe = "Scythe" golden_scythe = "Golden Scythe" diff --git a/worlds/stardew_valley/strings/wallet_item_names.py b/worlds/stardew_valley/strings/wallet_item_names.py index 28f09b0558fc..32655efe88c2 100644 --- a/worlds/stardew_valley/strings/wallet_item_names.py +++ b/worlds/stardew_valley/strings/wallet_item_names.py @@ -8,3 +8,4 @@ class Wallet: skull_key = "Skull Key" dark_talisman = "Dark Talisman" club_card = "Club Card" + mastery_of_the_five_ways = "Mastery Of The Five Ways" diff --git a/worlds/stardew_valley/test/TestBooksanity.py b/worlds/stardew_valley/test/TestBooksanity.py new file mode 100644 index 000000000000..3ca52f5728c1 --- /dev/null +++ b/worlds/stardew_valley/test/TestBooksanity.py @@ -0,0 +1,207 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Booksanity, Shipsanity +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.book_names import Book, LostBook + +power_books = [Book.animal_catalogue, Book.book_of_mysteries, + Book.the_alleyway_buffet, Book.the_art_o_crabbing, Book.dwarvish_safety_manual, + Book.jewels_of_the_sea, Book.raccoon_journal, Book.woodys_secret, Book.jack_be_nimble_jack_be_thick, Book.friendship_101, + Book.monster_compendium, Book.mapping_cave_systems, Book.treasure_appraisal_guide, Book.way_of_the_wind_pt_1, Book.way_of_the_wind_pt_2, + Book.horse_the_book, Book.ol_slitherlegs, Book.price_catalogue, Book.the_diamond_hunter, ] + +skill_books = [Book.combat_quarterly, Book.woodcutters_weekly, Book.book_of_stars, Book.stardew_valley_almanac, Book.bait_and_bobber, Book.mining_monthly, + Book.queen_of_sauce_cookbook, ] + +lost_books = [ + LostBook.tips_on_farming, LostBook.this_is_a_book_by_marnie, LostBook.on_foraging, LostBook.the_fisherman_act_1, + LostBook.how_deep_do_the_mines_go, LostBook.an_old_farmers_journal, LostBook.scarecrows, LostBook.the_secret_of_the_stardrop, + LostBook.journey_of_the_prairie_king_the_smash_hit_video_game, LostBook.a_study_on_diamond_yields, LostBook.brewmasters_guide, + LostBook.mysteries_of_the_dwarves, LostBook.highlights_from_the_book_of_yoba, LostBook.marriage_guide_for_farmers, LostBook.the_fisherman_act_ii, + LostBook.technology_report, LostBook.secrets_of_the_legendary_fish, LostBook.gunther_tunnel_notice, LostBook.note_from_gunther, + LostBook.goblins_by_m_jasper, LostBook.secret_statues_acrostics, ] + +lost_book = "Progressive Lost Book" + + +class TestBooksanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_none, + } + + def test_no_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertNotIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowers(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityPowersAndSkills(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_power_skill, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_no_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertNotIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertNotIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestBooksanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Shipsanity: Shipsanity.option_everything, + Booksanity: Booksanity.option_all, + } + + def test_all_power_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_skill_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in skill_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_lost_books_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for book in lost_books: + with self.subTest(book): + self.assertIn(f"Read {book}", location_names) + + def test_all_power_items(self): + item_names = {location.name for location in self.multiworld.get_items()} + for book in power_books: + with self.subTest(book): + self.assertIn(f"Power: {book}", item_names) + with self.subTest(lost_book): + self.assertIn(lost_book, item_names) + + def test_can_ship_all_books(self): + self.collect_everything() + shipsanity_prefix = "Shipsanity: " + for location in self.multiworld.get_locations(): + if not location.name.startswith(shipsanity_prefix): + continue + item_to_ship = location.name[len(shipsanity_prefix):] + if item_to_ship not in power_books and item_to_ship not in skill_books: + continue + with self.subTest(location.name): + self.assert_reach_location_true(location, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index cd6828cd79e5..091f39b2568e 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,8 +1,12 @@ import unittest -from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic +from . import SVTestBase +from .. import BundleRandomization +from ..data.bundle_data import all_bundle_items_except_money, quality_crops_items_thematic, quality_foraging_items, quality_fish_items +from ..options import BundlePlando +from ..strings.bundle_names import BundleName from ..strings.crop_names import Fruit -from ..strings.quality_names import CropQuality +from ..strings.quality_names import CropQuality, ForageQuality, FishQuality class TestBundles(unittest.TestCase): @@ -27,3 +31,60 @@ def test_quality_crops_have_correct_quality(self): with self.subTest(bundle_item.item_name): self.assertEqual(bundle_item.quality, CropQuality.gold) + def test_quality_foraging_have_correct_amounts(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 3) + + def test_quality_foraging_have_correct_quality(self): + for bundle_item in quality_foraging_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, ForageQuality.gold) + + def test_quality_fish_have_correct_amounts(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.amount, 2) + + def test_quality_fish_have_correct_quality(self): + for bundle_item in quality_fish_items: + with self.subTest(bundle_item.item_name): + self.assertEqual(bundle_item.quality, FishQuality.gold) + + +class TestRemixedPlandoBundles(SVTestBase): + plando_bundles = {BundleName.money_2500, BundleName.money_5000, BundleName.money_10000, BundleName.gambler, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.deep_fishing, BundleName.spring_fish, BundleName.legendary_fish, BundleName.bait} + options = { + BundleRandomization: BundleRandomization.option_remixed, + BundlePlando: frozenset(plando_bundles) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.plando_bundles: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + self.assertNotIn(BundleName.money_25000, location_names) + self.assertNotIn(BundleName.carnival, location_names) + self.assertNotIn(BundleName.night_fish, location_names) + self.assertNotIn(BundleName.specialty_fish, location_names) + self.assertNotIn(BundleName.specific_bait, location_names) + + +class TestRemixedAnywhereBundles(SVTestBase): + fish_bundle_names = {BundleName.spring_fish, BundleName.summer_fish, BundleName.fall_fish, BundleName.winter_fish, BundleName.ocean_fish, + BundleName.lake_fish, BundleName.river_fish, BundleName.night_fish, BundleName.legendary_fish, BundleName.specialty_fish, + BundleName.bait, BundleName.specific_bait, BundleName.crab_pot, BundleName.tackle, BundleName.quality_fish, + BundleName.rain_fish, BundleName.master_fisher} + options = { + BundleRandomization: BundleRandomization.option_remixed_anywhere, + BundlePlando: frozenset(fish_bundle_names) + } + + def test_all_plando_bundles_are_there(self): + location_names = {location.name for location in self.multiworld.get_locations()} + for bundle_name in self.fish_bundle_names: + with self.subTest(f"{bundle_name}"): + self.assertIn(bundle_name, location_names) + diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py index a77dc17319b7..86550705b917 100644 --- a/worlds/stardew_valley/test/TestData.py +++ b/worlds/stardew_valley/test/TestData.py @@ -2,19 +2,52 @@ from ..items import load_item_csv from ..locations import load_location_csv +from ..options import Mods class TestCsvIntegrity(unittest.TestCase): def test_items_integrity(self): items = load_item_csv() - for item in items: - self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all items have an id"): + for item in items: + self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [item.code_without_offset for item in items] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [item.name for item in items] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {item.mod_name for item in items} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) def test_locations_integrity(self): locations = load_location_csv() - for location in locations: - self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." - " Run the script `update_data.py` to generate them.") + with self.subTest("Test all locations have an id"): + for location in locations: + self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." + " Run the script `update_data.py` to generate them.") + with self.subTest("Test all ids are unique"): + all_ids = [location.code_without_offset for location in locations] + unique_ids = set(all_ids) + self.assertEqual(len(all_ids), len(unique_ids)) + + with self.subTest("Test all names are unique"): + all_names = [location.name for location in locations] + unique_names = set(all_names) + self.assertEqual(len(all_names), len(unique_names)) + + with self.subTest("Test all mod names are valid"): + mod_names = {location.mod_name for location in locations} + for mod_name in mod_names: + if mod_name: + self.assertIn(mod_name, Mods.valid_keys) diff --git a/worlds/stardew_valley/test/TestFarmType.py b/worlds/stardew_valley/test/TestFarmType.py new file mode 100644 index 000000000000..f78edc3eece8 --- /dev/null +++ b/worlds/stardew_valley/test/TestFarmType.py @@ -0,0 +1,31 @@ +from . import SVTestBase +from .assertion import WorldAssertMixin +from .. import options + + +class TestStartInventoryStandardFarm(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_standard, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 3) + self.assertNotIn("Progressive Coop", start_items) + + +class TestStartInventoryMeadowLands(WorldAssertMixin, SVTestBase): + options = { + options.FarmType.internal_name: options.FarmType.option_meadowlands, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + } + + def test_start_inventory_progressive_coops(self): + start_items = dict(map(lambda x: (x.name, self.multiworld.precollected_items[self.player].count(x)), self.multiworld.precollected_items[self.player])) + items = dict(map(lambda x: (x.name, self.multiworld.itempool.count(x)), self.multiworld.itempool)) + self.assertIn("Progressive Coop", items) + self.assertEqual(items["Progressive Coop"], 2) + self.assertIn("Progressive Coop", start_items) + self.assertEqual(start_items["Progressive Coop"], 1) diff --git a/worlds/stardew_valley/test/TestFill.py b/worlds/stardew_valley/test/TestFill.py new file mode 100644 index 000000000000..0bfacb6ef6f5 --- /dev/null +++ b/worlds/stardew_valley/test/TestFill.py @@ -0,0 +1,30 @@ +from . import SVTestBase, minimal_locations_maximal_items +from .assertion import WorldAssertMixin +from .. import options +from ..mods.mod_data import ModNames + + +class TestMinLocationsMaxItems(WorldAssertMixin, SVTestBase): + options = minimal_locations_maximal_items() + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) + + +class TestSpecificSeedForTroubleshooting(WorldAssertMixin, SVTestBase): + options = { + options.Fishsanity: options.Fishsanity.option_all, + options.Goal: options.Goal.option_master_angler, + options.QuestLocations: -1, + options.Mods: (ModNames.sve,), + } + seed = 65453499742665118161 + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFishsanity.py b/worlds/stardew_valley/test/TestFishsanity.py new file mode 100644 index 000000000000..c5d87c0f8dd7 --- /dev/null +++ b/worlds/stardew_valley/test/TestFishsanity.py @@ -0,0 +1,405 @@ +import unittest +from typing import ClassVar, Set + +from . import SVTestBase +from .assertion import WorldAssertMixin +from ..content.feature import fishsanity +from ..mods.mod_data import ModNames +from ..options import Fishsanity, ExcludeGingerIsland, Mods, SpecialOrderLocations, Goal, QuestLocations +from ..strings.fish_names import Fish, SVEFish, DistantLandsFish + +pelican_town_legendary_fishes = {Fish.angler, Fish.crimsonfish, Fish.glacierfish, Fish.legend, Fish.mutant_carp, } +pelican_town_hard_special_fishes = {Fish.lava_eel, Fish.octopus, Fish.scorpion_carp, Fish.ice_pip, Fish.super_cucumber, } +pelican_town_medium_special_fishes = {Fish.blobfish, Fish.dorado, } +pelican_town_hard_normal_fishes = {Fish.lingcod, Fish.pufferfish, Fish.void_salmon, } +pelican_town_medium_normal_fishes = { + Fish.albacore, Fish.catfish, Fish.eel, Fish.flounder, Fish.ghostfish, Fish.goby, Fish.halibut, Fish.largemouth_bass, Fish.midnight_carp, + Fish.midnight_squid, Fish.pike, Fish.red_mullet, Fish.salmon, Fish.sandfish, Fish.slimejack, Fish.stonefish, Fish.spook_fish, Fish.squid, Fish.sturgeon, + Fish.tiger_trout, Fish.tilapia, Fish.tuna, Fish.woodskip, +} +pelican_town_easy_normal_fishes = { + Fish.anchovy, Fish.bream, Fish.bullhead, Fish.carp, Fish.chub, Fish.herring, Fish.perch, Fish.rainbow_trout, Fish.red_snapper, Fish.sardine, Fish.shad, + Fish.sea_cucumber, Fish.shad, Fish.smallmouth_bass, Fish.sunfish, Fish.walleye, +} +pelican_town_crab_pot_fishes = { + Fish.clam, Fish.cockle, Fish.crab, Fish.crayfish, Fish.lobster, Fish.mussel, Fish.oyster, Fish.periwinkle, Fish.shrimp, Fish.snail, +} + +ginger_island_hard_fishes = {Fish.pufferfish, Fish.stingray, Fish.super_cucumber, } +ginger_island_medium_fishes = {Fish.blue_discus, Fish.lionfish, Fish.tilapia, Fish.tuna, } +qi_board_legendary_fishes = {Fish.ms_angler, Fish.son_of_crimsonfish, Fish.glacierfish_jr, Fish.legend_ii, Fish.radioactive_carp, } + +sve_pelican_town_hard_fishes = { + SVEFish.grass_carp, SVEFish.king_salmon, SVEFish.kittyfish, SVEFish.meteor_carp, SVEFish.puppyfish, SVEFish.radioactive_bass, SVEFish.undeadfish, + SVEFish.void_eel, +} +sve_pelican_town_medium_fishes = { + SVEFish.bonefish, SVEFish.butterfish, SVEFish.frog, SVEFish.goldenfish, SVEFish.snatcher_worm, SVEFish.water_grub, +} +sve_pelican_town_easy_fishes = {SVEFish.bull_trout, SVEFish.minnow, } +sve_ginger_island_hard_fishes = {SVEFish.gemfish, SVEFish.shiny_lunaloo, } +sve_ginger_island_medium_fishes = {SVEFish.daggerfish, SVEFish.lunaloo, SVEFish.starfish, SVEFish.torpedo_trout, } +sve_ginger_island_easy_fishes = {SVEFish.baby_lunaloo, SVEFish.clownfish, SVEFish.seahorse, SVEFish.sea_sponge, } + +distant_lands_hard_fishes = {DistantLandsFish.giant_horsehoe_crab, } +distant_lands_easy_fishes = {DistantLandsFish.void_minnow, DistantLandsFish.purple_algae, DistantLandsFish.swamp_leech, } + + +def complete_options_with_default(options): + return { + **{ + ExcludeGingerIsland: ExcludeGingerIsland.default, + Mods: Mods.default, + SpecialOrderLocations: SpecialOrderLocations.default, + }, + **options + } + + +class SVFishsanityTestBase(SVTestBase): + expected_fishes: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFishsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_fishsanity(self): + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_fishes() + + def check_all_locations_match_expected_fishes(self): + location_fishes = { + name + for location_name in self.get_real_location_names() + if (name := fishsanity.extract_fish_from_location_name(location_name)) is not None + } + + self.assertEqual(location_fishes, self.expected_fishes) + + +class TestFishsanityNoneVanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_none, + }) + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFishsanityLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + }) + expected_fishes = pelican_town_legendary_fishes + + +class TestFishsanityLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = pelican_town_legendary_fishes | qi_board_legendary_fishes + + +class TestFishsanitySpecial(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_special, + }) + expected_fishes = pelican_town_legendary_fishes | pelican_town_hard_special_fishes | pelican_town_medium_special_fishes + + +class TestFishsanityAll_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityAll_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_hard_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_hard_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes + ) + + +class TestFishsanityAll_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + distant_lands_hard_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityAll_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes | + qi_board_legendary_fishes + ) + + +class TestFishsanityAll_ExcludeGingerIsland_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_all, + ExcludeGingerIsland: ExcludeGingerIsland.option_true, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + }) + expected_fishes = ( + pelican_town_legendary_fishes | + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityExcludeLegendaries_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeLegendaries_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_legendaries, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_hard_special_fishes | + pelican_town_medium_special_fishes | + pelican_town_hard_normal_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_hard_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityExcludeHardFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + sve_pelican_town_medium_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_medium_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityExcludeHardFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_exclude_hard_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_medium_special_fishes | + pelican_town_medium_normal_fishes | + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + ginger_island_medium_fishes + ) + + +class TestFishsanityOnlyEasyFishes_Vanilla(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityOnlyEasyFishes_SVE(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.sve, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + sve_pelican_town_easy_fishes | + sve_ginger_island_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_DistantLands(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + Mods: ModNames.distant_lands, + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes | + distant_lands_easy_fishes + ) + + +class TestFishsanityOnlyEasyFishes_QiBoard(SVFishsanityTestBase): + options = complete_options_with_default({ + Fishsanity: Fishsanity.option_only_easy_fish, + SpecialOrderLocations: SpecialOrderLocations.option_board_qi, + ExcludeGingerIsland: ExcludeGingerIsland.option_false + }) + expected_fishes = ( + pelican_town_easy_normal_fishes | + pelican_town_crab_pot_fishes + ) + + +class TestFishsanityMasterAnglerSVEWithoutQuests(WorldAssertMixin, SVTestBase): + options = { + Fishsanity: Fishsanity.option_all, + Goal: Goal.option_master_angler, + QuestLocations: -1, + Mods: (ModNames.sve,), + } + + def run_default_tests(self) -> bool: + return True + + def test_fill(self): + self.assert_basic_checks(self.multiworld) diff --git a/worlds/stardew_valley/test/TestFriendsanity.py b/worlds/stardew_valley/test/TestFriendsanity.py new file mode 100644 index 000000000000..842c0edd0980 --- /dev/null +++ b/worlds/stardew_valley/test/TestFriendsanity.py @@ -0,0 +1,159 @@ +import unittest +from collections import Counter +from typing import ClassVar, Set + +from . import SVTestBase +from ..content.feature import friendsanity +from ..options import Friendsanity, FriendsanityHeartSize + +all_vanilla_bachelor = { + "Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", "Maru" +} + +all_vanilla_starting_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", +} + +all_vanilla_npc = { + "Alex", "Elliott", "Harvey", "Sam", "Sebastian", "Shane", "Abigail", "Emily", "Haley", "Leah", "Maru", "Penny", "Caroline", "Clint", "Demetrius", "Evelyn", + "George", "Gus", "Jas", "Jodi", "Lewis", "Linus", "Marnie", "Pam", "Pierre", "Robin", "Vincent", "Willy", "Wizard", "Pet", "Sandy", "Dwarf", "Kent", "Leo", + "Krobus" +} + + +class SVFriendsanityTestBase(SVTestBase): + expected_npcs: ClassVar[Set[str]] = set() + expected_pet_heart_size: ClassVar[Set[str]] = set() + expected_bachelor_heart_size: ClassVar[Set[str]] = set() + expected_other_heart_size: ClassVar[Set[str]] = set() + + @classmethod + def setUpClass(cls) -> None: + if cls is SVFriendsanityTestBase: + raise unittest.SkipTest("Base tests disabled") + + super().setUpClass() + + def test_friendsanity(self): + with self.subTest("Items are valid"): + self.check_all_items_match_expected_npcs() + with self.subTest("Correct number of items"): + self.check_correct_number_of_items() + with self.subTest("Locations are valid"): + self.check_all_locations_match_expected_npcs() + with self.subTest("Locations heart size are valid"): + self.check_all_locations_match_heart_size() + + def check_all_items_match_expected_npcs(self): + npc_names = { + name + for item in self.multiworld.itempool + if (name := friendsanity.extract_npc_from_item_name(item.name)) is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_correct_number_of_items(self): + item_by_npc = Counter() + for item in self.multiworld.itempool: + name = friendsanity.extract_npc_from_item_name(item.name) + if name is None: + continue + + item_by_npc[name] += 1 + + for name, count in item_by_npc.items(): + + if name == "Pet": + self.assertEqual(count, len(self.expected_pet_heart_size)) + elif self.world.content.villagers[name].bachelor: + self.assertEqual(count, len(self.expected_bachelor_heart_size)) + else: + self.assertEqual(count, len(self.expected_other_heart_size)) + + def check_all_locations_match_expected_npcs(self): + npc_names = { + name_and_heart[0] + for location_name in self.get_real_location_names() + if (name_and_heart := friendsanity.extract_npc_from_location_name(location_name))[0] is not None + } + + self.assertEqual(npc_names, self.expected_npcs) + + def check_all_locations_match_heart_size(self): + for location_name in self.get_real_location_names(): + name, heart_size = friendsanity.extract_npc_from_location_name(location_name) + if name is None: + continue + + if name == "Pet": + self.assertIn(heart_size, self.expected_pet_heart_size) + elif self.world.content.villagers[name].bachelor: + self.assertIn(heart_size, self.expected_bachelor_heart_size) + else: + self.assertIn(heart_size, self.expected_other_heart_size) + + +class TestFriendsanityNone(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_none, + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False + + +class TestFriendsanityBachelors(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_bachelors, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_bachelor + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + + +class TestFriendsanityStartingNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_starting_npcs, + FriendsanityHeartSize: 1, + } + expected_npcs = all_vanilla_starting_npc + expected_pet_heart_size = {1, 2, 3, 4, 5} + expected_bachelor_heart_size = {1, 2, 3, 4, 5, 6, 7, 8} + expected_other_heart_size = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + +class TestFriendsanityAllNpcs(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all, + FriendsanityHeartSize: 4, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {4, 5} + expected_bachelor_heart_size = {4, 8} + expected_other_heart_size = {4, 8, 10} + + +class TestFriendsanityHeartSize3(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 3, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {3, 5} + expected_bachelor_heart_size = {3, 6, 9, 12, 14} + expected_other_heart_size = {3, 6, 9, 10} + + +class TestFriendsanityHeartSize5(SVFriendsanityTestBase): + options = { + Friendsanity: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize: 5, + } + expected_npcs = all_vanilla_npc + expected_pet_heart_size = {5} + expected_bachelor_heart_size = {5, 10, 14} + expected_other_heart_size = {5, 10} diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 1b4d1476b900..8431e6857eaf 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,26 +1,27 @@ from typing import List from BaseClasses import ItemClassification, Item -from . import SVTestBase, allsanity_options_without_mods, \ - allsanity_options_with_mods, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_options +from . import SVTestBase from .. import items, location_table, options -from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name -from ..items import Group, item_table +from ..items import Group from ..locations import LocationTags -from ..mods.mod_data import ModNames from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \ - FriendsanityHeartSize + SkillProgression, Booksanity, Walnutsanity from ..strings.region_names import Region class TestBaseItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + Booksanity.internal_name: Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, } def test_all_progression_items_are_added_to_the_pool(self): @@ -65,12 +66,14 @@ def test_does_not_create_exactly_two_items(self): class TestNoGingerIslandItemGeneration(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, Shipsanity.internal_name: Shipsanity.option_everything, Chefsanity.internal_name: Chefsanity.option_all, Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + Booksanity.internal_name: Booksanity.option_all, } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -117,7 +120,16 @@ def test_does_not_create_exactly_two_items(self): class TestMonstersanityNone(SVTestBase): - options = {options.Monstersanity.internal_name: options.Monstersanity.option_none} + options = { + options.Monstersanity.internal_name: options.Monstersanity.option_none, + # Not really necessary, but it adds more locations, so we don't have to remove useful items. + options.Fishsanity.internal_name: options.Fishsanity.option_all + } + + @property + def run_default_tests(self) -> bool: + # None is default + return False def test_when_generate_world_then_5_generic_weapons_in_the_pool(self): item_pool = [item.name for item in self.multiworld.itempool] @@ -367,408 +379,15 @@ def generate_items_for_skull_100(self) -> List[Item]: return [*combat_levels, *mining_levels, *pickaxes, *swords, bus, skull_key] -class TestLocationGeneration(SVTestBase): - - def test_all_location_created_are_in_location_table(self): - for location in self.get_real_locations(): - self.assertIn(location.name, location_table) - - -class TestMinLocationAndMaxItem(SVTestBase): - options = minimal_locations_maximal_items() - - # They do not pass and I don't know why. - skip_base_tests = True - - def test_minimal_location_maximal_items_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") - - -class TestMinLocationAndMaxItemWithIsland(SVTestBase): - options = minimal_locations_maximal_items_with_island() - - def test_minimal_location_maximal_items_with_island_still_valid(self): - valid_locations = self.get_real_locations() - number_locations = len(valid_locations) - number_items = len([item for item in self.multiworld.itempool - if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) - self.assertGreaterEqual(number_locations, number_items) - print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") - - -class TestMinSanityHasAllExpectedLocations(SVTestBase): - options = get_minsanity_options() - - def test_minsanity_has_fewer_than_locations(self): - expected_locations = 76 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertLessEqual(number_locations, expected_locations) - print(f"Stardew Valley - Minsanity Locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tDisappeared Locations Detected!" - f"\n\tPlease update test_minsanity_has_fewer_than_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): - options = default_options() - - def test_default_settings_has_exactly_locations(self): - expected_locations = 422 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - print(f"Stardew Valley - Default options locations: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_default_settings_has_exactly_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_without_mods() - - def test_allsanity_without_mods_has_at_least_locations(self): - expected_locations = 1956 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): - options = allsanity_options_with_mods() - - def test_allsanity_with_mods_has_at_least_locations(self): - expected_locations = 2804 - real_locations = self.get_real_locations() - number_locations = len(real_locations) - self.assertGreaterEqual(number_locations, expected_locations) - print(f"\nStardew Valley - Allsanity Locations with all mods: {number_locations}") - if number_locations != expected_locations: - print(f"\tNew locations detected!" - f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" - f"\n\t\tExpected: {expected_locations}" - f"\n\t\tActual: {number_locations}") - - -class TestFriendsanityNone(SVTestBase): +class TestShipsanityNone(SVTestBase): options = { - Friendsanity.internal_name: Friendsanity.option_none, + Shipsanity.internal_name: Shipsanity.option_none } - @property def run_default_tests(self) -> bool: # None is default return False - def test_friendsanity_none(self): - with self.subTest("No Items"): - self.check_no_friendsanity_items() - with self.subTest("No Locations"): - self.check_no_friendsanity_locations() - - def check_no_friendsanity_items(self): - for item in self.multiworld.itempool: - self.assertFalse(item.name.endswith(" <3")) - - def check_no_friendsanity_locations(self): - for location_name in self.get_real_location_names(): - self.assertFalse(location_name.startswith("Friendsanity")) - - -class TestFriendsanityBachelors(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_bachelors, - FriendsanityHeartSize.internal_name: 1, - } - bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", - "Maru"} - - def test_friendsanity_only_bachelors(self): - with self.subTest("Items are valid"): - self.check_only_bachelors_items() - with self.subTest("Locations are valid"): - self.check_only_bachelors_locations() - - def check_only_bachelors_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertIn(villager_name, self.bachelors) - - def check_only_bachelors_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertIn(name, self.bachelors) - self.assertLessEqual(int(hearts), 8) - - -class TestFriendsanityStartingNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_starting_npcs, - FriendsanityHeartSize.internal_name: 1, - } - excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"} - - def test_friendsanity_only_starting_npcs(self): - with self.subTest("Items are valid"): - self.check_only_starting_npcs_items() - with self.subTest("Locations are valid"): - self.check_only_starting_npcs_locations() - - def check_only_starting_npcs_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotIn(villager_name, self.excluded_npcs) - - def check_only_starting_npcs_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotIn(name, self.excluded_npcs) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityAllNpcs(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - } - - def test_friendsanity_all_npcs(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 2) - else: - self.assertEqual(number_heart_items, 3) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 4 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10) - - -class TestFriendsanityAllNpcsExcludingGingerIsland(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all, - FriendsanityHeartSize.internal_name: 4, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true - } - - def test_friendsanity_all_npcs_exclude_island(self): - with self.subTest("Items"): - self.check_items() - with self.subTest("Locations"): - self.check_locations() - - def check_items(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertNotEqual(villager_name, "Leo") - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_locations(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if location_name.startswith(prefix): - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = parts[1] - self.assertNotEqual(name, "Leo") - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertLessEqual(int(hearts), 5) - elif all_villagers_by_name[name].bachelor: - self.assertLessEqual(int(hearts), 8) - else: - self.assertLessEqual(int(hearts), 10) - - -class TestFriendsanityHeartSize3(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 5) - else: - self.assertEqual(number_heart_items, 4) - self.assertEqual(item_names.count("Pet <3"), 2) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 3 or hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 12 or hearts == 14) - else: - self.assertTrue(hearts == 3 or hearts == 6 or hearts == 9 or hearts == 10) - - -class TestFriendsanityHeartSize5(SVTestBase): - options = { - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 5, - } - - def test_friendsanity_all_npcs_with_marriage(self): - with self.subTest("Items are valid"): - self.check_items_are_valid() - with self.subTest("Correct number of items"): - self.check_correct_number_of_items() - with self.subTest("Locations are valid"): - self.check_locations_are_valid() - - def check_items_are_valid(self): - suffix = " <3" - for item in self.multiworld.itempool: - if item.name.endswith(suffix): - villager_name = item.name[:item.name.index(suffix)] - self.assertTrue(villager_name in all_villagers_by_mod_by_name[ModNames.vanilla] or villager_name == "Pet") - - def check_correct_number_of_items(self): - suffix = " <3" - item_names = [item.name for item in self.multiworld.itempool] - for villager_name in all_villagers_by_mod_by_name[ModNames.vanilla]: - heart_item_name = f"{villager_name}{suffix}" - number_heart_items = item_names.count(heart_item_name) - if all_villagers_by_name[villager_name].bachelor: - self.assertEqual(number_heart_items, 3) - else: - self.assertEqual(number_heart_items, 2) - self.assertEqual(item_names.count("Pet <3"), 1) - - def check_locations_are_valid(self): - prefix = "Friendsanity: " - suffix = " <3" - for location_name in self.get_real_location_names(): - if not location_name.startswith(prefix): - continue - name_no_prefix = location_name[len(prefix):] - name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] - parts = name_trimmed.split(" ") - name = parts[0] - hearts = int(parts[1]) - self.assertTrue(name in all_villagers_by_mod_by_name[ModNames.vanilla] or name == "Pet") - if name == "Pet": - self.assertTrue(hearts == 5) - elif all_villagers_by_name[name].bachelor: - self.assertTrue(hearts == 5 or hearts == 10 or hearts == 14) - else: - self.assertTrue(hearts == 5 or hearts == 10) - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - def test_no_shipsanity_locations(self): for location in self.get_real_locations(): with self.subTest(location.name): @@ -779,6 +398,7 @@ def test_no_shipsanity_locations(self): class TestShipsanityCrops(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -825,7 +445,7 @@ def test_only_mainland_crop_shipsanity_locations(self): class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_crops, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_crop_shipsanity_locations(self): @@ -848,6 +468,7 @@ def test_island_crops_without_qi_fruit_shipsanity_locations(self): class TestShipsanityFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -896,7 +517,7 @@ def test_exclude_island_fish_shipsanity_locations(self): class TestShipsanityFishExcludeQiOrders(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_fish_shipsanity_locations(self): @@ -920,6 +541,7 @@ def test_include_island_fish_no_extended_family_shipsanity_locations(self): class TestShipsanityFullShipment(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -973,7 +595,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla } def test_only_full_shipment_shipsanity_locations(self): @@ -1000,6 +622,7 @@ def test_exclude_qi_board_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFish(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi } @@ -1069,7 +692,7 @@ def test_exclude_island_items_shipsanity_locations(self): class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase): options = { Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board } def test_only_full_shipment_and_fish_shipsanity_locations(self): diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 48bc1b152138..671fe6387258 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -1,16 +1,11 @@ -import sys -import random -import sys - from BaseClasses import MultiWorld, get_seed -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, get_minsanity_options +from . import setup_solo_multiworld, SVTestCase, allsanity_no_mods_6_x_x, get_minsanity_options, solo_multiworld from .. import StardewValleyWorld from ..items import Group, item_table from ..options import Friendsanity, SeasonRandomization, Museumsanity, Shipsanity, Goal from ..strings.wallet_item_names import Wallet all_seasons = ["Spring", "Summer", "Fall", "Winter"] -all_farms = ["Standard Farm", "Riverland Farm", "Forest Farm", "Hill-top Farm", "Wilderness Farm", "Four Corners Farm", "Beach Farm"] class TestItems(SVTestCase): @@ -48,16 +43,16 @@ def test_babies_come_in_all_shapes_and_sizes(self): self.assertEqual(len(baby_permutations), 4) def test_correct_number_of_stardrops(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name] - self.assertEqual(len(stardrop_items), 7) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + stardrop_items = [item for item in multiworld.get_items() if item.name == "Stardrop"] + self.assertEqual(len(stardrop_items), 7) def test_no_duplicate_rings(self): - allsanity_options = allsanity_options_without_mods() - multiworld = setup_solo_multiworld(allsanity_options) - ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] - self.assertEqual(len(ring_items), len(set(ring_items))) + allsanity_options = allsanity_no_mods_6_x_x() + with solo_multiworld(allsanity_options) as (multiworld, _): + ring_items = [item.name for item in multiworld.get_items() if Group.RING in item_table[item.name].groups] + self.assertEqual(len(ring_items), len(set(ring_items))) def test_can_start_in_any_season(self): starting_seasons_rolled = set() @@ -75,66 +70,54 @@ def test_can_start_in_any_season(self): starting_seasons_rolled.add(f"{starting_season_items[0]}") self.assertEqual(len(starting_seasons_rolled), 4) - def test_can_start_on_any_farm(self): - starting_farms_rolled = set() - for attempt_number in range(60): - if len(starting_farms_rolled) >= 7: - print(f"Already got all 7 farm types, breaking early [{attempt_number} generations]") - break - seed = random.randrange(sys.maxsize) - multiworld = setup_solo_multiworld(seed=seed, _cache={}) - starting_farm = multiworld.worlds[1].fill_slot_data()["farm_type"] - starting_farms_rolled.add(starting_farm) - self.assertEqual(len(starting_farms_rolled), 7) - class TestMetalDetectors(SVTestCase): def test_minsanity_1_metal_detector(self): options = get_minsanity_options() - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_museumsanity_2_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_shipsanity_full_shipment_1_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_full_shipment - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 1) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 1) def test_shipsanity_everything_2_metal_detector(self): options = get_minsanity_options().copy() options[Shipsanity.internal_name] = Shipsanity.option_everything - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_complete_collection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_complete_collection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_perfection_2_metal_detector(self): options = get_minsanity_options().copy() options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 2) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 2) def test_maxsanity_4_metal_detector(self): options = get_minsanity_options().copy() options[Museumsanity.internal_name] = Museumsanity.option_all options[Shipsanity.internal_name] = Shipsanity.option_everything options[Goal.internal_name] = Goal.option_perfection - multiworld = setup_solo_multiworld(options) - items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] - self.assertEquals(len(items), 4) + with solo_multiworld(options) as (multiworld, _): + items = [item.name for item in multiworld.get_items() if item.name == Wallet.metal_detector] + self.assertEqual(len(items), 4) diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 84d38ffeb449..65f7352a5e36 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,12 +1,13 @@ -from unittest import TestCase +import typing +import unittest +from unittest import TestCase, SkipTest -from . import setup_solo_multiworld, allsanity_options_with_mods -from .assertion import RuleAssertMixin +from BaseClasses import MultiWorld +from . import RuleAssertMixin, setup_solo_multiworld, allsanity_mods_6_x_x, minimal_locations_maximal_items +from .. import StardewValleyWorld from ..data.bundle_data import all_bundle_items_except_money - -multi_world = setup_solo_multiworld(allsanity_options_with_mods(), _cache={}) -world = multi_world.worlds[1] -logic = world.logic +from ..logic.logic import StardewLogic +from ..options import BundleRandomization def collect_all(mw): @@ -14,77 +15,91 @@ def collect_all(mw): mw.state.collect(item, event=True) -collect_all(multi_world) +class LogicTestBase(RuleAssertMixin, TestCase): + options: typing.Dict[str, typing.Any] = {} + multiworld: MultiWorld + logic: StardewLogic + world: StardewValleyWorld + + @classmethod + def setUpClass(cls) -> None: + if cls is LogicTestBase: + raise SkipTest("Not running test on base class.") + def setUp(self) -> None: + self.multiworld = setup_solo_multiworld(self.options, _cache={}) + collect_all(self.multiworld) + self.world = typing.cast(StardewValleyWorld, self.multiworld.worlds[1]) + self.logic = self.world.logic -class TestLogic(RuleAssertMixin, TestCase): def test_given_bundle_item_then_is_available_in_logic(self): for bundle_item in all_bundle_items_except_money: + if not bundle_item.can_appear(self.world.content, self.world.options): + continue + with self.subTest(msg=bundle_item.item_name): - self.assertIn(bundle_item.item_name, logic.registry.item_rules) + self.assertIn(bundle_item.get_item(), self.logic.registry.item_rules) def test_given_item_rule_then_can_be_resolved(self): - for item in logic.registry.item_rules.keys(): + for item in self.logic.registry.item_rules.keys(): with self.subTest(msg=item): - rule = logic.registry.item_rules[item] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.item_rules[item] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_building_rule_then_can_be_resolved(self): - for building in logic.registry.building_rules.keys(): + for building in self.logic.registry.building_rules.keys(): with self.subTest(msg=building): - rule = logic.registry.building_rules[building] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.building_rules[building] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_quest_rule_then_can_be_resolved(self): - for quest in logic.registry.quest_rules.keys(): + for quest in self.logic.registry.quest_rules.keys(): with self.subTest(msg=quest): - rule = logic.registry.quest_rules[quest] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.quest_rules[quest] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_special_order_rule_then_can_be_resolved(self): - for special_order in logic.registry.special_order_rules.keys(): + for special_order in self.logic.registry.special_order_rules.keys(): with self.subTest(msg=special_order): - rule = logic.registry.special_order_rules[special_order] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_tree_fruit_rule_then_can_be_resolved(self): - for tree_fruit in logic.registry.tree_fruit_rules.keys(): - with self.subTest(msg=tree_fruit): - rule = logic.registry.tree_fruit_rules[tree_fruit] - self.assert_rule_can_be_resolved(rule, multi_world.state) - - def test_given_seed_rule_then_can_be_resolved(self): - for seed in logic.registry.seed_rules.keys(): - with self.subTest(msg=seed): - rule = logic.registry.seed_rules[seed] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.special_order_rules[special_order] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_crop_rule_then_can_be_resolved(self): - for crop in logic.registry.crop_rules.keys(): + for crop in self.logic.registry.crop_rules.keys(): with self.subTest(msg=crop): - rule = logic.registry.crop_rules[crop] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.crop_rules[crop] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_fish_rule_then_can_be_resolved(self): - for fish in logic.registry.fish_rules.keys(): + for fish in self.logic.registry.fish_rules.keys(): with self.subTest(msg=fish): - rule = logic.registry.fish_rules[fish] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.fish_rules[fish] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_museum_rule_then_can_be_resolved(self): - for donation in logic.registry.museum_rules.keys(): + for donation in self.logic.registry.museum_rules.keys(): with self.subTest(msg=donation): - rule = logic.registry.museum_rules[donation] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.museum_rules[donation] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_cooking_rule_then_can_be_resolved(self): - for cooking_rule in logic.registry.cooking_rules.keys(): + for cooking_rule in self.logic.registry.cooking_rules.keys(): with self.subTest(msg=cooking_rule): - rule = logic.registry.cooking_rules[cooking_rule] - self.assert_rule_can_be_resolved(rule, multi_world.state) + rule = self.logic.registry.cooking_rules[cooking_rule] + self.assert_rule_can_be_resolved(rule, self.multiworld.state) def test_given_location_rule_then_can_be_resolved(self): - for location in multi_world.get_locations(1): + for location in self.multiworld.get_locations(1): with self.subTest(msg=location.name): rule = location.access_rule - self.assert_rule_can_be_resolved(rule, multi_world.state) + self.assert_rule_can_be_resolved(rule, self.multiworld.state) + + +class TestAllSanityLogic(LogicTestBase): + options = allsanity_mods_6_x_x() + + +@unittest.skip("This test does not pass because some content is still not in content packs.") +class TestMinLocationsMaxItemsLogic(LogicTestBase): + options = minimal_locations_maximal_items() + options[BundleRandomization.internal_name] = BundleRandomization.default diff --git a/worlds/stardew_valley/test/TestMultiplePlayers.py b/worlds/stardew_valley/test/TestMultiplePlayers.py index 39be7d6f7ab2..2f2092fdf7b6 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -19,7 +19,7 @@ def test_different_festival_settings(self): multiworld = setup_multiworld(multiplayer_options) self.check_location_rule(multiworld, 1, FestivalCheck.egg_hunt, False) - self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, False) + self.check_location_rule(multiworld, 2, FestivalCheck.egg_hunt, True, True) self.check_location_rule(multiworld, 3, FestivalCheck.egg_hunt, True, True) def test_different_money_settings(self): diff --git a/worlds/stardew_valley/test/TestNumberLocations.py b/worlds/stardew_valley/test/TestNumberLocations.py new file mode 100644 index 000000000000..ef552c10e8d5 --- /dev/null +++ b/worlds/stardew_valley/test/TestNumberLocations.py @@ -0,0 +1,98 @@ +from . import SVTestBase, allsanity_no_mods_6_x_x, \ + allsanity_mods_6_x_x, minimal_locations_maximal_items, minimal_locations_maximal_items_with_island, get_minsanity_options, default_6_x_x +from .. import location_table +from ..items import Group, item_table + + +class TestLocationGeneration(SVTestBase): + + def test_all_location_created_are_in_location_table(self): + for location in self.get_real_locations(): + self.assertIn(location.name, location_table) + + +class TestMinLocationAndMaxItem(SVTestBase): + options = minimal_locations_maximal_items() + + def test_minimal_location_maximal_items_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND EXCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinLocationAndMaxItemWithIsland(SVTestBase): + options = minimal_locations_maximal_items_with_island() + + def test_minimal_location_maximal_items_with_island_still_valid(self): + valid_locations = self.get_real_locations() + number_locations = len(valid_locations) + number_items = len([item for item in self.multiworld.itempool + if Group.RESOURCE_PACK not in item_table[item.name].groups and Group.TRAP not in item_table[item.name].groups]) + print(f"Stardew Valley - Minimum Locations: {number_locations}, Maximum Items: {number_items} [ISLAND INCLUDED]") + self.assertGreaterEqual(number_locations, number_items) + + +class TestMinSanityHasAllExpectedLocations(SVTestBase): + options = get_minsanity_options() + + def test_minsanity_has_fewer_than_locations(self): + expected_locations = 85 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Minsanity Locations: {number_locations}") + self.assertLessEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tDisappeared Locations Detected!" + f"\n\tPlease update test_minsanity_has_fewer_than_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestDefaultSettingsHasAllExpectedLocations(SVTestBase): + options = default_6_x_x() + + def test_default_settings_has_exactly_locations(self): + expected_locations = 491 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Default options locations: {number_locations}") + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_default_settings_has_exactly_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanitySettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_no_mods_6_x_x() + + def test_allsanity_without_mods_has_at_least_locations(self): + expected_locations = 2238 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations without mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_without_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") + + +class TestAllSanityWithModsSettingsHasAllExpectedLocations(SVTestBase): + options = allsanity_mods_6_x_x() + + def test_allsanity_with_mods_has_at_least_locations(self): + expected_locations = 3096 + real_locations = self.get_real_locations() + number_locations = len(real_locations) + print(f"Stardew Valley - Allsanity Locations with all mods: {number_locations}") + self.assertGreaterEqual(number_locations, expected_locations) + if number_locations != expected_locations: + print(f"\tNew locations detected!" + f"\n\tPlease update test_allsanity_with_mods_has_at_least_locations" + f"\n\t\tExpected: {expected_locations}" + f"\n\t\tActual: {number_locations}") diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index d13f9b8a051a..2824a10c38af 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,12 +1,13 @@ import itertools from Options import NamedRange -from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods +from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from .assertion import WorldAssertMixin from .long.option_names import all_option_choices from .. import items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations +from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \ + SkillProgression from ..strings.goal_names import Goal as GoalName from ..strings.season_names import Season from ..strings.special_order_names import SpecialOrder @@ -24,7 +25,7 @@ def test_given_special_range_when_generate_then_basic_checks(self): continue for value in option.special_range_names: world_options = {option_name: option.special_range_names[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) def test_given_choice_when_generate_then_basic_checks(self): @@ -34,7 +35,7 @@ def test_given_choice_when_generate_then_basic_checks(self): continue for value in option.options: world_options = {option_name: option.options[value]} - with self.solo_world_sub_test(f"{option_name}: {value}", world_options, dirty_state=True) as (multiworld, _): + with self.solo_world_sub_test(f"{option_name}: {value}", world_options) as (multiworld, _): self.assert_basic_checks(multiworld) @@ -57,58 +58,71 @@ def test_given_goal_when_generate_then_victory_is_in_correct_location(self): class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - precollected_items = {item.name for item in multi_world.precollected_items[1]} - self.assertTrue(all([season in precollected_items for season in SEASONS])) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + self.assertTrue(all([season in precollected_items for season in SEASONS])) def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_randomized} - multi_world = setup_solo_multiworld(world_options) - precollected_items = {item.name for item in multi_world.precollected_items[1]} - items = {item.name for item in multi_world.get_items()} | precollected_items - self.assertTrue(all([season in items for season in SEASONS])) - self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) + with solo_multiworld(world_options) as (multi_world, _): + precollected_items = {item.name for item in multi_world.precollected_items[1]} + items = {item.name for item in multi_world.get_items()} | precollected_items + self.assertTrue(all([season in items for season in SEASONS])) + self.assertEqual(len(SEASONS.intersection(precollected_items)), 1) def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - self.assertEqual(items.count(Season.progressive), 3) + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + self.assertEqual(items.count(Season.progressive), 3) class TestToolProgression(SVTestCase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} - multi_world = setup_solo_multiworld(world_options) - - items = {item.name for item in multi_world.get_items()} - for tool in TOOLS: - self.assertNotIn(tool, items) - - def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): - world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - items = [item.name for item in multi_world.get_items()] - for tool in TOOLS: - self.assertEqual(items.count("Progressive " + tool), 4) + with solo_multiworld(world_options) as (multi_world, _): + items = {item.name for item in multi_world.get_items()} + for tool in TOOLS: + self.assertNotIn(tool, items) + + def test_given_progressive_when_generate_then_each_tool_is_in_pool_4_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + self.assertEqual(count, 4, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 1, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") + + def test_given_progressive_with_masteries_when_generate_then_fishing_rod_is_in_the_pool_5_times(self): + world_options = {ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries} + with solo_multiworld(world_options) as (multi_world, _): + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + count = items.count("Progressive " + tool) + expected_count = 5 if tool == "Fishing Rod" else 4 + self.assertEqual(count, expected_count, f"Progressive {tool} was there {count} times") + scythe_count = items.count("Progressive Scythe") + self.assertEqual(scythe_count, 2, f"Progressive Scythe was there {scythe_count} times") + self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated") def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - multi_world = setup_solo_multiworld(world_options) - - locations = {locations.name for locations in multi_world.get_locations(1)} - for material, tool in itertools.product(ToolMaterial.tiers.values(), - [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): - if material == ToolMaterial.basic: - continue - self.assertIn(f"{material} {tool} Upgrade", locations) - self.assertIn("Purchase Training Rod", locations) - self.assertIn("Bamboo Pole Cutscene", locations) - self.assertIn("Purchase Fiberglass Rod", locations) - self.assertIn("Purchase Iridium Rod", locations) + with solo_multiworld(world_options) as (multi_world, _): + locations = {locations.name for locations in multi_world.get_locations(1)} + for material, tool in itertools.product(ToolMaterial.tiers.values(), + [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): + if material == ToolMaterial.basic: + continue + self.assertIn(f"{material} {tool} Upgrade", locations) + self.assertIn("Purchase Training Rod", locations) + self.assertIn("Bamboo Pole Cutscene", locations) + self.assertIn("Purchase Fiberglass Rod", locations) + self.assertIn("Purchase Iridium Rod", locations) class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase): @@ -123,7 +137,7 @@ def test_given_choice_when_generate_exclude_ginger_island(self): option: option_choice } - with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options, dirty_state=True) as (multiworld, stardew_world): + with self.solo_world_sub_test(f"{option.internal_name}: {option_choice}", world_options) as (multiworld, stardew_world): # Some options, like goals, will force Ginger island back in the game. We want to skip testing those. if stardew_world.options.exclude_ginger_island != ExcludeGingerIsland.option_true: @@ -140,7 +154,7 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): ExcludeGingerIsland: exclude_island } - with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options, dirty_state=True) \ + with self.solo_world_sub_test(f"Goal: {goal}, {ExcludeGingerIsland.internal_name}: {exclude_island}", world_options) \ as (multiworld, stardew_world): self.assertEqual(stardew_world.options.exclude_ginger_island, ExcludeGingerIsland.option_false) self.assert_basic_checks(multiworld) @@ -148,77 +162,77 @@ def test_given_island_related_goal_then_override_exclude_ginger_island(self): class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = allsanity_options_without_mods().copy() + world_options = allsanity_no_mods_6_x_x().copy() world_options[TrapItems.internal_name] = TrapItems.option_no_traps - multi_world = setup_solo_multiworld(world_options) - - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] - multiworld_items = [item.name for item in multi_world.get_items()] + with solo_multiworld(world_options) as (multi_world, _): + trap_items = [item_data.name for item_data in items_by_group[Group.TRAP]] + multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"{item}"): - self.assertNotIn(item, multiworld_items) + for item in trap_items: + with self.subTest(f"{item}"): + self.assertNotIn(item, multiworld_items) def test_given_traps_when_generate_then_all_traps_in_pool(self): trap_option = TrapItems for value in trap_option.options: if value == "no_traps": continue - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({TrapItems.internal_name: trap_option.options[value]}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + with solo_multiworld(world_options) as (multi_world, _): + 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()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) class TestSpecialOrders(SVTestCase): def test_given_disabled_then_no_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_vanilla} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_BOARD, location.tags) + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) def test_given_board_only_then_no_qi_order_in_pool(self): - world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_only} - multi_world = setup_solo_multiworld(world_options) + world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} - for location_name in locations_in_pool: - location = location_table[location_name] - self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) + locations_in_pool = {location.name for location in multi_world.get_locations() if location.name in location_table} + for location_name in locations_in_pool: + location = location_table[location_name] + self.assertNotIn(LocationTags.SPECIAL_ORDER_QI, location.tags) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_then_all_orders_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories} - multi_world = setup_solo_multiworld(world_options) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_victories, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): - locations_in_pool = {location.name for location in multi_world.get_locations()} - for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: - if qi_location.mod_name: - continue - self.assertIn(qi_location.name, locations_in_pool) + locations_in_pool = {location.name for location in multi_world.get_locations()} + for qi_location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI]: + if qi_location.mod_name: + continue + self.assertIn(qi_location.name, locations_in_pool) - for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: - if board_location.mod_name: - continue - self.assertIn(board_location.name, locations_in_pool) + for board_location in locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]: + if board_location.mod_name: + continue + self.assertIn(board_location.name, locations_in_pool) def test_given_board_and_qi_without_arcade_machines_then_lets_play_a_game_not_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled} - multi_world = setup_solo_multiworld(world_options) - - locations_in_pool = {location.name for location in multi_world.get_locations()} - self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false} + with solo_multiworld(world_options) as (multi_world, _): + locations_in_pool = {location.name for location in multi_world.get_locations()} + self.assertNotIn(SpecialOrder.lets_play_a_game, locations_in_pool) diff --git a/worlds/stardew_valley/test/TestOptionsPairs.py b/worlds/stardew_valley/test/TestOptionsPairs.py index 9109c39562ee..d953696e887d 100644 --- a/worlds/stardew_valley/test/TestOptionsPairs.py +++ b/worlds/stardew_valley/test/TestOptionsPairs.py @@ -47,7 +47,7 @@ def test_given_option_pair_then_basic_checks(self): class TestCraftMasterNoSpecialOrder(WorldAssertMixin, SVTestBase): options = { options.Goal.internal_name: Goal.option_craft_master, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.alias_disabled, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, options.Craftsanity.internal_name: options.Craftsanity.option_none } diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 0137bab9148b..a25feea22085 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -4,7 +4,7 @@ from BaseClasses import get_seed from . import SVTestCase, complete_options_with_default -from ..options import EntranceRandomization, ExcludeGingerIsland +from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions from ..strings.entrance_names import Entrance as EntranceName from ..strings.region_names import Region as RegionName @@ -56,10 +56,12 @@ class TestEntranceRando(SVTestCase): def test_entrance_randomization(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) seed = get_seed() rand = random.Random(seed) @@ -80,11 +82,13 @@ def test_entrance_randomization(self): def test_entrance_randomization_without_island(self): for option, flag in [(EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ EntranceRandomization.internal_name: option, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) seed = get_seed() rand = random.Random(seed) @@ -111,7 +115,8 @@ def test_entrance_randomization_without_island(self): def test_cannot_put_island_access_on_island(self): sv_options = complete_options_with_default({ EntranceRandomization.internal_name: EntranceRandomization.option_buildings, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, }) for i in range(0, 100 if self.skip_long_tests else 10000): diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py deleted file mode 100644 index 3ee921bd2bc2..000000000000 --- a/worlds/stardew_valley/test/TestRules.py +++ /dev/null @@ -1,797 +0,0 @@ -from collections import Counter - -from . import SVTestBase -from .. import options, HasProgressionPercent -from ..data.craftable_data import all_crafting_recipes_by_name -from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ToolProgression, BuildingProgression, ExcludeGingerIsland, Chefsanity, Craftsanity, Shipsanity, SeasonRandomization, Friendsanity, \ - FriendsanityHeartSize, BundleRandomization, SkillProgression -from ..strings.entrance_names import Entrance -from ..strings.region_names import Region -from ..strings.tool_names import Tool, ToolMaterial - - -class TestProgressiveToolsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - } - - def test_sturgeon(self): - self.multiworld.state.prog_items = {1: Counter()} - - sturgeon_rule = self.world.logic.has("Sturgeon") - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - summer = self.world.create_item("Summer") - self.multiworld.state.collect(summer, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_rod = self.world.create_item("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, event=False) - self.multiworld.state.collect(fishing_rod, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - fishing_level = self.world.create_item("Fishing Level") - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.multiworld.state.collect(fishing_level, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(summer) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - winter = self.world.create_item("Winter") - self.multiworld.state.collect(winter, event=False) - self.assert_rule_true(sturgeon_rule, self.multiworld.state) - - self.remove(fishing_rod) - self.assert_rule_false(sturgeon_rule, self.multiworld.state) - - def test_old_master_cannoli(self): - self.multiworld.state.prog_items = {1: Counter()} - - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - - rule = self.world.logic.region.can_reach_location("Old Master Cannoli") - self.assert_rule_false(rule, self.multiworld.state) - - fall = self.world.create_item("Fall") - self.multiworld.state.collect(fall, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - tuesday = self.world.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - rare_seed = self.world.create_item("Rare Seed") - self.multiworld.state.collect(rare_seed, event=False) - self.assert_rule_true(rule, self.multiworld.state) - - self.remove(fall) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(tuesday) - - green_house = self.world.create_item("Greenhouse") - self.multiworld.state.collect(green_house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - - friday = self.world.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=False) - self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) - - self.remove(green_house) - self.assert_rule_false(rule, self.multiworld.state) - self.remove(friday) - - -class TestBundlesLogic(SVTestBase): - options = { - BundleRandomization.internal_name: BundleRandomization.option_vanilla - } - - def test_vault_2500g_bundle(self): - self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) - - -class TestBuildingLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) - - def test_big_coop_blueprint(self): - big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=False) - self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") - - def test_deluxe_coop_blueprint(self): - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.collect_lots_of_money() - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) - self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) - - def test_big_shed_blueprint(self): - big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.collect_lots_of_money() - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Can Construct Buildings"), event=True) - self.assertFalse(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - self.multiworld.state.collect(self.world.create_item("Progressive Shed"), event=True) - self.assertTrue(big_shed_rule(self.multiworld.state), - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") - - -class TestArcadeMachinesLogic(SVTestBase): - options = { - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - } - - def test_prairie_king(self): - self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - - boots = self.world.create_item("JotPK: Progressive Boots") - gun = self.world.create_item("JotPK: Progressive Gun") - ammo = self.world.create_item("JotPK: Progressive Ammo") - life = self.world.create_item("JotPK: Extra Life") - drop = self.world.create_item("JotPK: Increased Drop Rate") - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(ammo) - self.remove(life) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) - self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) - self.remove(boots) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - -class TestWeaponsLogic(SVTestBase): - options = { - ToolProgression.internal_name: ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - } - - def test_mine(self): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.world.create_item("Bus Repair"), event=True) - self.multiworld.state.collect(self.world.create_item("Skull Key"), event=True) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 1) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) - self.GiveItemAndCheckReachableMine("Progressive Club", 1) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 2) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) - self.GiveItemAndCheckReachableMine("Progressive Club", 2) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 3) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) - self.GiveItemAndCheckReachableMine("Progressive Club", 3) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 4) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) - self.GiveItemAndCheckReachableMine("Progressive Club", 4) - - self.GiveItemAndCheckReachableMine("Progressive Sword", 5) - self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) - self.GiveItemAndCheckReachableMine("Progressive Club", 5) - - def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): - item = self.multiworld.create_item(item_name, self.player) - self.multiworld.state.collect(item, event=True) - rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() - if reachable_level > 0: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() - if reachable_level > 1: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() - if reachable_level > 2: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.mine.can_mine_in_the_skull_cavern() - if reachable_level > 3: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() - if reachable_level > 4: - self.assert_rule_true(rule, self.multiworld.state) - else: - self.assert_rule_false(rule, self.multiworld.state) - - -class TestRecipeLearnLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestRecipeReceiveLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - Chefsanity.internal_name: Chefsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_learn_qos_recipe(self): - location = "Cook Radish Salad" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - self.multiworld.state.collect(self.world.create_item("Radish Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Summer"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - spring = self.world.create_item("Spring") - qos = self.world.create_item("The Queen of Sauce") - self.multiworld.state.collect(spring, event=False) - self.multiworld.state.collect(qos, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(spring) - self.multiworld.state.remove(qos) - - self.multiworld.state.collect(self.world.create_item("Radish Salad Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_get_chefsanity_check_recipe(self): - location = "Radish Salad Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Spring"), event=False) - self.collect_lots_of_money() - self.assert_rule_false(rule, self.multiworld.state) - - seeds = self.world.create_item("Radish Seeds") - summer = self.world.create_item("Summer") - house = self.world.create_item("Progressive House") - self.multiworld.state.collect(seeds, event=False) - self.multiworld.state.collect(summer, event=False) - self.multiworld.state.collect(house, event=False) - self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.remove(seeds) - self.multiworld.state.remove(summer) - self.multiworld.state.remove(house) - - self.multiworld.state.collect(self.world.create_item("The Queen of Sauce"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - location = "Craft Marble Brazier" - rule = self.world.logic.region.can_reach_location(location) - self.collect([self.world.create_item("Progressive Pickaxe")] * 4) - self.collect([self.world.create_item("Progressive Fishing Rod")] * 4) - self.collect([self.world.create_item("Progressive Sword")] * 4) - self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) - self.collect([self.world.create_item("Mining Level")] * 10) - self.collect([self.world.create_item("Combat Level")] * 10) - self.collect([self.world.create_item("Fishing Level")] * 10) - self.collect_all_the_money() - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Marble Brazier Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_learn_crafting_recipe(self): - location = "Marble Brazier Recipe" - rule = self.world.logic.region.can_reach_location(location) - self.assert_rule_false(rule, self.multiworld.state) - - self.collect_lots_of_money() - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_all, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Torch Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - SeasonRandomization.internal_name: SeasonRandomization.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_recipe(self): - recipe = all_crafting_recipes_by_name["Wood Floor"] - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_true(rule, self.multiworld.state) - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - result = rule(self.multiworld.state) - self.assertFalse(result) - - self.collect([self.world.create_item("Progressive Season")] * 2) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestNoCraftsanityWithFestivalsLogic(SVTestBase): - options = { - BuildingProgression.internal_name: BuildingProgression.option_progressive, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, - Craftsanity.internal_name: Craftsanity.option_none, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, - } - - def test_can_craft_festival_recipe(self): - recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] - self.multiworld.state.collect(self.world.create_item("Pumpkin Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.collect_lots_of_money() - rule = self.world.logic.crafting.can_craft(recipe) - self.assert_rule_false(rule, self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item("Jack-O-Lantern Recipe"), event=False) - self.assert_rule_true(rule, self.multiworld.state) - - -class TestDonationLogicAll(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_all - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicRandomized(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_randomized - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - donation_locations = [location for location in self.get_real_locations() if - LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] - - for donation in donation_locations: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in donation_locations: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -class TestDonationLogicMilestones(SVTestBase): - options = { - options.Museumsanity.internal_name: options.Museumsanity.option_milestones - } - - def test_cannot_make_any_donation_without_museum_access(self): - railroad_item = "Railroad Boulder Removed" - swap_museum_and_bathhouse(self.multiworld, self.player) - collect_all_except(self.multiworld, railroad_item) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: - self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) - - -def swap_museum_and_bathhouse(multiworld, player): - museum_region = multiworld.get_region(Region.museum, player) - bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) - museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) - bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) - museum_entrance.connect(bathhouse_region) - bathhouse_entrance.connect(museum_region) - - -class TestToolVanillaRequiresBlacksmith(SVTestBase): - options = { - options.EntranceRandomization: options.EntranceRandomization.option_buildings, - options.ToolProgression: options.ToolProgression.option_vanilla, - } - seed = 4111845104987680262 - - # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. - - def test_cannot_get_any_tool_without_blacksmith_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: - for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: - self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) - - def test_cannot_get_fishing_rod_without_willy_access(self): - railroad_item = "Railroad Boulder Removed" - place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) - collect_all_except(self.multiworld, railroad_item) - - for fishing_rod_level in [3, 4]: - self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - - for fishing_rod_level in [3, 4]: - self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) - - -def place_region_at_entrance(multiworld, player, region, entrance): - region_to_place = multiworld.get_region(region, player) - entrance_to_place_region = multiworld.get_entrance(entrance, player) - - entrance_to_switch = region_to_place.entrances[0] - region_to_switch = entrance_to_place_region.connected_region - entrance_to_switch.connect(region_to_switch) - entrance_to_place_region.connect(region_to_place) - - -def collect_all_except(multiworld, item_to_not_collect: str): - for item in multiworld.get_items(): - if item.name != item_to_not_collect: - multiworld.state.collect(item) - - -class TestFriendsanityDatingRules(SVTestBase): - options = { - SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 3 - } - - def test_earning_dating_heart_requires_dating(self): - self.collect_all_the_money() - self.multiworld.state.collect(self.world.create_item("Fall"), event=False) - self.multiworld.state.collect(self.world.create_item("Beach Bridge"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive House"), event=False) - for i in range(3): - self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Weapon"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Barn"), event=False) - for i in range(10): - self.multiworld.state.collect(self.world.create_item("Foraging Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Farming Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Mining Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Combat Level"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - self.multiworld.state.collect(self.world.create_item("Progressive Mine Elevator"), event=False) - - npc = "Abigail" - heart_name = f"{npc} <3" - step = 3 - - self.assert_can_reach_heart_up_to(npc, 3, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.world.create_item(heart_name), event=False) - self.assert_can_reach_heart_up_to(npc, 14, step) - - def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): - prefix = "Friendsanity: " - suffix = " <3" - for i in range(1, max_reachable + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") - for i in range(max_reachable + 1, 14 + 1): - if i % step != 0 and i != 14: - continue - location = f"{prefix}{npc} {i}{suffix}" - can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) - self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") - - -class TestShipsanityNone(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_none - } - - def test_no_shipsanity_locations(self): - for location in self.get_real_locations(): - self.assertFalse("Shipsanity" in location.name) - self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) - - -class TestShipsanityCrops(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_crops - } - - def test_only_crop_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) - - -class TestShipsanityFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_fish - } - - def test_only_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipment(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment - } - - def test_only_full_shipment_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) - self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) - - -class TestShipsanityFullShipmentWithFish(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish - } - - def test_only_full_shipment_and_fish_shipsanity_locations(self): - for location in self.get_real_locations(): - if LocationTags.SHIPSANITY in location_table[location.name].tags: - self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or - LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) - - -class TestShipsanityEverything(SVTestBase): - options = { - Shipsanity.internal_name: Shipsanity.option_everything, - BuildingProgression.internal_name: BuildingProgression.option_progressive - } - - def test_all_shipsanity_locations_require_shipping_bin(self): - bin_name = "Shipping Bin" - collect_all_except(self.multiworld, bin_name) - shipsanity_locations = [location for location in self.get_real_locations() if - LocationTags.SHIPSANITY in location_table[location.name].tags] - bin_item = self.world.create_item(bin_name) - for location in shipsanity_locations: - with self.subTest(location.name): - self.remove(bin_item) - self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) - self.multiworld.state.collect(bin_item, event=False) - shipsanity_rule = self.world.logic.region.can_reach_location(location.name) - self.assert_rule_true(shipsanity_rule, self.multiworld.state) - self.remove(bin_item) - - -class TestVanillaSkillLogicSimplification(SVTestBase): - options = { - SkillProgression.internal_name: SkillProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_progressive, - } - - def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): - rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) - self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) diff --git a/worlds/stardew_valley/test/TestStardewRule.py b/worlds/stardew_valley/test/TestStardewRule.py index 89317d90e4e2..93b32b0d8ab4 100644 --- a/worlds/stardew_valley/test/TestStardewRule.py +++ b/worlds/stardew_valley/test/TestStardewRule.py @@ -1,7 +1,9 @@ import unittest +from typing import cast from unittest.mock import MagicMock, Mock -from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_ +from .. import StardewRule +from ..stardew_rule import Received, And, Or, HasProgressionPercent, false_, true_, Count class TestSimplification(unittest.TestCase): @@ -72,7 +74,7 @@ def test_propagate_evaluate_while_simplifying(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, expected_result)) - rule = And(Or(other_rule)) + rule = And(Or(cast(StardewRule, other_rule))) _, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -101,8 +103,9 @@ def test_short_circuit_when_complement_found(self): def test_short_circuit_when_combinable_rules_is_false(self): collection_state = MagicMock() + collection_state.has = Mock(return_value=False) other_rule = MagicMock() - rule = And(HasProgressionPercent(1, 10), other_rule) + rule = And(Received("Potato", 1, 10), cast(StardewRule, other_rule)) rule.evaluate_while_simplifying(collection_state) @@ -110,16 +113,16 @@ def test_short_circuit_when_combinable_rules_is_false(self): def test_identity_is_removed_from_other_rules(self): collection_state = MagicMock() - rule = Or(false_, HasProgressionPercent(1, 10)) + rule = Or(false_, Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) self.assertEqual(1, len(rule.current_rules)) - self.assertIn(HasProgressionPercent(1, 10), rule.current_rules) + self.assertIn(Received("Potato", 1, 10), rule.current_rules) def test_complement_replaces_combinable_rules(self): collection_state = MagicMock() - rule = Or(HasProgressionPercent(1, 10), true_) + rule = Or(Received("Potato", 1, 10), true_) rule.evaluate_while_simplifying(collection_state) @@ -129,7 +132,7 @@ def test_simplifying_to_complement_propagates_complement(self): expected_simplified = true_ expected_result = True collection_state = MagicMock() - rule = Or(Or(expected_simplified), HasProgressionPercent(1, 10)) + rule = Or(Or(expected_simplified), Received("Potato", 1, 10)) actual_simplified, actual_result = rule.evaluate_while_simplifying(collection_state) @@ -141,7 +144,7 @@ def test_already_simplified_rules_are_not_simplified_again(self): collection_state = MagicMock() other_rule = MagicMock() other_rule.evaluate_while_simplifying = Mock(return_value=(other_rule, False)) - rule = Or(other_rule, HasProgressionPercent(1, 10)) + rule = Or(cast(StardewRule, other_rule), Received("Potato", 1, 10)) rule.evaluate_while_simplifying(collection_state) other_rule.assert_not_called() @@ -157,7 +160,7 @@ def test_continue_simplification_after_short_circuited(self): a_rule.evaluate_while_simplifying = Mock(return_value=(a_rule, False)) another_rule = MagicMock() another_rule.evaluate_while_simplifying = Mock(return_value=(another_rule, False)) - rule = And(a_rule, another_rule) + rule = And(cast(StardewRule, a_rule), cast(StardewRule, another_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -183,7 +186,7 @@ class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase): def test_nested_call_in_the_internal_rule_being_evaluated_does_check_the_internal_rule(self): collection_state = MagicMock() internal_rule = MagicMock() - rule = Or(internal_rule) + rule = Or(cast(StardewRule, internal_rule)) called_once = False internal_call_result = None @@ -212,7 +215,7 @@ def test_nested_call_to_already_simplified_rule_does_not_steal_rule_to_simplify_ an_internal_rule.evaluate_while_simplifying = Mock(return_value=(an_internal_rule, True)) another_internal_rule = MagicMock() another_internal_rule.evaluate_while_simplifying = Mock(return_value=(another_internal_rule, True)) - rule = Or(an_internal_rule, another_internal_rule) + rule = Or(cast(StardewRule, an_internal_rule), cast(StardewRule, another_internal_rule)) rule.evaluate_while_simplifying(collection_state) # This test is completely messed up because sets are used internally and order of the rules cannot be ensured. @@ -242,3 +245,61 @@ def call_to_already_simplified(state): self.assertTrue(called_once) self.assertTrue(internal_call_result) self.assertTrue(actual_result) + + +class TestCount(unittest.TestCase): + + def test_duplicate_rule_count_double(self): + expected_result = True + collection_state = MagicMock() + simplified_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), other_rule, other_rule], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + def test_simplified_rule_is_reused(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock(return_value=expected_result) + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule), cast(StardewRule, other_rule), cast(StardewRule, other_rule)], 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + self.assertEqual(expected_result, actual_result) + + other_rule.evaluate_while_simplifying.reset_mock() + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_not_called() + simplified_rule.assert_called() + self.assertEqual(expected_result, actual_result) + + def test_break_if_not_enough_rule_to_complete(self): + expected_result = False + collection_state = MagicMock() + simplified_rule = Mock() + never_called_rule = Mock() + other_rule = Mock(spec=StardewRule) + other_rule.evaluate_while_simplifying = Mock(return_value=(simplified_rule, expected_result)) + rule = Count([cast(StardewRule, other_rule)] * 4, 2) + + actual_result = rule(collection_state) + + other_rule.evaluate_while_simplifying.assert_called_once_with(collection_state) + never_called_rule.assert_not_called() + never_called_rule.evaluate_while_simplifying.assert_not_called() + self.assertEqual(expected_result, actual_result) + + def test_evaluate_without_shortcircuit_when_rules_are_all_different(self): + rule = Count([cast(StardewRule, Mock()) for i in range(5)], 2) + + self.assertEqual(rule.evaluate, rule.evaluate_without_shortcircuit) diff --git a/worlds/stardew_valley/test/TestStartInventory.py b/worlds/stardew_valley/test/TestStartInventory.py index 826f49b1ac83..dc44a1bb4598 100644 --- a/worlds/stardew_valley/test/TestStartInventory.py +++ b/worlds/stardew_valley/test/TestStartInventory.py @@ -17,7 +17,7 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_very_cheap, options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_only, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board, options.QuestLocations.internal_name: -1, options.Fishsanity.internal_name: options.Fishsanity.option_only_easy_fish, options.Museumsanity.internal_name: options.Museumsanity.option_randomized, @@ -29,13 +29,13 @@ class TestStartInventoryAllsanity(WorldAssertMixin, SVTestBase): options.Friendsanity.internal_name: options.Friendsanity.option_bachelors, options.FriendsanityHeartSize.internal_name: 3, options.NumberOfMovementBuffs.internal_name: 10, - options.NumberOfLuckBuffs.internal_name: 12, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.Mods.internal_name: ["Tractor Mod", "Bigger Backpack", "Luck Skill", "Magic", "Socializing Skill", "Archaeology", "Cooking Skill", "Binning Skill"], - "start_inventory": {"Movement Speed Bonus": 2} + "start_inventory": {"Progressive Pickaxe": 2} } - def test_start_inventory_movement_speed(self): + def test_start_inventory_progression_items_does_not_break_progression_percent(self): self.assert_basic_checks_with_subtests(self.multiworld) self.assert_can_win(self.multiworld) diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py new file mode 100644 index 000000000000..e1ab348def41 --- /dev/null +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -0,0 +1,209 @@ +from . import SVTestBase +from ..options import ExcludeGingerIsland, Walnutsanity +from ..strings.ap_names.ap_option_names import OptionName + + +class TestWalnutsanityNone(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_none, + } + + def test_no_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 0, and collect 40 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.collect("Progressive House") + items = self.collect("5 Golden Walnuts", 10) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Island North Turtle") + self.collect("Island Resort") + self.collect("Open Professor Snail Cave") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Dig Site Bridge") + self.collect("Island Farmhouse") + self.collect("Qi Walnut Room") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.collect("Progressive Slingshot") + self.collect("Progressive Weapon", 5) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Watering Can", 4) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityPuzzles(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_puzzles}), + } + + def test_only_puzzle_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + def test_field_office_locations_require_professor_snail(self): + location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection", + "Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ] + locations = [location for location in self.multiworld.get_locations() if location.name in location_names] + self.collect("Island Obelisk") + self.collect("Island North Turtle") + self.collect("Island West Turtle") + self.collect("Island Resort") + self.collect("Dig Site Bridge") + self.collect("Progressive House") + self.collect("Progressive Pan") + self.collect("Progressive Fishing Rod") + self.collect("Progressive Watering Can") + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Sword", 5) + self.collect("Combat Level", 10) + self.collect("Mining Level", 10) + for location in locations: + self.assert_reach_location_false(location, self.multiworld.state) + self.collect("Open Professor Snail Cave") + for location in locations: + self.assert_reach_location_true(location, self.multiworld.state) + + +class TestWalnutsanityBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityPuzzlesAndBushes(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_puzzles, OptionName.walnutsanity_bushes}), + } + + def test_only_bush_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 25, and collect 15 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + items = self.collect("5 Golden Walnuts", 5) + + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Island North Turtle") + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + + +class TestWalnutsanityDigSpots(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_dig_spots}), + } + + def test_only_dig_spots_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertNotIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertNotIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityRepeatables(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: frozenset({OptionName.walnutsanity_repeatables}), + } + + def test_only_repeatable_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertNotIn("Journal Scrap #6", location_names) + self.assertNotIn("Starfish Triangle", location_names) + self.assertNotIn("Bush Behind Coconut Tree", location_names) + self.assertNotIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertNotIn("Cliff Over Island South Bush", location_names) + + +class TestWalnutsanityAll(SVTestBase): + options = { + ExcludeGingerIsland: ExcludeGingerIsland.option_false, + Walnutsanity: Walnutsanity.preset_all, + } + + def test_all_walnut_locations(self): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Open Golden Coconut", location_names) + self.assertIn("Fishing Walnut 4", location_names) + self.assertIn("Journal Scrap #6", location_names) + self.assertIn("Starfish Triangle", location_names) + self.assertIn("Bush Behind Coconut Tree", location_names) + self.assertIn("Purple Starfish Island Survey", location_names) + self.assertIn("Volcano Monsters Walnut 3", location_names) + self.assertIn("Cliff Over Island South Bush", location_names) + + def test_logic_received_walnuts(self): + # You need to receive 40, and collect 4 + self.collect("Island Obelisk") + self.collect("Island West Turtle") + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 8) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("3 Golden Walnuts", 14) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("Golden Walnut", 40) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + self.remove(items) + self.assertFalse(self.multiworld.state.can_reach_location("Parrot Express", self.player)) + items = self.collect("5 Golden Walnuts", 4) + items = self.collect("3 Golden Walnuts", 6) + items = self.collect("Golden Walnut", 2) + self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player)) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 1a463d9fc280..bee02f3c3d68 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,201 +1,183 @@ +import logging import os +import threading import unittest from argparse import Namespace from contextlib import contextmanager -from typing import Dict, ClassVar, Iterable, Hashable, Tuple, Optional, List, Union, Any +from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, get_seed, Location +from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item, ItemClassification from Options import VerifyKeys -from Utils import cache_argsless from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from worlds.AutoWorld import call_all from .assertion import RuleAssertMixin -from .. import StardewValleyWorld, options -from ..mods.mod_data import all_mods +from .. import StardewValleyWorld, options, StardewItem from ..options import StardewValleyOptions, StardewValleyOption +logger = logging.getLogger(__name__) + DEFAULT_TEST_SEED = get_seed() +logger.info(f"Default Test Seed: {DEFAULT_TEST_SEED}") -# TODO is this caching really changing anything? -@cache_argsless -def disable_5_x_x_options(): +def default_6_x_x(): return { - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, - options.Chefsanity.internal_name: options.Chefsanity.option_none, - options.Craftsanity.internal_name: options.Craftsanity.option_none + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.default, + options.BackpackProgression.internal_name: options.BackpackProgression.default, + options.Booksanity.internal_name: options.Booksanity.default, + options.BuildingProgression.internal_name: options.BuildingProgression.default, + options.BundlePrice.internal_name: options.BundlePrice.default, + options.BundleRandomization.internal_name: options.BundleRandomization.default, + options.Chefsanity.internal_name: options.Chefsanity.default, + options.Cooksanity.internal_name: options.Cooksanity.default, + options.Craftsanity.internal_name: options.Craftsanity.default, + options.Cropsanity.internal_name: options.Cropsanity.default, + options.ElevatorProgression.internal_name: options.ElevatorProgression.default, + options.EntranceRandomization.internal_name: options.EntranceRandomization.default, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.default, + options.FestivalLocations.internal_name: options.FestivalLocations.default, + options.Fishsanity.internal_name: options.Fishsanity.default, + options.Friendsanity.internal_name: options.Friendsanity.default, + options.FriendsanityHeartSize.internal_name: options.FriendsanityHeartSize.default, + options.Goal.internal_name: options.Goal.default, + options.Mods.internal_name: options.Mods.default, + options.Monstersanity.internal_name: options.Monstersanity.default, + options.Museumsanity.internal_name: options.Museumsanity.default, + options.NumberOfMovementBuffs.internal_name: options.NumberOfMovementBuffs.default, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.default, + options.QuestLocations.internal_name: options.QuestLocations.default, + options.SeasonRandomization.internal_name: options.SeasonRandomization.default, + options.Shipsanity.internal_name: options.Shipsanity.default, + options.SkillProgression.internal_name: options.SkillProgression.default, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.default, + options.ToolProgression.internal_name: options.ToolProgression.default, + options.TrapItems.internal_name: options.TrapItems.default, + options.Walnutsanity.internal_name: options.Walnutsanity.default } -@cache_argsless -def default_4_x_x_options(): - option_dict = default_options().copy() - option_dict.update(disable_5_x_x_options()) - option_dict.update({ - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - }) - return option_dict +def allsanity_no_mods_6_x_x(): + return { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, + options.Booksanity.internal_name: options.Booksanity.option_all, + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, + options.Chefsanity.internal_name: options.Chefsanity.option_all, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + options.Craftsanity.internal_name: options.Craftsanity.option_all, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, + options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + options.FriendsanityHeartSize.internal_name: 1, + options.Goal.internal_name: options.Goal.option_perfection, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, + options.Museumsanity.internal_name: options.Museumsanity.option_all, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, + options.NumberOfMovementBuffs.internal_name: 12, + options.QuestLocations.internal_name: 56, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Shipsanity.internal_name: options.Shipsanity.option_everything, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.TrapItems.internal_name: options.TrapItems.option_nightmare, + options.Walnutsanity.internal_name: options.Walnutsanity.preset_all + } -@cache_argsless -def default_options(): - return {} +def allsanity_mods_6_x_x(): + allsanity = allsanity_no_mods_6_x_x() + allsanity.update({options.Mods.internal_name: frozenset(options.Mods.valid_keys)}) + return allsanity -@cache_argsless def get_minsanity_options(): return { - options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, - options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, - options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_very_cheap, + options.BundleRandomization.internal_name: options.BundleRandomization.option_vanilla, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_bottom_of_the_mines, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_none, options.NumberOfMovementBuffs.internal_name: 0, - options.NumberOfLuckBuffs.internal_name: 0, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_no_traps, - options.Mods.internal_name: frozenset(), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } -@cache_argsless def minimal_locations_maximal_items(): min_max_options = { - options.Goal.internal_name: options.Goal.option_craft_master, - options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_disabled, - options.QuestLocations.internal_name: -1, - options.Fishsanity.internal_name: options.Fishsanity.option_none, - options.Museumsanity.internal_name: options.Museumsanity.option_none, - options.Monstersanity.internal_name: options.Monstersanity.option_none, - options.Shipsanity.internal_name: options.Shipsanity.option_none, - options.Cooksanity.internal_name: options.Cooksanity.option_none, + options.BundlePrice.internal_name: options.BundlePrice.option_expensive, + options.BundleRandomization.internal_name: options.BundleRandomization.option_shuffled, options.Chefsanity.internal_name: options.Chefsanity.option_none, + options.Cooksanity.internal_name: options.Cooksanity.option_none, options.Craftsanity.internal_name: options.Craftsanity.option_none, + options.Cropsanity.internal_name: options.Cropsanity.option_disabled, + options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, + options.EntranceRandomization.internal_name: options.EntranceRandomization.option_disabled, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + options.Fishsanity.internal_name: options.Fishsanity.option_none, options.Friendsanity.internal_name: options.Friendsanity.option_none, options.FriendsanityHeartSize.internal_name: 8, + options.Goal.internal_name: options.Goal.option_craft_master, + options.Mods.internal_name: frozenset(), + options.Monstersanity.internal_name: options.Monstersanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.EnabledFillerBuffs.internal_name: options.EnabledFillerBuffs.preset_all, options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.QuestLocations.internal_name: -1, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.Shipsanity.internal_name: options.Shipsanity.option_none, + options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, + options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_vanilla, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, options.TrapItems.internal_name: options.TrapItems.option_nightmare, - options.Mods.internal_name: (), + options.Walnutsanity.internal_name: options.Walnutsanity.preset_none } return min_max_options -@cache_argsless def minimal_locations_maximal_items_with_island(): - min_max_options = minimal_locations_maximal_items().copy() + min_max_options = minimal_locations_maximal_items() min_max_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false}) return min_max_options -@cache_argsless -def allsanity_4_x_x_options_without_mods(): - option_dict = { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - option_dict.update(disable_5_x_x_options()) - return option_dict - - -@cache_argsless -def allsanity_options_without_mods(): - return { - options.Goal.internal_name: options.Goal.option_perfection, - options.BundleRandomization.internal_name: options.BundleRandomization.option_thematic, - options.BundlePrice.internal_name: options.BundlePrice.option_expensive, - options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, - options.Cropsanity.internal_name: options.Cropsanity.option_enabled, - options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive, - options.FestivalLocations.internal_name: options.FestivalLocations.option_hard, - options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive, - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.QuestLocations.internal_name: 56, - options.Fishsanity.internal_name: options.Fishsanity.option_all, - options.Museumsanity.internal_name: options.Museumsanity.option_all, - options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals, - options.Shipsanity.internal_name: options.Shipsanity.option_everything, - options.Cooksanity.internal_name: options.Cooksanity.option_all, - options.Chefsanity.internal_name: options.Chefsanity.option_all, - options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, - options.FriendsanityHeartSize.internal_name: 1, - options.NumberOfMovementBuffs.internal_name: 12, - options.NumberOfLuckBuffs.internal_name: 12, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.TrapItems.internal_name: options.TrapItems.option_nightmare, - } - - -@cache_argsless -def allsanity_options_with_mods(): - allsanity = allsanity_options_without_mods().copy() - allsanity.update({options.Mods.internal_name: all_mods}) - return allsanity - - class SVTestCase(unittest.TestCase): # Set False to not skip some 'extra' tests skip_base_tests: bool = True @@ -219,7 +201,6 @@ def solo_world_sub_test(self, msg: Optional[str] = None, *, seed=DEFAULT_TEST_SEED, world_caching=True, - dirty_state=False, **kwargs) -> Tuple[MultiWorld, StardewValleyWorld]: if msg is not None: msg += " " @@ -228,17 +209,8 @@ def solo_world_sub_test(self, msg: Optional[str] = None, msg += f"[Seed = {seed}]" with self.subTest(msg, **kwargs): - if world_caching: - multi_world = setup_solo_multiworld(world_options, seed) - if dirty_state: - original_state = multi_world.state.copy() - else: - multi_world = setup_solo_multiworld(world_options, seed, _cache={}) - - yield multi_world, multi_world.worlds[1] - - if world_caching and dirty_state: - multi_world.state = original_state + with solo_multiworld(world_options, seed=seed, world_caching=world_caching) as (multiworld, world): + yield multiworld, world class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): @@ -248,59 +220,140 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): seed = DEFAULT_TEST_SEED - options = get_minsanity_options() + @classmethod + def setUpClass(cls) -> None: + if cls is SVTestBase: + raise unittest.SkipTest("No running tests on SVTestBase import.") + + super().setUpClass() def world_setup(self, *args, **kwargs): self.options = parse_class_option_keys(self.options) - super().world_setup(seed=self.seed) + self.multiworld = setup_solo_multiworld(self.options, seed=self.seed) + self.multiworld.lock.acquire() + world = self.multiworld.worlds[self.player] + + self.original_state = self.multiworld.state.copy() + self.original_itempool = self.multiworld.itempool.copy() + self.original_prog_item_count = world.total_progression_items + self.unfilled_locations = self.multiworld.get_unfilled_locations(1) if self.constructed: - self.world = self.multiworld.worlds[self.player] # noqa + self.world = world # noqa + + def tearDown(self) -> None: + self.multiworld.state = self.original_state + self.multiworld.itempool = self.original_itempool + for location in self.unfilled_locations: + location.item = None + self.world.total_progression_items = self.original_prog_item_count + + self.multiworld.lock.release() @property def run_default_tests(self) -> bool: if self.skip_base_tests: return False - # world_setup is overridden, so it'd always run default tests when importing SVTestBase - is_not_stardew_test = type(self) is not SVTestBase - should_run_default_tests = is_not_stardew_test and super().run_default_tests - return should_run_default_tests + return super().run_default_tests def collect_lots_of_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(100): + required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25)) + for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) def collect_all_the_money(self): self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - for i in range(1000): + required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95)) + for i in range(required_prog_items): self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False) + def collect_everything(self): + non_event_items = [item for item in self.multiworld.get_items() if item.code] + for item in non_event_items: + self.multiworld.state.collect(item) + + def collect_all_except(self, item_to_not_collect: str): + for item in self.multiworld.get_items(): + if item.name != item_to_not_collect: + self.multiworld.state.collect(item) + def get_real_locations(self) -> List[Location]: return [location for location in self.multiworld.get_locations(self.player) if location.address is not None] def get_real_location_names(self) -> List[str]: return [location.name for location in self.get_real_locations()] + def collect(self, item: Union[str, Item, Iterable[Item]], count: int = 1) -> Union[None, Item, List[Item]]: + assert count > 0 + if not isinstance(item, str): + super().collect(item) + return + if count == 1: + item = self.create_item(item) + self.multiworld.state.collect(item) + return item + items = [] + for i in range(count): + item = self.create_item(item) + self.multiworld.state.collect(item) + items.append(item) + return items + + def create_item(self, item: str) -> StardewItem: + created_item = self.world.create_item(item) + if created_item.classification == ItemClassification.progression: + self.multiworld.worlds[self.player].total_progression_items -= 1 + return created_item + pre_generated_worlds = {} +@contextmanager +def solo_multiworld(world_options: Optional[Dict[Union[str, StardewValleyOption], Any]] = None, + *, + seed=DEFAULT_TEST_SEED, + world_caching=True) -> Tuple[MultiWorld, StardewValleyWorld]: + if not world_caching: + multiworld = setup_solo_multiworld(world_options, seed, _cache={}) + yield multiworld, multiworld.worlds[1] + else: + multiworld = setup_solo_multiworld(world_options, seed) + multiworld.lock.acquire() + world = multiworld.worlds[1] + + original_state = multiworld.state.copy() + original_itempool = multiworld.itempool.copy() + unfilled_locations = multiworld.get_unfilled_locations(1) + original_prog_item_count = world.total_progression_items + + yield multiworld, world + + multiworld.state = original_state + multiworld.itempool = original_itempool + for location in unfilled_locations: + location.item = None + multiworld.total_progression_items = original_prog_item_count + + multiworld.lock.release() + + # Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core. def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOption], str]] = None, seed=DEFAULT_TEST_SEED, - _cache: Dict[Hashable, MultiWorld] = {}, # noqa + _cache: Dict[frozenset, MultiWorld] = {}, # noqa _steps=gen_steps) -> MultiWorld: test_options = parse_class_option_keys(test_options) # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + # If the simple dict caching ends up taking too much memory, we could replace it with some kind of lru cache. should_cache = "start_inventory" not in test_options - frozen_options = frozenset({}) if should_cache: - frozen_options = frozenset(test_options.items()).union({seed}) - if frozen_options in _cache: - cached_multi_world = _cache[frozen_options] - print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}]") + frozen_options = frozenset(test_options.items()).union({("seed", seed)}) + cached_multi_world = search_world_cache(_cache, frozen_options) + if cached_multi_world: + print(f"Using cached solo multi world [Seed = {cached_multi_world.seed}] [Cache size = {len(_cache)}]") return cached_multi_world multiworld = setup_base_solo_multiworld(StardewValleyWorld, (), seed=seed) @@ -326,28 +379,47 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp call_all(multiworld, step) if should_cache: - _cache[frozen_options] = multiworld + add_to_world_cache(_cache, frozen_options, multiworld) # noqa + + # Lock is needed for multi-threading tests + setattr(multiworld, "lock", threading.Lock()) return multiworld -def parse_class_option_keys(test_options: dict) -> dict: +def parse_class_option_keys(test_options: Optional[Dict]) -> dict: """ Now the option class is allowed as key. """ + if test_options is None: + return {} parsed_options = {} - if test_options: - for option, value in test_options.items(): - if hasattr(option, "internal_name"): - assert option.internal_name not in test_options, "Defined two times by class and internal_name" - parsed_options[option.internal_name] = value - else: - assert option in StardewValleyOptions.type_hints, \ - f"All keys of world_options must be a possible Stardew Valley option, {option} is not." - parsed_options[option] = value + for option, value in test_options.items(): + if hasattr(option, "internal_name"): + assert option.internal_name not in test_options, "Defined two times by class and internal_name" + parsed_options[option.internal_name] = value + else: + assert option in StardewValleyOptions.type_hints, \ + f"All keys of world_options must be a possible Stardew Valley option, {option} is not." + parsed_options[option] = value return parsed_options +def search_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset) -> Optional[MultiWorld]: + try: + return cache[frozen_options] + except KeyError: + for cached_options, multi_world in cache.items(): + if frozen_options.issubset(cached_options): + return multi_world + return None + + +def add_to_world_cache(cache: Dict[frozenset, MultiWorld], frozen_options: frozenset, multi_world: MultiWorld) -> None: + # We could complete the key with all the default options, but that does not seem to improve performances. + cache[frozen_options] = multi_world + + def complete_options_with_default(options_to_complete=None) -> StardewValleyOptions: if options_to_complete is None: options_to_complete = {} diff --git a/worlds/stardew_valley/test/assertion/mod_assert.py b/worlds/stardew_valley/test/assertion/mod_assert.py index eec7f805d2c5..baba9bbaf856 100644 --- a/worlds/stardew_valley/test/assertion/mod_assert.py +++ b/worlds/stardew_valley/test/assertion/mod_assert.py @@ -1,4 +1,4 @@ -from typing import Union, List +from typing import Union, Iterable from unittest import TestCase from BaseClasses import MultiWorld @@ -7,9 +7,11 @@ class ModAssertMixin(TestCase): - def assert_stray_mod_items(self, chosen_mods: Union[List[str], str], multiworld: MultiWorld): + def assert_stray_mod_items(self, chosen_mods: Union[Iterable[str], str], multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] + else: + chosen_mods = list(chosen_mods) if ModNames.jasper in chosen_mods: # Jasper is a weird case because it shares NPC w/ SVE... diff --git a/worlds/stardew_valley/test/assertion/option_assert.py b/worlds/stardew_valley/test/assertion/option_assert.py index b384858f34f4..a07831f73e3f 100644 --- a/worlds/stardew_valley/test/assertion/option_assert.py +++ b/worlds/stardew_valley/test/assertion/option_assert.py @@ -63,8 +63,12 @@ def assert_cropsanity_same_number_items_and_locations(self, multiworld: MultiWor all_item_names = set(get_all_item_names(multiworld)) all_location_names = set(get_all_location_names(multiworld)) all_cropsanity_item_names = {item_name for item_name in all_item_names if Group.CROPSANITY in item_table[item_name].groups} - all_cropsanity_location_names = {location_name for location_name in all_location_names if LocationTags.CROPSANITY in location_table[location_name].tags} - self.assertEqual(len(all_cropsanity_item_names), len(all_cropsanity_location_names)) + all_cropsanity_location_names = {location_name + for location_name in all_location_names + if LocationTags.CROPSANITY in location_table[location_name].tags + # Qi Beans do not have an item + and location_name != "Harvest Qi Fruit"} + self.assertEqual(len(all_cropsanity_item_names) + 1, len(all_cropsanity_location_names)) def assert_all_rarecrows_exist(self, multiworld: MultiWorld): all_item_names = set(get_all_item_names(multiworld)) diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index f9b12394311a..5a1dad2925cf 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,17 +1,49 @@ from unittest import TestCase -from BaseClasses import CollectionState -from .rule_explain import explain -from ...stardew_rule import StardewRule, false_, MISSING_ITEM +from BaseClasses import CollectionState, Location +from ...stardew_rule import StardewRule, false_, MISSING_ITEM, Reach +from ...stardew_rule.rule_explain import explain class RuleAssertMixin(TestCase): def assert_rule_true(self, rule: StardewRule, state: CollectionState): - self.assertTrue(rule(state), explain(rule, state)) + expl = explain(rule, state) + try: + self.assertTrue(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") def assert_rule_false(self, rule: StardewRule, state: CollectionState): - self.assertFalse(rule(state), explain(rule, state, expected=False)) + expl = explain(rule, state, expected=False) + try: + self.assertFalse(rule(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): - self.assertNotIn(MISSING_ITEM, repr(rule)) - self.assertTrue(rule is false_ or rule(complete_state), explain(rule, complete_state)) + expl = explain(rule, complete_state) + try: + self.assertNotIn(MISSING_ITEM, repr(rule)) + self.assertTrue(rule is false_ or rule(complete_state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking rule {rule}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_true(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state) + try: + can_reach = location.can_reach(state) + self.assertTrue(can_reach, expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") + + def assert_reach_location_false(self, location: Location, state: CollectionState): + expl = explain(Reach(location.name, "Location", 1), state, expected=False) + try: + self.assertFalse(location.can_reach(state), expl) + except KeyError as e: + raise AssertionError(f"Error while checking location {location.name}: {e}" + f"\nExplanation: {expl}") diff --git a/worlds/stardew_valley/test/assertion/rule_explain.py b/worlds/stardew_valley/test/assertion/rule_explain.py deleted file mode 100644 index f9bf97603404..000000000000 --- a/worlds/stardew_valley/test/assertion/rule_explain.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from functools import cached_property, singledispatch -from typing import Iterable - -from BaseClasses import CollectionState -from worlds.generic.Rules import CollectionRule -from ...stardew_rule import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach - -max_explanation_depth = 10 - - -@dataclass -class RuleExplanation: - rule: StardewRule - state: CollectionState - expected: bool - sub_rules: Iterable[StardewRule] = field(default_factory=list) - - def summary(self, depth=0): - return " " * depth + f"{str(self.rule)} -> {self.result}" - - def __str__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) - if i.result is not self.expected else i.summary(depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - def __repr__(self, depth=0): - if not self.sub_rules or depth >= max_explanation_depth: - return self.summary(depth) - - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) - for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) - - @cached_property - def result(self): - return self.rule(self.state) - - @cached_property - def explained_sub_rules(self): - return [_explain(i, self.state, self.expected) for i in self.sub_rules] - - -def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: - if isinstance(rule, StardewRule): - return _explain(rule, state, expected) - else: - return f"Value of rule {str(rule)} was not {str(expected)} in {str(state)}" # noqa - - -@singledispatch -def _explain(rule: StardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected) - - -@_explain.register -def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.original_rules) - - -@_explain.register -def _(rule: Count, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules) - - -@_explain.register -def _(rule: Has, state: CollectionState, expected: bool) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [rule.other_rules[rule.item]]) - - -@_explain.register -def _(rule: TotalReceived, state: CollectionState, expected=True) -> RuleExplanation: - return RuleExplanation(rule, state, expected, [Received(i, rule.player, 1) for i in rule.items]) - - -@_explain.register -def _(rule: Reach, state: CollectionState, expected=True) -> RuleExplanation: - access_rules = None - if rule.resolution_hint == 'Location': - spot = state.multiworld.get_location(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - elif rule.resolution_hint == 'Entrance': - spot = state.multiworld.get_entrance(rule.spot, rule.player) - - if isinstance(spot.access_rule, StardewRule): - access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)] - - else: - spot = state.multiworld.get_region(rule.spot, rule.player) - access_rules = [*(Reach(e.name, "Entrance", rule.player) for e in spot.entrances)] - - if not access_rules: - return RuleExplanation(rule, state, expected) - - return RuleExplanation(rule, state, expected, access_rules) diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index 1e5512682f92..c1c24bdf75b4 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -53,7 +53,7 @@ def assert_same_number_items_locations(self, multiworld: MultiWorld): def assert_can_reach_everything(self, multiworld: MultiWorld): for location in multiworld.get_locations(): - self.assert_rule_true(location.access_rule, multiworld.state) + self.assert_reach_location_true(location, multiworld.state) def assert_basic_checks(self, multiworld: MultiWorld): self.assert_same_number_items_locations(multiworld) diff --git a/worlds/stardew_valley/test/content/TestArtisanEquipment.py b/worlds/stardew_valley/test/content/TestArtisanEquipment.py new file mode 100644 index 000000000000..32821511c44f --- /dev/null +++ b/worlds/stardew_valley/test/content/TestArtisanEquipment.py @@ -0,0 +1,54 @@ +from . import SVContentPackTestBase +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Vegetable, Fruit +from ...strings.food_names import Beverage +from ...strings.forageable_names import Forageable +from ...strings.machine_names import Machine +from ...strings.seed_names import Seed + +wine_base_fruits = [ + Fruit.ancient_fruit, Fruit.apple, Fruit.apricot, Forageable.blackberry, Fruit.blueberry, Forageable.cactus_fruit, Fruit.cherry, + Forageable.coconut, Fruit.cranberries, Forageable.crystal_fruit, Fruit.grape, Fruit.hot_pepper, Fruit.melon, Fruit.orange, Fruit.peach, + Fruit.pomegranate, Fruit.powdermelon, Fruit.rhubarb, Forageable.salmonberry, Forageable.spice_berry, Fruit.starfruit, Fruit.strawberry +] + +juice_base_vegetables = ( + Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.bok_choy, Vegetable.broccoli, Vegetable.carrot, Vegetable.cauliflower, + Vegetable.corn, Vegetable.eggplant, Forageable.fiddlehead_fern, Vegetable.garlic, Vegetable.green_bean, Vegetable.kale, Vegetable.parsnip, Vegetable.potato, + Vegetable.pumpkin, Vegetable.radish, Vegetable.red_cabbage, Vegetable.summer_squash, Vegetable.tomato, Vegetable.unmilled_rice, Vegetable.yam +) + +non_juice_base_vegetables = ( + Vegetable.hops, Vegetable.tea_leaves, Vegetable.wheat +) + + +class TestArtisanEquipment(SVContentPackTestBase): + + def test_keg_special_recipes(self): + self.assertIn(MachineSource(item=Vegetable.wheat, machine=Machine.keg), self.content.game_items[Beverage.beer].sources) + # self.assertIn(MachineSource(item=Ingredient.rice, machine=Machine.keg), self.content.game_items[Ingredient.vinegar].sources) + self.assertIn(MachineSource(item=Seed.coffee, machine=Machine.keg), self.content.game_items[Beverage.coffee].sources) + self.assertIn(MachineSource(item=Vegetable.tea_leaves, machine=Machine.keg), self.content.game_items[ArtisanGood.green_tea].sources) + self.assertIn(MachineSource(item=ArtisanGood.honey, machine=Machine.keg), self.content.game_items[ArtisanGood.mead].sources) + self.assertIn(MachineSource(item=Vegetable.hops, machine=Machine.keg), self.content.game_items[ArtisanGood.pale_ale].sources) + + def test_fruits_can_be_made_into_wines(self): + + for fruit in wine_base_fruits: + with self.subTest(fruit): + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(fruit)].sources) + self.assertIn(MachineSource(item=fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_vegetables_can_be_made_into_juices(self): + for vegetable in juice_base_vegetables: + with self.subTest(vegetable): + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_juice(vegetable)].sources) + self.assertIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + def test_non_juice_vegetables_cannot_be_made_into_juices(self): + for vegetable in non_juice_base_vegetables: + with self.subTest(vegetable): + self.assertNotIn(ArtisanGood.specific_juice(vegetable), self.content.game_items) + self.assertNotIn(MachineSource(item=vegetable, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) diff --git a/worlds/stardew_valley/test/content/TestGingerIsland.py b/worlds/stardew_valley/test/content/TestGingerIsland.py new file mode 100644 index 000000000000..7e7f866dfc8e --- /dev/null +++ b/worlds/stardew_valley/test/content/TestGingerIsland.py @@ -0,0 +1,55 @@ +from . import SVContentPackTestBase +from .. import SVTestBase +from ... import options +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit, Vegetable +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine +from ...strings.villager_names import NPC + + +class TestGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + + def test_leo_is_included(self): + self.assertIn(NPC.leo, self.content.villagers) + + def test_ginger_island_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.blue_discus, fish_names) + self.assertIn(Fish.lionfish, fish_names) + self.assertIn(Fish.stingray, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + self.assertEqual(63 + 3, len(self.content.fishes)) + + def test_ginger_island_fruits_can_be_made_into_wines(self): + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.banana)].sources) + self.assertIn(MachineSource(item=Fruit.banana, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.pineapple)].sources) + self.assertIn(MachineSource(item=Fruit.pineapple, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) + + def test_ginger_island_vegetables_can_be_made_into_wines(self): + taro_root_juice_sources = self.content.game_items[ArtisanGood.specific_juice(Vegetable.taro_root)].sources + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), taro_root_juice_sources) + self.assertIn(MachineSource(item=Vegetable.taro_root, machine=Machine.keg), self.content.game_items[ArtisanGood.juice].sources) + + +class TestWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true + } + + def test_leo_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + NPC.leo) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + NPC.leo) in location.name) diff --git a/worlds/stardew_valley/test/content/TestPelicanTown.py b/worlds/stardew_valley/test/content/TestPelicanTown.py new file mode 100644 index 000000000000..fa70916c9d33 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestPelicanTown.py @@ -0,0 +1,112 @@ +from . import SVContentPackTestBase +from ...strings.fish_names import Fish +from ...strings.villager_names import NPC + + +class TestPelicanTown(SVContentPackTestBase): + + def test_all_pelican_town_villagers_are_included(self): + self.assertIn(NPC.alex, self.content.villagers) + self.assertIn(NPC.elliott, self.content.villagers) + self.assertIn(NPC.harvey, self.content.villagers) + self.assertIn(NPC.sam, self.content.villagers) + self.assertIn(NPC.sebastian, self.content.villagers) + self.assertIn(NPC.shane, self.content.villagers) + self.assertIn(NPC.abigail, self.content.villagers) + self.assertIn(NPC.emily, self.content.villagers) + self.assertIn(NPC.haley, self.content.villagers) + self.assertIn(NPC.leah, self.content.villagers) + self.assertIn(NPC.maru, self.content.villagers) + self.assertIn(NPC.penny, self.content.villagers) + self.assertIn(NPC.caroline, self.content.villagers) + self.assertIn(NPC.clint, self.content.villagers) + self.assertIn(NPC.demetrius, self.content.villagers) + self.assertIn(NPC.dwarf, self.content.villagers) + self.assertIn(NPC.evelyn, self.content.villagers) + self.assertIn(NPC.george, self.content.villagers) + self.assertIn(NPC.gus, self.content.villagers) + self.assertIn(NPC.jas, self.content.villagers) + self.assertIn(NPC.jodi, self.content.villagers) + self.assertIn(NPC.kent, self.content.villagers) + self.assertIn(NPC.krobus, self.content.villagers) + self.assertIn(NPC.lewis, self.content.villagers) + self.assertIn(NPC.linus, self.content.villagers) + self.assertIn(NPC.marnie, self.content.villagers) + self.assertIn(NPC.pam, self.content.villagers) + self.assertIn(NPC.pierre, self.content.villagers) + self.assertIn(NPC.robin, self.content.villagers) + self.assertIn(NPC.sandy, self.content.villagers) + self.assertIn(NPC.vincent, self.content.villagers) + self.assertIn(NPC.willy, self.content.villagers) + self.assertIn(NPC.wizard, self.content.villagers) + + self.assertEqual(33, len(self.content.villagers)) + + def test_all_pelican_town_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.albacore, fish_names) + self.assertIn(Fish.anchovy, fish_names) + self.assertIn(Fish.bream, fish_names) + self.assertIn(Fish.bullhead, fish_names) + self.assertIn(Fish.carp, fish_names) + self.assertIn(Fish.catfish, fish_names) + self.assertIn(Fish.chub, fish_names) + self.assertIn(Fish.dorado, fish_names) + self.assertIn(Fish.eel, fish_names) + self.assertIn(Fish.flounder, fish_names) + self.assertIn(Fish.ghostfish, fish_names) + self.assertIn(Fish.goby, fish_names) + self.assertIn(Fish.halibut, fish_names) + self.assertIn(Fish.herring, fish_names) + self.assertIn(Fish.ice_pip, fish_names) + self.assertIn(Fish.largemouth_bass, fish_names) + self.assertIn(Fish.lava_eel, fish_names) + self.assertIn(Fish.lingcod, fish_names) + self.assertIn(Fish.midnight_carp, fish_names) + self.assertIn(Fish.octopus, fish_names) + self.assertIn(Fish.perch, fish_names) + self.assertIn(Fish.pike, fish_names) + self.assertIn(Fish.pufferfish, fish_names) + self.assertIn(Fish.rainbow_trout, fish_names) + self.assertIn(Fish.red_mullet, fish_names) + self.assertIn(Fish.red_snapper, fish_names) + self.assertIn(Fish.salmon, fish_names) + self.assertIn(Fish.sandfish, fish_names) + self.assertIn(Fish.sardine, fish_names) + self.assertIn(Fish.scorpion_carp, fish_names) + self.assertIn(Fish.sea_cucumber, fish_names) + self.assertIn(Fish.shad, fish_names) + self.assertIn(Fish.slimejack, fish_names) + self.assertIn(Fish.smallmouth_bass, fish_names) + self.assertIn(Fish.squid, fish_names) + self.assertIn(Fish.stonefish, fish_names) + self.assertIn(Fish.sturgeon, fish_names) + self.assertIn(Fish.sunfish, fish_names) + self.assertIn(Fish.super_cucumber, fish_names) + self.assertIn(Fish.tiger_trout, fish_names) + self.assertIn(Fish.tilapia, fish_names) + self.assertIn(Fish.tuna, fish_names) + self.assertIn(Fish.void_salmon, fish_names) + self.assertIn(Fish.walleye, fish_names) + self.assertIn(Fish.woodskip, fish_names) + self.assertIn(Fish.blobfish, fish_names) + self.assertIn(Fish.midnight_squid, fish_names) + self.assertIn(Fish.spook_fish, fish_names) + self.assertIn(Fish.angler, fish_names) + self.assertIn(Fish.crimsonfish, fish_names) + self.assertIn(Fish.glacierfish, fish_names) + self.assertIn(Fish.legend, fish_names) + self.assertIn(Fish.mutant_carp, fish_names) + self.assertIn(Fish.clam, fish_names) + self.assertIn(Fish.cockle, fish_names) + self.assertIn(Fish.crab, fish_names) + self.assertIn(Fish.crayfish, fish_names) + self.assertIn(Fish.lobster, fish_names) + self.assertIn(Fish.mussel, fish_names) + self.assertIn(Fish.oyster, fish_names) + self.assertIn(Fish.periwinkle, fish_names) + self.assertIn(Fish.shrimp, fish_names) + self.assertIn(Fish.snail, fish_names) + + self.assertEqual(63, len(self.content.fishes)) diff --git a/worlds/stardew_valley/test/content/TestQiBoard.py b/worlds/stardew_valley/test/content/TestQiBoard.py new file mode 100644 index 000000000000..b9d940d2c887 --- /dev/null +++ b/worlds/stardew_valley/test/content/TestQiBoard.py @@ -0,0 +1,27 @@ +from . import SVContentPackTestBase +from ...content import content_packs +from ...data.artisan import MachineSource +from ...strings.artisan_good_names import ArtisanGood +from ...strings.crop_names import Fruit +from ...strings.fish_names import Fish +from ...strings.machine_names import Machine + + +class TestQiBoard(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack, content_packs.qi_board_content_pack) + + def test_extended_family_fishes_are_included(self): + fish_names = self.content.fishes.keys() + + self.assertIn(Fish.ms_angler, fish_names) + self.assertIn(Fish.son_of_crimsonfish, fish_names) + self.assertIn(Fish.glacierfish_jr, fish_names) + self.assertIn(Fish.legend_ii, fish_names) + self.assertIn(Fish.radioactive_carp, fish_names) + + # 63 from pelican town + 3 ginger island exclusive + 5 extended family + self.assertEqual(63 + 3 + 5, len(self.content.fishes)) + + def test_wines(self): + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.qi_fruit)].sources) + self.assertIn(MachineSource(item=Fruit.qi_fruit, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py new file mode 100644 index 000000000000..4130dae90dc3 --- /dev/null +++ b/worlds/stardew_valley/test/content/__init__.py @@ -0,0 +1,23 @@ +import unittest +from typing import ClassVar, Tuple + +from ...content import content_packs, ContentPack, StardewContent, unpack_content, StardewFeatures, feature + +default_features = StardewFeatures( + feature.booksanity.BooksanityDisabled(), + feature.cropsanity.CropsanityDisabled(), + feature.fishsanity.FishsanityNone(), + feature.friendsanity.FriendsanityNone() +) + + +class SVContentPackTestBase(unittest.TestCase): + vanilla_packs: ClassVar[Tuple[ContentPack]] = (content_packs.pelican_town, content_packs.the_desert, content_packs.the_farm, content_packs.the_mines) + mods: ClassVar[Tuple[str]] = () + + content: ClassVar[StardewContent] + + @classmethod + def setUpClass(cls) -> None: + packs = cls.vanilla_packs + tuple(content_packs.by_mod[mod] for mod in cls.mods) + cls.content = unpack_content(default_features, packs) diff --git a/worlds/stardew_valley/test/content/feature/TestFriendsanity.py b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py new file mode 100644 index 000000000000..804ac0978bb5 --- /dev/null +++ b/worlds/stardew_valley/test/content/feature/TestFriendsanity.py @@ -0,0 +1,33 @@ +import unittest + +from ....content.feature import friendsanity + + +class TestHeartSteps(unittest.TestCase): + + def test_given_size_of_one_when_calculate_steps_then_advance_one_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(4, 1) + + self.assertEqual(steps, (1, 2, 3, 4)) + + def test_given_size_of_two_when_calculate_steps_then_advance_two_heart_at_the_time(self): + steps = friendsanity.get_heart_steps(6, 2) + + self.assertEqual(steps, (2, 4, 6)) + + def test_given_size_of_three_and_max_heart_not_multiple_of_three_when_calculate_steps_then_add_max_as_last_step(self): + steps = friendsanity.get_heart_steps(7, 3) + + self.assertEqual(steps, (3, 6, 7)) + + +class TestExtractNpcFromLocation(unittest.TestCase): + + def test_given_npc_with_space_in_name_when_extract_then_find_name_and_heart(self): + npc = "Mr. Ginger" + location_name = friendsanity.to_location_name(npc, 34) + + found_name, found_heart = friendsanity.extract_npc_from_location_name(location_name) + + self.assertEqual(found_name, npc) + self.assertEqual(found_heart, 34) diff --git a/worlds/stardew_valley/test/content/feature/__init__.py b/worlds/stardew_valley/test/content/feature/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/content/mods/TestDeepwoods.py b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py new file mode 100644 index 000000000000..381502da13ba --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestDeepwoods.py @@ -0,0 +1,14 @@ +from ....data.artisan import MachineSource +from ....mods.mod_data import ModNames +from ....strings.artisan_good_names import ArtisanGood +from ....strings.crop_names import Fruit +from ....strings.machine_names import Machine +from ....test.content import SVContentPackTestBase + + +class TestArtisanEquipment(SVContentPackTestBase): + mods = (ModNames.deepwoods,) + + def test_mango_wine_exists(self): + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.specific_wine(Fruit.mango)].sources) + self.assertIn(MachineSource(item=Fruit.mango, machine=Machine.keg), self.content.game_items[ArtisanGood.wine].sources) diff --git a/worlds/stardew_valley/test/content/mods/TestJasper.py b/worlds/stardew_valley/test/content/mods/TestJasper.py new file mode 100644 index 000000000000..40927e67c258 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestJasper.py @@ -0,0 +1,27 @@ +from .. import SVContentPackTestBase +from ....mods.mod_data import ModNames +from ....strings.villager_names import ModNPC + + +class TestJasperWithoutSVE(SVContentPackTestBase): + mods = (ModNames.jasper,) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.jasper) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.jasper) + + +class TestJasperWithSVE(SVContentPackTestBase): + mods = (ModNames.jasper, ModNames.sve) + + def test_gunther_is_added(self): + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.gunther].mod_name, ModNames.sve) + + def test_marlon_is_added(self): + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertEqual(self.content.villagers[ModNPC.marlon].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/content/mods/TestSVE.py b/worlds/stardew_valley/test/content/mods/TestSVE.py new file mode 100644 index 000000000000..4065498d6be7 --- /dev/null +++ b/worlds/stardew_valley/test/content/mods/TestSVE.py @@ -0,0 +1,143 @@ +from .. import SVContentPackTestBase +from ... import SVTestBase +from .... import options +from ....content import content_packs +from ....mods.mod_data import ModNames +from ....strings.fish_names import SVEFish +from ....strings.villager_names import ModNPC, NPC + +vanilla_villagers = 33 +vanilla_villagers_with_leo = 34 +sve_villagers = 13 +sve_villagers_with_lance = 14 +vanilla_pelican_town_fish = 63 +vanilla_ginger_island_fish = 3 +sve_pelican_town_fish = 16 +sve_ginger_island_fish = 10 + + +class TestVanilla(SVContentPackTestBase): + + def test_wizard_is_not_bachelor(self): + self.assertFalse(self.content.villagers[NPC.wizard].bachelor) + + +class TestSVE(SVContentPackTestBase): + mods = (ModNames.sve,) + + def test_lance_is_not_included(self): + self.assertNotIn(ModNPC.lance, self.content.villagers) + + def test_wizard_is_bachelor(self): + self.assertTrue(self.content.villagers[NPC.wizard].bachelor) + self.assertEqual(self.content.villagers[NPC.wizard].mod_name, ModNames.sve) + + def test_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers + sve_villagers, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + sve_pelican_town_fish, len(self.content.fishes)) + + +class TestSVEWithGingerIsland(SVContentPackTestBase): + vanilla_packs = SVContentPackTestBase.vanilla_packs + (content_packs.ginger_island_content_pack,) + mods = (ModNames.sve,) + + def test_lance_is_included(self): + self.assertIn(ModNPC.lance, self.content.villagers) + + def test_other_sve_npc_are_included(self): + self.assertIn(ModNPC.apples, self.content.villagers) + self.assertIn(ModNPC.claire, self.content.villagers) + self.assertIn(ModNPC.olivia, self.content.villagers) + self.assertIn(ModNPC.sophia, self.content.villagers) + self.assertIn(ModNPC.victor, self.content.villagers) + self.assertIn(ModNPC.andy, self.content.villagers) + self.assertIn(ModNPC.gunther, self.content.villagers) + self.assertIn(ModNPC.martin, self.content.villagers) + self.assertIn(ModNPC.marlon, self.content.villagers) + self.assertIn(ModNPC.morgan, self.content.villagers) + self.assertIn(ModNPC.morris, self.content.villagers) + self.assertIn(ModNPC.scarlett, self.content.villagers) + self.assertIn(ModNPC.susan, self.content.villagers) + + self.assertEqual(vanilla_villagers_with_leo + sve_villagers_with_lance, len(self.content.villagers)) + + def test_sve_has_sve_fish(self): + fish_names = self.content.fishes.keys() + + self.assertIn(SVEFish.baby_lunaloo, fish_names) + self.assertIn(SVEFish.bonefish, fish_names) + self.assertIn(SVEFish.bull_trout, fish_names) + self.assertIn(SVEFish.butterfish, fish_names) + self.assertIn(SVEFish.clownfish, fish_names) + self.assertIn(SVEFish.daggerfish, fish_names) + self.assertIn(SVEFish.frog, fish_names) + self.assertIn(SVEFish.gemfish, fish_names) + self.assertIn(SVEFish.goldenfish, fish_names) + self.assertIn(SVEFish.grass_carp, fish_names) + self.assertIn(SVEFish.king_salmon, fish_names) + self.assertIn(SVEFish.kittyfish, fish_names) + self.assertIn(SVEFish.lunaloo, fish_names) + self.assertIn(SVEFish.meteor_carp, fish_names) + self.assertIn(SVEFish.minnow, fish_names) + self.assertIn(SVEFish.puppyfish, fish_names) + self.assertIn(SVEFish.radioactive_bass, fish_names) + self.assertIn(SVEFish.seahorse, fish_names) + self.assertIn(SVEFish.shiny_lunaloo, fish_names) + self.assertIn(SVEFish.snatcher_worm, fish_names) + self.assertIn(SVEFish.starfish, fish_names) + self.assertIn(SVEFish.torpedo_trout, fish_names) + self.assertIn(SVEFish.undeadfish, fish_names) + self.assertIn(SVEFish.void_eel, fish_names) + self.assertIn(SVEFish.water_grub, fish_names) + self.assertIn(SVEFish.sea_sponge, fish_names) + + self.assertEqual(vanilla_pelican_town_fish + vanilla_ginger_island_fish + sve_pelican_town_fish + sve_ginger_island_fish, len(self.content.fishes)) + + +class TestSVEWithoutGingerIslandE2E(SVTestBase): + options = { + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_true, + options.Mods: ModNames.sve + } + + def test_lance_is_not_in_the_pool(self): + for item in self.multiworld.itempool: + self.assertFalse(("Friendsanity: " + ModNPC.lance) in item.name) + + for location in self.multiworld.get_locations(self.player): + self.assertFalse(("Friendsanity: " + ModNPC.lance) in location.name) diff --git a/worlds/stardew_valley/test/content/mods/__init__.py b/worlds/stardew_valley/test/content/mods/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index 9f76c10a9da4..395c48ee698a 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -2,22 +2,25 @@ from itertools import combinations, product from BaseClasses import get_seed -from .option_names import all_option_choices +from .option_names import all_option_choices, get_option_choices from .. import SVTestCase from ..assertion import WorldAssertMixin, ModAssertMixin from ... import options -from ...mods.mod_data import all_mods, ModNames +from ...mods.mod_data import ModNames assert unittest class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): - def test_given_mod_pairs_when_generate_then_basic_checks(self): - if self.skip_long_tests: - return + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + if cls.skip_long_tests: + raise unittest.SkipTest("Long tests disabled") - for mod_pair in combinations(all_mods, 2): + def test_given_mod_pairs_when_generate_then_basic_checks(self): + for mod_pair in combinations(options.Mods.valid_keys, 2): world_options = { options.Mods: frozenset(mod_pair) } @@ -27,10 +30,7 @@ def test_given_mod_pairs_when_generate_then_basic_checks(self): self.assert_stray_mod_items(list(mod_pair), multiworld) def test_given_mod_names_when_generate_paired_with_other_options_then_basic_checks(self): - if self.skip_long_tests: - return - - for mod, (option, value) in product(all_mods, all_option_choices): + for mod, (option, value) in product(options.Mods.valid_keys, all_option_choices): world_options = { option: value, options.Mods: mod @@ -40,12 +40,28 @@ def test_given_mod_names_when_generate_paired_with_other_options_then_basic_chec self.assert_basic_checks(multiworld) self.assert_stray_mod_items(mod, multiworld) - # @unittest.skip + def test_given_no_quest_all_mods_when_generate_with_all_goals_then_basic_checks(self): + for goal, (option, value) in product(get_option_choices(options.Goal), all_option_choices): + if option is options.QuestLocations: + continue + + world_options = { + options.Goal: goal, + option: value, + options.QuestLocations: -1, + options.Mods: frozenset(options.Mods.valid_keys), + } + + with self.solo_world_sub_test(f"Goal: {goal}, {option.internal_name}: {value}", world_options, world_caching=False) as (multiworld, _): + self.assert_basic_checks(multiworld) + + @unittest.skip def test_troubleshoot_option(self): - seed = get_seed(45949559493817417717) + seed = get_seed(78709133382876990000) + world_options = { - options.ElevatorProgression: options.ElevatorProgression.option_vanilla, - options.Mods: ModNames.deepwoods + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.Mods: ModNames.sve } with self.solo_world_sub_test(world_options=world_options, seed=seed, world_caching=False) as (multiworld, _): diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index ca9fc01b2922..0c8cfcb1e107 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,15 +1,18 @@ +import unittest from itertools import combinations +from BaseClasses import get_seed from .option_names import all_option_choices -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase, solo_multiworld from ..assertion.world_assert import WorldAssertMixin from ... import options +from ...mods.mod_data import ModNames class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") for (option1, option1_choice), (option2, option2_choice) in combinations(all_option_choices, 2): if option1 is option2: @@ -31,13 +34,14 @@ class TestDynamicOptionDebug(WorldAssertMixin, SVTestCase): def test_option_pair_debug(self): option_dict = { - options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, - options.Monstersanity.internal_name: options.Monstersanity.option_one_per_monster, + options.Goal.internal_name: options.Goal.option_master_angler, + options.QuestLocations.internal_name: -1, + options.Fishsanity.internal_name: options.Fishsanity.option_all, + options.Mods.internal_name: frozenset({ModNames.sve}), } for i in range(1): - # seed = int(random() * pow(10, 18) - 1) - seed = 823942126251776128 + seed = get_seed() with self.subTest(f"Seed: {seed}"): print(f"Seed: {seed}") - multiworld = setup_solo_multiworld(option_dict, seed) - self.assert_basic_checks(multiworld) + with solo_multiworld(option_dict, seed=seed) as (multiworld, _): + self.assert_basic_checks(multiworld) diff --git a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py index 66bc5aeba8bb..f233fc36dc84 100644 --- a/worlds/stardew_valley/test/long/TestPreRolledRandomness.py +++ b/worlds/stardew_valley/test/long/TestPreRolledRandomness.py @@ -1,3 +1,5 @@ +import unittest + from BaseClasses import get_seed from .. import SVTestCase from ..assertion import WorldAssertMixin @@ -7,7 +9,8 @@ class TestGeneratePreRolledRandomness(WorldAssertMixin, SVTestCase): def test_given_pre_rolled_difficult_randomness_when_generate_then_basic_checks(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + choices = { options.EntranceRandomization.internal_name: options.EntranceRandomization.option_buildings, options.BundleRandomization.internal_name: options.BundleRandomization.option_remixed, diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index f3702c05f42b..6d4931280a79 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -1,10 +1,11 @@ import random +import unittest from typing import Dict from BaseClasses import MultiWorld, get_seed from Options import NamedRange, Range from .option_names import options_to_include -from .. import setup_solo_multiworld, SVTestCase +from .. import SVTestCase from ..assertion import GoalAssertMixin, OptionAssertMixin, WorldAssertMixin @@ -18,12 +19,6 @@ def get_option_choices(option) -> Dict[str, int]: return {} -def generate_random_multiworld(world_id: int): - world_options = generate_random_world_options(world_id) - multiworld = setup_solo_multiworld(world_options, seed=world_id) - return multiworld - - def generate_random_world_options(seed: int) -> Dict[str, int]: num_options = len(options_to_include) world_options = dict() @@ -57,7 +52,8 @@ def get_number_log_steps(number_worlds: int) -> int: class TestGenerateManyWorlds(GoalAssertMixin, OptionAssertMixin, WorldAssertMixin, SVTestCase): def test_generate_many_worlds_then_check_results(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") + number_worlds = 10 if self.skip_long_tests else 1000 seed = get_seed() self.generate_and_check_many_worlds(number_worlds, seed) diff --git a/worlds/stardew_valley/test/mods/TestModFish.py b/worlds/stardew_valley/test/mods/TestModFish.py deleted file mode 100644 index 81ac6ac0fb99..000000000000 --- a/worlds/stardew_valley/test/mods/TestModFish.py +++ /dev/null @@ -1,226 +0,0 @@ -import unittest -from typing import Set - -from ...data.fish_data import get_fish_for_mods -from ...mods.mod_data import ModNames -from ...strings.fish_names import Fish, SVEFish - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetFishForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_no_mods_no_sve_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertNotIn(SVEFish.baby_lunaloo, fish_names) - self.assertNotIn(SVEFish.bonefish, fish_names) - self.assertNotIn(SVEFish.bull_trout, fish_names) - self.assertNotIn(SVEFish.butterfish, fish_names) - self.assertNotIn(SVEFish.clownfish, fish_names) - self.assertNotIn(SVEFish.daggerfish, fish_names) - self.assertNotIn(SVEFish.frog, fish_names) - self.assertNotIn(SVEFish.gemfish, fish_names) - self.assertNotIn(SVEFish.goldenfish, fish_names) - self.assertNotIn(SVEFish.grass_carp, fish_names) - self.assertNotIn(SVEFish.king_salmon, fish_names) - self.assertNotIn(SVEFish.kittyfish, fish_names) - self.assertNotIn(SVEFish.lunaloo, fish_names) - self.assertNotIn(SVEFish.meteor_carp, fish_names) - self.assertNotIn(SVEFish.minnow, fish_names) - self.assertNotIn(SVEFish.puppyfish, fish_names) - self.assertNotIn(SVEFish.radioactive_bass, fish_names) - self.assertNotIn(SVEFish.seahorse, fish_names) - self.assertNotIn(SVEFish.shiny_lunaloo, fish_names) - self.assertNotIn(SVEFish.snatcher_worm, fish_names) - self.assertNotIn(SVEFish.starfish, fish_names) - self.assertNotIn(SVEFish.torpedo_trout, fish_names) - self.assertNotIn(SVEFish.undeadfish, fish_names) - self.assertNotIn(SVEFish.void_eel, fish_names) - self.assertNotIn(SVEFish.water_grub, fish_names) - self.assertNotIn(SVEFish.sea_sponge, fish_names) - self.assertNotIn(SVEFish.dulse_seaweed, fish_names) - - def test_sve_all_vanilla_fish(self): - all_fish = get_fish_for_mods(no_mods) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(Fish.albacore, fish_names) - self.assertIn(Fish.anchovy, fish_names) - self.assertIn(Fish.blue_discus, fish_names) - self.assertIn(Fish.bream, fish_names) - self.assertIn(Fish.bullhead, fish_names) - self.assertIn(Fish.carp, fish_names) - self.assertIn(Fish.catfish, fish_names) - self.assertIn(Fish.chub, fish_names) - self.assertIn(Fish.dorado, fish_names) - self.assertIn(Fish.eel, fish_names) - self.assertIn(Fish.flounder, fish_names) - self.assertIn(Fish.ghostfish, fish_names) - self.assertIn(Fish.halibut, fish_names) - self.assertIn(Fish.herring, fish_names) - self.assertIn(Fish.ice_pip, fish_names) - self.assertIn(Fish.largemouth_bass, fish_names) - self.assertIn(Fish.lava_eel, fish_names) - self.assertIn(Fish.lingcod, fish_names) - self.assertIn(Fish.lionfish, fish_names) - self.assertIn(Fish.midnight_carp, fish_names) - self.assertIn(Fish.octopus, fish_names) - self.assertIn(Fish.perch, fish_names) - self.assertIn(Fish.pike, fish_names) - self.assertIn(Fish.pufferfish, fish_names) - self.assertIn(Fish.rainbow_trout, fish_names) - self.assertIn(Fish.red_mullet, fish_names) - self.assertIn(Fish.red_snapper, fish_names) - self.assertIn(Fish.salmon, fish_names) - self.assertIn(Fish.sandfish, fish_names) - self.assertIn(Fish.sardine, fish_names) - self.assertIn(Fish.scorpion_carp, fish_names) - self.assertIn(Fish.sea_cucumber, fish_names) - self.assertIn(Fish.shad, fish_names) - self.assertIn(Fish.slimejack, fish_names) - self.assertIn(Fish.smallmouth_bass, fish_names) - self.assertIn(Fish.squid, fish_names) - self.assertIn(Fish.stingray, fish_names) - self.assertIn(Fish.stonefish, fish_names) - self.assertIn(Fish.sturgeon, fish_names) - self.assertIn(Fish.sunfish, fish_names) - self.assertIn(Fish.super_cucumber, fish_names) - self.assertIn(Fish.tiger_trout, fish_names) - self.assertIn(Fish.tilapia, fish_names) - self.assertIn(Fish.tuna, fish_names) - self.assertIn(Fish.void_salmon, fish_names) - self.assertIn(Fish.walleye, fish_names) - self.assertIn(Fish.woodskip, fish_names) - self.assertIn(Fish.blob_fish, fish_names) - self.assertIn(Fish.midnight_squid, fish_names) - self.assertIn(Fish.spook_fish, fish_names) - self.assertIn(Fish.angler, fish_names) - self.assertIn(Fish.crimsonfish, fish_names) - self.assertIn(Fish.glacierfish, fish_names) - self.assertIn(Fish.legend, fish_names) - self.assertIn(Fish.mutant_carp, fish_names) - self.assertIn(Fish.ms_angler, fish_names) - self.assertIn(Fish.son_of_crimsonfish, fish_names) - self.assertIn(Fish.glacierfish_jr, fish_names) - self.assertIn(Fish.legend_ii, fish_names) - self.assertIn(Fish.radioactive_carp, fish_names) - self.assertIn(Fish.clam, fish_names) - self.assertIn(Fish.cockle, fish_names) - self.assertIn(Fish.crab, fish_names) - self.assertIn(Fish.crayfish, fish_names) - self.assertIn(Fish.lobster, fish_names) - self.assertIn(Fish.mussel, fish_names) - self.assertIn(Fish.oyster, fish_names) - self.assertIn(Fish.periwinkle, fish_names) - self.assertIn(Fish.shrimp, fish_names) - self.assertIn(Fish.snail, fish_names) - - def test_sve_has_sve_fish(self): - all_fish = get_fish_for_mods(sve) - fish_names = {fish.name for fish in all_fish} - - self.assertIn(SVEFish.baby_lunaloo, fish_names) - self.assertIn(SVEFish.bonefish, fish_names) - self.assertIn(SVEFish.bull_trout, fish_names) - self.assertIn(SVEFish.butterfish, fish_names) - self.assertIn(SVEFish.clownfish, fish_names) - self.assertIn(SVEFish.daggerfish, fish_names) - self.assertIn(SVEFish.frog, fish_names) - self.assertIn(SVEFish.gemfish, fish_names) - self.assertIn(SVEFish.goldenfish, fish_names) - self.assertIn(SVEFish.grass_carp, fish_names) - self.assertIn(SVEFish.king_salmon, fish_names) - self.assertIn(SVEFish.kittyfish, fish_names) - self.assertIn(SVEFish.lunaloo, fish_names) - self.assertIn(SVEFish.meteor_carp, fish_names) - self.assertIn(SVEFish.minnow, fish_names) - self.assertIn(SVEFish.puppyfish, fish_names) - self.assertIn(SVEFish.radioactive_bass, fish_names) - self.assertIn(SVEFish.seahorse, fish_names) - self.assertIn(SVEFish.shiny_lunaloo, fish_names) - self.assertIn(SVEFish.snatcher_worm, fish_names) - self.assertIn(SVEFish.starfish, fish_names) - self.assertIn(SVEFish.torpedo_trout, fish_names) - self.assertIn(SVEFish.undeadfish, fish_names) - self.assertIn(SVEFish.void_eel, fish_names) - self.assertIn(SVEFish.water_grub, fish_names) - self.assertIn(SVEFish.sea_sponge, fish_names) - self.assertIn(SVEFish.dulse_seaweed, fish_names) diff --git a/worlds/stardew_valley/test/mods/TestModVillagers.py b/worlds/stardew_valley/test/mods/TestModVillagers.py deleted file mode 100644 index 3be437c3f737..000000000000 --- a/worlds/stardew_valley/test/mods/TestModVillagers.py +++ /dev/null @@ -1,132 +0,0 @@ -import unittest -from typing import Set - -from ...data.villagers_data import get_villagers_for_mods -from ...mods.mod_data import ModNames -from ...strings.villager_names import NPC, ModNPC - -no_mods: Set[str] = set() -sve: Set[str] = {ModNames.sve} - - -class TestGetVillagersForMods(unittest.TestCase): - - def test_no_mods_all_vanilla_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertIn(NPC.alex, villager_names) - self.assertIn(NPC.elliott, villager_names) - self.assertIn(NPC.harvey, villager_names) - self.assertIn(NPC.sam, villager_names) - self.assertIn(NPC.sebastian, villager_names) - self.assertIn(NPC.shane, villager_names) - self.assertIn(NPC.abigail, villager_names) - self.assertIn(NPC.emily, villager_names) - self.assertIn(NPC.haley, villager_names) - self.assertIn(NPC.leah, villager_names) - self.assertIn(NPC.maru, villager_names) - self.assertIn(NPC.penny, villager_names) - self.assertIn(NPC.caroline, villager_names) - self.assertIn(NPC.clint, villager_names) - self.assertIn(NPC.demetrius, villager_names) - self.assertIn(NPC.dwarf, villager_names) - self.assertIn(NPC.evelyn, villager_names) - self.assertIn(NPC.george, villager_names) - self.assertIn(NPC.gus, villager_names) - self.assertIn(NPC.jas, villager_names) - self.assertIn(NPC.jodi, villager_names) - self.assertIn(NPC.kent, villager_names) - self.assertIn(NPC.krobus, villager_names) - self.assertIn(NPC.leo, villager_names) - self.assertIn(NPC.lewis, villager_names) - self.assertIn(NPC.linus, villager_names) - self.assertIn(NPC.marnie, villager_names) - self.assertIn(NPC.pam, villager_names) - self.assertIn(NPC.pierre, villager_names) - self.assertIn(NPC.robin, villager_names) - self.assertIn(NPC.sandy, villager_names) - self.assertIn(NPC.vincent, villager_names) - self.assertIn(NPC.willy, villager_names) - self.assertIn(NPC.wizard, villager_names) - - def test_no_mods_no_mod_villagers(self): - villagers = get_villagers_for_mods(no_mods) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.lance, villager_names) - self.assertNotIn(ModNPC.apples, villager_names) - self.assertNotIn(ModNPC.claire, villager_names) - self.assertNotIn(ModNPC.olivia, villager_names) - self.assertNotIn(ModNPC.sophia, villager_names) - self.assertNotIn(ModNPC.victor, villager_names) - self.assertNotIn(ModNPC.andy, villager_names) - self.assertNotIn(ModNPC.gunther, villager_names) - self.assertNotIn(ModNPC.martin, villager_names) - self.assertNotIn(ModNPC.marlon, villager_names) - self.assertNotIn(ModNPC.morgan, villager_names) - self.assertNotIn(ModNPC.morris, villager_names) - self.assertNotIn(ModNPC.scarlett, villager_names) - self.assertNotIn(ModNPC.susan, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_sve_has_sve_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertIn(ModNPC.lance, villager_names) - self.assertIn(ModNPC.apples, villager_names) - self.assertIn(ModNPC.claire, villager_names) - self.assertIn(ModNPC.olivia, villager_names) - self.assertIn(ModNPC.sophia, villager_names) - self.assertIn(ModNPC.victor, villager_names) - self.assertIn(ModNPC.andy, villager_names) - self.assertIn(ModNPC.gunther, villager_names) - self.assertIn(ModNPC.martin, villager_names) - self.assertIn(ModNPC.marlon, villager_names) - self.assertIn(ModNPC.morgan, villager_names) - self.assertIn(ModNPC.morris, villager_names) - self.assertIn(ModNPC.scarlett, villager_names) - self.assertIn(ModNPC.susan, villager_names) - - def test_sve_has_no_other_mod_villagers(self): - villagers = get_villagers_for_mods(sve) - villager_names = {villager.name for villager in villagers} - - self.assertNotIn(ModNPC.alec, villager_names) - self.assertNotIn(ModNPC.ayeisha, villager_names) - self.assertNotIn(ModNPC.delores, villager_names) - self.assertNotIn(ModNPC.eugene, villager_names) - self.assertNotIn(ModNPC.jasper, villager_names) - self.assertNotIn(ModNPC.juna, villager_names) - self.assertNotIn(ModNPC.mr_ginger, villager_names) - self.assertNotIn(ModNPC.riley, villager_names) - self.assertNotIn(ModNPC.shiko, villager_names) - self.assertNotIn(ModNPC.wellwick, villager_names) - self.assertNotIn(ModNPC.yoba, villager_names) - self.assertNotIn(ModNPC.goblin, villager_names) - self.assertNotIn(ModNPC.alecto, villager_names) - - def test_no_mods_wizard_is_not_bachelor(self): - villagers = get_villagers_for_mods(no_mods) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertFalse(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.vanilla) - - def test_sve_wizard_is_bachelor(self): - villagers = get_villagers_for_mods(sve) - villagers_by_name = {villager.name: villager for villager in villagers} - self.assertTrue(villagers_by_name[NPC.wizard].bachelor) - self.assertEqual(villagers_by_name[NPC.wizard].mod_name, ModNames.sve) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 57bca5f25645..5e7e9d4143bd 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -1,48 +1,48 @@ import random from BaseClasses import get_seed -from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods, complete_options_with_default +from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld from ..assertion import ModAssertMixin, WorldAssertMixin from ... import items, Group, ItemClassification from ... import options from ...items import items_by_group -from ...mods.mod_data import all_mods +from ...options import SkillProgression, Walnutsanity from ...regions import RandomizationFlag, randomize_connections, create_final_connections_and_regions class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): - for mod in all_mods: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}, dirty_state=True) as (multi_world, _): + for mod in options.Mods.valid_keys: + with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basic_checks(self): for option in options.EntranceRandomization.options: - for mod in all_mods: + for mod in options.Mods.valid_keys: world_options = { options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], options.Mods: mod } - with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) def test_allsanity_all_mods_when_generate_then_basic_checks(self): - with self.solo_world_sub_test(world_options=allsanity_options_with_mods(), dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=allsanity_mods_6_x_x()) as (multi_world, _): self.assert_basic_checks(multi_world) def test_allsanity_all_mods_exclude_island_when_generate_then_basic_checks(self): - world_options = allsanity_options_with_mods() + world_options = allsanity_mods_6_x_x() world_options.update({options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true}) - with self.solo_world_sub_test(world_options=world_options, dirty_state=True) as (multi_world, _): + with self.solo_world_sub_test(world_options=world_options) as (multi_world, _): self.assert_basic_checks(multi_world) class TestBaseLocationDependencies(SVTestBase): options = { - options.Mods.internal_name: all_mods, + options.Mods.internal_name: frozenset(options.Mods.valid_keys), options.ToolProgression.internal_name: options.ToolProgression.option_progressive, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized } @@ -50,13 +50,17 @@ class TestBaseLocationDependencies(SVTestBase): class TestBaseItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, options.SpecialOrderLocations.internal_name: options.SpecialOrderLocations.option_board_qi, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + Walnutsanity.internal_name: Walnutsanity.preset_all, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_are_added_to_the_pool(self): @@ -78,13 +82,15 @@ def test_all_progression_items_are_added_to_the_pool(self): class TestNoGingerIslandModItemGeneration(SVTestBase): options = { - options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, options.Shipsanity.internal_name: options.Shipsanity.option_everything, options.Chefsanity.internal_name: options.Chefsanity.option_all, options.Craftsanity.internal_name: options.Craftsanity.option_all, - options.Mods.internal_name: all_mods + options.Booksanity.internal_name: options.Booksanity.option_all, + options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) } def test_all_progression_items_except_island_are_added_to_the_pool(self): @@ -112,11 +118,13 @@ class TestModEntranceRando(SVTestCase): def test_mod_entrance_randomization(self): for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION), + (options.EntranceRandomization.option_buildings_without_house, RandomizationFlag.BUILDINGS), (options.EntranceRandomization.option_buildings, RandomizationFlag.BUILDINGS)]: sv_options = complete_options_with_default({ options.EntranceRandomization.internal_name: option, options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_false, - options.Mods.internal_name: all_mods + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + options.Mods.internal_name: frozenset(options.Mods.valid_keys) }) seed = get_seed() rand = random.Random(seed) @@ -143,11 +151,11 @@ def test_given_traps_when_generate_then_all_traps_in_pool(self): if value == "no_traps": continue - world_options = allsanity_options_without_mods() - world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods: "Magic"}) - multi_world = setup_solo_multiworld(world_options) - trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] - multiworld_items = [item.name for item in multi_world.get_items()] - for item in trap_items: - with self.subTest(f"Option: {value}, Item: {item}"): - self.assertIn(item, multiworld_items) + world_options = allsanity_no_mods_6_x_x() + world_options.update({options.TrapItems.internal_name: options.TrapItems.options[value], options.Mods.internal_name: "Magic"}) + with solo_multiworld(world_options) as (multi_world, _): + 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()] + for item in trap_items: + with self.subTest(f"Option: {value}, Item: {item}"): + self.assertIn(item, multiworld_items) diff --git a/worlds/stardew_valley/test/performance/TestPerformance.py b/worlds/stardew_valley/test/performance/TestPerformance.py index 0d453942c35f..b5ad0cae66c6 100644 --- a/worlds/stardew_valley/test/performance/TestPerformance.py +++ b/worlds/stardew_valley/test/performance/TestPerformance.py @@ -8,13 +8,10 @@ from BaseClasses import get_seed from Fill import distribute_items_restrictive, balance_multiworld_progression from worlds import AutoWorld -from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_4_x_x_options, \ - allsanity_4_x_x_options_without_mods, default_options, allsanity_options_without_mods, allsanity_options_with_mods +from .. import SVTestCase, minimal_locations_maximal_items, setup_multiworld, default_6_x_x, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x -assert default_4_x_x_options -assert allsanity_4_x_x_options_without_mods -assert default_options -assert allsanity_options_without_mods +assert default_6_x_x +assert allsanity_no_mods_6_x_x default_number_generations = 25 acceptable_deviation = 4 @@ -45,8 +42,6 @@ class SVPerformanceTestCase(SVTestCase): acceptable_time_per_player: float results: List[PerformanceResults] - # Set False to run tests that take long - skip_performance_tests: bool = True # Set False to not call the fill in the tests""" skip_fill: bool = True # Set True to print results as CSV""" @@ -54,10 +49,11 @@ class SVPerformanceTestCase(SVTestCase): @classmethod def setUpClass(cls) -> None: - super().setUpClass() performance_tests_key = "performance" - if performance_tests_key in os.environ: - cls.skip_performance_tests = not bool(os.environ[performance_tests_key]) + if performance_tests_key not in os.environ or os.environ[performance_tests_key] != "True": + raise unittest.SkipTest("Performance tests disabled") + + super().setUpClass() fill_tests_key = "fill" if fill_tests_key in os.environ: @@ -102,7 +98,7 @@ def performance_test_multiworld(self, options): acceptable_average_time = self.acceptable_time_per_player * amount_of_players total_time = 0 all_times = [] - seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [87876703343494157696] * self.number_generations + seeds = [get_seed() for _ in range(self.number_generations)] if not self.fixed_seed else [85635032403287291967] * self.number_generations for i, seed in enumerate(seeds): with self.subTest(f"Seed: {seed}"): @@ -139,38 +135,26 @@ def size_name(number_players): class TestDefaultOptions(SVPerformanceTestCase): acceptable_time_per_player = 2 - options = default_options() + options = default_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -182,33 +166,21 @@ class TestMinLocationMaxItems(SVPerformanceTestCase): results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -216,39 +188,27 @@ def test_10_player(self): class TestAllsanityWithoutMods(SVPerformanceTestCase): acceptable_time_per_player = 10 - options = allsanity_options_without_mods() + options = allsanity_no_mods_6_x_x() results = [] def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_5_player(self): - if self.skip_performance_tests: - return - number_players = 5 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @unittest.skip def test_10_player(self): - if self.skip_performance_tests: - return - number_players = 10 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) @@ -256,21 +216,17 @@ def test_10_player(self): class TestAllsanityWithMods(SVPerformanceTestCase): acceptable_time_per_player = 25 - options = allsanity_options_with_mods() + options = allsanity_mods_6_x_x() results = [] + @unittest.skip def test_solo(self): - if self.skip_performance_tests: - return - number_players = 1 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) + @unittest.skip def test_duo(self): - if self.skip_performance_tests: - return - number_players = 2 multiworld_options = [self.options] * number_players self.performance_test_multiworld(multiworld_options) diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py new file mode 100644 index 000000000000..fb62a456378a --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -0,0 +1,97 @@ +from ... import options +from ...test import SVTestBase + + +class TestArcadeMachinesLogic(SVTestBase): + options = { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + } + + def test_prairie_king(self): + self.assertFalse(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + + boots = self.create_item("JotPK: Progressive Boots") + gun = self.create_item("JotPK: Progressive Gun") + ammo = self.create_item("JotPK: Progressive Ammo") + life = self.create_item("JotPK: Extra Life") + drop = self.create_item("JotPK: Increased Drop Rate") + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(ammo) + self.remove(life) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertFalse(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 1")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 2")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach("JotPK World 3")(self.multiworld.state)) + self.assertTrue(self.world.logic.region.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state)) + self.remove(boots) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py new file mode 100644 index 000000000000..b00e4138a195 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -0,0 +1,62 @@ +from ...options import BuildingProgression, FarmType +from ...test import SVTestBase + + +class TestBuildingLogic(SVTestBase): + options = { + FarmType.internal_name: FarmType.option_standard, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + } + + def test_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("Coop Blueprint")(self.multiworld.state)) + + def test_big_coop_blueprint(self): + big_coop_blueprint_rule = self.world.logic.region.can_reach_location("Big Coop Blueprint") + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=False) + self.assertTrue(big_coop_blueprint_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}") + + def test_deluxe_coop_blueprint(self): + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.collect_lots_of_money() + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True) + self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item("Progressive Coop"), event=True) + self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) + + def test_big_shed_blueprint(self): + big_shed_rule = self.world.logic.region.can_reach_location("Big Shed Blueprint") + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.collect_lots_of_money() + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Can Construct Buildings"), event=True) + self.assertFalse(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") + + self.multiworld.state.collect(self.create_item("Progressive Shed"), event=True) + self.assertTrue(big_shed_rule(self.multiworld.state), + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}") diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py new file mode 100644 index 000000000000..25d4c70b2ab0 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -0,0 +1,66 @@ +from ... import options +from ...options import BundleRandomization +from ...strings.bundle_names import BundleName +from ...test import SVTestBase + + +class TestBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.default, + } + + def test_vault_2500g_bundle(self): + self.assertFalse(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + self.collect_lots_of_money() + self.assertTrue(self.world.logic.region.can_reach_location("2,500g Bundle")(self.multiworld.state)) + + +class TestRemixedBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_remixed, + options.BundlePrice: options.BundlePrice.default, + options.BundlePlando: frozenset({BundleName.sticky}) + } + + def test_sticky_bundle_has_grind_rules(self): + self.assertFalse(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + self.collect_all_the_money() + self.assertTrue(self.world.logic.region.can_reach_location("Sticky Bundle")(self.multiworld.state)) + + +class TestRaccoonBundlesLogic(SVTestBase): + options = { + options.BundleRandomization: BundleRandomization.option_vanilla, + options.BundlePrice: options.BundlePrice.option_normal, + options.Craftsanity: options.Craftsanity.option_all, + } + seed = 1234 # Magic seed that does what I want. Might need to get changed if we change the randomness behavior of raccoon bundles + + def test_raccoon_bundles_rely_on_previous_ones(self): + # The first raccoon bundle is a fishing one + raccoon_rule_1 = self.world.logic.region.can_reach_location("Raccoon Request 1") + + # The 3th raccoon bundle is a foraging one + raccoon_rule_3 = self.world.logic.region.can_reach_location("Raccoon Request 3") + self.collect("Progressive Raccoon", 6) + self.collect("Progressive Mine Elevator", 24) + self.collect("Mining Level", 12) + self.collect("Combat Level", 12) + self.collect("Progressive Axe", 4) + self.collect("Progressive Pickaxe", 4) + self.collect("Progressive Weapon", 4) + self.collect("Dehydrator Recipe") + self.collect("Mushroom Boxes") + self.collect("Progressive Fishing Rod", 4) + self.collect("Fishing Level", 10) + + self.assertFalse(raccoon_rule_1(self.multiworld.state)) + self.assertFalse(raccoon_rule_3(self.multiworld.state)) + + self.collect("Fish Smoker Recipe") + + self.assertTrue(raccoon_rule_1(self.multiworld.state)) + self.assertTrue(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py new file mode 100644 index 000000000000..81a91d1e7482 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -0,0 +1,83 @@ +from ... import options +from ...options import BuildingProgression, ExcludeGingerIsland, Chefsanity +from ...test import SVTestBase + + +class TestRecipeLearnLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Spring"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestRecipeReceiveLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.Cooksanity.internal_name: options.Cooksanity.option_all, + Chefsanity.internal_name: Chefsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_learn_qos_recipe(self): + location = "Cook Radish Salad" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + spring = self.create_item("Spring") + qos = self.create_item("The Queen of Sauce") + self.multiworld.state.collect(spring, event=False) + self.multiworld.state.collect(qos, event=False) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(spring) + self.multiworld.state.remove(qos) + + self.multiworld.state.collect(self.create_item("Radish Salad Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + def test_get_chefsanity_check_recipe(self): + location = "Radish Salad Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Spring"), event=False) + self.collect_lots_of_money() + self.assert_rule_false(rule, self.multiworld.state) + + seeds = self.create_item("Radish Seeds") + summer = self.create_item("Summer") + house = self.create_item("Progressive House") + self.multiworld.state.collect(seeds, event=False) + self.multiworld.state.collect(summer, event=False) + self.multiworld.state.collect(house, event=False) + self.assert_rule_false(rule, self.multiworld.state) + self.multiworld.state.remove(seeds) + self.multiworld.state.remove(summer) + self.multiworld.state.remove(house) + + self.multiworld.state.collect(self.create_item("The Queen of Sauce"), event=False) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py new file mode 100644 index 000000000000..59d41f6a63d6 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -0,0 +1,123 @@ +from ... import options +from ...data.craftable_data import all_crafting_recipes_by_name +from ...options import BuildingProgression, ExcludeGingerIsland, Craftsanity, SeasonRandomization +from ...test import SVTestBase + + +class TestCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + location = "Craft Marble Brazier" + rule = self.world.logic.region.can_reach_location(location) + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_learn_crafting_recipe(self): + location = "Marble Brazier Recipe" + rule = self.world.logic.region.can_reach_location(location) + self.assert_rule_false(rule, self.multiworld.state) + + self.collect_lots_of_money() + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_all, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestNoCraftsanityLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_disabled, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_recipe(self): + recipe = all_crafting_recipes_by_name["Wood Floor"] + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_true(rule, self.multiworld.state) + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + result = rule(self.multiworld.state) + self.assertFalse(result) + + self.collect([self.create_item("Progressive Season")] * 2) + self.assert_rule_true(rule, self.multiworld.state) + + +class TestNoCraftsanityWithFestivalsLogic(SVTestBase): + options = { + BuildingProgression.internal_name: BuildingProgression.option_progressive, + options.Cropsanity.internal_name: options.Cropsanity.option_enabled, + options.FestivalLocations.internal_name: options.FestivalLocations.option_easy, + Craftsanity.internal_name: Craftsanity.option_none, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, + } + + def test_can_craft_festival_recipe(self): + recipe = all_crafting_recipes_by_name["Jack-O-Lantern"] + self.multiworld.state.collect(self.create_item("Pumpkin Seeds"), event=False) + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.collect_lots_of_money() + rule = self.world.logic.crafting.can_craft(recipe) + self.assert_rule_false(rule, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.assert_rule_true(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestDonations.py b/worlds/stardew_valley/test/rules/TestDonations.py new file mode 100644 index 000000000000..84ceac50ff5a --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -0,0 +1,73 @@ +from ... import options +from ...locations import locations_by_tag, LocationTags, location_table +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...test import SVTestBase + + +class TestDonationLogicAll(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_all + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicRandomized(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_randomized + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + donation_locations = [location for location in self.get_real_locations() if + LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags] + + for donation in donation_locations: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in donation_locations: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +class TestDonationLogicMilestones(SVTestBase): + options = { + options.Museumsanity.internal_name: options.Museumsanity.option_milestones + } + + def test_cannot_make_any_donation_without_museum_access(self): + railroad_item = "Railroad Boulder Removed" + swap_museum_and_bathhouse(self.multiworld, self.player) + self.collect_all_except(railroad_item) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) + + +def swap_museum_and_bathhouse(multiworld, player): + museum_region = multiworld.get_region(Region.museum, player) + bathhouse_region = multiworld.get_region(Region.bathhouse_entrance, player) + museum_entrance = multiworld.get_entrance(Entrance.town_to_museum, player) + bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) + museum_entrance.connect(bathhouse_region) + bathhouse_entrance.connect(museum_region) diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py new file mode 100644 index 000000000000..43c5e55c7fca --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -0,0 +1,58 @@ +from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize +from ...test import SVTestBase + + +class TestFriendsanityDatingRules(SVTestBase): + options = { + SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 3 + } + + def test_earning_dating_heart_requires_dating(self): + self.collect_all_the_money() + self.multiworld.state.collect(self.create_item("Fall"), event=False) + self.multiworld.state.collect(self.create_item("Beach Bridge"), event=False) + self.multiworld.state.collect(self.create_item("Progressive House"), event=False) + for i in range(3): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Weapon"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Barn"), event=False) + for i in range(10): + self.multiworld.state.collect(self.create_item("Foraging Level"), event=False) + self.multiworld.state.collect(self.create_item("Farming Level"), event=False) + self.multiworld.state.collect(self.create_item("Mining Level"), event=False) + self.multiworld.state.collect(self.create_item("Combat Level"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), event=False) + + npc = "Abigail" + heart_name = f"{npc} <3" + step = 3 + + self.assert_can_reach_heart_up_to(npc, 3, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 6, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 8, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 10, step) + self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.assert_can_reach_heart_up_to(npc, 14, step) + + def assert_can_reach_heart_up_to(self, npc: str, max_reachable: int, step: int): + prefix = "Friendsanity: " + suffix = " <3" + for i in range(1, max_reachable + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertTrue(can_reach, f"Should be able to earn relationship up to {i} hearts") + for i in range(max_reachable + 1, 14 + 1): + if i % step != 0 and i != 14: + continue + location = f"{prefix}{npc} {i}{suffix}" + can_reach = self.world.logic.region.can_reach_location(location)(self.multiworld.state) + self.assertFalse(can_reach, f"Should not be able to earn relationship up to {i} hearts") diff --git a/worlds/stardew_valley/test/rules/TestMuseum.py b/worlds/stardew_valley/test/rules/TestMuseum.py new file mode 100644 index 000000000000..35dad8f43ebc --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestMuseum.py @@ -0,0 +1,16 @@ +from collections import Counter + +from ...options import Museumsanity +from .. import SVTestBase + + +class TestMuseumMilestones(SVTestBase): + options = { + Museumsanity.internal_name: Museumsanity.option_milestones + } + + def test_50_milestone(self): + self.multiworld.state.prog_items = {1: Counter()} + + milestone_rule = self.world.logic.museum.can_find_museum_items(50) + self.assert_rule_false(milestone_rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py new file mode 100644 index 000000000000..378933b7d75d --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -0,0 +1,82 @@ +from ...locations import LocationTags, location_table +from ...options import BuildingProgression, Shipsanity +from ...test import SVTestBase + + +class TestShipsanityNone(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_none + } + + def test_no_shipsanity_locations(self): + for location in self.get_real_locations(): + self.assertFalse("Shipsanity" in location.name) + self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags) + + +class TestShipsanityCrops(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_crops + } + + def test_only_crop_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags) + + +class TestShipsanityFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_fish + } + + def test_only_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipment(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment + } + + def test_only_full_shipment_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags) + self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags) + + +class TestShipsanityFullShipmentWithFish(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_full_shipment_with_fish + } + + def test_only_full_shipment_and_fish_shipsanity_locations(self): + for location in self.get_real_locations(): + if LocationTags.SHIPSANITY in location_table[location.name].tags: + self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or + LocationTags.SHIPSANITY_FISH in location_table[location.name].tags) + + +class TestShipsanityEverything(SVTestBase): + options = { + Shipsanity.internal_name: Shipsanity.option_everything, + BuildingProgression.internal_name: BuildingProgression.option_progressive + } + + def test_all_shipsanity_locations_require_shipping_bin(self): + bin_name = "Shipping Bin" + self.collect_all_except(bin_name) + shipsanity_locations = [location for location in self.get_real_locations() if + LocationTags.SHIPSANITY in location_table[location.name].tags] + bin_item = self.create_item(bin_name) + for location in shipsanity_locations: + with self.subTest(location.name): + self.remove(bin_item) + self.assertFalse(self.world.logic.region.can_reach_location(location.name)(self.multiworld.state)) + self.multiworld.state.collect(bin_item, event=False) + shipsanity_rule = self.world.logic.region.can_reach_location(location.name) + self.assert_rule_true(shipsanity_rule, self.multiworld.state) + self.remove(bin_item) diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py new file mode 100644 index 000000000000..1c6874f31529 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -0,0 +1,40 @@ +from ... import HasProgressionPercent +from ...options import ToolProgression, SkillProgression, Mods +from ...strings.skill_names import all_skills +from ...test import SVTestBase + + +class TestVanillaSkillLogicSimplification(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_progressive, + } + + def test_skill_logic_has_level_only_uses_one_has_progression_percent(self): + rule = self.multiworld.worlds[1].logic.skill.has_level("Farming", 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) == HasProgressionPercent)) + + +class TestAllSkillsRequirePrevious(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + Mods.internal_name: frozenset(Mods.valid_keys), + } + + def test_all_skill_levels_require_previous_level(self): + for skill in all_skills: + self.collect_everything() + self.remove_by_name(f"{skill} Level") + for level in range(1, 11): + location_name = f"Level {level} {skill}" + with self.subTest(location_name): + can_reach = self.can_reach_location(location_name) + if level > 1: + self.assertFalse(can_reach) + self.collect(f"{skill} Level") + can_reach = self.can_reach_location(location_name) + self.assertTrue(can_reach) + self.multiworld.state = self.original_state.copy() + + + diff --git a/worlds/stardew_valley/test/rules/TestStateRules.py b/worlds/stardew_valley/test/rules/TestStateRules.py new file mode 100644 index 000000000000..4f53b9a7f536 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestStateRules.py @@ -0,0 +1,12 @@ +import unittest + +from BaseClasses import ItemClassification +from ...test import solo_multiworld + + +class TestHasProgressionPercent(unittest.TestCase): + def test_max_item_amount_is_full_collection(self): + # Not caching because it fails too often for some reason + with solo_multiworld(world_caching=False) as (multiworld, world): + progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification) + self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory diff --git a/worlds/stardew_valley/test/rules/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py new file mode 100644 index 000000000000..a1fb152812c8 --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -0,0 +1,141 @@ +from collections import Counter + +from .. import SVTestBase +from ... import Event, options +from ...options import ToolProgression, SeasonRandomization +from ...strings.entrance_names import Entrance +from ...strings.region_names import Region +from ...strings.tool_names import Tool, ToolMaterial + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + } + + def test_sturgeon(self): + self.multiworld.state.prog_items = {1: Counter()} + + sturgeon_rule = self.world.logic.has("Sturgeon") + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + summer = self.create_item("Summer") + self.multiworld.state.collect(summer, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_rod = self.create_item("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod, event=False) + self.multiworld.state.collect(fishing_rod, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + fishing_level = self.create_item("Fishing Level") + self.multiworld.state.collect(fishing_level, event=False) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.multiworld.state.collect(fishing_level, event=False) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(summer) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + winter = self.create_item("Winter") + self.multiworld.state.collect(winter, event=False) + self.assert_rule_true(sturgeon_rule, self.multiworld.state) + + self.remove(fishing_rod) + self.assert_rule_false(sturgeon_rule, self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.prog_items = {1: Counter()} + + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), event=False) + self.multiworld.state.collect(self.create_item("Summer"), event=False) + self.collect_lots_of_money() + + rule = self.world.logic.region.can_reach_location("Old Master Cannoli") + self.assert_rule_false(rule, self.multiworld.state) + + fall = self.create_item("Fall") + self.multiworld.state.collect(fall, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + tuesday = self.create_item("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + rare_seed = self.create_item("Rare Seed") + self.multiworld.state.collect(rare_seed, event=False) + self.assert_rule_true(rule, self.multiworld.state) + + self.remove(fall) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(tuesday) + + green_house = self.create_item("Greenhouse") + self.collect(self.create_item(Event.fall_farming)) + self.multiworld.state.collect(green_house, event=False) + self.assert_rule_false(rule, self.multiworld.state) + + friday = self.create_item("Traveling Merchant: Friday") + self.multiworld.state.collect(friday, event=False) + self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) + + self.remove(green_house) + self.remove(self.create_item(Event.fall_farming)) + self.assert_rule_false(rule, self.multiworld.state) + self.remove(friday) + + +class TestToolVanillaRequiresBlacksmith(SVTestBase): + options = { + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.ToolProgression: options.ToolProgression.option_vanilla, + } + seed = 4111845104987680262 + + # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. + + def test_cannot_get_any_tool_without_blacksmith_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + def test_cannot_get_fishing_rod_without_willy_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) + self.collect_all_except(railroad_item) + + for fishing_rod_level in [3, 4]: + self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + self.multiworld.state.collect(self.create_item(railroad_item), event=False) + + for fishing_rod_level in [3, 4]: + self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + +def place_region_at_entrance(multiworld, player, region, entrance): + region_to_place = multiworld.get_region(region, player) + entrance_to_place_region = multiworld.get_entrance(entrance, player) + + entrance_to_switch = region_to_place.entrances[0] + region_to_switch = entrance_to_place_region.connected_region + entrance_to_switch.connect(region_to_switch) + entrance_to_place_region.connect(region_to_place) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py new file mode 100644 index 000000000000..77887f8eca0c --- /dev/null +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -0,0 +1,75 @@ +from ... import options +from ...options import ToolProgression +from ...test import SVTestBase + + +class TestWeaponsLogic(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.create_item("Progressive House"), event=True) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.multiworld.state.collect(self.create_item("Bus Repair"), event=True) + self.multiworld.state.collect(self.create_item("Skull Key"), event=True) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 1) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) + self.GiveItemAndCheckReachableMine("Progressive Club", 1) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 2) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 2) + self.GiveItemAndCheckReachableMine("Progressive Club", 2) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 3) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 3) + self.GiveItemAndCheckReachableMine("Progressive Club", 3) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 4) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 4) + self.GiveItemAndCheckReachableMine("Progressive Club", 4) + + self.GiveItemAndCheckReachableMine("Progressive Sword", 5) + self.GiveItemAndCheckReachableMine("Progressive Dagger", 5) + self.GiveItemAndCheckReachableMine("Progressive Club", 5) + + def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.state.collect(item, event=True) + rule = self.world.logic.mine.can_mine_in_the_mines_floor_1_40() + if reachable_level > 0: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_41_80() + if reachable_level > 1: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_mines_floor_81_120() + if reachable_level > 2: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.mine.can_mine_in_the_skull_cavern() + if reachable_level > 3: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) + + rule = self.world.logic.ability.can_mine_perfectly_in_the_skull_cavern() + if reachable_level > 4: + self.assert_rule_true(rule, self.multiworld.state) + else: + self.assert_rule_false(rule, self.multiworld.state) diff --git a/worlds/stardew_valley/test/rules/__init__.py b/worlds/stardew_valley/test/rules/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/__init__.py b/worlds/stardew_valley/test/script/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/stardew_valley/test/script/benchmark_locations.py b/worlds/stardew_valley/test/script/benchmark_locations.py new file mode 100644 index 000000000000..04553e39968e --- /dev/null +++ b/worlds/stardew_valley/test/script/benchmark_locations.py @@ -0,0 +1,140 @@ +""" +Copy of the script in test/benchmark, adapted to Stardew Valley. + +Run with `python -m worlds.stardew_valley.test.script.benchmark_locations --options minimal_locations_maximal_items` +""" + +import argparse +import collections +import gc +import logging +import os +import sys +import time +import typing + +from BaseClasses import CollectionState, Location +from Utils import init_logging +from worlds.stardew_valley.stardew_rule.rule_explain import explain +from ... import test + + +def run_locations_benchmark(): + init_logging("Benchmark Runner") + logger = logging.getLogger("Benchmark") + + class BenchmarkRunner: + gen_steps: typing.Tuple[str, ...] = ( + "generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + rule_iterations: int = 100_000 + + @staticmethod + def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str: + return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top)) + + def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float: + with TimeIt(f"{test_location.game} {self.rule_iterations} " + f"runs of {test_location}.access_rule({state_name})", logger) as t: + for _ in range(self.rule_iterations): + test_location.access_rule(state) + # if time is taken to disentangle complex ref chains, + # this time should be attributed to the rule. + gc.collect() + return t.dif + + def main(self): + game = "Stardew Valley" + summary_data: typing.Dict[str, collections.Counter[str]] = { + "empty_state": collections.Counter(), + "all_state": collections.Counter(), + } + try: + parser = argparse.ArgumentParser() + parser.add_argument('--options', help="Define the option set to use, from the preset in test/__init__.py .", type=str, required=True) + parser.add_argument('--seed', help="Define the seed to use.", type=int, required=True) + parser.add_argument('--location', help="Define the specific location to benchmark.", type=str, default=None) + parser.add_argument('--state', help="Define the state in which the location will be benchmarked.", type=str, default=None) + args = parser.parse_args() + options_set = args.options + options = getattr(test, options_set)() + seed = args.seed + location = args.location + state = args.state + + multiworld = test.setup_solo_multiworld(options, seed) + gc.collect() + + if location: + locations = [multiworld.get_location(location, 1)] + else: + locations = sorted(multiworld.get_unfilled_locations()) + + all_state = multiworld.get_all_state(False) + for location in locations: + if state != "all_state": + time_taken = self.location_test(location, multiworld.state, "empty_state") + summary_data["empty_state"][location.name] = time_taken + + if state != "empty_state": + time_taken = self.location_test(location, all_state, "all_state") + summary_data["all_state"][location.name] = time_taken + + total_empty_state = sum(summary_data["empty_state"].values()) + total_all_state = sum(summary_data["all_state"].values()) + + logger.info(f"{game} took {total_empty_state / len(locations):.4f} " + f"seconds per location in empty_state and {total_all_state / len(locations):.4f} " + f"in all_state. (all times summed for {self.rule_iterations} runs.)") + logger.info(f"Top times in empty_state:\n" + f"{self.format_times_from_counter(summary_data['empty_state'])}") + logger.info(f"Top times in all_state:\n" + f"{self.format_times_from_counter(summary_data['all_state'])}") + + if len(locations) == 1: + logger.info(str(explain(locations[0].access_rule, all_state, False))) + + except Exception as e: + logger.exception(e) + + runner = BenchmarkRunner() + runner.main() + + +class TimeIt: + def __init__(self, name: str, time_logger=None): + self.name = name + self.logger = time_logger + self.timer = None + self.end_timer = None + + def __enter__(self): + self.timer = time.perf_counter() + return self + + @property + def dif(self): + return self.end_timer - self.timer + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self.end_timer: + self.end_timer = time.perf_counter() + if self.logger: + self.logger.info(f"{self.dif:.4f} seconds in {self.name}.") + + +def change_home(): + """Allow scripts to run from "this" folder.""" + old_home = os.path.dirname(__file__) + sys.path.remove(old_home) + new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) + os.chdir(new_home) + sys.path.append(new_home) + # fallback to local import + sys.path.append(old_home) + + from Utils import local_path + local_path.cached_path = new_home + + +if __name__ == "__main__": + run_locations_benchmark() diff --git a/worlds/stardew_valley/test/stability/StabilityOutputScript.py b/worlds/stardew_valley/test/stability/StabilityOutputScript.py index baf17dde8423..4b31011d9f49 100644 --- a/worlds/stardew_valley/test/stability/StabilityOutputScript.py +++ b/worlds/stardew_valley/test/stability/StabilityOutputScript.py @@ -1,7 +1,7 @@ import argparse import json -from ...test import setup_solo_multiworld, allsanity_options_with_mods +from ...test import setup_solo_multiworld, allsanity_mods_6_x_x if __name__ == "__main__": parser = argparse.ArgumentParser() @@ -11,7 +11,7 @@ seed = args.seed multi_world = setup_solo_multiworld( - allsanity_options_with_mods(), + allsanity_mods_6_x_x(), seed=seed ) diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 48cd663cb301..aaa8b331846a 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -2,10 +2,14 @@ import re import subprocess import sys +import unittest from BaseClasses import get_seed from .. import SVTestCase +# There seems to be 4 bytes that appear at random at the end of the output, breaking the json... I don't know where they came from. +BYTES_TO_REMOVE = 4 + # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") # Python 3.10.2\r\n @@ -18,16 +22,16 @@ class TestGenerationIsStable(SVTestCase): def test_all_locations_and_items_are_the_same_between_two_generations(self): if self.skip_long_tests: - return + raise unittest.SkipTest("Long tests disabled") # seed = get_seed(33778671150797368040) # troubleshooting seed - seed = get_seed() + seed = get_seed(74716545478307145559) output_a = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) output_b = subprocess.check_output([sys.executable, '-m', 'worlds.stardew_valley.test.stability.StabilityOutputScript', '--seed', str(seed)]) - result_a = json.loads(output_a) - result_b = json.loads(output_b) + result_a = json.loads(output_a[:-BYTES_TO_REMOVE]) + result_b = json.loads(output_b[:-BYTES_TO_REMOVE]) for i, ((room_a, bundles_a), (room_b, bundles_b)) in enumerate(zip(result_a["bundles"].items(), result_b["bundles"].items())): self.assertEqual(room_a, room_b, f"Bundle rooms at index {i} is different between both executions. Seed={seed}") From c96c554dfa68a1b1818031a1049961a25e205621 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:51:10 +0200 Subject: [PATCH 13/78] Tests, WebHost: add tests for host_room and minor cleanup (#3619) * Tests, WebHost: move out setUp and fix typing in api_generate Also fixes a typo and changes client to be per-test rather than a ClassVar * Tests, WebHost: add tests for display_log endpoint * Tests, WebHost: add tests for host_room endpoint * Tests, WebHost: enable Flask DEBUG mode for tests This provides the actual error if a test raised an exception on the server. * Tests, WebHost: use user_path for logs This is what custom_server does now. * Tests, WebHost: avoid triggering security scans --- test/webhost/__init__.py | 36 ++++++ test/webhost/test_api_generate.py | 25 +--- test/webhost/test_host_room.py | 192 ++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 20 deletions(-) create mode 100644 test/webhost/test_host_room.py diff --git a/test/webhost/__init__.py b/test/webhost/__init__.py index e69de29bb2d1..2eb340722a3a 100644 --- a/test/webhost/__init__.py +++ b/test/webhost/__init__.py @@ -0,0 +1,36 @@ +import unittest +import typing +from uuid import uuid4 + +from flask import Flask +from flask.testing import FlaskClient + + +class TestBase(unittest.TestCase): + app: typing.ClassVar[Flask] + client: FlaskClient + + @classmethod + def setUpClass(cls) -> None: + from WebHostLib import app as raw_app + from WebHost import get_app + + raw_app.config["PONY"] = { + "provider": "sqlite", + "filename": ":memory:", + "create_db": True, + } + raw_app.config.update({ + "TESTING": True, + "DEBUG": True, + }) + try: + cls.app = get_app() + except AssertionError as e: + # since we only have 1 global app object, this might fail, but luckily all tests use the same config + if "register_blueprint" not in e.args[0]: + raise + cls.app = raw_app + + def setUp(self) -> None: + self.client = self.app.test_client() diff --git a/test/webhost/test_api_generate.py b/test/webhost/test_api_generate.py index bd78edd9c700..591c61d74880 100644 --- a/test/webhost/test_api_generate.py +++ b/test/webhost/test_api_generate.py @@ -1,31 +1,16 @@ import io -import unittest import json import yaml +from . import TestBase -class TestDocs(unittest.TestCase): - @classmethod - def setUpClass(cls) -> None: - from WebHostLib import app as raw_app - from WebHost import get_app - raw_app.config["PONY"] = { - "provider": "sqlite", - "filename": ":memory:", - "create_db": True, - } - raw_app.config.update({ - "TESTING": True, - }) - app = get_app() - - cls.client = app.test_client() - def test_correct_error_empty_request(self): +class TestAPIGenerate(TestBase): + def test_correct_error_empty_request(self) -> None: response = self.client.post("/api/generate") self.assertIn("No options found. Expected file attachment or json weights.", response.text) - def test_generation_queued_weights(self): + def test_generation_queued_weights(self) -> None: options = { "Tester1": { @@ -43,7 +28,7 @@ def test_generation_queued_weights(self): self.assertTrue(json_data["text"].startswith("Generation of seed ")) self.assertTrue(json_data["text"].endswith(" started successfully.")) - def test_generation_queued_file(self): + def test_generation_queued_file(self) -> None: options = { "game": "Archipelago", "name": "Tester", diff --git a/test/webhost/test_host_room.py b/test/webhost/test_host_room.py new file mode 100644 index 000000000000..e9dae41dd06f --- /dev/null +++ b/test/webhost/test_host_room.py @@ -0,0 +1,192 @@ +import os +from uuid import UUID, uuid4, uuid5 + +from flask import url_for + +from . import TestBase + + +class TestHostFakeRoom(TestBase): + room_id: UUID + log_filename: str + + def setUp(self) -> None: + from pony.orm import db_session + from Utils import user_path + from WebHostLib.models import Room, Seed + + super().setUp() + + with self.client.session_transaction() as session: + session["_id"] = uuid4() + with db_session: + # create an empty seed and a room from it + seed = Seed(multidata=b"", owner=session["_id"]) + room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) + self.room_id = room.id + self.log_filename = user_path("logs", f"{self.room_id}.txt") + + def tearDown(self) -> None: + from pony.orm import db_session, select + from WebHostLib.models import Command, Room + + with db_session: + for command in select(command for command in Command if command.room.id == self.room_id): # type: ignore + command.delete() + room: Room = Room.get(id=self.room_id) + room.seed.delete() + room.delete() + + try: + os.unlink(self.log_filename) + except FileNotFoundError: + pass + + def test_display_log_missing_full(self) -> None: + """ + Verify that we get a 200 response even if log is missing. + This is required to not get an error for fetch. + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 200) + + def test_display_log_missing_range(self) -> None: + """ + Verify that we get a full response for missing log even if we asked for range. + This is required for the JS logic to differentiate between log update and log error message. + """ + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 200) + + def test_display_log_denied(self) -> None: + """Verify that only the owner can see the log.""" + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 403) + + def test_display_log_missing_room(self) -> None: + """Verify log for missing room gives an error as opposed to missing log for existing room.""" + missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("display_log", room=missing_room_id)) + self.assertEqual(response.status_code, 404) + + def test_display_log_full(self) -> None: + """Verify full log response.""" + with open(self.log_filename, "w", encoding="utf-8") as f: + text = "x" * 200 + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.get_data(True), text) + + def test_display_log_range(self) -> None: + """Verify that Range header in request gives a range in response.""" + with open(self.log_filename, "w", encoding="utf-8") as f: + f.write(" " * 100) + text = "x" * 100 + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 206) + self.assertEqual(response.get_data(True), text) + + def test_display_log_range_bom(self) -> None: + """Verify that a BOM in the log file is skipped for range.""" + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + f.write(" " * 100) + text = "x" * 100 + f.write(text) + self.assertEqual(f.tell(), 203) # including BOM + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("display_log", room=self.room_id), headers={ + "Range": "bytes=100-" + }) + self.assertEqual(response.status_code, 206) + self.assertEqual(response.get_data(True), text) + + def test_host_room_missing(self) -> None: + """Verify that missing room gives a 404 response.""" + missing_room_id = uuid5(uuid4(), "") # rooms are always uuid4, so this can't exist + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("host_room", room=missing_room_id)) + self.assertEqual(response.status_code, 404) + + def test_host_room_own(self) -> None: + """Verify that own room gives the full output.""" + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + text = "* should be visible *" + f.write(text) + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.get(url_for("host_room", room=self.room_id)) + response_text = response.get_data(True) + self.assertEqual(response.status_code, 200) + self.assertIn("href=\"/seed/", response_text) + self.assertIn(text, response_text) + + def test_host_room_other(self) -> None: + """Verify that non-own room gives the reduced output.""" + from pony.orm import db_session + from WebHostLib.models import Room + + with db_session: + room: Room = Room.get(id=self.room_id) + room.last_port = 12345 + + with open(self.log_filename, "w", encoding="utf-8-sig") as f: + text = "* should not be visible *" + f.write(text) + + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.get(url_for("host_room", room=self.room_id)) + response_text = response.get_data(True) + self.assertEqual(response.status_code, 200) + self.assertNotIn("href=\"/seed/", response_text) + self.assertNotIn(text, response_text) + self.assertIn("/connect ", response_text) + self.assertIn(":12345", response_text) + + def test_host_room_own_post(self) -> None: + """Verify command from owner gets queued for the server and response is redirect.""" + from pony.orm import db_session, select + from WebHostLib.models import Command + + with self.app.app_context(), self.app.test_request_context(): + response = self.client.post(url_for("host_room", room=self.room_id), data={ + "cmd": "/help" + }) + self.assertEqual(response.status_code, 302, response.text)\ + + with db_session: + commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore + self.assertIn("/help", (command.commandtext for command in commands)) + + def test_host_room_other_post(self) -> None: + """Verify command from non-owner does not get queued for the server.""" + from pony.orm import db_session, select + from WebHostLib.models import Command + + other_client = self.app.test_client() + with self.app.app_context(), self.app.test_request_context(): + response = other_client.post(url_for("host_room", room=self.room_id), data={ + "cmd": "/help" + }) + self.assertLess(response.status_code, 500) + + with db_session: + commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore + self.assertNotIn("/help", (command.commandtext for command in commands)) From 8c861390668fa02282dd5a67e24a149a65318f79 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:15:29 -0400 Subject: [PATCH 14/78] ALTTP: Bombable Wall to Crystaroller Room Logic (#3627) --- worlds/alttp/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 171c82f9b226..67684a6f3ced 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -488,7 +488,7 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location('Turtle Rock - Roller Room - Right', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(multiworld.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player))) set_rule(multiworld.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) - set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10)) + set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player)) set_rule(multiworld.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player) or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player)) set_rule(multiworld.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player)) From 1e3a4b6db5acff8e82bdecdc79773f2923de919a Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Wed, 10 Jul 2024 23:11:47 -0700 Subject: [PATCH 15/78] Zillion: more rooms added to map_gen option (#3634) --- worlds/zillion/client.py | 5 +++++ worlds/zillion/gen_data.py | 7 +++++++ worlds/zillion/requirements.txt | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py index be32028463c7..09d0565e1c5e 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -347,6 +347,11 @@ def process_from_game_queue(self) -> None: "operations": [{"operation": "replace", "value": doors_b64}] } async_start(self.send_msgs([payload])) + elif isinstance(event_from_game, events.MapEventFromGame): + row = event_from_game.map_index // 8 + col = event_from_game.map_index % 8 + room_name = f"({chr(row + 64)}-{col + 1})" + logger.info(f"You are at {room_name}") else: logger.warning(f"WARNING: unhandled event from game {event_from_game}") diff --git a/worlds/zillion/gen_data.py b/worlds/zillion/gen_data.py index aa24ff8961b3..13cbee9ced20 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -28,6 +28,13 @@ def to_json(self) -> str: def from_json(gen_data_str: str) -> "GenData": """ the reverse of `to_json` """ from_json = json.loads(gen_data_str) + + # backwards compatibility for seeds generated before new map_gen options + room_gen = from_json["zz_game"]["options"].get("room_gen", None) + if room_gen is not None: + from_json["zz_game"]["options"]["map_gen"] = {False: "none", True: "rooms"}.get(room_gen, "none") + del from_json["zz_game"]["options"]["room_gen"] + return GenData( from_json["multi_items"], ZzGame.from_jsonable(from_json["zz_game"]), diff --git a/worlds/zillion/requirements.txt b/worlds/zillion/requirements.txt index b4f554902f48..d6b01ac107ae 100644 --- a/worlds/zillion/requirements.txt +++ b/worlds/zillion/requirements.txt @@ -1,2 +1,2 @@ -zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@4a2fec0aa1c529df866e510cdfcf6dca4d53679b#0.8.0 +zilliandomizer @ git+https://github.com/beauxq/zilliandomizer@33045067f626266850f91c8045b9d3a9f52d02b0#0.9.0 typing-extensions>=4.7, <5 From eaec41d8854ef53bf626a1dce4f81a87cd867500 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 11 Jul 2024 16:44:29 -0400 Subject: [PATCH 16/78] TUNIC: Fix event region for Quarry fuse (#3635) --- worlds/tunic/er_scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 8689a51b7601..0bd8c5e80681 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -67,7 +67,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: "Eastern Vault West Fuses": "Eastern Vault Fortress", "Eastern Vault East Fuse": "Eastern Vault Fortress", "Quarry Connector Fuse": "Quarry Connector", - "Quarry Fuse": "Quarry", + "Quarry Fuse": "Quarry Entry", "Ziggurat Fuse": "Rooted Ziggurat Lower Back", "West Garden Fuse": "West Garden", "Library Fuse": "Library Lab", From 187f9dac9425b916f2f60cd673f1acf31390fc69 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:56:27 +0200 Subject: [PATCH 17/78] customserver: preemtively run GC before starting room (#3637) GC seems to be lazy. --- WebHostLib/customserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 9f70165b61e5..50c316f3b750 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -325,6 +325,7 @@ def _done(self, task: asyncio.Future): def run(self): while 1: next_room = rooms_to_run.get(block=True, timeout=None) + gc.collect(0) task = asyncio.run_coroutine_threadsafe(start_room(next_room), loop) self._tasks.append(task) task.add_done_callback(self._done) From 948f50f35db1b13336f08b0e5740d900904404b0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:56:56 +0200 Subject: [PATCH 18/78] customserver: fix minor memory leak (#3636) Old code keeps ref to last started room's task and thus never fully cleans it up. --- WebHostLib/customserver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 50c316f3b750..ccffc40b384d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -330,6 +330,7 @@ def run(self): self._tasks.append(task) task.add_done_callback(self._done) logging.info(f"Starting room {next_room} on {name}.") + del task # delete reference to task object starter = Starter() starter.daemon = True From 48dc14421e125d20d651f8b0368b20b484250631 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 14 Jul 2024 05:05:50 -0700 Subject: [PATCH 19/78] Pokemon Emerald: Fix logic for coin case location (#3631) --- worlds/pokemon_emerald/rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index f93441baeac1..5b2aaa1ffcd0 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -558,6 +558,10 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_BASEMENT_KEY_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) ) + set_rule( + get_location("NPC_GIFT_RECEIVED_COIN_CASE"), + lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) + ) # Route 117 set_rule( @@ -1638,10 +1642,6 @@ def get_location(location: str): get_location("NPC_GIFT_GOT_TM_THUNDERBOLT_FROM_WATTSON"), lambda state: state.has("EVENT_DEFEAT_NORMAN", world.player) and state.has("EVENT_TURN_OFF_GENERATOR", world.player) ) - set_rule( - get_location("NPC_GIFT_RECEIVED_COIN_CASE"), - lambda state: state.has("EVENT_BUY_HARBOR_MAIL", world.player) - ) # Fallarbor Town set_rule( From 08a36ec223b1c7bc27cca3b766744fcbdd844326 Mon Sep 17 00:00:00 2001 From: dennisw100 <100dennisw@gmail.com> Date: Sun, 14 Jul 2024 14:11:52 +0200 Subject: [PATCH 20/78] Undertale: Fixed output location of the patched game in UndertaleClient.py (#3418) * Update UndertaleClient.py Fixed output location of the patched game Fixed the error that when the client is opened outside of the archipelago folder, the patched folder would be created in there which on windows ends up trying to create it in the system32 folder Bug Report: https://discord.com/channels/731205301247803413/1148330675452264499/1237412436382973962 * Undertale: removed unnecessary wrapping in UndertaleClient.py I did not know os.path.join was unnecessary in this case the more you know. --- UndertaleClient.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/UndertaleClient.py b/UndertaleClient.py index 415d7e7f21a3..dfacee148abc 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ def _cmd_resync(self): def _cmd_patch(self): """Patch the game. Only use this command if /auto_patch fails.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) self.ctx.patch_game() self.output("Patched.") @@ -43,7 +43,7 @@ def _cmd_savepath(self, directory: str): def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): """Patch the game automatically.""" if isinstance(self.ctx, UndertaleContext): - os.makedirs(name=os.path.join(os.getcwd(), "Undertale"), exist_ok=True) + os.makedirs(name=Utils.user_path("Undertale"), exist_ok=True) tempInstall = steaminstall if not os.path.isfile(os.path.join(tempInstall, "data.win")): tempInstall = None @@ -62,7 +62,7 @@ def _cmd_auto_patch(self, steaminstall: typing.Optional[str] = None): for file_name in os.listdir(tempInstall): if file_name != "steam_api.dll": shutil.copy(os.path.join(tempInstall, file_name), - os.path.join(os.getcwd(), "Undertale", file_name)) + Utils.user_path("Undertale", file_name)) self.ctx.patch_game() self.output("Patching successful!") @@ -111,12 +111,12 @@ def __init__(self, server_address, password): self.save_game_folder = os.path.expandvars(r"%localappdata%/UNDERTALE") def patch_game(self): - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "rb") as f: + with open(Utils.user_path("Undertale", "data.win"), "rb") as f: patchedFile = bsdiff4.patch(f.read(), undertale.data_path("patch.bsdiff")) - with open(os.path.join(os.getcwd(), "Undertale", "data.win"), "wb") as f: + with open(Utils.user_path("Undertale", "data.win"), "wb") as f: f.write(patchedFile) - os.makedirs(name=os.path.join(os.getcwd(), "Undertale", "Custom Sprites"), exist_ok=True) - with open(os.path.expandvars(os.path.join(os.getcwd(), "Undertale", "Custom Sprites", + os.makedirs(name=Utils.user_path("Undertale", "Custom Sprites"), exist_ok=True) + with open(os.path.expandvars(Utils.user_path("Undertale", "Custom Sprites", "Which Character.txt")), "w") as f: f.writelines(["// Put the folder name of the sprites you want to play as, make sure it is the only " "line other than this one.\n", "frisk"]) From e76d32e9089efa0554034a0c2c9049f03026267e Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Sun, 14 Jul 2024 08:17:05 -0400 Subject: [PATCH 21/78] AHIT: Fix act shuffle test fail (#3522) --- worlds/ahit/Regions.py | 3 +++ worlds/ahit/Rules.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 0ba0f5b9a5a4..c6aeaa357799 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -292,6 +292,9 @@ # See above comment "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations", "Murder on the Owl Express"], + + # was causing test failures + "Time Rift - Balcony": ["Alpine Free Roam"], } diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 71f74b17d7ed..b0513c433289 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -863,6 +863,8 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]): if world.is_dlc1(): for entrance in regions["Time Rift - Balcony"].entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, world.multiworld.get_entrance("The Arctic Cruise - Finale", + world.player).connected_region, entrance) for entrance in regions["Time Rift - Deep Sea"].entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) @@ -939,6 +941,7 @@ def set_default_rift_rules(world: "HatInTimeWorld"): if world.is_dlc1(): for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: add_rule(entrance, lambda state: can_clear_required_act(state, world, "The Arctic Cruise - Finale")) + reg_act_connection(world, "Rock the Boat", entrance.name) for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) From 925e02dca72420d94971db28c2c15c4ddd3092c2 Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Mon, 15 Jul 2024 06:09:02 -0700 Subject: [PATCH 22/78] Raft: Move to new Options API (#3587) --- worlds/raft/Options.py | 31 ++++++++++++++------------ worlds/raft/Rules.py | 4 ++-- worlds/raft/__init__.py | 48 ++++++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index 696d4dbab477..efe460b50353 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,4 +1,5 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink +from dataclasses import dataclass +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink, PerGameCommonOptions class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -47,6 +48,8 @@ class IslandFrequencyLocations(Choice): option_progressive = 4 option_anywhere = 5 default = 2 + def is_filling_frequencies_in_world(self): + return self.value <= self.option_random_on_island_random_order class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -76,16 +79,16 @@ class PaddleboardMode(Toggle): """Sets later story islands to be in logic without an Engine or Steering Wheel. May require lots of paddling.""" display_name = "Paddleboard Mode" -raft_options = { - "minimum_resource_pack_amount": MinimumResourcePackAmount, - "maximum_resource_pack_amount": MaximumResourcePackAmount, - "duplicate_items": DuplicateItems, - "filler_item_types": FillerItemTypes, - "island_frequency_locations": IslandFrequencyLocations, - "island_generation_distance": IslandGenerationDistance, - "expensive_research": ExpensiveResearch, - "progressive_items": ProgressiveItems, - "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode, - "death_link": DeathLink -} +@dataclass +class RaftOptions(PerGameCommonOptions): + minimum_resource_pack_amount: MinimumResourcePackAmount + maximum_resource_pack_amount: MaximumResourcePackAmount + duplicate_items: DuplicateItems + filler_item_types: FillerItemTypes + island_frequency_locations: IslandFrequencyLocations + island_generation_distance: IslandGenerationDistance + expensive_research: ExpensiveResearch + progressive_items: ProgressiveItems + big_island_early_crafting: BigIslandEarlyCrafting + paddleboard_mode: PaddleboardMode + death_link: DeathLink diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index e84068a6f584..b6bd49c187cd 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -5,10 +5,10 @@ class RaftLogic(LogicMixin): def raft_paddleboard_mode_enabled(self, player): - return self.multiworld.paddleboard_mode[player].value + return bool(self.multiworld.worlds[player].options.paddleboard_mode) def raft_big_islands_available(self, player): - return self.multiworld.big_island_early_crafting[player].value or self.raft_can_access_radio_tower(player) + return bool(self.multiworld.worlds[player].options.big_island_early_crafting) or self.raft_can_access_radio_tower(player) def raft_can_smelt_items(self, player): return self.has("Smelter", player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index e96cd4471268..71d5d1c7e44b 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -6,7 +6,7 @@ from .Regions import create_regions, getConnectionName from .Rules import set_rules -from .Options import raft_options +from .Options import RaftOptions from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, Tutorial from ..AutoWorld import World, WebWorld @@ -37,16 +37,17 @@ class RaftWorld(World): lastItemId = max(filter(lambda val: val is not None, item_name_to_id.values())) location_name_to_id = locations_lookup_name_to_id - option_definitions = raft_options + options_dataclass = RaftOptions + options: RaftOptions required_client_version = (0, 3, 4) def create_items(self): - minRPSpecified = self.multiworld.minimum_resource_pack_amount[self.player].value - maxRPSpecified = self.multiworld.maximum_resource_pack_amount[self.player].value + minRPSpecified = self.options.minimum_resource_pack_amount.value + maxRPSpecified = self.options.maximum_resource_pack_amount.value minimumResourcePackAmount = min(minRPSpecified, maxRPSpecified) maximumResourcePackAmount = max(minRPSpecified, maxRPSpecified) - isFillingFrequencies = self.multiworld.island_frequency_locations[self.player].value <= 3 + isFillingFrequencies = self.options.island_frequency_locations.is_filling_frequencies_in_world() # Generate item pool pool = [] frequencyItems = [] @@ -64,20 +65,20 @@ def create_items(self): extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.multiworld.filler_item_types[self.player].value != 1): # Use resource packs + if (self.options.filler_item_types != self.options.filler_item_types.option_duplicates): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.multiworld.filler_item_types[self.player].value != 0: # Use duplicate items + if self.options.filler_item_types != self.options.filler_item_types.option_resource_packs: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.multiworld.island_frequency_locations[self.player].value != 5: # Not completely random locations + if self.options.island_frequency_locations != self.options.island_frequency_locations.option_anywhere: # Not completely random locations # If we let frequencies stay in with progressive-frequencies, the progressive-frequency item # will be included 7 times. This is a massive flood of progressive-frequency items, so we # instead add progressive-frequency as its own item a smaller amount of times to prevent # flooding the duplicate item pool with them. - if self.multiworld.island_frequency_locations[self.player].value == 4: + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive: for _ in range(2): # Progressives are not in item_pool, need to create faux item for duplicate item pool # This can still be filtered out later by duplicate_items setting @@ -86,9 +87,9 @@ def create_items(self): dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.multiworld.duplicate_items[self.player].value == 0): # Progression only + if (self.options.duplicate_items == self.options.duplicate_items.option_progression): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.multiworld.duplicate_items[self.player].value == 1): # Non-progression only + elif (self.options.duplicate_items == self.options.duplicate_items.option_non_progression): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -115,14 +116,14 @@ def create_regions(self): create_regions(self.multiworld, self.player) def get_pre_fill_items(self): - if self.multiworld.island_frequency_locations[self.player] in [0, 1, 2, 3]: + if self.options.island_frequency_locations.is_filling_frequencies_in_world(): return [loc.item for loc in self.multiworld.get_filled_locations()] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.multiworld.island_frequency_locations[self.player].value == 4) - or (not isFrequency and self.multiworld.progressive_items[self.player].value)) + shouldUseProgressive = bool((isFrequency and self.options.island_frequency_locations == self.options.island_frequency_locations.option_progressive) + or (not isFrequency and self.options.progressive_items)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] return self.create_item(name) @@ -152,7 +153,7 @@ def collect_item(self, state, item, remove=False): return super(RaftWorld, self).collect_item(state, item, remove) def pre_fill(self): - if self.multiworld.island_frequency_locations[self.player] == 0: # Vanilla + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_vanilla: self.setLocationItem("Radio Tower Frequency to Vasagatan", "Vasagatan Frequency") self.setLocationItem("Vasagatan Frequency to Balboa", "Balboa Island Frequency") self.setLocationItem("Relay Station quest", "Caravan Island Frequency") @@ -160,7 +161,7 @@ def pre_fill(self): self.setLocationItem("Tangaroa Frequency to Varuna Point", "Varuna Point Frequency") self.setLocationItem("Varuna Point Frequency to Temperance", "Temperance Frequency") self.setLocationItem("Temperance Frequency to Utopia", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] == 1: # Random on island + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island: self.setLocationItemFromRegion("RadioTower", "Vasagatan Frequency") self.setLocationItemFromRegion("Vasagatan", "Balboa Island Frequency") self.setLocationItemFromRegion("BalboaIsland", "Caravan Island Frequency") @@ -168,7 +169,10 @@ def pre_fill(self): self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") self.setLocationItemFromRegion("Temperance", "Utopia Frequency") - elif self.multiworld.island_frequency_locations[self.player] in [2, 3]: + elif self.options.island_frequency_locations in [ + self.options.island_frequency_locations.option_random_island_order, + self.options.island_frequency_locations.option_random_on_island_random_order + ]: locationToFrequencyItemMap = { "Vasagatan": "Vasagatan Frequency", "BalboaIsland": "Balboa Island Frequency", @@ -196,9 +200,9 @@ def pre_fill(self): else: currentLocation = availableLocationList[0] # Utopia (only one left in list) availableLocationList.remove(currentLocation) - if self.multiworld.island_frequency_locations[self.player] == 2: # Random island order + if self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_island_order: self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) - elif self.multiworld.island_frequency_locations[self.player] == 3: # Random on island random order + elif self.options.island_frequency_locations == self.options.island_frequency_locations.option_random_on_island_random_order: self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) previousLocation = currentLocation @@ -215,9 +219,9 @@ def setLocationItemFromRegion(self, region: str, itemName: str): def fill_slot_data(self): return { - "IslandGenerationDistance": self.multiworld.island_generation_distance[self.player].value, - "ExpensiveResearch": bool(self.multiworld.expensive_research[self.player].value), - "DeathLink": bool(self.multiworld.death_link[self.player].value) + "IslandGenerationDistance": self.options.island_generation_distance.value, + "ExpensiveResearch": bool(self.options.expensive_research), + "DeathLink": bool(self.options.death_link) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): From 7b39b23f73d2d13ac2d859ad546308f216041693 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 17 Jul 2024 22:33:51 +0200 Subject: [PATCH 23/78] Subnautica: increase minimum client version (#3657) --- worlds/subnautica/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 856117469e55..58d8fa543a6d 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -44,7 +44,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations options_dataclass = options.SubnauticaOptions options: options.SubnauticaOptions - required_client_version = (0, 4, 1) + required_client_version = (0, 5, 0) creatures_to_scan: List[str] From 4d1507cd0e1a7cc24b7564b5dfee84209c76a9d0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 18 Jul 2024 00:49:59 +0200 Subject: [PATCH 24/78] Core: Update cx_freeze to 7.2.0 and freeze it (#3648) supersedes ArchipelagoMW/Archipelago#3405 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85c0f9f7ff13..cb4d1a7511b6 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it try: - requirement = 'cx-Freeze==7.0.0' + requirement = 'cx-Freeze==7.2.0' import pkg_resources try: pkg_resources.require(requirement) From e33a9991ef381b27ed09a075925254b3f7620527 Mon Sep 17 00:00:00 2001 From: gurglemurgle5 <95941332+gurglemurgle5@users.noreply.github.com> Date: Fri, 19 Jul 2024 01:37:59 -0500 Subject: [PATCH 25/78] CommonClient: Escape markup sent in chat messages (#3659) * escape markup in uncolored text * Fix comment to allign with style guide Fixes the comment so it follows the style guide, along with making it better explain the code. * Make more concise --- kvui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kvui.py b/kvui.py index 500203a8818f..1409e2dc0d45 100644 --- a/kvui.py +++ b/kvui.py @@ -836,6 +836,10 @@ def _handle_color(self, node: JSONMessagePart): return self._handle_text(node) def _handle_text(self, node: JSONMessagePart): + # All other text goes through _handle_color, and we don't want to escape markup twice, + # or mess up text that already has intentional markup applied to it + if node.get("type", "text") == "text": + node["text"] = escape_markup(node["text"]) for ref in node.get("refs", []): node["text"] = f"[ref={self.ref_count}|{ref}]{node['text']}[/ref]" self.ref_count += 1 From 34e7748f23083d26b126a52ca447afe3d93ff4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 20 Jul 2024 15:24:24 -0400 Subject: [PATCH 26/78] Stardew Valley: Make sure number of month in time logic is a int to improve performance by ~20% (#3665) * make sure number of month is actually a int * improve rule explain like in pr * remove redundant if in can_complete_bundle * assert number is int so cache is not bloated --- worlds/stardew_valley/logic/bundle_logic.py | 6 ++-- worlds/stardew_valley/logic/museum_logic.py | 2 +- worlds/stardew_valley/logic/time_logic.py | 2 ++ worlds/stardew_valley/stardew_rule/base.py | 2 +- .../stardew_rule/rule_explain.py | 33 +++++++++++++++++-- worlds/stardew_valley/stardew_rule/state.py | 2 +- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 4ca5fd81fc76..98fda1c73c7d 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -27,8 +27,8 @@ def __init__(self, *args, **kwargs): self.bundle = BundleLogic(*args, **kwargs) -class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, SkillLogicMixin, -QuestLogicMixin]]): +class BundleLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMixin, RegionLogicMixin, MoneyLogicMixin, QualityLogicMixin, FishingLogicMixin, +SkillLogicMixin, QuestLogicMixin]]): # Should be cached def can_complete_bundle(self, bundle: Bundle) -> StardewRule: item_rules = [] @@ -45,7 +45,7 @@ def can_complete_bundle(self, bundle: Bundle) -> StardewRule: qualities.append(bundle_item.quality) quality_rules = self.get_quality_rules(qualities) item_rules = self.logic.has_n(*item_rules, count=bundle.number_required) - time_rule = True_() if time_to_grind <= 0 else self.logic.time.has_lived_months(time_to_grind) + time_rule = self.logic.time.has_lived_months(time_to_grind) return can_speak_junimo & item_rules & quality_rules & time_rule def get_quality_rules(self, qualities: List[str]) -> StardewRule: diff --git a/worlds/stardew_valley/logic/museum_logic.py b/worlds/stardew_valley/logic/museum_logic.py index 4ba5364f5524..36ba62b31fcb 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -41,7 +41,7 @@ def can_find_museum_item(self, item: MuseumItem) -> StardewRule: else: geodes_rule = False_() # monster_rule = self.can_farm_monster(item.monsters) - time_needed_to_grind = (20 - item.difficulty) / 2 + time_needed_to_grind = int((20 - item.difficulty) // 2) time_rule = self.logic.time.has_lived_months(time_needed_to_grind) pan_rule = False_() if item.item_name == Mineral.earth_crystal or item.item_name == Mineral.fire_quartz or item.item_name == Mineral.frozen_tear: diff --git a/worlds/stardew_valley/logic/time_logic.py b/worlds/stardew_valley/logic/time_logic.py index 94e0e277c86c..2ba76579ff45 100644 --- a/worlds/stardew_valley/logic/time_logic.py +++ b/worlds/stardew_valley/logic/time_logic.py @@ -26,8 +26,10 @@ class TimeLogic(BaseLogic[Union[TimeLogicMixin, HasLogicMixin]]): @cache_self1 def has_lived_months(self, number: int) -> StardewRule: + assert isinstance(number, int), "Can't have lived a fraction of a month. Use // instead of / when dividing." if number <= 0: return self.logic.true_ + number = min(number, MAX_MONTHS) return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT) diff --git a/worlds/stardew_valley/stardew_rule/base.py b/worlds/stardew_valley/stardew_rule/base.py index 576cd36851fb..3e6eb327ea99 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -431,7 +431,7 @@ def rules_count(self): return len(self.rules) def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" + return f"Received {self.count} [{', '.join(f'{value}x {repr(rule)}' for rule, value in self.counter.items())}]" @dataclass(frozen=True) diff --git a/worlds/stardew_valley/stardew_rule/rule_explain.py b/worlds/stardew_valley/stardew_rule/rule_explain.py index 61a88ceb6996..a9767c7b72d5 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -34,7 +34,7 @@ def __str__(self, depth=0): if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__str__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__str__(depth + 1) if i.result is not self.expected else i.summary(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @@ -42,7 +42,7 @@ def __repr__(self, depth=0): if not self.sub_rules: return self.summary(depth) - return self.summary(depth) + "\n" + "\n".join(RuleExplanation.__repr__(i, depth + 1) + return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1) for i in sorted(self.explained_sub_rules, key=lambda x: x.result)) @cached_property @@ -61,6 +61,33 @@ def explained_sub_rules(self) -> List[RuleExplanation]: return [_explain(i, self.state, self.expected, self.explored_rules_key) for i in self.sub_rules] +@dataclass +class CountSubRuleExplanation(RuleExplanation): + count: int = 1 + + @staticmethod + def from_explanation(expl: RuleExplanation, count: int) -> CountSubRuleExplanation: + return CountSubRuleExplanation(expl.rule, expl.state, expl.expected, expl.sub_rules, expl.explored_rules_key, expl.current_rule_explored, count) + + def summary(self, depth=0) -> str: + summary = " " * depth + f"{self.count}x {str(self.rule)} -> {self.result}" + if self.current_rule_explored: + summary += " [Already explained]" + return summary + + +@dataclass +class CountExplanation(RuleExplanation): + rule: Count + + @cached_property + def explained_sub_rules(self) -> List[RuleExplanation]: + return [ + CountSubRuleExplanation.from_explanation(_explain(rule, self.state, self.expected, self.explored_rules_key), count) + for rule, count in self.rule.counter.items() + ] + + def explain(rule: CollectionRule, state: CollectionState, expected: bool = True) -> RuleExplanation: if isinstance(rule, StardewRule): return _explain(rule, state, expected, explored_spots=set()) @@ -80,7 +107,7 @@ def _(rule: AggregatingStardewRule, state: CollectionState, expected: bool, expl @_explain.register def _(rule: Count, state: CollectionState, expected: bool, explored_spots: Set[Tuple[str, str]]) -> RuleExplanation: - return RuleExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) + return CountExplanation(rule, state, expected, rule.rules, explored_rules_key=explored_spots) @_explain.register diff --git a/worlds/stardew_valley/stardew_rule/state.py b/worlds/stardew_valley/stardew_rule/state.py index cf0996a63bbc..5f5e61b3d4e5 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -122,4 +122,4 @@ def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRul return self, self(state) def __repr__(self): - return f"Received {self.percent}% progression items." + return f"Received {self.percent}% progression items" From 7039b17bf6e00735b5698e149c5cbce0df729e2b Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 21 Jul 2024 18:12:11 -0500 Subject: [PATCH 27/78] CommonClient: fix bug when using Connect button without a disconnect (#3609) * makes the kivy connect button do the same username forgetting that /connect does to fix an issue where losing connection would make you unable to connect to a different server * extract duplicate code --- kvui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kvui.py b/kvui.py index 1409e2dc0d45..a63d636960a7 100644 --- a/kvui.py +++ b/kvui.py @@ -595,8 +595,8 @@ def command_button_action(self, button): "!help for server commands.") def connect_button_action(self, button): + self.ctx.username = None if self.ctx.server: - self.ctx.username = None async_start(self.ctx.disconnect()) else: async_start(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", ""))) From d7d45654290f0c04e9f601c097d8c1d160a18908 Mon Sep 17 00:00:00 2001 From: Rensen3 <127029481+Rensen3@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:27:10 +0200 Subject: [PATCH 28/78] YGO06: fixes non-deterministic bug by changing sets to lists (#3674) --- worlds/yugioh06/boosterpacks.py | 186 +++++++++++++++--------------- worlds/yugioh06/structure_deck.py | 50 ++++---- 2 files changed, 120 insertions(+), 116 deletions(-) diff --git a/worlds/yugioh06/boosterpacks.py b/worlds/yugioh06/boosterpacks.py index f6f4ec7732c3..645977d28def 100644 --- a/worlds/yugioh06/boosterpacks.py +++ b/worlds/yugioh06/boosterpacks.py @@ -1,13 +1,13 @@ -from typing import Dict, Set +from typing import Dict, List -booster_contents: Dict[str, Set[str]] = { - "LEGEND OF B.E.W.D.": { +booster_contents: Dict[str, List[str]] = { + "LEGEND OF B.E.W.D.": [ "Exodia", "Dark Magician", "Polymerization", "Skull Servant" - }, - "METAL RAIDERS": { + ], + "METAL RAIDERS": [ "Petit Moth", "Cocoon of Evolution", "Time Wizard", @@ -30,8 +30,8 @@ "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ "Senju of the Thousand Hands", "Sonic Bird", "Mystical Space Typhoon" - }, - "LABYRINTH OF NIGHTMARE": { + ], + "LABYRINTH OF NIGHTMARE": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -119,8 +119,8 @@ "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ "Spell Reproduction", "Contract with the Abyss", "Dark Master - Zorc" - }, - "INVASION OF CHAOS": { + ], + "INVASION OF CHAOS": [ "Ojama Delta Hurricane", "Ojama Yellow", "Ojama Black", @@ -241,8 +241,8 @@ "Cursed Seal of the Forbidden Spell", "Stray Lambs", "Manju of the Ten Thousand Hands" - }, - "ANCIENT SANCTUARY": { + ], + "ANCIENT SANCTUARY": [ "Monster Gate", "Wall of Revealing Light", "Mystik Wok", @@ -255,8 +255,8 @@ "King of the Swamp", "Enemy Controller", "Enchanting Fitting Room" - }, - "SOUL OF THE DUELIST": { + ], + "SOUL OF THE DUELIST": [ "Ninja Grandmaster Sasuke", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -272,8 +272,8 @@ "Level Up!", "Howling Insect", "Mobius the Frost Monarch" - }, - "RISE OF DESTINY": { + ], + "RISE OF DESTINY": [ "Homunculus the Alchemic Being", "Thestalos the Firestorm Monarch", "Roc from the Valley of Haze", @@ -283,8 +283,8 @@ "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ "Elemental Hero Thunder Giant", "Aussa the Earth Charmer", "Brain Control" - }, - "CYBERNETIC REVOLUTION": { + ], + "CYBERNETIC REVOLUTION": [ "Power Bond", "Cyber Dragon", "Cyber Twin Dragon", @@ -322,8 +322,8 @@ "Miracle Fusion", "Elemental Hero Bubbleman", "Jerry Beans Man" - }, - "ELEMENTAL ENERGY": { + ], + "ELEMENTAL ENERGY": [ "V-Tiger Jet", "W-Wing Catapult", "VW-Tiger Catapult", @@ -344,8 +344,8 @@ "Elemental Hero Bladedge", "Pot of Avarice", "B.E.S. Tetran" - }, - "SHADOW OF INFINITY": { + ], + "SHADOW OF INFINITY": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -357,8 +357,8 @@ "Gokipon", "Demise, King of Armageddon", "Anteatereatingant" - }, - "GAME GIFT COLLECTION": { + ], + "GAME GIFT COLLECTION": [ "Ritual", "Valkyrion the Magna Warrior", "Alpha the Magnet Warrior", @@ -383,8 +383,8 @@ "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ "Curse of Vampire", "Elemental Hero Flame Wingman", "Magician of Black Chaos" - }, - "Fairy Collection": { + ], + "Fairy Collection": [ "Silpheed", "Dunames Dark Witch", "Hysteric Fairy", @@ -416,8 +416,8 @@ "Asura Priest", "Manju of the Ten Thousand Hands", "Senju of the Thousand Hands" - }, - "Dragon Collection": { + ], + "Dragon Collection": [ "Victory D.", "Chaos Emperor Dragon - Envoy of the End", "Kaiser Glider", @@ -434,16 +434,16 @@ "Troop Dragon", "Horus the Black Flame Dragon LV4", "Pitch-Dark Dragon" - }, - "Warrior Collection A": { + ], + "Warrior Collection A": [ "Gate Guardian", "Gearfried the Iron Knight", "Dimensional Warrior", "Command Knight", "The Last Warrior from Another Planet", "Dream Clown" - }, - "Warrior Collection B": { + ], + "Warrior Collection B": [ "Don Zaloog", "Dark Scorpion - Chick the Yellow", "Dark Scorpion - Meanae the Thorn", @@ -467,8 +467,8 @@ "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ "Spear Cretin", "Versago the Destroyer", "Toon Summoned Skull" - }, - "Fiend Collection B": { + ], + "Fiend Collection B": [ "Raviel, Lord of Phantasms", "Yata-Garasu", "Helpoemer", @@ -505,15 +505,15 @@ "Jowls of Dark Demise", "D. D. Trainer", "Earthbound Spirit" - }, - "Machine Collection A": { + ], + "Machine Collection A": [ "Cyber-Stein", "Mechanicalchaser", "Jinzo", "UFO Turtle", "Cyber-Tech Alligator" - }, - "Machine Collection B": { + ], + "Machine Collection B": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -531,8 +531,8 @@ "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ "Injection Fairy Lily", "Cosmo Queen", "Magician of Black Chaos" - }, - "Spellcaster Collection B": { + ], + "Spellcaster Collection B": [ "Jowgen the Spiritualist", "Tsukuyomi", "Manticore of Darkness", @@ -574,8 +574,8 @@ "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ "Des Lacooda", "Wandering Mummy", "Royal Keeper" - }, - "Special Monsters A": { + ], + "Special Monsters A": [ "X-Head Cannon", "Y-Dragon Head", "Z-Metal Tank", @@ -626,8 +626,8 @@ "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ "Level Up!", "Ultimate Insect Lv3", "Ultimate Insect Lv5" - }, - "Reverse Collection": { + ], + "Reverse Collection": [ "Magical Merchant", "Castle of Dark Illusions", "Magician of Faith", @@ -675,8 +675,8 @@ "Spear Cretin", "Nobleman of Crossout", "Aussa the Earth Charmer" - }, - "LP Recovery Collection": { + ], + "LP Recovery Collection": [ "Mystik Wok", "Poison of the Old Man", "Hysteric Fairy", @@ -691,8 +691,8 @@ "Elemental Hero Steam Healer", "Fushi No Tori", "Emergency Provisions" - }, - "Special Summon Collection A": { + ], + "Special Summon Collection A": [ "Perfectly Ultimate Great Moth", "Dark Sage", "Polymerization", @@ -726,8 +726,8 @@ "Morphing Jar #2", "Spear Cretin", "Dark Magic Curtain" - }, - "Special Summon Collection B": { + ], + "Special Summon Collection B": [ "Monster Gate", "Chaos Emperor Dragon - Envoy of the End", "Ojama Trio", @@ -756,8 +756,8 @@ "Tribute Doll", "Enchanting Fitting Room", "Stray Lambs" - }, - "Special Summon Collection C": { + ], + "Special Summon Collection C": [ "Hamon, Lord of Striking Thunder", "Raviel, Lord of Phantasms", "Uria, Lord of Searing Flames", @@ -782,13 +782,13 @@ "Ultimate Insect Lv5", "Rescue Cat", "Anteatereatingant" - }, - "Equipment Collection": { + ], + "Equipment Collection": [ "Megamorph", "Cestus of Dagla", "United We Stand" - }, - "Continuous Spell/Trap A": { + ], + "Continuous Spell/Trap A": [ "Destiny Board", "Spirit Message 'I'", "Spirit Message 'N'", @@ -801,8 +801,8 @@ "Solemn Wishes", "Embodiment of Apophis", "Toon World" - }, - "Continuous Spell/Trap B": { + ], + "Continuous Spell/Trap B": [ "Hamon, Lord of Striking Thunder", "Uria, Lord of Searing Flames", "Wave-Motion Cannon", @@ -815,8 +815,8 @@ "Skull Zoma", "Pitch-Black Power Stone", "Metal Reflect Slime" - }, - "Quick/Counter Collection": { + ], + "Quick/Counter Collection": [ "Mystik Wok", "Poison of the Old Man", "Scapegoat", @@ -841,8 +841,8 @@ "Book of Moon", "Serial Spell", "Mystical Space Typhoon" - }, - "Direct Damage Collection": { + ], + "Direct Damage Collection": [ "Hamon, Lord of Striking Thunder", "Chaos Emperor Dragon - Envoy of the End", "Dark Snake Syndrome", @@ -868,8 +868,8 @@ "Jowls of Dark Demise", "Stealth Bird", "Elemental Hero Bladedge", - }, - "Direct Attack Collection": { + ], + "Direct Attack Collection": [ "Victory D.", "Dark Scorpion Combination", "Spirit Reaper", @@ -880,8 +880,8 @@ "Toon Mermaid", "Toon Summoned Skull", "Toon Dark Magician Girl" - }, - "Monster Destroy Collection": { + ], + "Monster Destroy Collection": [ "Hamon, Lord of Striking Thunder", "Inferno", "Ninja Grandmaster Sasuke", @@ -912,12 +912,12 @@ "Offerings to the Doomed", "Divine Wrath", "Dream Clown" - }, + ], } def get_booster_locations(booster: str) -> Dict[str, str]: return { f"{booster} {i}": content - for i, content in enumerate(booster_contents[booster]) + for i, content in enumerate(booster_contents[booster], 1) } diff --git a/worlds/yugioh06/structure_deck.py b/worlds/yugioh06/structure_deck.py index d58223f2e216..3559e7c5153e 100644 --- a/worlds/yugioh06/structure_deck.py +++ b/worlds/yugioh06/structure_deck.py @@ -1,7 +1,7 @@ -from typing import Dict, Set +from typing import Dict, List -structure_contents: Dict[str, Set] = { - "dragons_roar": { +structure_contents: Dict[str, List[str]] = { + "dragons_roar": [ "Luster Dragon", "Armed Dragon LV3", "Armed Dragon LV5", @@ -14,9 +14,9 @@ "Stamping Destruction", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "zombie_madness": { + "Mystical Space Typhoon" + ], + "zombie_madness": [ "Pyramid Turtle", "Regenerating Mummy", "Ryu Kokki", @@ -26,9 +26,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "blazing_destruction": { + "Mystical Space Typhoon" + ], + "blazing_destruction": [ "Inferno", "Solar Flare Dragon", "UFO Turtle", @@ -38,9 +38,9 @@ "Level Limit - Area B", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "fury_from_the_deep": { + "Mystical Space Typhoon" + ], + "fury_from_the_deep": [ "Mother Grizzly", "Water Beaters", "Gravity Bind", @@ -48,9 +48,9 @@ "Mobius the Frost Monarch", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "warriors_triumph": { + "Mystical Space Typhoon" + ], + "warriors_triumph": [ "Gearfried the Iron Knight", "D.D. Warrior Lady", "Marauding Captain", @@ -60,9 +60,9 @@ "Reload", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "spellcasters_judgement": { + "Mystical Space Typhoon" + ], + "spellcasters_judgement": [ "Dark Magician", "Apprentice Magician", "Breaker the Magical Warrior", @@ -70,14 +70,18 @@ "Skilled Dark Magician", "Tsukuyomi", "Magical Dimension", - "Mage PowerSpell-Counter Cards", + "Mage Power", + "Spell-Counter Cards", "Heavy Storm", "Dust Tornado", - "Mystical Space Typhoon", - }, - "none": {}, + "Mystical Space Typhoon" + ], + "none": [], } def get_deck_content_locations(deck: str) -> Dict[str, str]: - return {f"{deck} {i}": content for i, content in enumerate(structure_contents[deck])} + return { + f"{deck} {i}": content + for i, content in enumerate(structure_contents[deck], 1) + } From 12f1ef873c2442ae923fd0585c2252c6033c1075 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:47:46 -0500 Subject: [PATCH 29/78] A Short Hike: Fix Boat Rental purchase being incorrectly calculated (#3639) --- worlds/shorthike/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py index 319ad8f20e1b..657035a03011 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ class LocationInfo(TypedDict): {"name": "Boat Rental", "id": base_id + 55, "inGameId": "DadDeer[0]", - "needsShovel": False, "purchase": True, + "needsShovel": False, "purchase": 100, "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, {"name": "Boat Challenge Reward", "id": base_id + 56, From 48a0fb05a2e4d5da7727f576e0b54725950b8ee3 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 02:52:44 +0300 Subject: [PATCH 30/78] Stardew Valley: Removed Stardrop Tea from Full Shipment (#3655) --- worlds/stardew_valley/data/locations.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index bb2ed2e2ce1f..0c5a12fb573b 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2221,7 +2221,7 @@ id,region,name,tags,mod_name 3817,Shipping,Shipsanity: Raisins,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3818,Shipping,Shipsanity: Dried Fruit,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3819,Shipping,Shipsanity: Dried Mushrooms,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", -3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", +3820,Shipping,Shipsanity: Stardrop Tea,"SHIPSANITY", 3821,Shipping,Shipsanity: Prize Ticket,"SHIPSANITY", 3822,Shipping,Shipsanity: Treasure Totem,"SHIPSANITY,REQUIRES_MASTERIES", 3823,Shipping,Shipsanity: Challenge Bait,"SHIPSANITY,REQUIRES_MASTERIES", From e59bec36ec792937d2916f0256c3655cc27a39fa Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 22 Jul 2024 09:32:40 +0300 Subject: [PATCH 31/78] Stardew Valley: Add gourmand frog rules for completing his tasks sequentially (#3652) --- worlds/stardew_valley/rules.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index c30d04c8a6f2..62a5cc52181b 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -482,8 +482,10 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) & logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south))) MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & logic.region.can_reach(Region.island_west)) - MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west)) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon")) + MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & + logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) From f7989780fa023d7c8be2ad7ed956db9a52483af9 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:17:34 -0600 Subject: [PATCH 32/78] Bomb Rush Cyberfunk: Fix final graffiti location being unobtainable (#3669) --- worlds/bomb_rush_cyberfunk/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/bomb_rush_cyberfunk/Locations.py b/worlds/bomb_rush_cyberfunk/Locations.py index 863e2ad020c0..7ea959019067 100644 --- a/worlds/bomb_rush_cyberfunk/Locations.py +++ b/worlds/bomb_rush_cyberfunk/Locations.py @@ -762,7 +762,7 @@ class EventDict(TypedDict): 'game_id': "graf385"}, {'name': "Tagged 389 Graffiti Spots", 'stage': Stages.Misc, - 'game_id': "graf379"}, + 'game_id': "graf389"}, ] From c12d3dd6ade3b0d2ad65095e2b7058741376ad91 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 23 Jul 2024 01:36:42 +0300 Subject: [PATCH 33/78] Stardew valley: Fix Queen of Sauce Cookbook conditions (#3651) * - Extracted walnut logic to a Mixin so it can be used in content pack requirements * - Add 100 walnut requirements to the Queen of Sauce Cookbook * - Woops a file wasn't added to previous commits * - Make the queen of sauce cookbook a ginger island only thing, due to the walnut requirement * - Moved the book in the correct content pack * - Removed an empty class that I'm not sure where it came from --- worlds/stardew_valley/__init__.py | 2 +- .../content/vanilla/ginger_island.py | 6 +- .../content/vanilla/pelican_town.py | 3 - worlds/stardew_valley/data/locations.csv | 4 +- worlds/stardew_valley/data/requirement.py | 5 + worlds/stardew_valley/logic/logic.py | 116 +-------------- .../stardew_valley/logic/requirement_logic.py | 9 +- worlds/stardew_valley/logic/walnut_logic.py | 135 ++++++++++++++++++ worlds/stardew_valley/rules.py | 24 ++-- 9 files changed, 171 insertions(+), 133 deletions(-) create mode 100644 worlds/stardew_valley/logic/walnut_logic.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 07235ad2983a..1aba9af7ab56 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -255,7 +255,7 @@ def setup_victory(self): Event.victory) elif self.options.goal == Goal.option_greatest_walnut_hunter: self.create_event_location(location_table[GoalName.greatest_walnut_hunter], - self.logic.has_walnut(130), + self.logic.walnut.has_walnut(130), Event.victory) elif self.options.goal == Goal.option_protector_of_the_valley: self.create_event_location(location_table[GoalName.protector_of_the_valley], diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py index d824deff3903..2fbcb032799e 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -3,6 +3,7 @@ from ...data import villagers_data, fish_data from ...data.game_item import ItemTag, Tag from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource +from ...data.requirement import WalnutRequirement from ...data.shop import ShopSource from ...strings.book_names import Book from ...strings.crop_names import Fruit, Vegetable @@ -10,7 +11,7 @@ from ...strings.forageable_names import Forageable, Mushroom from ...strings.fruit_tree_names import Sapling from ...strings.metal_names import Fossil, Mineral -from ...strings.region_names import Region +from ...strings.region_names import Region, LogicRegion from ...strings.season_names import Season from ...strings.seed_names import Seed @@ -62,6 +63,9 @@ def harvest_source_hook(self, content: StardewContent): Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), ShopSource(items_price=((10, Mineral.diamond),), shop_region=Region.volcano_dwarf_shop), ), + Book.queen_of_sauce_cookbook: ( + Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), + ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2, other_requirements=(WalnutRequirement(100),)),), # Worst book ever }, fishes=( diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 2c687eacbdde..917e8cca220a 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -290,9 +290,6 @@ Book.woodcutters_weekly: ( Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), ShopSource(money_price=5000, shop_region=LogicRegion.bookseller_1),), - Book.queen_of_sauce_cookbook: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=50000, shop_region=LogicRegion.bookseller_2),), # Worst book ever }, fishes=( fish_data.albacore, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 0c5a12fb573b..6e30d2b8c858 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2252,7 +2252,7 @@ id,region,name,tags,mod_name 3848,Shipping,Shipsanity: Way Of The Wind pt. 1,"SHIPSANITY", 3849,Shipping,Shipsanity: Mapping Cave Systems,"SHIPSANITY", 3850,Shipping,Shipsanity: Price Catalogue,"SHIPSANITY", -3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY", +3851,Shipping,Shipsanity: Queen Of Sauce Cookbook,"SHIPSANITY,GINGER_ISLAND", 3852,Shipping,Shipsanity: The Diamond Hunter,"SHIPSANITY,GINGER_ISLAND", 3853,Shipping,Shipsanity: Book of Mysteries,"SHIPSANITY", 3854,Shipping,Shipsanity: Animal Catalogue,"SHIPSANITY", @@ -2292,7 +2292,7 @@ id,region,name,tags,mod_name 4032,Farm,Read Book Of Stars,"BOOKSANITY,BOOKSANITY_SKILL", 4033,Farm,Read Combat Quarterly,"BOOKSANITY,BOOKSANITY_SKILL", 4034,Farm,Read Mining Monthly,"BOOKSANITY,BOOKSANITY_SKILL", -4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL", +4035,Farm,Read Queen Of Sauce Cookbook,"BOOKSANITY,BOOKSANITY_SKILL,GINGER_ISLAND", 4036,Farm,Read Stardew Valley Almanac,"BOOKSANITY,BOOKSANITY_SKILL", 4037,Farm,Read Woodcutter's Weekly,"BOOKSANITY,BOOKSANITY_SKILL", 4051,Museum,Read Tips on Farming,"BOOKSANITY,BOOKSANITY_LOST", diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 7e9466630fc3..4744f9dffdfe 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -29,3 +29,8 @@ class SeasonRequirement(Requirement): @dataclass(frozen=True) class YearRequirement(Requirement): year: int + + +@dataclass(frozen=True) +class WalnutRequirement(Requirement): + amount: int diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 74cdaf2374e1..fb0d938fbb1e 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -from functools import cached_property from typing import Collection, Callable from .ability_logic import AbilityLogicMixin @@ -43,6 +42,7 @@ from .tool_logic import ToolLogicMixin from .traveling_merchant_logic import TravelingMerchantLogicMixin from .wallet_logic import WalletLogicMixin +from .walnut_logic import WalnutLogicMixin from ..content.game_content import StardewContent from ..data.craftable_data import all_crafting_recipes from ..data.museum_data import all_museum_items @@ -50,16 +50,14 @@ from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.mod_logic import ModLogicMixin from ..mods.mod_data import ModNames -from ..options import SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, StardewValleyOptions, Walnutsanity +from ..options import ExcludeGingerIsland, FestivalLocations, StardewValleyOptions from ..stardew_rule import False_, True_, StardewRule from ..strings.animal_names import Animal from ..strings.animal_product_names import AnimalProduct -from ..strings.ap_names.ap_option_names import OptionName from ..strings.ap_names.community_upgrade_names import CommunityUpgrade -from ..strings.ap_names.event_names import Event from ..strings.artisan_good_names import ArtisanGood from ..strings.building_names import Building -from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds +from ..strings.craftable_names import Consumable, Ring, Fishing, Lighting, WildSeeds from ..strings.crop_names import Fruit, Vegetable from ..strings.currency_names import Currency from ..strings.decoration_names import Decoration @@ -96,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin, SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin, SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin, - RequirementLogicMixin, BookLogicMixin, GrindLogicMixin): + RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin): player: int options: StardewValleyOptions content: StardewContent @@ -461,32 +459,6 @@ def setup_events(self, register_event: Callable[[str, str, StardewRule], None]) def can_smelt(self, item: str) -> StardewRule: return self.has(Machine.furnace) & self.has(item) - @cached_property - def can_start_field_office(self) -> StardewRule: - field_office = self.region.can_reach(Region.field_office) - professor_snail = self.received("Open Professor Snail Cave") - return field_office & professor_snail - - def can_complete_large_animal_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) - return self.can_start_field_office & fossils - - def can_complete_snake_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) - return self.can_start_field_office & fossils - - def can_complete_frog_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_frog) - return self.can_start_field_office & fossils - - def can_complete_bat_collection(self) -> StardewRule: - fossils = self.has_all(Fossil.mummified_bat) - return self.can_start_field_office & fossils - - def can_complete_field_office(self) -> StardewRule: - return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ - self.can_complete_frog_collection() & self.can_complete_bat_collection() - def can_finish_grandpa_evaluation(self) -> StardewRule: # https://stardewvalleywiki.com/Grandpa rules_worth_a_point = [ @@ -566,86 +538,6 @@ def has_island_trader(self) -> StardewRule: return False_() return self.region.can_reach(Region.island_trader) - def has_walnut(self, number: int) -> StardewRule: - if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: - return False_() - if number <= 0: - return True_() - - if self.options.walnutsanity == Walnutsanity.preset_none: - return self.can_get_walnuts(number) - if self.options.walnutsanity == Walnutsanity.preset_all: - return self.has_received_walnuts(number) - puzzle_walnuts = 61 - bush_walnuts = 25 - dig_walnuts = 18 - repeatable_walnuts = 33 - total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts - walnuts_to_receive = 0 - walnuts_to_collect = number - if OptionName.walnutsanity_puzzles in self.options.walnutsanity: - puzzle_walnut_rate = puzzle_walnuts / total_walnuts - puzzle_walnuts_required = round(puzzle_walnut_rate * number) - walnuts_to_receive += puzzle_walnuts_required - walnuts_to_collect -= puzzle_walnuts_required - if OptionName.walnutsanity_bushes in self.options.walnutsanity: - bush_walnuts_rate = bush_walnuts / total_walnuts - bush_walnuts_required = round(bush_walnuts_rate * number) - walnuts_to_receive += bush_walnuts_required - walnuts_to_collect -= bush_walnuts_required - if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: - dig_walnuts_rate = dig_walnuts / total_walnuts - dig_walnuts_required = round(dig_walnuts_rate * number) - walnuts_to_receive += dig_walnuts_required - walnuts_to_collect -= dig_walnuts_required - if OptionName.walnutsanity_repeatables in self.options.walnutsanity: - repeatable_walnuts_rate = repeatable_walnuts / total_walnuts - repeatable_walnuts_required = round(repeatable_walnuts_rate * number) - walnuts_to_receive += repeatable_walnuts_required - walnuts_to_collect -= repeatable_walnuts_required - return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) - - def has_received_walnuts(self, number: int) -> StardewRule: - return self.received(Event.received_walnuts, number) - - def can_get_walnuts(self, number: int) -> StardewRule: - # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations - reach_south = self.region.can_reach(Region.island_south) - reach_north = self.region.can_reach(Region.island_north) - reach_west = self.region.can_reach(Region.island_west) - reach_hut = self.region.can_reach(Region.leo_hut) - reach_southeast = self.region.can_reach(Region.island_south_east) - reach_field_office = self.region.can_reach(Region.field_office) - reach_pirate_cove = self.region.can_reach(Region.pirate_cove) - reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) - reach_volcano_regions = [self.region.can_reach(Region.volcano), - self.region.can_reach(Region.volcano_secret_beach), - self.region.can_reach(Region.volcano_floor_5), - self.region.can_reach(Region.volcano_floor_10)] - reach_volcano = self.logic.or_(*reach_volcano_regions) - reach_all_volcano = self.logic.and_(*reach_volcano_regions) - reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] - reach_caves = self.logic.and_(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site), - self.region.can_reach(Region.gourmand_frog_cave), - self.region.can_reach(Region.colored_crystals_cave), - self.region.can_reach(Region.shipwreck), self.combat.has_slingshot) - reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, - reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) - if number <= 5: - return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) - if number <= 10: - return self.count(2, *reach_walnut_regions) - if number <= 15: - return self.count(3, *reach_walnut_regions) - if number <= 20: - return self.logic.and_(*reach_walnut_regions) - if number <= 50: - return reach_entire_island - gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) - return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \ - self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \ - self.can_complete_field_office() - def has_all_stardrops(self) -> StardewRule: other_rules = [] number_of_stardrops_to_receive = 0 diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 87d9ee021524..9356440ac6a8 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -9,8 +9,9 @@ from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin +from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -20,7 +21,7 @@ def __init__(self, *args, **kwargs): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -50,3 +51,7 @@ def _(self, requirement: SeasonRequirement): @meet_requirement.register def _(self, requirement: YearRequirement): return self.logic.time.has_year(requirement.year) + + @meet_requirement.register + def _(self, requirement: WalnutRequirement): + return self.logic.walnut.has_walnut(requirement.amount) diff --git a/worlds/stardew_valley/logic/walnut_logic.py b/worlds/stardew_valley/logic/walnut_logic.py new file mode 100644 index 000000000000..14fe1c339090 --- /dev/null +++ b/worlds/stardew_valley/logic/walnut_logic.py @@ -0,0 +1,135 @@ +from functools import cached_property +from typing import Union + +from .ability_logic import AbilityLogicMixin +from .base_logic import BaseLogic, BaseLogicMixin +from .combat_logic import CombatLogicMixin +from .has_logic import HasLogicMixin +from .received_logic import ReceivedLogicMixin +from .region_logic import RegionLogicMixin +from ..strings.ap_names.event_names import Event +from ..options import ExcludeGingerIsland, Walnutsanity +from ..stardew_rule import StardewRule, False_, True_ +from ..strings.ap_names.ap_option_names import OptionName +from ..strings.craftable_names import Furniture +from ..strings.crop_names import Fruit +from ..strings.metal_names import Mineral, Fossil +from ..strings.region_names import Region +from ..strings.seed_names import Seed + + +class WalnutLogicMixin(BaseLogicMixin): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.walnut = WalnutLogic(*args, **kwargs) + + +class WalnutLogic(BaseLogic[Union[WalnutLogicMixin, ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, CombatLogicMixin, + AbilityLogicMixin]]): + + def has_walnut(self, number: int) -> StardewRule: + if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true: + return False_() + if number <= 0: + return True_() + + if self.options.walnutsanity == Walnutsanity.preset_none: + return self.can_get_walnuts(number) + if self.options.walnutsanity == Walnutsanity.preset_all: + return self.has_received_walnuts(number) + puzzle_walnuts = 61 + bush_walnuts = 25 + dig_walnuts = 18 + repeatable_walnuts = 33 + total_walnuts = puzzle_walnuts + bush_walnuts + dig_walnuts + repeatable_walnuts + walnuts_to_receive = 0 + walnuts_to_collect = number + if OptionName.walnutsanity_puzzles in self.options.walnutsanity: + puzzle_walnut_rate = puzzle_walnuts / total_walnuts + puzzle_walnuts_required = round(puzzle_walnut_rate * number) + walnuts_to_receive += puzzle_walnuts_required + walnuts_to_collect -= puzzle_walnuts_required + if OptionName.walnutsanity_bushes in self.options.walnutsanity: + bush_walnuts_rate = bush_walnuts / total_walnuts + bush_walnuts_required = round(bush_walnuts_rate * number) + walnuts_to_receive += bush_walnuts_required + walnuts_to_collect -= bush_walnuts_required + if OptionName.walnutsanity_dig_spots in self.options.walnutsanity: + dig_walnuts_rate = dig_walnuts / total_walnuts + dig_walnuts_required = round(dig_walnuts_rate * number) + walnuts_to_receive += dig_walnuts_required + walnuts_to_collect -= dig_walnuts_required + if OptionName.walnutsanity_repeatables in self.options.walnutsanity: + repeatable_walnuts_rate = repeatable_walnuts / total_walnuts + repeatable_walnuts_required = round(repeatable_walnuts_rate * number) + walnuts_to_receive += repeatable_walnuts_required + walnuts_to_collect -= repeatable_walnuts_required + return self.has_received_walnuts(walnuts_to_receive) & self.can_get_walnuts(walnuts_to_collect) + + def has_received_walnuts(self, number: int) -> StardewRule: + return self.logic.received(Event.received_walnuts, number) + + def can_get_walnuts(self, number: int) -> StardewRule: + # https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations + reach_south = self.logic.region.can_reach(Region.island_south) + reach_north = self.logic.region.can_reach(Region.island_north) + reach_west = self.logic.region.can_reach(Region.island_west) + reach_hut = self.logic.region.can_reach(Region.leo_hut) + reach_southeast = self.logic.region.can_reach(Region.island_south_east) + reach_field_office = self.logic.region.can_reach(Region.field_office) + reach_pirate_cove = self.logic.region.can_reach(Region.pirate_cove) + reach_outside_areas = self.logic.and_(reach_south, reach_north, reach_west, reach_hut) + reach_volcano_regions = [self.logic.region.can_reach(Region.volcano), + self.logic.region.can_reach(Region.volcano_secret_beach), + self.logic.region.can_reach(Region.volcano_floor_5), + self.logic.region.can_reach(Region.volcano_floor_10)] + reach_volcano = self.logic.or_(*reach_volcano_regions) + reach_all_volcano = self.logic.and_(*reach_volcano_regions) + reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office] + reach_caves = self.logic.and_(self.logic.region.can_reach(Region.qi_walnut_room), self.logic.region.can_reach(Region.dig_site), + self.logic.region.can_reach(Region.gourmand_frog_cave), + self.logic.region.can_reach(Region.colored_crystals_cave), + self.logic.region.can_reach(Region.shipwreck), self.logic.combat.has_slingshot) + reach_entire_island = self.logic.and_(reach_outside_areas, reach_all_volcano, + reach_caves, reach_southeast, reach_field_office, reach_pirate_cove) + if number <= 5: + return self.logic.or_(reach_south, reach_north, reach_west, reach_volcano) + if number <= 10: + return self.logic.count(2, *reach_walnut_regions) + if number <= 15: + return self.logic.count(3, *reach_walnut_regions) + if number <= 20: + return self.logic.and_(*reach_walnut_regions) + if number <= 50: + return reach_entire_island + gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz) + return reach_entire_island & self.logic.has(Fruit.banana) & self.logic.has_all(*gems) & \ + self.logic.ability.can_mine_perfectly() & self.logic.ability.can_fish_perfectly() & \ + self.logic.has(Furniture.flute_block) & self.logic.has(Seed.melon) & self.logic.has(Seed.wheat) & \ + self.logic.has(Seed.garlic) & self.can_complete_field_office() + + @cached_property + def can_start_field_office(self) -> StardewRule: + field_office = self.logic.region.can_reach(Region.field_office) + professor_snail = self.logic.received("Open Professor Snail Cave") + return field_office & professor_snail + + def can_complete_large_animal_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.fossilized_leg, Fossil.fossilized_ribs, Fossil.fossilized_skull, Fossil.fossilized_spine, Fossil.fossilized_tail) + return self.can_start_field_office & fossils + + def can_complete_snake_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.snake_skull, Fossil.snake_vertebrae) + return self.can_start_field_office & fossils + + def can_complete_frog_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_frog) + return self.can_start_field_office & fossils + + def can_complete_bat_collection(self) -> StardewRule: + fossils = self.logic.has_all(Fossil.mummified_bat) + return self.can_start_field_office & fossils + + def can_complete_field_office(self) -> StardewRule: + return self.can_complete_large_animal_collection() & self.can_complete_snake_collection() & \ + self.can_complete_frog_collection() & self.can_complete_bat_collection() diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 62a5cc52181b..7c1fdbda3cf4 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -375,7 +375,7 @@ def set_ginger_island_rules(logic: StardewLogic, multiworld, player, world_optio MultiWorldRules.add_rule(multiworld.get_location("Open Professor Snail Cave", player), logic.has(Bomb.cherry_bomb)) MultiWorldRules.add_rule(multiworld.get_location("Complete Island Field Office", player), - logic.can_complete_field_office()) + logic.walnut.can_complete_field_office()) set_walnut_rules(logic, multiworld, player, world_options) @@ -432,10 +432,10 @@ def set_island_entrances_rules(logic: StardewLogic, multiworld, player, world_op def set_island_parrot_rules(logic: StardewLogic, multiworld, player): # Logic rules require more walnuts than in reality, to allow the player to spend them "wrong" - has_walnut = logic.has_walnut(5) - has_5_walnut = logic.has_walnut(15) - has_10_walnut = logic.has_walnut(40) - has_20_walnut = logic.has_walnut(60) + has_walnut = logic.walnut.has_walnut(5) + has_5_walnut = logic.walnut.has_walnut(15) + has_10_walnut = logic.walnut.has_walnut(40) + has_20_walnut = logic.walnut.has_walnut(60) MultiWorldRules.add_rule(multiworld.get_location("Leo's Parrot", player), has_walnut) MultiWorldRules.add_rule(multiworld.get_location("Island West Turtle", player), @@ -471,7 +471,7 @@ def set_walnut_rules(logic: StardewLogic, multiworld, player, world_options: Sta set_walnut_repeatable_rules(logic, multiworld, player, world_options) -def set_walnut_puzzle_rules(logic, multiworld, player, world_options): +def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_options): if OptionName.walnutsanity_puzzles not in world_options.walnutsanity: return @@ -487,12 +487,12 @@ def set_walnut_puzzle_rules(logic, multiworld, player, world_options): MultiWorldRules.add_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) & logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat")) MultiWorldRules.add_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium)) - MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.can_complete_large_animal_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.can_complete_snake_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.can_complete_frog_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.can_complete_bat_collection()) - MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.can_start_field_office) - MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection()) + MultiWorldRules.add_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office) + MultiWorldRules.add_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office) MultiWorldRules.add_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot) MultiWorldRules.add_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1)) MultiWorldRules.add_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block)) From b840c3fe1a609466c3b103a1972d5ec9d862df54 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Mon, 22 Jul 2024 18:43:41 -0400 Subject: [PATCH 34/78] TUNIC: Move 3 locations to Quarry Back (#3649) * Move 3 locations to Quarry Back * Change the non-er region too --- worlds/tunic/locations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 2d87140fe50f..09916228163d 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -208,15 +208,15 @@ class TunicLocationData(NamedTuple): "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"), - "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"), "Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"), - "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"), + "Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"), "Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"), From 9c2933f8033c8e8b9d9acdd341b043d7eca89d76 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 22 Jul 2024 18:45:49 -0400 Subject: [PATCH 35/78] Lingo: Fix Early Color Hallways painting in pilgrimages (#3645) --- worlds/lingo/data/LL1.yaml | 1 - worlds/lingo/data/generated.dat | Bin 136017 -> 136017 bytes worlds/lingo/regions.py | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 4d6771a7350d..970063d58542 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -3265,7 +3265,6 @@ door: Traveled Entrance Color Hallways: door: Color Hallways Entrance - warp: True panels: Achievement: id: Countdown Panels/Panel_traveled_traveled diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 6c8c925138aa5ac61edb22d194cfb8e9b9e4a492..065308628b9fc15f0d6001d6d8b30382ba8278d1 100644 GIT binary patch delta 131 zcmcb(jN{@mjtS}tCO}|joR*qqY;0j+WS(edkZ5X{Vqk7*kerreVUe`aJ None: RoomAndDoor("Pilgrim Antechamber", "Sun Painting"), EntranceType.PAINTING, False, world) if early_color_hallways: - connect_entrance(regions, regions["Starting Room"], regions["Outside The Undeterred"], "Early Color Hallways", + connect_entrance(regions, regions["Starting Room"], regions["Color Hallways"], "Early Color Hallways", None, EntranceType.PAINTING, False, world) if painting_shuffle: From 51883757367e7ee859442ea2f55d59cc565f1704 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Tue, 23 Jul 2024 02:34:47 -0400 Subject: [PATCH 36/78] Lingo: Add pilgrimage logic through Starting Room (#3654) * Lingo: Add pilgrimage logic through Starting Room * Added unit test * Reverse order of two doors in unit test * Remove print statements from TestPilgrimage * Update generated.dat --- worlds/lingo/data/LL1.yaml | 9 ++++++++ worlds/lingo/data/generated.dat | Bin 136017 -> 136277 bytes worlds/lingo/test/TestPilgrimage.py | 32 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 970063d58542..e12ca022973b 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -140,6 +140,15 @@ painting: True The Colorful: painting: True + Welcome Back Area: + room: Welcome Back Area + door: Shortcut to Starting Room + Second Room: + door: Main Door + Hidden Room: + door: Back Right Door + Rhyme Room (Looped Square): + door: Rhyme Room Entrance panels: HI: id: Entry Room/Panel_hi_hi diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 065308628b9fc15f0d6001d6d8b30382ba8278d1..3ed6cb24f7d289c38189932fe5ccf705bdaf0262 100644 GIT binary patch delta 30433 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 Date: Tue, 23 Jul 2024 03:04:24 -0400 Subject: [PATCH 37/78] TUNIC: Add setting to disable local spoiler to host yaml (#3661) * Add TunicSettings class for host yaml options * Update __init__.py * Update worlds/tunic/__init__.py Co-authored-by: Scipio Wright * Use self.settings * Remove unused import --------- Co-authored-by: Scipio Wright --- worlds/tunic/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index f63193e6aeef..9b28d1d451a8 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Any, Tuple, TypedDict +from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union from logging import warning from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names @@ -12,6 +12,14 @@ from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP +from settings import Group, Bool + + +class TunicSettings(Group): + class DisableLocalSpoiler(Bool): + """Disallows the TUNIC client from creating a local spoiler log.""" + + disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False class TunicWeb(WebWorld): @@ -57,6 +65,7 @@ class TunicWorld(World): options: TunicOptions options_dataclass = TunicOptions + settings: ClassVar[TunicSettings] item_name_groups = item_name_groups location_name_groups = location_name_groups @@ -373,7 +382,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], "Hexagon Quest Goal": self.options.hexagon_goal.value, - "Entrance Rando": self.tunic_portal_pairs + "Entrance Rando": self.tunic_portal_pairs, + "disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race), } for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): From dc50444edddeba1003c1fc4a76e5cac9fb2e257e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:13:41 +0200 Subject: [PATCH 38/78] The Witness: Small naming inconsistencies (#3618) --- worlds/witness/data/WitnessLogic.txt | 4 ++-- worlds/witness/data/WitnessLogicExpert.txt | 4 ++-- worlds/witness/data/WitnessLogicVanilla.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/worlds/witness/data/WitnessLogic.txt b/worlds/witness/data/WitnessLogic.txt index 272ed176e342..b7814626ada0 100644 --- a/worlds/witness/data/WitnessLogic.txt +++ b/worlds/witness/data/WitnessLogic.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True diff --git a/worlds/witness/data/WitnessLogicExpert.txt b/worlds/witness/data/WitnessLogicExpert.txt index 63e7e36c243e..1d1d010fde88 100644 --- a/worlds/witness/data/WitnessLogicExpert.txt +++ b/worlds/witness/data/WitnessLogicExpert.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Shapers & Dots & Full Dots 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers & Shapers & Dots & Full Dots @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Symmetry & Black/White Squares & Stars & Stars + Same Colored Symbol & Triangles & Colored Dots Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True diff --git a/worlds/witness/data/WitnessLogicVanilla.txt b/worlds/witness/data/WitnessLogicVanilla.txt index 1aa9655361f9..851031ab72f0 100644 --- a/worlds/witness/data/WitnessLogicVanilla.txt +++ b/worlds/witness/data/WitnessLogicVanilla.txt @@ -805,7 +805,7 @@ Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near 159334 - 0x036CE (Rotating Bridge CW EP) - 0x181F5 - True Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482 - Swamp Long Bridge - 0xFFD00 & 0xFFD02 - The Ocean - 0x09DB8: -158903 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True +159803 - 0xFFD02 (Beyond Rotating Bridge Reached Independently) - True - True 158328 - 0x09DB8 (Boat Spawn) - True - Boat 158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers 158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers @@ -1088,7 +1088,7 @@ Mountain Bottom Floor Pillars Room (Mountain Bottom Floor) - Elevator - 0x339BB 158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry Elevator (Mountain Bottom Floor): -158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True +158530 - 0x3D9A6 (Elevator Door Close Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True 158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True 158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True From ad5089b5a3ed67c326b5fbd036ba4b08bf3477f2 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 24 Jul 2024 07:36:41 -0400 Subject: [PATCH 39/78] DLC Quest - Add option groups to DLC Quest (#3677) * - Add option groups to DLC Quest * - Slight reorganisation * - Add type hint --- worlds/dlcquest/__init__.py | 2 ++ worlds/dlcquest/option_groups.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 worlds/dlcquest/option_groups.py diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index a9dfcc5044b1..2fc0da075d22 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,11 +8,13 @@ from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", "A guide to setting up the Archipelago DLCQuest game on your computer.", diff --git a/worlds/dlcquest/option_groups.py b/worlds/dlcquest/option_groups.py new file mode 100644 index 000000000000..9510c061e18f --- /dev/null +++ b/worlds/dlcquest/option_groups.py @@ -0,0 +1,27 @@ +from typing import List + +from Options import ProgressionBalancing, Accessibility, OptionGroup +from .Options import (Campaign, ItemShuffle, TimeIsMoney, EndingChoice, PermanentCoins, DoubleJumpGlitch, CoinSanity, + CoinSanityRange, DeathLink) + +dlcq_option_groups: List[OptionGroup] = [ + OptionGroup("General", [ + Campaign, + ItemShuffle, + CoinSanity, + ]), + OptionGroup("Customization", [ + EndingChoice, + PermanentCoins, + CoinSanityRange, + ]), + OptionGroup("Tedious and Grind", [ + TimeIsMoney, + DoubleJumpGlitch, + ]), + OptionGroup("Advanced Options", [ + DeathLink, + ProgressionBalancing, + Accessibility, + ]), +] From e7dbfa7fcd073a50a2b9a1786d6385657d6b8d73 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:46:14 -0400 Subject: [PATCH 40/78] FFMQ: Efficiency Improvement and Use New Options Methods (#2767) * FFMQ Efficiency improvement and use new options methods * Hard check for 0x01 game status * Fixes * Why were Mac's Ship entrance hints excluded? * Two remaining per_slot_randoms purged * reformat generate_early * Utils.parse_yaml --- worlds/ffmq/Client.py | 2 +- worlds/ffmq/Items.py | 20 +- worlds/ffmq/Options.py | 69 +- worlds/ffmq/Output.py | 74 +- worlds/ffmq/Regions.py | 47 +- worlds/ffmq/__init__.py | 62 +- worlds/ffmq/data/entrances.yaml | 2450 ------------------- worlds/ffmq/data/rooms.py | 2 + worlds/ffmq/data/rooms.yaml | 4026 ------------------------------- 9 files changed, 134 insertions(+), 6618 deletions(-) delete mode 100644 worlds/ffmq/data/entrances.yaml create mode 100644 worlds/ffmq/data/rooms.py delete mode 100644 worlds/ffmq/data/rooms.yaml diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 7de486314c6c..6cb35dd3b4be 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ async def game_watcher(self, ctx): received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1]) data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START) check_2 = await snes_read(ctx, 0xF53749, 1) - if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'): + if check_1 != b'01' or check_2 != b'01': return def get_range(data_range): diff --git a/worlds/ffmq/Items.py b/worlds/ffmq/Items.py index d0898d7e81c8..f1c102d34ef8 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -222,10 +222,10 @@ def yaml_item(text): def create_items(self) -> None: items = [] - starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ") + starting_weapon = self.options.starting_weapon.current_key.title().replace("_", " ") self.multiworld.push_precollected(self.create_item(starting_weapon)) self.multiworld.push_precollected(self.create_item("Steel Armor")) - if self.multiworld.sky_coin_mode[self.player] == "start_with": + if self.options.sky_coin_mode == "start_with": self.multiworld.push_precollected(self.create_item("Sky Coin")) precollected_item_names = {item.name for item in self.multiworld.precollected_items[self.player]} @@ -233,28 +233,28 @@ def create_items(self) -> None: def add_item(item_name): if item_name in ["Steel Armor", "Sky Fragment"] or "Progressive" in item_name: return - if item_name.lower().replace(" ", "_") == self.multiworld.starting_weapon[self.player].current_key: + if item_name.lower().replace(" ", "_") == self.options.starting_weapon.current_key: return - if self.multiworld.progressive_gear[self.player]: + if self.options.progressive_gear: for item_group in prog_map: if item_name in self.item_name_groups[item_group]: item_name = prog_map[item_group] break if item_name == "Sky Coin": - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": for _ in range(40): items.append(self.create_item("Sky Fragment")) return - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": items.append(self.create_filler()) return if item_name in precollected_item_names: items.append(self.create_filler()) return i = self.create_item(item_name) - if self.multiworld.logic[self.player] != "friendly" and item_name in ("Magic Mirror", "Mask"): + if self.options.logic != "friendly" and item_name in ("Magic Mirror", "Mask"): i.classification = ItemClassification.useful - if (self.multiworld.logic[self.player] == "expert" and self.multiworld.map_shuffle[self.player] == "none" and + if (self.options.logic == "expert" and self.options.map_shuffle == "none" and item_name == "Exit Book"): i.classification = ItemClassification.progression items.append(i) @@ -263,11 +263,11 @@ def add_item(item_name): for item in self.item_name_groups[item_group]: add_item(item) - if self.multiworld.brown_boxes[self.player] == "include": + if self.options.brown_boxes == "include": filler_items = [] for item, count in fillers.items(): filler_items += [self.create_item(item) for _ in range(count)] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": self.multiworld.random.shuffle(filler_items) filler_items = filler_items[39:] items += filler_items diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index af3625f28a9d..41c397315f87 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -1,4 +1,5 @@ -from Options import Choice, FreeText, Toggle, Range +from Options import Choice, FreeText, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class Logic(Choice): @@ -321,36 +322,36 @@ class KaelisMomFightsMinotaur(Toggle): default = 0 -option_definitions = { - "logic": Logic, - "brown_boxes": BrownBoxes, - "sky_coin_mode": SkyCoinMode, - "shattered_sky_coin_quantity": ShatteredSkyCoinQuantity, - "starting_weapon": StartingWeapon, - "progressive_gear": ProgressiveGear, - "leveling_curve": LevelingCurve, - "starting_companion": StartingCompanion, - "available_companions": AvailableCompanions, - "companions_locations": CompanionsLocations, - "kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur, - "companion_leveling_type": CompanionLevelingType, - "companion_spellbook_type": CompanionSpellbookType, - "enemies_density": EnemiesDensity, - "enemies_scaling_lower": EnemiesScalingLower, - "enemies_scaling_upper": EnemiesScalingUpper, - "bosses_scaling_lower": BossesScalingLower, - "bosses_scaling_upper": BossesScalingUpper, - "enemizer_attacks": EnemizerAttacks, - "enemizer_groups": EnemizerGroups, - "shuffle_res_weak_types": ShuffleResWeakType, - "shuffle_enemies_position": ShuffleEnemiesPositions, - "progressive_formations": ProgressiveFormations, - "doom_castle_mode": DoomCastle, - "doom_castle_shortcut": DoomCastleShortcut, - "tweak_frustrating_dungeons": TweakFrustratingDungeons, - "map_shuffle": MapShuffle, - "crest_shuffle": CrestShuffle, - "shuffle_battlefield_rewards": ShuffleBattlefieldRewards, - "map_shuffle_seed": MapShuffleSeed, - "battlefields_battles_quantities": BattlefieldsBattlesQuantities, -} +@dataclass +class FFMQOptions(PerGameCommonOptions): + logic: Logic + brown_boxes: BrownBoxes + sky_coin_mode: SkyCoinMode + shattered_sky_coin_quantity: ShatteredSkyCoinQuantity + starting_weapon: StartingWeapon + progressive_gear: ProgressiveGear + leveling_curve: LevelingCurve + starting_companion: StartingCompanion + available_companions: AvailableCompanions + companions_locations: CompanionsLocations + kaelis_mom_fight_minotaur: KaelisMomFightsMinotaur + companion_leveling_type: CompanionLevelingType + companion_spellbook_type: CompanionSpellbookType + enemies_density: EnemiesDensity + enemies_scaling_lower: EnemiesScalingLower + enemies_scaling_upper: EnemiesScalingUpper + bosses_scaling_lower: BossesScalingLower + bosses_scaling_upper: BossesScalingUpper + enemizer_attacks: EnemizerAttacks + enemizer_groups: EnemizerGroups + shuffle_res_weak_types: ShuffleResWeakType + shuffle_enemies_position: ShuffleEnemiesPositions + progressive_formations: ProgressiveFormations + doom_castle_mode: DoomCastle + doom_castle_shortcut: DoomCastleShortcut + tweak_frustrating_dungeons: TweakFrustratingDungeons + map_shuffle: MapShuffle + crest_shuffle: CrestShuffle + shuffle_battlefield_rewards: ShuffleBattlefieldRewards + map_shuffle_seed: MapShuffleSeed + battlefields_battles_quantities: BattlefieldsBattlesQuantities diff --git a/worlds/ffmq/Output.py b/worlds/ffmq/Output.py index 1b17aaa98f28..1e436a90c5fd 100644 --- a/worlds/ffmq/Output.py +++ b/worlds/ffmq/Output.py @@ -1,13 +1,13 @@ import yaml import os import zipfile +import Utils from copy import deepcopy from .Regions import object_id_table -from Utils import __version__ from worlds.Files import APPatch import pkgutil -settings_template = yaml.load(pkgutil.get_data(__name__, "data/settings.yaml"), yaml.Loader) +settings_template = Utils.parse_yaml(pkgutil.get_data(__name__, "data/settings.yaml")) def generate_output(self, output_directory): @@ -21,7 +21,7 @@ def output_item_name(item): item_name = "".join(item_name.split(" ")) else: if item.advancement or item.useful or (item.trap and - self.multiworld.per_slot_randoms[self.player].randint(0, 1)): + self.random.randint(0, 1)): item_name = "APItem" else: item_name = "APItemFiller" @@ -46,60 +46,60 @@ def tf(option): options = deepcopy(settings_template) options["name"] = self.multiworld.player_name[self.player] option_writes = { - "enemies_density": cc(self.multiworld.enemies_density[self.player]), + "enemies_density": cc(self.options.enemies_density), "chests_shuffle": "Include", - "shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle", + "shuffle_boxes_content": self.options.brown_boxes == "shuffle", "npcs_shuffle": "Include", "battlefields_shuffle": "Include", - "logic_options": cc(self.multiworld.logic[self.player]), - "shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]), - "enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]), - "enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]), - "bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]), - "bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]), - "enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]), - "leveling_curve": cc(self.multiworld.leveling_curve[self.player]), - "battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if - self.multiworld.battlefields_battles_quantities[self.player].value < 5 else + "logic_options": cc(self.options.logic), + "shuffle_enemies_position": tf(self.options.shuffle_enemies_position), + "enemies_scaling_lower": cc(self.options.enemies_scaling_lower), + "enemies_scaling_upper": cc(self.options.enemies_scaling_upper), + "bosses_scaling_lower": cc(self.options.bosses_scaling_lower), + "bosses_scaling_upper": cc(self.options.bosses_scaling_upper), + "enemizer_attacks": cc(self.options.enemizer_attacks), + "leveling_curve": cc(self.options.leveling_curve), + "battles_quantity": cc(self.options.battlefields_battles_quantities) if + self.options.battlefields_battles_quantities.value < 5 else "RandomLow" if - self.multiworld.battlefields_battles_quantities[self.player].value == 5 else + self.options.battlefields_battles_quantities.value == 5 else "RandomHigh", - "shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]), + "shuffle_battlefield_rewards": tf(self.options.shuffle_battlefield_rewards), "random_starting_weapon": True, - "progressive_gear": tf(self.multiworld.progressive_gear[self.player]), - "tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]), - "doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]), - "doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]), - "sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]), - "sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]), + "progressive_gear": tf(self.options.progressive_gear), + "tweaked_dungeons": tf(self.options.tweak_frustrating_dungeons), + "doom_castle_mode": cc(self.options.doom_castle_mode), + "doom_castle_shortcut": tf(self.options.doom_castle_shortcut), + "sky_coin_mode": cc(self.options.sky_coin_mode), + "sky_coin_fragments_qty": cc(self.options.shattered_sky_coin_quantity), "enable_spoilers": False, - "progressive_formations": cc(self.multiworld.progressive_formations[self.player]), - "map_shuffling": cc(self.multiworld.map_shuffle[self.player]), - "crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]), - "enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]), - "shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]), - "companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]), - "companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]), - "starting_companion": cc(self.multiworld.starting_companion[self.player]), + "progressive_formations": cc(self.options.progressive_formations), + "map_shuffling": cc(self.options.map_shuffle), + "crest_shuffle": tf(self.options.crest_shuffle), + "enemizer_groups": cc(self.options.enemizer_groups), + "shuffle_res_weak_type": tf(self.options.shuffle_res_weak_types), + "companion_leveling_type": cc(self.options.companion_leveling_type), + "companion_spellbook_type": cc(self.options.companion_spellbook_type), + "starting_companion": cc(self.options.starting_companion), "available_companions": ["Zero", "One", "Two", - "Three", "Four"][self.multiworld.available_companions[self.player].value], - "companions_locations": cc(self.multiworld.companions_locations[self.player]), - "kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]), + "Three", "Four"][self.options.available_companions.value], + "companions_locations": cc(self.options.companions_locations), + "kaelis_mom_fight_minotaur": tf(self.options.kaelis_mom_fight_minotaur), } for option, data in option_writes.items(): options["Final Fantasy Mystic Quest"][option][data] = 1 - rom_name = f'MQ{__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] + rom_name = f'MQ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed_name:11}'[:21] self.rom_name = bytearray(rom_name, 'utf8') self.rom_name_available_event.set() setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed": - hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} + hex(self.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper()} starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]] - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": + if self.options.sky_coin_mode == "shattered_sky_coin": starting_items.append("SkyCoin") file_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.apmq") diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index 8b83c88e72c9..f7b9b9eed4d8 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -1,11 +1,9 @@ from BaseClasses import Region, MultiWorld, Entrance, Location, LocationProgressType, ItemClassification from worlds.generic.Rules import add_rule +from .data.rooms import rooms, entrances from .Items import item_groups, yaml_item -import pkgutil -import yaml -rooms = yaml.load(pkgutil.get_data(__name__, "data/rooms.yaml"), yaml.Loader) -entrance_names = {entrance["id"]: entrance["name"] for entrance in yaml.load(pkgutil.get_data(__name__, "data/entrances.yaml"), yaml.Loader)} +entrance_names = {entrance["id"]: entrance["name"] for entrance in entrances} object_id_table = {} object_type_table = {} @@ -69,7 +67,7 @@ def create_regions(self): location_table else None, object["type"], object["access"], self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp", - "BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and + "BattlefieldXp") and (object["type"] != "Box" or self.options.brown_boxes == "include") and not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"])) dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player) @@ -91,15 +89,13 @@ def create_regions(self): if "entrance" in link and link["entrance"] != -1: spoiler = False if link["entrance"] in crest_warps: - if self.multiworld.crest_shuffle[self.player]: + if self.options.crest_shuffle: spoiler = True - elif self.multiworld.map_shuffle[self.player] == "everything": + elif self.options.map_shuffle == "everything": spoiler = True - elif "Subregion" in region.name and self.multiworld.map_shuffle[self.player] not in ("dungeons", - "none"): + elif "Subregion" in region.name and self.options.map_shuffle not in ("dungeons", "none"): spoiler = True - elif "Subregion" not in region.name and self.multiworld.map_shuffle[self.player] not in ("none", - "overworld"): + elif "Subregion" not in region.name and self.options.map_shuffle not in ("none", "overworld"): spoiler = True if spoiler: @@ -111,6 +107,7 @@ def create_regions(self): connection.connect(connect_room) break + non_dead_end_crest_rooms = [ 'Libra Temple', 'Aquaria Gemini Room', "GrenadeMan's Mobius Room", 'Fireburg Gemini Room', 'Sealed Temple', 'Alive Forest', 'Kaidge Temple Upper Ledge', @@ -140,7 +137,7 @@ def hard_boss_logic(state): add_rule(self.multiworld.get_location("Gidrah", self.player), hard_boss_logic) add_rule(self.multiworld.get_location("Dullahan", self.player), hard_boss_logic) - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: for boss in ("Freezer Crab", "Ice Golem", "Jinn", "Medusa", "Dualhead Hydra"): loc = self.multiworld.get_location(boss, self.player) checked_regions = {loc.parent_region} @@ -158,12 +155,12 @@ def check_foresta(region): return True check_foresta(loc.parent_region) - if self.multiworld.logic[self.player] == "friendly": + if self.options.logic == "friendly": process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player), ["MagicMirror"]) process_rules(self.multiworld.get_entrance("Overworld - Volcano", self.player), ["Mask"]) - if self.multiworld.map_shuffle[self.player] in ("none", "overworld"): + if self.options.map_shuffle in ("none", "overworld"): process_rules(self.multiworld.get_entrance("Overworld - Bone Dungeon", self.player), ["Bomb"]) process_rules(self.multiworld.get_entrance("Overworld - Wintry Cave", self.player), @@ -185,8 +182,8 @@ def check_foresta(region): process_rules(self.multiworld.get_entrance("Overworld - Mac Ship Doom", self.player), ["DragonClaw", "CaptainCap"]) - if self.multiworld.logic[self.player] == "expert": - if self.multiworld.map_shuffle[self.player] == "none" and not self.multiworld.crest_shuffle[self.player]: + if self.options.logic == "expert": + if self.options.map_shuffle == "none" and not self.options.crest_shuffle: inner_room = self.multiworld.get_region("Wintry Temple Inner Room", self.player) connection = Entrance(self.player, "Sealed Temple Exit Trick", inner_room) connection.connect(self.multiworld.get_region("Wintry Temple Outer Room", self.player)) @@ -198,14 +195,14 @@ def check_foresta(region): if entrance.connected_region.name in non_dead_end_crest_rooms: entrance.access_rule = lambda state: False - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - logic_coins = [16, 24, 32, 32, 38][self.multiworld.shattered_sky_coin_quantity[self.player].value] + if self.options.sky_coin_mode == "shattered_sky_coin": + logic_coins = [16, 24, 32, 32, 38][self.options.shattered_sky_coin_quantity.value] self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Fragment", self.player, logic_coins) - elif self.multiworld.sky_coin_mode[self.player] == "save_the_crystals": + elif self.options.sky_coin_mode == "save_the_crystals": self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has_all(["Flamerus Rex", "Dualhead Hydra", "Ice Golem", "Pazuzu"], self.player) - elif self.multiworld.sky_coin_mode[self.player] in ("standard", "start_with"): + elif self.options.sky_coin_mode in ("standard", "start_with"): self.multiworld.get_entrance("Focus Tower 1F - Sky Door", self.player).access_rule = \ lambda state: state.has("Sky Coin", self.player) @@ -213,26 +210,24 @@ def check_foresta(region): def stage_set_rules(multiworld): # If there's no enemies, there's no repeatable income sources no_enemies_players = [player for player in multiworld.get_game_players("Final Fantasy Mystic Quest") - if multiworld.enemies_density[player] == "none"] + if multiworld.worlds[player].options.enemies_density == "none"] if (len([item for item in multiworld.itempool if item.classification in (ItemClassification.filler, ItemClassification.trap)]) > len([player for player in no_enemies_players if - multiworld.accessibility[player] == "minimal"]) * 3): + multiworld.worlds[player].options.accessibility == "minimal"]) * 3): for player in no_enemies_players: for location in vendor_locations: - if multiworld.accessibility[player] == "locations": + if multiworld.worlds[player].options.accessibility == "locations": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False else: # There are not enough junk items to fill non-minimal players' vendors. Just set an item rule not allowing - # advancement items so that useful items can be placed + # advancement items so that useful items can be placed. for player in no_enemies_players: for location in vendor_locations: multiworld.get_location(location, player).item_rule = lambda item: not item.advancement - - class FFMQLocation(Location): game = "Final Fantasy Mystic Quest" diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index ac3e91370933..c464203dc6a4 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -10,7 +10,7 @@ non_dead_end_crest_warps from .Items import item_table, item_groups, create_items, FFMQItem, fillers from .Output import generate_output -from .Options import option_definitions +from .Options import FFMQOptions from .Client import FFMQClient @@ -45,7 +45,8 @@ class FFMQWorld(World): item_name_to_id = {name: data.id for name, data in item_table.items() if data.id is not None} location_name_to_id = location_table - option_definitions = option_definitions + options_dataclass = FFMQOptions + options: FFMQOptions topology_present = True @@ -67,20 +68,14 @@ def __init__(self, world, player: int): super().__init__(world, player) def generate_early(self): - if self.multiworld.sky_coin_mode[self.player] == "shattered_sky_coin": - self.multiworld.brown_boxes[self.player].value = 1 - if self.multiworld.enemies_scaling_lower[self.player].value > \ - self.multiworld.enemies_scaling_upper[self.player].value: - (self.multiworld.enemies_scaling_lower[self.player].value, - self.multiworld.enemies_scaling_upper[self.player].value) =\ - (self.multiworld.enemies_scaling_upper[self.player].value, - self.multiworld.enemies_scaling_lower[self.player].value) - if self.multiworld.bosses_scaling_lower[self.player].value > \ - self.multiworld.bosses_scaling_upper[self.player].value: - (self.multiworld.bosses_scaling_lower[self.player].value, - self.multiworld.bosses_scaling_upper[self.player].value) =\ - (self.multiworld.bosses_scaling_upper[self.player].value, - self.multiworld.bosses_scaling_lower[self.player].value) + if self.options.sky_coin_mode == "shattered_sky_coin": + self.options.brown_boxes.value = 1 + if self.options.enemies_scaling_lower.value > self.options.enemies_scaling_upper.value: + self.options.enemies_scaling_lower.value, self.options.enemies_scaling_upper.value = \ + self.options.enemies_scaling_upper.value, self.options.enemies_scaling_lower.value + if self.options.bosses_scaling_lower.value > self.options.bosses_scaling_upper.value: + self.options.bosses_scaling_lower.value, self.options.bosses_scaling_upper.value = \ + self.options.bosses_scaling_upper.value, self.options.bosses_scaling_lower.value @classmethod def stage_generate_early(cls, multiworld): @@ -94,20 +89,20 @@ def stage_generate_early(cls, multiworld): rooms_data = {} for world in multiworld.get_game_worlds("Final Fantasy Mystic Quest"): - if (world.multiworld.map_shuffle[world.player] or world.multiworld.crest_shuffle[world.player] or - world.multiworld.crest_shuffle[world.player]): - if world.multiworld.map_shuffle_seed[world.player].value.isdigit(): - multiworld.random.seed(int(world.multiworld.map_shuffle_seed[world.player].value)) - elif world.multiworld.map_shuffle_seed[world.player].value != "random": - multiworld.random.seed(int(hash(world.multiworld.map_shuffle_seed[world.player].value)) - + int(world.multiworld.seed)) + if (world.options.map_shuffle or world.options.crest_shuffle or world.options.shuffle_battlefield_rewards + or world.options.companions_locations): + if world.options.map_shuffle_seed.value.isdigit(): + multiworld.random.seed(int(world.options.map_shuffle_seed.value)) + elif world.options.map_shuffle_seed.value != "random": + multiworld.random.seed(int(hash(world.options.map_shuffle_seed.value)) + + int(world.multiworld.seed)) seed = hex(multiworld.random.randint(0, 0xFFFFFFFF)).split("0x")[1].upper() - map_shuffle = multiworld.map_shuffle[world.player].value - crest_shuffle = multiworld.crest_shuffle[world.player].current_key - battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key - companion_shuffle = multiworld.companions_locations[world.player].value - kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key + map_shuffle = world.options.map_shuffle.value + crest_shuffle = world.options.crest_shuffle.current_key + battlefield_shuffle = world.options.shuffle_battlefield_rewards.current_key + companion_shuffle = world.options.companions_locations.value + kaeli_mom = world.options.kaelis_mom_fight_minotaur.current_key query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}" @@ -175,14 +170,14 @@ def get_filler_item_name(self): def extend_hint_information(self, hint_data): hint_data[self.player] = {} - if self.multiworld.map_shuffle[self.player]: + if self.options.map_shuffle: single_location_regions = ["Subregion Volcano Battlefield", "Subregion Mac's Ship", "Subregion Doom Castle"] for subregion in ["Subregion Foresta", "Subregion Aquaria", "Subregion Frozen Fields", "Subregion Fireburg", "Subregion Volcano Battlefield", "Subregion Windia", "Subregion Mac's Ship", "Subregion Doom Castle"]: region = self.multiworld.get_region(subregion, self.player) for location in region.locations: - if location.address and self.multiworld.map_shuffle[self.player] != "dungeons": + if location.address and self.options.map_shuffle != "dungeons": hint_data[self.player][location.address] = (subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else "")) @@ -202,14 +197,13 @@ def extend_hint_information(self, hint_data): for location in exit_check.connected_region.locations: if location.address: hint = [] - if self.multiworld.map_shuffle[self.player] != "dungeons": + if self.options.map_shuffle != "dungeons": hint.append((subregion.split("Subregion ")[-1] + (" Region" if subregion not in single_location_regions else ""))) - if self.multiworld.map_shuffle[self.player] != "overworld" and subregion not in \ - ("Subregion Mac's Ship", "Subregion Doom Castle"): + if self.options.map_shuffle != "overworld": hint.append(overworld_spot.name.split("Overworld - ")[-1].replace("Pazuzu", "Pazuzu's")) - hint = " - ".join(hint) + hint = " - ".join(hint).replace(" - Mac Ship", "") if location.address in hint_data[self.player]: hint_data[self.player][location.address] += f"/{hint}" else: diff --git a/worlds/ffmq/data/entrances.yaml b/worlds/ffmq/data/entrances.yaml deleted file mode 100644 index 1dfef2655c37..000000000000 --- a/worlds/ffmq/data/entrances.yaml +++ /dev/null @@ -1,2450 +0,0 @@ -- name: Doom Castle - Sand Floor - To Sky Door - Sand Floor - id: 0 - area: 7 - coordinates: [24, 19] - teleporter: [0, 0] -- name: Doom Castle - Sand Floor - Main Entrance - Sand Floor - id: 1 - area: 7 - coordinates: [19, 43] - teleporter: [1, 6] -- name: Doom Castle - Aero Room - Aero Room Entrance - id: 2 - area: 7 - coordinates: [27, 39] - teleporter: [1, 0] -- name: Focus Tower B1 - Main Loop - South Entrance - id: 3 - area: 8 - coordinates: [43, 60] - teleporter: [2, 6] -- name: Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall - id: 4 - area: 8 - coordinates: [37, 41] - teleporter: [4, 0] -- name: Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room - id: 5 - area: 8 - coordinates: [59, 35] - teleporter: [5, 0] -- name: Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest - id: 6 - area: 8 - coordinates: [57, 59] - teleporter: [8, 0] -- name: Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door - id: 7 - area: 8 - coordinates: [51, 49] - teleporter: [6, 0] -- name: Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor - id: 8 - area: 8 - coordinates: [51, 45] - teleporter: [7, 0] -- name: Focus Tower 1F - Focus Tower West Entrance - id: 9 - area: 9 - coordinates: [25, 29] - teleporter: [3, 6] -- name: Focus Tower 1F - To Focus Tower 2F - From SandCoin - id: 10 - area: 9 - coordinates: [16, 4] - teleporter: [10, 0] -- name: Focus Tower 1F - To Focus Tower B1 - Main Hall - id: 11 - area: 9 - coordinates: [4, 23] - teleporter: [11, 0] -- name: Focus Tower 1F - To Focus Tower B1 - To Aero Chest - id: 12 - area: 9 - coordinates: [26, 17] - teleporter: [12, 0] -- name: Focus Tower 1F - Sky Door - id: 13 - area: 9 - coordinates: [16, 24] - teleporter: [13, 0] -- name: Focus Tower 1F - To Focus Tower 2F - From RiverCoin - id: 14 - area: 9 - coordinates: [16, 10] - teleporter: [14, 0] -- name: Focus Tower 1F - To Focus Tower B1 - From Sky Door - id: 15 - area: 9 - coordinates: [16, 29] - teleporter: [15, 0] -- name: Focus Tower 2F - Sand Coin Passage - North Entrance - id: 16 - area: 10 - coordinates: [49, 30] - teleporter: [4, 6] -- name: Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin - id: 17 - area: 10 - coordinates: [47, 33] - teleporter: [17, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin - id: 18 - area: 10 - coordinates: [47, 41] - teleporter: [18, 0] -- name: Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor - id: 19 - area: 10 - coordinates: [38, 40] - teleporter: [20, 0] -- name: Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor - id: 20 - area: 10 - coordinates: [56, 40] - teleporter: [19, 0] -- name: Focus Tower 2F - Venus Chest Room - Pillar Script - id: 21 - area: 10 - coordinates: [48, 53] - teleporter: [13, 8] -- name: Focus Tower 3F - Lower Floor - To Fireburg Entrance - id: 22 - area: 11 - coordinates: [11, 39] - teleporter: [6, 6] -- name: Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar - id: 23 - area: 11 - coordinates: [6, 47] - teleporter: [24, 0] -- name: Focus Tower 3F - Upper Floor - To Aquaria Entrance - id: 24 - area: 11 - coordinates: [21, 38] - teleporter: [5, 6] -- name: Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room - id: 25 - area: 11 - coordinates: [24, 47] - teleporter: [23, 0] -- name: Level Forest - Boulder Script - id: 26 - area: 14 - coordinates: [52, 15] - teleporter: [0, 8] -- name: Level Forest - Rotten Tree Script - id: 27 - area: 14 - coordinates: [47, 6] - teleporter: [2, 8] -- name: Level Forest - Exit Level Forest 1 - id: 28 - area: 14 - coordinates: [46, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 2 - id: 29 - area: 14 - coordinates: [46, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 3 - id: 30 - area: 14 - coordinates: [47, 25] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 4 - id: 31 - area: 14 - coordinates: [47, 26] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 5 - id: 32 - area: 14 - coordinates: [60, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 6 - id: 33 - area: 14 - coordinates: [61, 14] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 7 - id: 34 - area: 14 - coordinates: [46, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 8 - id: 35 - area: 14 - coordinates: [46, 3] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest 9 - id: 36 - area: 14 - coordinates: [47, 4] - teleporter: [25, 0] -- name: Level Forest - Exit Level Forest A - id: 37 - area: 14 - coordinates: [47, 3] - teleporter: [25, 0] -- name: Foresta - Exit Foresta 1 - id: 38 - area: 15 - coordinates: [10, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 2 - id: 39 - area: 15 - coordinates: [10, 26] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 3 - id: 40 - area: 15 - coordinates: [11, 25] - teleporter: [31, 0] -- name: Foresta - Exit Foresta 4 - id: 41 - area: 15 - coordinates: [11, 26] - teleporter: [31, 0] -- name: Foresta - Old Man House - Front Door - id: 42 - area: 15 - coordinates: [25, 17] - teleporter: [32, 4] -- name: Foresta - Old Man House - Back Door - id: 43 - area: 15 - coordinates: [25, 14] - teleporter: [33, 0] -- name: Foresta - Kaeli's House - id: 44 - area: 15 - coordinates: [7, 21] - teleporter: [0, 5] -- name: Foresta - Rest House - id: 45 - area: 15 - coordinates: [23, 23] - teleporter: [1, 5] -- name: Kaeli's House - Kaeli's House Entrance - id: 46 - area: 16 - coordinates: [11, 20] - teleporter: [86, 3] -- name: Foresta Houses - Old Man's House - Old Man Front Exit - id: 47 - area: 17 - coordinates: [35, 44] - teleporter: [34, 0] -- name: Foresta Houses - Old Man's House - Old Man Back Exit - id: 48 - area: 17 - coordinates: [35, 27] - teleporter: [35, 0] -- name: Foresta - Old Man House - Barrel Tile Script # New, use the focus tower column's script - id: 483 - area: 17 - coordinates: [0x23, 0x1E] - teleporter: [0x0D, 8] -- name: Foresta Houses - Rest House - Bed Script - id: 49 - area: 17 - coordinates: [30, 6] - teleporter: [1, 8] -- name: Foresta Houses - Rest House - Rest House Exit - id: 50 - area: 17 - coordinates: [35, 20] - teleporter: [87, 3] -- name: Foresta Houses - Libra House - Libra House Script - id: 51 - area: 17 - coordinates: [8, 49] - teleporter: [67, 8] -- name: Foresta Houses - Gemini House - Gemini House Script - id: 52 - area: 17 - coordinates: [26, 55] - teleporter: [68, 8] -- name: Foresta Houses - Mobius House - Mobius House Script - id: 53 - area: 17 - coordinates: [14, 33] - teleporter: [69, 8] -- name: Sand Temple - Sand Temple Entrance - id: 54 - area: 18 - coordinates: [56, 27] - teleporter: [36, 0] -- name: Bone Dungeon 1F - Bone Dungeon Entrance - id: 55 - area: 19 - coordinates: [13, 60] - teleporter: [37, 0] -- name: Bone Dungeon 1F - To Bone Dungeon B1 - id: 56 - area: 19 - coordinates: [13, 39] - teleporter: [2, 2] -- name: Bone Dungeon B1 - Waterway - Exit Waterway - id: 57 - area: 20 - coordinates: [27, 39] - teleporter: [3, 2] -- name: Bone Dungeon B1 - Waterway - Tristam's Script - id: 58 - area: 20 - coordinates: [27, 45] - teleporter: [3, 8] -- name: Bone Dungeon B1 - Waterway - To Bone Dungeon 1F - id: 59 - area: 20 - coordinates: [54, 61] - teleporter: [88, 3] -- name: Bone Dungeon B1 - Checker Room - Exit Checker Room - id: 60 - area: 20 - coordinates: [23, 40] - teleporter: [4, 2] -- name: Bone Dungeon B1 - Checker Room - To Waterway - id: 61 - area: 20 - coordinates: [39, 49] - teleporter: [89, 3] -- name: Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room - id: 62 - area: 20 - coordinates: [5, 33] - teleporter: [91, 3] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage - id: 63 - area: 21 - coordinates: [19, 13] - teleporter: [5, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room - id: 64 - area: 21 - coordinates: [29, 15] - teleporter: [6, 2] -- name: Bonne Dungeon B2 - Exploding Skull Room - To Checker Room - id: 65 - area: 21 - coordinates: [8, 25] - teleporter: [90, 3] -- name: Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room - id: 66 - area: 21 - coordinates: [59, 12] - teleporter: [93, 3] -- name: Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room - id: 67 - area: 21 - coordinates: [59, 28] - teleporter: [94, 3] -- name: Bonne Dungeon B2 - Two Skulls Room - To Box Room - id: 68 - area: 21 - coordinates: [53, 7] - teleporter: [7, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Quake Room - id: 69 - area: 21 - coordinates: [41, 3] - teleporter: [8, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To Boss Room - id: 70 - area: 21 - coordinates: [47, 57] - teleporter: [9, 2] -- name: Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room - id: 71 - area: 21 - coordinates: [54, 23] - teleporter: [92, 3] -- name: Bone Dungeon B2 - Boss Room - Flamerus Rex Script - id: 72 - area: 22 - coordinates: [29, 19] - teleporter: [4, 8] -- name: Bone Dungeon B2 - Boss Room - Tristam Leave Script - id: 73 - area: 22 - coordinates: [29, 23] - teleporter: [75, 8] -- name: Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room - id: 74 - area: 22 - coordinates: [30, 27] - teleporter: [95, 3] -- name: Libra Temple - Entrance - id: 75 - area: 23 - coordinates: [10, 15] - teleporter: [13, 6] -- name: Libra Temple - Libra Tile Script - id: 76 - area: 23 - coordinates: [9, 8] - teleporter: [59, 8] -- name: Aquaria Winter - Winter Entrance 1 - id: 77 - area: 24 - coordinates: [25, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 2 - id: 78 - area: 24 - coordinates: [25, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 3 - id: 79 - area: 24 - coordinates: [26, 25] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Entrance 4 - id: 80 - area: 24 - coordinates: [26, 26] - teleporter: [8, 6] -- name: Aquaria Winter - Winter Phoebe's House Entrance Script #Modified to not be a script - id: 81 - area: 24 - coordinates: [8, 19] - teleporter: [10, 5] # original value [5, 8] -- name: Aquaria Winter - Winter Vendor House Entrance - id: 82 - area: 24 - coordinates: [8, 5] - teleporter: [44, 4] -- name: Aquaria Winter - Winter INN Entrance - id: 83 - area: 24 - coordinates: [26, 17] - teleporter: [11, 5] -- name: Aquaria Summer - Summer Entrance 1 - id: 84 - area: 25 - coordinates: [57, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 2 - id: 85 - area: 25 - coordinates: [57, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 3 - id: 86 - area: 25 - coordinates: [58, 25] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Entrance 4 - id: 87 - area: 25 - coordinates: [58, 26] - teleporter: [8, 6] -- name: Aquaria Summer - Summer Phoebe's House Entrance - id: 88 - area: 25 - coordinates: [40, 19] - teleporter: [10, 5] -- name: Aquaria Summer - Spencer's Place Entrance Top - id: 89 - area: 25 - coordinates: [40, 16] - teleporter: [42, 0] -- name: Aquaria Summer - Spencer's Place Entrance Side - id: 90 - area: 25 - coordinates: [41, 18] - teleporter: [43, 0] -- name: Aquaria Summer - Summer Vendor House Entrance - id: 91 - area: 25 - coordinates: [40, 5] - teleporter: [44, 4] -- name: Aquaria Summer - Summer INN Entrance - id: 92 - area: 25 - coordinates: [58, 17] - teleporter: [11, 5] -- name: Phoebe's House - Entrance # Change to a script, same as vendor house - id: 93 - area: 26 - coordinates: [29, 14] - teleporter: [5, 8] # Original Value [11,3] -- name: Aquaria Vendor House - Vendor House Entrance's Script - id: 94 - area: 27 - coordinates: [7, 10] - teleporter: [40, 8] -- name: Aquaria Vendor House - Vendor House Stairs - id: 95 - area: 27 - coordinates: [1, 4] - teleporter: [47, 0] -- name: Aquaria Gemini Room - Gemini Script - id: 96 - area: 27 - coordinates: [2, 40] - teleporter: [72, 8] -- name: Aquaria Gemini Room - Gemini Room Stairs - id: 97 - area: 27 - coordinates: [4, 39] - teleporter: [48, 0] -- name: Aquaria INN - Aquaria INN entrance # Change to a script, same as vendor house - id: 98 - area: 27 - coordinates: [51, 46] - teleporter: [75, 8] # Original value [48,3] -- name: Wintry Cave 1F - Main Entrance - id: 99 - area: 28 - coordinates: [50, 58] - teleporter: [49, 0] -- name: Wintry Cave 1F - To 3F Top - id: 100 - area: 28 - coordinates: [40, 25] - teleporter: [14, 2] -- name: Wintry Cave 1F - To 2F - id: 101 - area: 28 - coordinates: [10, 43] - teleporter: [15, 2] -- name: Wintry Cave 1F - Phoebe's Script - id: 102 - area: 28 - coordinates: [44, 37] - teleporter: [6, 8] -- name: Wintry Cave 2F - To 3F Bottom - id: 103 - area: 29 - coordinates: [58, 5] - teleporter: [50, 0] -- name: Wintry Cave 2F - To 1F - id: 104 - area: 29 - coordinates: [38, 18] - teleporter: [97, 3] -- name: Wintry Cave 3F Top - Exit from 3F Top - id: 105 - area: 30 - coordinates: [24, 6] - teleporter: [96, 3] -- name: Wintry Cave 3F Bottom - Exit to 2F - id: 106 - area: 31 - coordinates: [4, 29] - teleporter: [51, 0] -- name: Life Temple - Entrance - id: 107 - area: 32 - coordinates: [9, 60] - teleporter: [14, 6] -- name: Life Temple - Libra Tile Script - id: 108 - area: 32 - coordinates: [3, 55] - teleporter: [60, 8] -- name: Life Temple - Mysterious Man Script - id: 109 - area: 32 - coordinates: [9, 44] - teleporter: [78, 8] -- name: Fall Basin - Back Exit Script - id: 110 - area: 33 - coordinates: [17, 5] - teleporter: [9, 0] # Remove script [42, 8] for overworld teleport (but not main exit) -- name: Fall Basin - Main Exit - id: 111 - area: 33 - coordinates: [15, 26] - teleporter: [53, 0] -- name: Fall Basin - Phoebe's Script - id: 112 - area: 33 - coordinates: [17, 6] - teleporter: [9, 8] -- name: Ice Pyramid B1 Taunt Room - To Climbing Wall Room - id: 113 - area: 34 - coordinates: [43, 6] - teleporter: [55, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 1 - id: 114 - area: 35 - coordinates: [18, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - Main Entrance 2 - id: 115 - area: 35 - coordinates: [19, 36] - teleporter: [56, 0] -- name: Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room - id: 116 - area: 35 - coordinates: [3, 27] - teleporter: [57, 0] -- name: Ice Pyramid 1F Maze - West Center Stairs to 2F West Room - id: 117 - area: 35 - coordinates: [11, 15] - teleporter: [58, 0] -- name: Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room - id: 118 - area: 35 - coordinates: [25, 16] - teleporter: [59, 0] -- name: Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room - id: 119 - area: 35 - coordinates: [31, 1] - teleporter: [60, 0] -- name: Ice Pyramid 1F Maze - East Stairs to 2F North Corridor - id: 120 - area: 35 - coordinates: [34, 9] - teleporter: [61, 0] -- name: Ice Pyramid 1F Maze - Statue's Script - id: 121 - area: 35 - coordinates: [21, 32] - teleporter: [77, 8] -- name: Ice Pyramid 2F South Tiled Room - To 1F - id: 122 - area: 36 - coordinates: [4, 26] - teleporter: [62, 0] -- name: Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room - id: 123 - area: 36 - coordinates: [22, 17] - teleporter: [67, 0] -- name: Ice Pyramid 2F West Room - To 1F - id: 124 - area: 36 - coordinates: [9, 10] - teleporter: [63, 0] -- name: Ice Pyramid 2F Center Room - To 1F - id: 125 - area: 36 - coordinates: [22, 14] - teleporter: [64, 0] -- name: Ice Pyramid 2F Small North Room - To 1F - id: 126 - area: 36 - coordinates: [26, 4] - teleporter: [65, 0] -- name: Ice Pyramid 2F North Corridor - To 1F - id: 127 - area: 36 - coordinates: [32, 8] - teleporter: [66, 0] -- name: Ice Pyramid 2F North Corridor - To 3F Main Loop - id: 128 - area: 36 - coordinates: [12, 7] - teleporter: [68, 0] -- name: Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room - id: 129 - area: 37 - coordinates: [24, 54] - teleporter: [69, 0] -- name: Ice Pyramid 3F Main Loop - To 2F Corridor - id: 130 - area: 37 - coordinates: [16, 45] - teleporter: [70, 0] -- name: Ice Pyramid 3F Main Loop - To 4F - id: 131 - area: 37 - coordinates: [19, 43] - teleporter: [71, 0] -- name: Ice Pyramid 4F Treasure Room - To 3F Main Loop - id: 132 - area: 38 - coordinates: [52, 5] - teleporter: [72, 0] -- name: Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room - id: 133 - area: 38 - coordinates: [62, 19] - teleporter: [73, 0] -- name: Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room - id: 134 - area: 39 - coordinates: [54, 63] - teleporter: [74, 0] -- name: Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate - id: 135 - area: 39 - coordinates: [47, 54] - teleporter: [77, 8] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room - id: 136 - area: 39 - coordinates: [39, 43] - teleporter: [75, 0] -- name: Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room - id: 137 - area: 39 - coordinates: [39, 60] - teleporter: [76, 0] -- name: Ice Pyramid - Duplicate Ice Golem Room # not used? - id: 138 - area: 40 - coordinates: [44, 43] - teleporter: [77, 0] -- name: Ice Pyramid Climbing Wall Room - To Taunt Room - id: 139 - area: 41 - coordinates: [4, 59] - teleporter: [78, 0] -- name: Ice Pyramid Climbing Wall Room - To 5F Stairs - id: 140 - area: 41 - coordinates: [4, 45] - teleporter: [79, 0] -- name: Ice Pyramid Ice Golem Room - To 5F Stairs - id: 141 - area: 42 - coordinates: [44, 43] - teleporter: [80, 0] -- name: Ice Pyramid Ice Golem Room - Ice Golem Script - id: 142 - area: 42 - coordinates: [53, 32] - teleporter: [10, 8] -- name: Spencer Waterfall - To Spencer Cave - id: 143 - area: 43 - coordinates: [48, 57] - teleporter: [81, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 1 - id: 144 - area: 43 - coordinates: [40, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 2 - id: 145 - area: 43 - coordinates: [40, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 3 - id: 146 - area: 43 - coordinates: [41, 5] - teleporter: [82, 0] -- name: Spencer Waterfall - Upper Exit to Aquaria 4 - id: 147 - area: 43 - coordinates: [41, 6] - teleporter: [82, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 1 - id: 148 - area: 43 - coordinates: [46, 8] - teleporter: [83, 0] -- name: Spencer Waterfall - Right Exit to Aquaria 2 - id: 149 - area: 43 - coordinates: [47, 8] - teleporter: [83, 0] -- name: Spencer Cave Normal Main - To Waterfall - id: 150 - area: 44 - coordinates: [14, 39] - teleporter: [85, 0] -- name: Spencer Cave Normal From Overworld - Exit to Overworld - id: 151 - area: 44 - coordinates: [15, 57] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Exit to Overworld - id: 152 - area: 45 - coordinates: [40, 29] - teleporter: [7, 6] -- name: Spencer Cave Unplug - Libra Teleporter Start Script - id: 153 - area: 45 - coordinates: [28, 21] - teleporter: [33, 8] -- name: Spencer Cave Unplug - Libra Teleporter End Script - id: 154 - area: 45 - coordinates: [46, 4] - teleporter: [34, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Chest Script - id: 155 - area: 45 - coordinates: [21, 9] - teleporter: [35, 8] -- name: Spencer Cave Unplug - Mobius Teleporter Start Script - id: 156 - area: 45 - coordinates: [29, 28] - teleporter: [36, 8] -- name: Wintry Temple Outer Room - Main Entrance - id: 157 - area: 46 - coordinates: [8, 31] - teleporter: [15, 6] -- name: Wintry Temple Inner Room - Gemini Tile to Sealed temple - id: 158 - area: 46 - coordinates: [9, 24] - teleporter: [62, 8] -- name: Fireburg - To Overworld - id: 159 - area: 47 - coordinates: [4, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 160 - area: 47 - coordinates: [5, 13] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 161 - area: 47 - coordinates: [28, 15] - teleporter: [9, 6] -- name: Fireburg - To Overworld - id: 162 - area: 47 - coordinates: [27, 15] - teleporter: [9, 6] -- name: Fireburg - Vendor House - id: 163 - area: 47 - coordinates: [10, 24] - teleporter: [91, 0] -- name: Fireburg - Reuben House - id: 164 - area: 47 - coordinates: [14, 6] - teleporter: [98, 8] # Script for reuben, original value [16, 2] -- name: Fireburg - Hotel - id: 165 - area: 47 - coordinates: [20, 8] - teleporter: [96, 8] # It's a script now for tristam, original value [17, 2] -- name: Fireburg - GrenadeMan House Script - id: 166 - area: 47 - coordinates: [12, 18] - teleporter: [11, 8] -- name: Reuben House - Main Entrance - id: 167 - area: 48 - coordinates: [33, 46] - teleporter: [98, 3] -- name: GrenadeMan House - Entrance Script - id: 168 - area: 49 - coordinates: [55, 60] - teleporter: [9, 8] -- name: GrenadeMan House - To Mobius Crest Room - id: 169 - area: 49 - coordinates: [57, 52] - teleporter: [93, 0] -- name: GrenadeMan Mobius Room - Stairs to House - id: 170 - area: 49 - coordinates: [39, 26] - teleporter: [94, 0] -- name: GrenadeMan Mobius Room - Mobius Teleporter Script - id: 171 - area: 49 - coordinates: [39, 23] - teleporter: [54, 8] -- name: Fireburg Vendor House - Entrance Script # No use to be a script - id: 172 - area: 49 - coordinates: [7, 10] - teleporter: [95, 0] # Original value [39, 8] -- name: Fireburg Vendor House - Stairs to Gemini Room - id: 173 - area: 49 - coordinates: [1, 4] - teleporter: [96, 0] -- name: Fireburg Gemini Room - Stairs to Vendor House - id: 174 - area: 49 - coordinates: [4, 39] - teleporter: [97, 0] -- name: Fireburg Gemini Room - Gemini Teleporter Script - id: 175 - area: 49 - coordinates: [2, 40] - teleporter: [45, 8] -- name: Fireburg Hotel Lobby - Stairs to beds - id: 176 - area: 49 - coordinates: [4, 50] - teleporter: [213, 0] -- name: Fireburg Hotel Lobby - Entrance - id: 177 - area: 49 - coordinates: [17, 56] - teleporter: [99, 3] -- name: Fireburg Hotel Beds - Stairs to Hotel Lobby - id: 178 - area: 49 - coordinates: [45, 59] - teleporter: [214, 0] -- name: Mine Exterior - Main Entrance - id: 179 - area: 50 - coordinates: [5, 28] - teleporter: [98, 0] -- name: Mine Exterior - To Cliff - id: 180 - area: 50 - coordinates: [58, 29] - teleporter: [99, 0] -- name: Mine Exterior - To Parallel Room - id: 181 - area: 50 - coordinates: [8, 7] - teleporter: [20, 2] -- name: Mine Exterior - To Crescent Room - id: 182 - area: 50 - coordinates: [26, 15] - teleporter: [21, 2] -- name: Mine Exterior - To Climbing Room - id: 183 - area: 50 - coordinates: [21, 35] - teleporter: [22, 2] -- name: Mine Exterior - Jinn Fight Script - id: 184 - area: 50 - coordinates: [58, 31] - teleporter: [74, 8] -- name: Mine Parallel Room - To Mine Exterior - id: 185 - area: 51 - coordinates: [7, 60] - teleporter: [100, 3] -- name: Mine Crescent Room - To Mine Exterior - id: 186 - area: 51 - coordinates: [22, 61] - teleporter: [101, 3] -- name: Mine Climbing Room - To Mine Exterior - id: 187 - area: 51 - coordinates: [56, 21] - teleporter: [102, 3] -- name: Mine Cliff - Entrance - id: 188 - area: 52 - coordinates: [9, 5] - teleporter: [100, 0] -- name: Mine Cliff - Reuben Grenade Script - id: 189 - area: 52 - coordinates: [15, 7] - teleporter: [12, 8] -- name: Sealed Temple - To Overworld - id: 190 - area: 53 - coordinates: [58, 43] - teleporter: [16, 6] -- name: Sealed Temple - Gemini Tile Script - id: 191 - area: 53 - coordinates: [56, 38] - teleporter: [63, 8] -- name: Volcano Base - Main Entrance 1 - id: 192 - area: 54 - coordinates: [23, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 2 - id: 193 - area: 54 - coordinates: [23, 26] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 3 - id: 194 - area: 54 - coordinates: [24, 25] - teleporter: [103, 0] -- name: Volcano Base - Main Entrance 4 - id: 195 - area: 54 - coordinates: [24, 26] - teleporter: [103, 0] -- name: Volcano Base - Left Stairs Script - id: 196 - area: 54 - coordinates: [20, 5] - teleporter: [31, 8] -- name: Volcano Base - Right Stairs Script - id: 197 - area: 54 - coordinates: [32, 5] - teleporter: [30, 8] -- name: Volcano Top Right - Top Exit - id: 198 - area: 55 - coordinates: [44, 8] - teleporter: [9, 0] # Original value [103, 0] changed to volcano escape so floor shuffling doesn't pick it up -- name: Volcano Top Left - To Right-Left Path Script - id: 199 - area: 55 - coordinates: [40, 24] - teleporter: [26, 8] -- name: Volcano Top Right - To Left-Right Path Script - id: 200 - area: 55 - coordinates: [52, 24] - teleporter: [79, 8] # Original Value [26, 8] -- name: Volcano Right Path - To Volcano Base Script - id: 201 - area: 56 - coordinates: [48, 42] - teleporter: [15, 8] # Original Value [27, 8] -- name: Volcano Left Path - To Volcano Cross Left-Right - id: 202 - area: 56 - coordinates: [40, 31] - teleporter: [25, 2] -- name: Volcano Left Path - To Volcano Cross Right-Left - id: 203 - area: 56 - coordinates: [52, 29] - teleporter: [26, 2] -- name: Volcano Left Path - To Volcano Base Script - id: 204 - area: 56 - coordinates: [36, 42] - teleporter: [27, 8] -- name: Volcano Cross Left-Right - To Volcano Left Path - id: 205 - area: 56 - coordinates: [10, 42] - teleporter: [103, 3] -- name: Volcano Cross Left-Right - To Volcano Top Right Script - id: 206 - area: 56 - coordinates: [16, 24] - teleporter: [29, 8] -- name: Volcano Cross Right-Left - To Volcano Top Left Script - id: 207 - area: 56 - coordinates: [8, 22] - teleporter: [28, 8] -- name: Volcano Cross Right-Left - To Volcano Left Path - id: 208 - area: 56 - coordinates: [16, 42] - teleporter: [104, 3] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 1 - id: 209 - area: 57 - coordinates: [32, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - Main Entrance 2 - id: 210 - area: 57 - coordinates: [33, 5] - teleporter: [104, 0] -- name: Lava Dome Inner Ring Main Loop - To Three Steps Room - id: 211 - area: 57 - coordinates: [14, 5] - teleporter: [105, 0] -- name: Lava Dome Inner Ring Main Loop - To Life Chest Room Lower - id: 212 - area: 57 - coordinates: [40, 17] - teleporter: [106, 0] -- name: Lava Dome Inner Ring Main Loop - To Big Jump Room Left - id: 213 - area: 57 - coordinates: [8, 11] - teleporter: [108, 0] -- name: Lava Dome Inner Ring Main Loop - To Split Corridor Room - id: 214 - area: 57 - coordinates: [11, 19] - teleporter: [111, 0] -- name: Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher - id: 215 - area: 57 - coordinates: [32, 11] - teleporter: [107, 0] -- name: Lava Dome Inner Ring Plate Ledge - To Plate Corridor - id: 216 - area: 57 - coordinates: [12, 23] - teleporter: [109, 0] -- name: Lava Dome Inner Ring Plate Ledge - Plate Script - id: 217 - area: 57 - coordinates: [5, 23] - teleporter: [47, 8] -- name: Lava Dome Inner Ring Upper Ledges - To Pointless Room - id: 218 - area: 57 - coordinates: [0, 9] - teleporter: [110, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room - id: 219 - area: 57 - coordinates: [0, 15] - teleporter: [112, 0] -- name: Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor - id: 220 - area: 57 - coordinates: [54, 5] - teleporter: [113, 0] -- name: Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II - id: 221 - area: 57 - coordinates: [54, 21] - teleporter: [114, 0] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1 - id: 222 - area: 57 - coordinates: [62, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2 - id: 223 - area: 57 - coordinates: [63, 20] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3 - id: 224 - area: 57 - coordinates: [62, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4 - id: 225 - area: 57 - coordinates: [63, 21] - teleporter: [29, 2] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor - id: 226 - area: 57 - coordinates: [50, 25] - teleporter: [115, 0] -- name: Lava Dome Jump Maze II - Lower Right Entrance - id: 227 - area: 58 - coordinates: [55, 28] - teleporter: [116, 0] -- name: Lava Dome Jump Maze II - Upper Entrance - id: 228 - area: 58 - coordinates: [35, 3] - teleporter: [119, 0] -- name: Lava Dome Jump Maze II - Lower Left Entrance - id: 229 - area: 58 - coordinates: [34, 27] - teleporter: [120, 0] -- name: Lava Dome Up-Down Corridor - Upper Entrance - id: 230 - area: 58 - coordinates: [29, 8] - teleporter: [117, 0] -- name: Lava Dome Up-Down Corridor - Lower Entrance - id: 231 - area: 58 - coordinates: [28, 25] - teleporter: [118, 0] -- name: Lava Dome Jump Maze I - South Entrance - id: 232 - area: 59 - coordinates: [20, 27] - teleporter: [121, 0] -- name: Lava Dome Jump Maze I - North Entrance - id: 233 - area: 59 - coordinates: [7, 3] - teleporter: [122, 0] -- name: Lava Dome Pointless Room - Entrance - id: 234 - area: 60 - coordinates: [2, 7] - teleporter: [123, 0] -- name: Lava Dome Pointless Room - Visit Quest Script 1 - id: 490 - area: 60 - coordinates: [4, 4] - teleporter: [99, 8] -- name: Lava Dome Pointless Room - Visit Quest Script 2 - id: 491 - area: 60 - coordinates: [4, 5] - teleporter: [99, 8] -- name: Lava Dome Lower Moon Helm Room - Left Entrance - id: 235 - area: 60 - coordinates: [2, 19] - teleporter: [124, 0] -- name: Lava Dome Lower Moon Helm Room - Right Entrance - id: 236 - area: 60 - coordinates: [11, 21] - teleporter: [125, 0] -- name: Lava Dome Moon Helm Room - Entrance - id: 237 - area: 60 - coordinates: [15, 23] - teleporter: [126, 0] -- name: Lava Dome Three Jumps Room - To Main Loop - id: 238 - area: 61 - coordinates: [58, 15] - teleporter: [127, 0] -- name: Lava Dome Life Chest Room - Lower South Entrance - id: 239 - area: 61 - coordinates: [38, 27] - teleporter: [128, 0] -- name: Lava Dome Life Chest Room - Upper South Entrance - id: 240 - area: 61 - coordinates: [28, 23] - teleporter: [129, 0] -- name: Lava Dome Big Jump Room - Left Entrance - id: 241 - area: 62 - coordinates: [42, 51] - teleporter: [133, 0] -- name: Lava Dome Big Jump Room - North Entrance - id: 242 - area: 62 - coordinates: [30, 29] - teleporter: [131, 0] -- name: Lava Dome Big Jump Room - Lower Right Stairs - id: 243 - area: 62 - coordinates: [61, 59] - teleporter: [132, 0] -- name: Lava Dome Split Corridor - Upper Stairs - id: 244 - area: 62 - coordinates: [30, 43] - teleporter: [130, 0] -- name: Lava Dome Split Corridor - Lower Stairs - id: 245 - area: 62 - coordinates: [36, 61] - teleporter: [134, 0] -- name: Lava Dome Plate Corridor - Right Entrance - id: 246 - area: 63 - coordinates: [19, 29] - teleporter: [135, 0] -- name: Lava Dome Plate Corridor - Left Entrance - id: 247 - area: 63 - coordinates: [60, 21] - teleporter: [137, 0] -- name: Lava Dome Four Boxes Stairs - Upper Entrance - id: 248 - area: 63 - coordinates: [22, 3] - teleporter: [136, 0] -- name: Lava Dome Four Boxes Stairs - Lower Entrance - id: 249 - area: 63 - coordinates: [22, 17] - teleporter: [16, 0] -- name: Lava Dome Hydra Room - South Entrance - id: 250 - area: 64 - coordinates: [14, 59] - teleporter: [105, 3] -- name: Lava Dome Hydra Room - North Exit - id: 251 - area: 64 - coordinates: [25, 31] - teleporter: [138, 0] -- name: Lava Dome Hydra Room - Hydra Script - id: 252 - area: 64 - coordinates: [14, 36] - teleporter: [14, 8] -- name: Lava Dome Escape Corridor - South Entrance - id: 253 - area: 65 - coordinates: [22, 17] - teleporter: [139, 0] -- name: Lava Dome Escape Corridor - North Entrance - id: 254 - area: 65 - coordinates: [22, 3] - teleporter: [9, 0] -- name: Rope Bridge - West Entrance 1 - id: 255 - area: 66 - coordinates: [3, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 2 - id: 256 - area: 66 - coordinates: [3, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 3 - id: 257 - area: 66 - coordinates: [3, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 4 - id: 258 - area: 66 - coordinates: [3, 13] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 5 - id: 259 - area: 66 - coordinates: [4, 10] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 6 - id: 260 - area: 66 - coordinates: [4, 11] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 7 - id: 261 - area: 66 - coordinates: [4, 12] - teleporter: [140, 0] -- name: Rope Bridge - West Entrance 8 - id: 262 - area: 66 - coordinates: [4, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 1 - id: 263 - area: 66 - coordinates: [59, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 2 - id: 264 - area: 66 - coordinates: [59, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 3 - id: 265 - area: 66 - coordinates: [59, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 4 - id: 266 - area: 66 - coordinates: [59, 13] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 5 - id: 267 - area: 66 - coordinates: [60, 10] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 6 - id: 268 - area: 66 - coordinates: [60, 11] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 7 - id: 269 - area: 66 - coordinates: [60, 12] - teleporter: [140, 0] -- name: Rope Bridge - East Entrance 8 - id: 270 - area: 66 - coordinates: [60, 13] - teleporter: [140, 0] -- name: Rope Bridge - Reuben Fall Script - id: 271 - area: 66 - coordinates: [13, 12] - teleporter: [15, 8] -- name: Alive Forest - West Entrance 1 - id: 272 - area: 67 - coordinates: [8, 13] - teleporter: [142, 0] -- name: Alive Forest - West Entrance 2 - id: 273 - area: 67 - coordinates: [9, 13] - teleporter: [142, 0] -- name: Alive Forest - Giant Tree Entrance - id: 274 - area: 67 - coordinates: [42, 42] - teleporter: [143, 0] -- name: Alive Forest - Libra Teleporter Script - id: 275 - area: 67 - coordinates: [8, 52] - teleporter: [64, 8] -- name: Alive Forest - Gemini Teleporter Script - id: 276 - area: 67 - coordinates: [57, 49] - teleporter: [65, 8] -- name: Alive Forest - Mobius Teleporter Script - id: 277 - area: 67 - coordinates: [24, 10] - teleporter: [66, 8] -- name: Giant Tree 1F - Entrance Script 1 - id: 278 - area: 68 - coordinates: [18, 31] - teleporter: [56, 1] # The script is restored if no map shuffling [49, 8] -- name: Giant Tree 1F - Entrance Script 2 - id: 279 - area: 68 - coordinates: [19, 31] - teleporter: [56, 1] # Same [49, 8] -- name: Giant Tree 1F - North Entrance To 2F - id: 280 - area: 68 - coordinates: [16, 1] - teleporter: [144, 0] -- name: Giant Tree 2F Main Lobby - North Entrance to 1F - id: 281 - area: 69 - coordinates: [44, 33] - teleporter: [145, 0] -- name: Giant Tree 2F Main Lobby - Central Entrance to 3F - id: 282 - area: 69 - coordinates: [42, 47] - teleporter: [146, 0] -- name: Giant Tree 2F Main Lobby - West Entrance to Mushroom Room - id: 283 - area: 69 - coordinates: [58, 49] - teleporter: [149, 0] -- name: Giant Tree 2F West Ledge - To 3F Northwest Ledge - id: 284 - area: 69 - coordinates: [34, 37] - teleporter: [147, 0] -- name: Giant Tree 2F Fall From Vine Script - id: 482 - area: 69 - coordinates: [0x2E, 0x33] - teleporter: [76, 8] -- name: Giant Tree Meteor Chest Room - To 2F Mushroom Room - id: 285 - area: 69 - coordinates: [58, 44] - teleporter: [148, 0] -- name: Giant Tree 2F Mushroom Room - Entrance - id: 286 - area: 70 - coordinates: [55, 18] - teleporter: [150, 0] -- name: Giant Tree 2F Mushroom Room - North Face to Meteor - id: 287 - area: 70 - coordinates: [56, 7] - teleporter: [151, 0] -- name: Giant Tree 3F Central Room - Central Entrance to 2F - id: 288 - area: 71 - coordinates: [46, 53] - teleporter: [152, 0] -- name: Giant Tree 3F Central Room - East Entrance to Worm Room - id: 289 - area: 71 - coordinates: [58, 39] - teleporter: [153, 0] -- name: Giant Tree 3F Lower Corridor - Entrance from Worm Room - id: 290 - area: 71 - coordinates: [45, 39] - teleporter: [154, 0] -- name: Giant Tree 3F West Platform - Lower Entrance - id: 291 - area: 71 - coordinates: [33, 43] - teleporter: [155, 0] -- name: Giant Tree 3F West Platform - Top Entrance - id: 292 - area: 71 - coordinates: [52, 25] - teleporter: [156, 0] -- name: Giant Tree Worm Room - East Entrance - id: 293 - area: 72 - coordinates: [20, 58] - teleporter: [157, 0] -- name: Giant Tree Worm Room - West Entrance - id: 294 - area: 72 - coordinates: [6, 56] - teleporter: [158, 0] -- name: Giant Tree 4F Lower Floor - Entrance - id: 295 - area: 73 - coordinates: [20, 7] - teleporter: [159, 0] -- name: Giant Tree 4F Lower Floor - Lower West Mouth - id: 296 - area: 73 - coordinates: [8, 23] - teleporter: [160, 0] -- name: Giant Tree 4F Lower Floor - Lower Central Mouth - id: 297 - area: 73 - coordinates: [14, 25] - teleporter: [161, 0] -- name: Giant Tree 4F Lower Floor - Lower East Mouth - id: 298 - area: 73 - coordinates: [20, 25] - teleporter: [162, 0] -- name: Giant Tree 4F Upper Floor - Upper West Mouth - id: 299 - area: 73 - coordinates: [8, 19] - teleporter: [163, 0] -- name: Giant Tree 4F Upper Floor - Upper Central Mouth - id: 300 - area: 73 - coordinates: [12, 17] - teleporter: [164, 0] -- name: Giant Tree 4F Slime Room - Exit - id: 301 - area: 74 - coordinates: [47, 10] - teleporter: [165, 0] -- name: Giant Tree 4F Slime Room - West Entrance - id: 302 - area: 74 - coordinates: [45, 24] - teleporter: [166, 0] -- name: Giant Tree 4F Slime Room - Central Entrance - id: 303 - area: 74 - coordinates: [50, 24] - teleporter: [167, 0] -- name: Giant Tree 4F Slime Room - East Entrance - id: 304 - area: 74 - coordinates: [57, 28] - teleporter: [168, 0] -- name: Giant Tree 5F - Entrance - id: 305 - area: 75 - coordinates: [14, 51] - teleporter: [169, 0] -- name: Giant Tree 5F - Giant Tree Face # Unused - id: 306 - area: 75 - coordinates: [14, 37] - teleporter: [170, 0] -- name: Kaidge Temple - Entrance - id: 307 - area: 77 - coordinates: [44, 63] - teleporter: [18, 6] -- name: Kaidge Temple - Mobius Teleporter Script - id: 308 - area: 77 - coordinates: [35, 57] - teleporter: [71, 8] -- name: Windhole Temple - Entrance - id: 309 - area: 78 - coordinates: [10, 29] - teleporter: [173, 0] -- name: Mount Gale - Entrance 1 - id: 310 - area: 79 - coordinates: [1, 45] - teleporter: [174, 0] -- name: Mount Gale - Entrance 2 - id: 311 - area: 79 - coordinates: [2, 45] - teleporter: [174, 0] -- name: Mount Gale - Visit Quest - id: 494 - area: 79 - coordinates: [44, 7] - teleporter: [101, 8] -- name: Windia - Main Entrance 1 - id: 312 - area: 80 - coordinates: [12, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 2 - id: 313 - area: 80 - coordinates: [13, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 3 - id: 314 - area: 80 - coordinates: [14, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 4 - id: 315 - area: 80 - coordinates: [15, 40] - teleporter: [10, 6] -- name: Windia - Main Entrance 5 - id: 316 - area: 80 - coordinates: [12, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 6 - id: 317 - area: 80 - coordinates: [13, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 7 - id: 318 - area: 80 - coordinates: [14, 41] - teleporter: [10, 6] -- name: Windia - Main Entrance 8 - id: 319 - area: 80 - coordinates: [15, 41] - teleporter: [10, 6] -- name: Windia - Otto's House - id: 320 - area: 80 - coordinates: [21, 39] - teleporter: [30, 5] -- name: Windia - INN's Script # Change to teleporter / Change back to script! - id: 321 - area: 80 - coordinates: [18, 34] - teleporter: [97, 8] # Original value [79, 8] > [31, 2] -- name: Windia - Vendor House - id: 322 - area: 80 - coordinates: [8, 36] - teleporter: [32, 5] -- name: Windia - Kid House - id: 323 - area: 80 - coordinates: [7, 23] - teleporter: [176, 4] -- name: Windia - Old People House - id: 324 - area: 80 - coordinates: [19, 21] - teleporter: [177, 4] -- name: Windia - Rainbow Bridge Script - id: 325 - area: 80 - coordinates: [21, 9] - teleporter: [10, 6] # Change to entrance, usually a script [41, 8] -- name: Otto's House - Attic Stairs - id: 326 - area: 81 - coordinates: [2, 19] - teleporter: [33, 2] -- name: Otto's House - Entrance - id: 327 - area: 81 - coordinates: [9, 30] - teleporter: [106, 3] -- name: Otto's Attic - Stairs - id: 328 - area: 81 - coordinates: [26, 23] - teleporter: [107, 3] -- name: Windia Kid House - Entrance Script # Change to teleporter - id: 329 - area: 82 - coordinates: [7, 10] - teleporter: [178, 0] # Original value [38, 8] -- name: Windia Kid House - Basement Stairs - id: 330 - area: 82 - coordinates: [1, 4] - teleporter: [180, 0] -- name: Windia Old People House - Entrance - id: 331 - area: 82 - coordinates: [55, 12] - teleporter: [179, 0] -- name: Windia Old People House - Basement Stairs - id: 332 - area: 82 - coordinates: [60, 5] - teleporter: [181, 0] -- name: Windia Kid House Basement - Stairs - id: 333 - area: 82 - coordinates: [43, 8] - teleporter: [182, 0] -- name: Windia Kid House Basement - Mobius Teleporter - id: 334 - area: 82 - coordinates: [41, 9] - teleporter: [44, 8] -- name: Windia Old People House Basement - Stairs - id: 335 - area: 82 - coordinates: [39, 26] - teleporter: [183, 0] -- name: Windia Old People House Basement - Mobius Teleporter Script - id: 336 - area: 82 - coordinates: [39, 23] - teleporter: [43, 8] -- name: Windia Inn Lobby - Stairs to Beds - id: 337 - area: 82 - coordinates: [45, 24] - teleporter: [102, 8] # Changed to script, original value [215, 0] -- name: Windia Inn Lobby - Exit - id: 338 - area: 82 - coordinates: [53, 30] - teleporter: [135, 3] -- name: Windia Inn Beds - Stairs to Lobby - id: 339 - area: 82 - coordinates: [33, 59] - teleporter: [216, 0] -- name: Windia Vendor House - Entrance - id: 340 - area: 82 - coordinates: [29, 14] - teleporter: [108, 3] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 1 - id: 341 - area: 83 - coordinates: [47, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 2 - id: 342 - area: 83 - coordinates: [47, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 3 - id: 343 - area: 83 - coordinates: [48, 29] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - Main Entrance 4 - id: 344 - area: 83 - coordinates: [48, 30] - teleporter: [184, 0] -- name: Pazuzu Tower 1F Main Lobby - East Entrance - id: 345 - area: 83 - coordinates: [55, 12] - teleporter: [185, 0] -- name: Pazuzu Tower 1F Main Lobby - South Stairs - id: 346 - area: 83 - coordinates: [51, 25] - teleporter: [186, 0] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 1 - id: 347 - area: 83 - coordinates: [47, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Main Lobby - Pazuzu Script 2 - id: 348 - area: 83 - coordinates: [48, 8] - teleporter: [16, 8] -- name: Pazuzu Tower 1F Boxes Room - West Stairs - id: 349 - area: 83 - coordinates: [38, 17] - teleporter: [187, 0] -- name: Pazuzu 2F - West Upper Stairs - id: 350 - area: 84 - coordinates: [7, 11] - teleporter: [188, 0] -- name: Pazuzu 2F - South Stairs - id: 351 - area: 84 - coordinates: [20, 24] - teleporter: [189, 0] -- name: Pazuzu 2F - West Lower Stairs - id: 352 - area: 84 - coordinates: [6, 17] - teleporter: [190, 0] -- name: Pazuzu 2F - Central Stairs - id: 353 - area: 84 - coordinates: [15, 15] - teleporter: [191, 0] -- name: Pazuzu 2F - Pazuzu 1 - id: 354 - area: 84 - coordinates: [15, 8] - teleporter: [17, 8] -- name: Pazuzu 2F - Pazuzu 2 - id: 355 - area: 84 - coordinates: [16, 8] - teleporter: [17, 8] -- name: Pazuzu 3F Main Room - North Stairs - id: 356 - area: 85 - coordinates: [23, 11] - teleporter: [192, 0] -- name: Pazuzu 3F Main Room - West Stairs - id: 357 - area: 85 - coordinates: [7, 15] - teleporter: [193, 0] -- name: Pazuzu 3F Main Room - Pazuzu Script 1 - id: 358 - area: 85 - coordinates: [15, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Main Room - Pazuzu Script 2 - id: 359 - area: 85 - coordinates: [16, 8] - teleporter: [18, 8] -- name: Pazuzu 3F Central Island - Central Stairs - id: 360 - area: 85 - coordinates: [15, 14] - teleporter: [194, 0] -- name: Pazuzu 3F Central Island - South Stairs - id: 361 - area: 85 - coordinates: [17, 25] - teleporter: [195, 0] -- name: Pazuzu 4F - Northwest Stairs - id: 362 - area: 86 - coordinates: [39, 12] - teleporter: [196, 0] -- name: Pazuzu 4F - Southwest Stairs - id: 363 - area: 86 - coordinates: [39, 19] - teleporter: [197, 0] -- name: Pazuzu 4F - South Stairs - id: 364 - area: 86 - coordinates: [47, 24] - teleporter: [198, 0] -- name: Pazuzu 4F - Northeast Stairs - id: 365 - area: 86 - coordinates: [54, 9] - teleporter: [199, 0] -- name: Pazuzu 4F - Pazuzu Script 1 - id: 366 - area: 86 - coordinates: [47, 8] - teleporter: [19, 8] -- name: Pazuzu 4F - Pazuzu Script 2 - id: 367 - area: 86 - coordinates: [48, 8] - teleporter: [19, 8] -- name: Pazuzu 5F Pazuzu Loop - West Stairs - id: 368 - area: 87 - coordinates: [9, 49] - teleporter: [200, 0] -- name: Pazuzu 5F Pazuzu Loop - South Stairs - id: 369 - area: 87 - coordinates: [16, 55] - teleporter: [201, 0] -- name: Pazuzu 5F Upper Loop - Northeast Stairs - id: 370 - area: 87 - coordinates: [22, 40] - teleporter: [202, 0] -- name: Pazuzu 5F Upper Loop - Northwest Stairs - id: 371 - area: 87 - coordinates: [9, 40] - teleporter: [203, 0] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 1 - id: 372 - area: 87 - coordinates: [15, 40] - teleporter: [20, 8] -- name: Pazuzu 5F Upper Loop - Pazuzu Script 2 - id: 373 - area: 87 - coordinates: [16, 40] - teleporter: [20, 8] -- name: Pazuzu 6F - West Stairs - id: 374 - area: 88 - coordinates: [41, 47] - teleporter: [204, 0] -- name: Pazuzu 6F - Northwest Stairs - id: 375 - area: 88 - coordinates: [41, 40] - teleporter: [205, 0] -- name: Pazuzu 6F - Northeast Stairs - id: 376 - area: 88 - coordinates: [54, 40] - teleporter: [206, 0] -- name: Pazuzu 6F - South Stairs - id: 377 - area: 88 - coordinates: [52, 56] - teleporter: [207, 0] -- name: Pazuzu 6F - Pazuzu Script 1 - id: 378 - area: 88 - coordinates: [47, 40] - teleporter: [21, 8] -- name: Pazuzu 6F - Pazuzu Script 2 - id: 379 - area: 88 - coordinates: [48, 40] - teleporter: [21, 8] -- name: Pazuzu 7F Main Room - Southwest Stairs - id: 380 - area: 89 - coordinates: [15, 54] - teleporter: [26, 0] -- name: Pazuzu 7F Main Room - Northeast Stairs - id: 381 - area: 89 - coordinates: [21, 40] - teleporter: [27, 0] -- name: Pazuzu 7F Main Room - Southeast Stairs - id: 382 - area: 89 - coordinates: [21, 56] - teleporter: [28, 0] -- name: Pazuzu 7F Main Room - Pazuzu Script 1 - id: 383 - area: 89 - coordinates: [15, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Pazuzu Script 2 - id: 384 - area: 89 - coordinates: [16, 44] - teleporter: [22, 8] -- name: Pazuzu 7F Main Room - Crystal Script # Added for floor shuffle - id: 480 - area: 89 - coordinates: [15, 40] - teleporter: [38, 8] -- name: Pazuzu 1F to 3F - South Stairs - id: 385 - area: 90 - coordinates: [43, 60] - teleporter: [29, 0] -- name: Pazuzu 1F to 3F - North Stairs - id: 386 - area: 90 - coordinates: [43, 36] - teleporter: [30, 0] -- name: Pazuzu 3F to 5F - South Stairs - id: 387 - area: 91 - coordinates: [43, 60] - teleporter: [40, 0] -- name: Pazuzu 3F to 5F - North Stairs - id: 388 - area: 91 - coordinates: [43, 36] - teleporter: [41, 0] -- name: Pazuzu 5F to 7F - South Stairs - id: 389 - area: 92 - coordinates: [43, 60] - teleporter: [38, 0] -- name: Pazuzu 5F to 7F - North Stairs - id: 390 - area: 92 - coordinates: [43, 36] - teleporter: [39, 0] -- name: Pazuzu 2F to 4F - South Stairs - id: 391 - area: 93 - coordinates: [43, 60] - teleporter: [21, 0] -- name: Pazuzu 2F to 4F - North Stairs - id: 392 - area: 93 - coordinates: [43, 36] - teleporter: [22, 0] -- name: Pazuzu 4F to 6F - South Stairs - id: 393 - area: 94 - coordinates: [43, 60] - teleporter: [2, 0] -- name: Pazuzu 4F to 6F - North Stairs - id: 394 - area: 94 - coordinates: [43, 36] - teleporter: [3, 0] -- name: Light Temple - Entrance - id: 395 - area: 95 - coordinates: [28, 57] - teleporter: [19, 6] -- name: Light Temple - Mobius Teleporter Script - id: 396 - area: 95 - coordinates: [29, 37] - teleporter: [70, 8] -- name: Light Temple - Visit Quest Script 1 - id: 492 - area: 95 - coordinates: [34, 39] - teleporter: [100, 8] -- name: Light Temple - Visit Quest Script 2 - id: 493 - area: 95 - coordinates: [35, 39] - teleporter: [100, 8] -- name: Ship Dock - Mobius Teleporter Script - id: 397 - area: 96 - coordinates: [15, 18] - teleporter: [61, 8] -- name: Ship Dock - From Overworld - id: 398 - area: 96 - coordinates: [15, 11] - teleporter: [73, 0] -- name: Ship Dock - Entrance - id: 399 - area: 96 - coordinates: [15, 23] - teleporter: [17, 6] -- name: Mac Ship Deck - East Entrance Script - id: 400 - area: 97 - coordinates: [26, 40] - teleporter: [37, 8] -- name: Mac Ship Deck - Central Stairs Script - id: 401 - area: 97 - coordinates: [16, 47] - teleporter: [50, 8] -- name: Mac Ship Deck - West Stairs Script - id: 402 - area: 97 - coordinates: [8, 34] - teleporter: [51, 8] -- name: Mac Ship Deck - East Stairs Script - id: 403 - area: 97 - coordinates: [24, 36] - teleporter: [52, 8] -- name: Mac Ship Deck - North Stairs Script - id: 404 - area: 97 - coordinates: [12, 9] - teleporter: [53, 8] -- name: Mac Ship B1 Outer Ring - South Stairs - id: 405 - area: 98 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring - West Stairs - id: 406 - area: 98 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring - East Stairs - id: 407 - area: 98 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Outer Ring - Northwest Stairs - id: 408 - area: 98 - coordinates: [10, 23] - teleporter: [88, 0] -- name: Mac Ship B1 Square Room - North Stairs - id: 409 - area: 98 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room - South Stairs - id: 410 - area: 98 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room - Stairs # Unused? - id: 411 - area: 98 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor - South Stairs - id: 412 - area: 98 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor - North Stairs - id: 413 - area: 98 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B2 South Corridor - South Stairs - id: 414 - area: 99 - coordinates: [48, 51] - teleporter: [57, 1] -- name: Mac Ship B2 South Corridor - North Stairs Script - id: 415 - area: 99 - coordinates: [48, 38] - teleporter: [55, 8] -- name: Mac Ship B2 North Corridor - South Stairs Script - id: 416 - area: 99 - coordinates: [48, 27] - teleporter: [56, 8] -- name: Mac Ship B2 North Corridor - North Stairs Script - id: 417 - area: 99 - coordinates: [48, 12] - teleporter: [57, 8] -- name: Mac Ship B2 Outer Ring - Northwest Stairs Script - id: 418 - area: 99 - coordinates: [55, 11] - teleporter: [58, 8] -- name: Mac Ship B1 Outer Ring Cleared - South Stairs - id: 419 - area: 100 - coordinates: [16, 45] - teleporter: [208, 0] -- name: Mac Ship B1 Outer Ring Cleared - West Stairs - id: 420 - area: 100 - coordinates: [8, 35] - teleporter: [175, 0] -- name: Mac Ship B1 Outer Ring Cleared - East Stairs - id: 421 - area: 100 - coordinates: [25, 37] - teleporter: [172, 0] -- name: Mac Ship B1 Square Room Cleared - North Stairs - id: 422 - area: 100 - coordinates: [14, 9] - teleporter: [141, 0] -- name: Mac Ship B1 Square Room Cleared - South Stairs - id: 423 - area: 100 - coordinates: [16, 12] - teleporter: [87, 0] -- name: Mac Ship B1 Mac Room Cleared - Main Stairs - id: 424 - area: 100 - coordinates: [16, 51] - teleporter: [101, 0] -- name: Mac Ship B1 Central Corridor Cleared - South Stairs - id: 425 - area: 100 - coordinates: [16, 38] - teleporter: [102, 0] -- name: Mac Ship B1 Central Corridor Cleared - North Stairs - id: 426 - area: 100 - coordinates: [16, 26] - teleporter: [86, 0] -- name: Mac Ship B1 Central Corridor Cleared - Northwest Stairs - id: 427 - area: 100 - coordinates: [23, 10] - teleporter: [88, 0] -- name: Doom Castle Corridor of Destiny - South Entrance - id: 428 - area: 101 - coordinates: [59, 29] - teleporter: [84, 0] -- name: Doom Castle Corridor of Destiny - Ice Floor Entrance - id: 429 - area: 101 - coordinates: [59, 21] - teleporter: [35, 2] -- name: Doom Castle Corridor of Destiny - Lava Floor Entrance - id: 430 - area: 101 - coordinates: [59, 13] - teleporter: [209, 0] -- name: Doom Castle Corridor of Destiny - Sky Floor Entrance - id: 431 - area: 101 - coordinates: [59, 5] - teleporter: [211, 0] -- name: Doom Castle Corridor of Destiny - Hero Room Entrance - id: 432 - area: 101 - coordinates: [59, 61] - teleporter: [13, 2] -- name: Doom Castle Ice Floor - Entrance - id: 433 - area: 102 - coordinates: [23, 42] - teleporter: [109, 3] -- name: Doom Castle Lava Floor - Entrance - id: 434 - area: 103 - coordinates: [23, 40] - teleporter: [210, 0] -- name: Doom Castle Sky Floor - Entrance - id: 435 - area: 104 - coordinates: [24, 41] - teleporter: [212, 0] -- name: Doom Castle Hero Room - Dark King Entrance 1 - id: 436 - area: 106 - coordinates: [15, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 2 - id: 437 - area: 106 - coordinates: [16, 5] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 3 - id: 438 - area: 106 - coordinates: [15, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Dark King Entrance 4 - id: 439 - area: 106 - coordinates: [16, 4] - teleporter: [54, 0] -- name: Doom Castle Hero Room - Hero Statue Script - id: 440 - area: 106 - coordinates: [15, 17] - teleporter: [24, 8] -- name: Doom Castle Hero Room - Entrance - id: 441 - area: 106 - coordinates: [15, 24] - teleporter: [110, 3] -- name: Doom Castle Dark King Room - Entrance - id: 442 - area: 107 - coordinates: [14, 26] - teleporter: [52, 0] -- name: Doom Castle Dark King Room - Dark King Script - id: 443 - area: 107 - coordinates: [14, 15] - teleporter: [25, 8] -- name: Doom Castle Dark King Room - Unknown - id: 444 - area: 107 - coordinates: [47, 54] - teleporter: [77, 0] -- name: Overworld - Level Forest - id: 445 - area: 0 - type: "Overworld" - teleporter: [0x2E, 8] -- name: Overworld - Foresta - id: 446 - area: 0 - type: "Overworld" - teleporter: [0x02, 1] -- name: Overworld - Sand Temple - id: 447 - area: 0 - type: "Overworld" - teleporter: [0x03, 1] -- name: Overworld - Bone Dungeon - id: 448 - area: 0 - type: "Overworld" - teleporter: [0x04, 1] -- name: Overworld - Focus Tower Foresta - id: 449 - area: 0 - type: "Overworld" - teleporter: [0x05, 1] -- name: Overworld - Focus Tower Aquaria - id: 450 - area: 0 - type: "Overworld" - teleporter: [0x13, 1] -- name: Overworld - Libra Temple - id: 451 - area: 0 - type: "Overworld" - teleporter: [0x07, 1] -- name: Overworld - Aquaria - id: 452 - area: 0 - type: "Overworld" - teleporter: [0x08, 8] -- name: Overworld - Wintry Cave - id: 453 - area: 0 - type: "Overworld" - teleporter: [0x0A, 1] -- name: Overworld - Life Temple - id: 454 - area: 0 - type: "Overworld" - teleporter: [0x0B, 1] -- name: Overworld - Falls Basin - id: 455 - area: 0 - type: "Overworld" - teleporter: [0x0C, 1] -- name: Overworld - Ice Pyramid - id: 456 - area: 0 - type: "Overworld" - teleporter: [0x0D, 1] # Will be switched to a script -- name: Overworld - Spencer's Place - id: 457 - area: 0 - type: "Overworld" - teleporter: [0x30, 8] -- name: Overworld - Wintry Temple - id: 458 - area: 0 - type: "Overworld" - teleporter: [0x10, 1] -- name: Overworld - Focus Tower Frozen Strip - id: 459 - area: 0 - type: "Overworld" - teleporter: [0x11, 1] -- name: Overworld - Focus Tower Fireburg - id: 460 - area: 0 - type: "Overworld" - teleporter: [0x12, 1] -- name: Overworld - Fireburg - id: 461 - area: 0 - type: "Overworld" - teleporter: [0x14, 1] -- name: Overworld - Mine - id: 462 - area: 0 - type: "Overworld" - teleporter: [0x15, 1] -- name: Overworld - Sealed Temple - id: 463 - area: 0 - type: "Overworld" - teleporter: [0x16, 1] -- name: Overworld - Volcano - id: 464 - area: 0 - type: "Overworld" - teleporter: [0x17, 1] -- name: Overworld - Lava Dome - id: 465 - area: 0 - type: "Overworld" - teleporter: [0x18, 1] -- name: Overworld - Focus Tower Windia - id: 466 - area: 0 - type: "Overworld" - teleporter: [0x06, 1] -- name: Overworld - Rope Bridge - id: 467 - area: 0 - type: "Overworld" - teleporter: [0x19, 1] -- name: Overworld - Alive Forest - id: 468 - area: 0 - type: "Overworld" - teleporter: [0x1A, 1] -- name: Overworld - Giant Tree - id: 469 - area: 0 - type: "Overworld" - teleporter: [0x1B, 1] -- name: Overworld - Kaidge Temple - id: 470 - area: 0 - type: "Overworld" - teleporter: [0x1C, 1] -- name: Overworld - Windia - id: 471 - area: 0 - type: "Overworld" - teleporter: [0x1D, 1] -- name: Overworld - Windhole Temple - id: 472 - area: 0 - type: "Overworld" - teleporter: [0x1E, 1] -- name: Overworld - Mount Gale - id: 473 - area: 0 - type: "Overworld" - teleporter: [0x1F, 1] -- name: Overworld - Pazuzu Tower - id: 474 - area: 0 - type: "Overworld" - teleporter: [0x20, 1] -- name: Overworld - Ship Dock - id: 475 - area: 0 - type: "Overworld" - teleporter: [0x3E, 1] -- name: Overworld - Doom Castle - id: 476 - area: 0 - type: "Overworld" - teleporter: [0x21, 1] -- name: Overworld - Light Temple - id: 477 - area: 0 - type: "Overworld" - teleporter: [0x22, 1] -- name: Overworld - Mac Ship - id: 478 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Overworld - Mac Ship Doom - id: 479 - area: 0 - type: "Overworld" - teleporter: [0x24, 1] -- name: Dummy House - Bed Script - id: 480 - area: 17 - coordinates: [0x28, 0x38] - teleporter: [1, 8] -- name: Dummy House - Entrance - id: 481 - area: 17 - coordinates: [0x29, 0x3B] - teleporter: [0, 10] #None diff --git a/worlds/ffmq/data/rooms.py b/worlds/ffmq/data/rooms.py new file mode 100644 index 000000000000..38634f107679 --- /dev/null +++ b/worlds/ffmq/data/rooms.py @@ -0,0 +1,2 @@ +rooms = [{'name': 'Overworld', 'id': 0, 'type': 'Overworld', 'game_objects': [], 'links': [{'target_room': 220, 'access': []}]}, {'name': 'Subregion Foresta', 'id': 220, 'type': 'Subregion', 'region': 'Foresta', 'game_objects': [{'name': 'Foresta South Battlefield', 'object_id': 1, 'location': 'ForestaSouthBattlefield', 'location_slot': 'ForestaSouthBattlefield', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Foresta West Battlefield', 'object_id': 2, 'location': 'ForestaWestBattlefield', 'location_slot': 'ForestaWestBattlefield', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Foresta East Battlefield', 'object_id': 3, 'location': 'ForestaEastBattlefield', 'location_slot': 'ForestaEastBattlefield', 'type': 'BattlefieldGp', 'access': []}], 'links': [{'target_room': 15, 'location': 'LevelForest', 'location_slot': 'LevelForest', 'entrance': 445, 'teleporter': [46, 8], 'access': []}, {'target_room': 16, 'location': 'Foresta', 'location_slot': 'Foresta', 'entrance': 446, 'teleporter': [2, 1], 'access': []}, {'target_room': 24, 'location': 'SandTemple', 'location_slot': 'SandTemple', 'entrance': 447, 'teleporter': [3, 1], 'access': []}, {'target_room': 25, 'location': 'BoneDungeon', 'location_slot': 'BoneDungeon', 'entrance': 448, 'teleporter': [4, 1], 'access': []}, {'target_room': 3, 'location': 'FocusTowerForesta', 'location_slot': 'FocusTowerForesta', 'entrance': 449, 'teleporter': [5, 1], 'access': []}, {'target_room': 221, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['RiverCoin']}, {'target_room': 226, 'access': ['SunCoin']}]}, {'name': 'Subregion Aquaria', 'id': 221, 'type': 'Subregion', 'region': 'Aquaria', 'game_objects': [{'name': 'South of Libra Temple Battlefield', 'object_id': 4, 'location': 'AquariaBattlefield01', 'location_slot': 'AquariaBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'East of Libra Temple Battlefield', 'object_id': 5, 'location': 'AquariaBattlefield02', 'location_slot': 'AquariaBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'South of Aquaria Battlefield', 'object_id': 6, 'location': 'AquariaBattlefield03', 'location_slot': 'AquariaBattlefield03', 'type': 'BattlefieldItem', 'access': []}, {'name': 'South of Wintry Cave Battlefield', 'object_id': 7, 'location': 'WintryBattlefield01', 'location_slot': 'WintryBattlefield01', 'type': 'BattlefieldXp', 'access': []}, {'name': 'West of Wintry Cave Battlefield', 'object_id': 8, 'location': 'WintryBattlefield02', 'location_slot': 'WintryBattlefield02', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Ice Pyramid Battlefield', 'object_id': 9, 'location': 'PyramidBattlefield01', 'location_slot': 'PyramidBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 10, 'location': 'FocusTowerAquaria', 'location_slot': 'FocusTowerAquaria', 'entrance': 450, 'teleporter': [19, 1], 'access': []}, {'target_room': 39, 'location': 'LibraTemple', 'location_slot': 'LibraTemple', 'entrance': 451, 'teleporter': [7, 1], 'access': []}, {'target_room': 40, 'location': 'Aquaria', 'location_slot': 'Aquaria', 'entrance': 452, 'teleporter': [8, 8], 'access': []}, {'target_room': 45, 'location': 'WintryCave', 'location_slot': 'WintryCave', 'entrance': 453, 'teleporter': [10, 1], 'access': []}, {'target_room': 52, 'location': 'FallsBasin', 'location_slot': 'FallsBasin', 'entrance': 455, 'teleporter': [12, 1], 'access': []}, {'target_room': 54, 'location': 'IcePyramid', 'location_slot': 'IcePyramid', 'entrance': 456, 'teleporter': [13, 1], 'access': []}, {'target_room': 220, 'access': ['SandCoin']}, {'target_room': 224, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Life Temple', 'id': 222, 'type': 'Subregion', 'region': 'LifeTemple', 'game_objects': [], 'links': [{'target_room': 51, 'location': 'LifeTemple', 'location_slot': 'LifeTemple', 'entrance': 454, 'teleporter': [11, 1], 'access': []}]}, {'name': 'Subregion Frozen Fields', 'id': 223, 'type': 'Subregion', 'region': 'AquariaFrozenField', 'game_objects': [{'name': 'North of Libra Temple Battlefield', 'object_id': 10, 'location': 'LibraBattlefield01', 'location_slot': 'LibraBattlefield01', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Aquaria Frozen Field Battlefield', 'object_id': 11, 'location': 'LibraBattlefield02', 'location_slot': 'LibraBattlefield02', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 74, 'location': 'WintryTemple', 'location_slot': 'WintryTemple', 'entrance': 458, 'teleporter': [16, 1], 'access': []}, {'target_room': 14, 'location': 'FocusTowerFrozen', 'location_slot': 'FocusTowerFrozen', 'entrance': 459, 'teleporter': [17, 1], 'access': []}, {'target_room': 221, 'access': []}, {'target_room': 225, 'access': ['SummerAquaria', 'DualheadHydra']}]}, {'name': 'Subregion Fireburg', 'id': 224, 'type': 'Subregion', 'region': 'Fireburg', 'game_objects': [{'name': 'Path to Fireburg Southern Battlefield', 'object_id': 12, 'location': 'FireburgBattlefield01', 'location_slot': 'FireburgBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Path to Fireburg Central Battlefield', 'object_id': 13, 'location': 'FireburgBattlefield02', 'location_slot': 'FireburgBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Path to Fireburg Northern Battlefield', 'object_id': 14, 'location': 'FireburgBattlefield03', 'location_slot': 'FireburgBattlefield03', 'type': 'BattlefieldXp', 'access': []}, {'name': 'Sealed Temple Battlefield', 'object_id': 15, 'location': 'MineBattlefield01', 'location_slot': 'MineBattlefield01', 'type': 'BattlefieldGp', 'access': []}, {'name': 'Mine Battlefield', 'object_id': 16, 'location': 'MineBattlefield02', 'location_slot': 'MineBattlefield02', 'type': 'BattlefieldItem', 'access': []}, {'name': 'Boulder Battlefield', 'object_id': 17, 'location': 'MineBattlefield03', 'location_slot': 'MineBattlefield03', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 13, 'location': 'FocusTowerFireburg', 'location_slot': 'FocusTowerFireburg', 'entrance': 460, 'teleporter': [18, 1], 'access': []}, {'target_room': 76, 'location': 'Fireburg', 'location_slot': 'Fireburg', 'entrance': 461, 'teleporter': [20, 1], 'access': []}, {'target_room': 84, 'location': 'Mine', 'location_slot': 'Mine', 'entrance': 462, 'teleporter': [21, 1], 'access': []}, {'target_room': 92, 'location': 'SealedTemple', 'location_slot': 'SealedTemple', 'entrance': 463, 'teleporter': [22, 1], 'access': []}, {'target_room': 93, 'location': 'Volcano', 'location_slot': 'Volcano', 'entrance': 464, 'teleporter': [23, 1], 'access': []}, {'target_room': 100, 'location': 'LavaDome', 'location_slot': 'LavaDome', 'entrance': 465, 'teleporter': [24, 1], 'access': []}, {'target_room': 220, 'access': ['RiverCoin']}, {'target_room': 221, 'access': ['SandCoin', 'RiverCoin']}, {'target_room': 226, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 225, 'access': ['DualheadHydra']}]}, {'name': 'Subregion Volcano Battlefield', 'id': 225, 'type': 'Subregion', 'region': 'VolcanoBattlefield', 'game_objects': [{'name': 'Volcano Battlefield', 'object_id': 18, 'location': 'VolcanoBattlefield01', 'location_slot': 'VolcanoBattlefield01', 'type': 'BattlefieldXp', 'access': []}], 'links': [{'target_room': 224, 'access': ['DualheadHydra']}, {'target_room': 223, 'access': ['SummerAquaria']}]}, {'name': 'Subregion Windia', 'id': 226, 'type': 'Subregion', 'region': 'Windia', 'game_objects': [{'name': 'Kaidge Temple Battlefield', 'object_id': 19, 'location': 'WindiaBattlefield01', 'location_slot': 'WindiaBattlefield01', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}, {'name': 'South of Windia Battlefield', 'object_id': 20, 'location': 'WindiaBattlefield02', 'location_slot': 'WindiaBattlefield02', 'type': 'BattlefieldXp', 'access': ['SandCoin', 'RiverCoin']}], 'links': [{'target_room': 9, 'location': 'FocusTowerWindia', 'location_slot': 'FocusTowerWindia', 'entrance': 466, 'teleporter': [6, 1], 'access': []}, {'target_room': 123, 'location': 'RopeBridge', 'location_slot': 'RopeBridge', 'entrance': 467, 'teleporter': [25, 1], 'access': []}, {'target_room': 124, 'location': 'AliveForest', 'location_slot': 'AliveForest', 'entrance': 468, 'teleporter': [26, 1], 'access': []}, {'target_room': 125, 'location': 'GiantTree', 'location_slot': 'GiantTree', 'entrance': 469, 'teleporter': [27, 1], 'access': ['Barred']}, {'target_room': 152, 'location': 'KaidgeTemple', 'location_slot': 'KaidgeTemple', 'entrance': 470, 'teleporter': [28, 1], 'access': []}, {'target_room': 156, 'location': 'Windia', 'location_slot': 'Windia', 'entrance': 471, 'teleporter': [29, 1], 'access': []}, {'target_room': 154, 'location': 'WindholeTemple', 'location_slot': 'WindholeTemple', 'entrance': 472, 'teleporter': [30, 1], 'access': []}, {'target_room': 155, 'location': 'MountGale', 'location_slot': 'MountGale', 'entrance': 473, 'teleporter': [31, 1], 'access': []}, {'target_room': 166, 'location': 'PazuzusTower', 'location_slot': 'PazuzusTower', 'entrance': 474, 'teleporter': [32, 1], 'access': []}, {'target_room': 220, 'access': ['SunCoin']}, {'target_room': 221, 'access': ['SandCoin', 'SunCoin']}, {'target_room': 224, 'access': ['RiverCoin', 'SunCoin']}, {'target_room': 227, 'access': ['RainbowBridge']}]}, {'name': "Subregion Spencer's Cave", 'id': 227, 'type': 'Subregion', 'region': 'SpencerCave', 'game_objects': [], 'links': [{'target_room': 73, 'location': 'SpencersPlace', 'location_slot': 'SpencersPlace', 'entrance': 457, 'teleporter': [48, 8], 'access': []}, {'target_room': 226, 'access': ['RainbowBridge']}]}, {'name': 'Subregion Ship Dock', 'id': 228, 'type': 'Subregion', 'region': 'ShipDock', 'game_objects': [], 'links': [{'target_room': 186, 'location': 'ShipDock', 'location_slot': 'ShipDock', 'entrance': 475, 'teleporter': [62, 1], 'access': []}, {'target_room': 229, 'access': ['ShipLiberated', 'ShipDockAccess']}]}, {'name': "Subregion Mac's Ship", 'id': 229, 'type': 'Subregion', 'region': 'MacShip', 'game_objects': [], 'links': [{'target_room': 187, 'location': 'MacsShip', 'location_slot': 'MacsShip', 'entrance': 478, 'teleporter': [36, 1], 'access': []}, {'target_room': 228, 'access': ['ShipLiberated', 'ShipDockAccess']}, {'target_room': 231, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Subregion Light Temple', 'id': 230, 'type': 'Subregion', 'region': 'LightTemple', 'game_objects': [], 'links': [{'target_room': 185, 'location': 'LightTemple', 'location_slot': 'LightTemple', 'entrance': 477, 'teleporter': [35, 1], 'access': []}]}, {'name': 'Subregion Doom Castle', 'id': 231, 'type': 'Subregion', 'region': 'DoomCastle', 'game_objects': [], 'links': [{'target_room': 1, 'location': 'DoomCastle', 'location_slot': 'DoomCastle', 'entrance': 476, 'teleporter': [33, 1], 'access': []}, {'target_room': 187, 'location': 'MacsShipDoom', 'location_slot': 'MacsShipDoom', 'entrance': 479, 'teleporter': [36, 1], 'access': ['Barred']}, {'target_room': 229, 'access': ['ShipLoaned', 'ShipDockAccess', 'ShipSteeringWheel']}]}, {'name': 'Doom Castle - Sand Floor', 'id': 1, 'game_objects': [{'name': 'Doom Castle B2 - Southeast Chest', 'object_id': 1, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Doom Castle B2 - Bone Ledge Box', 'object_id': 30, 'type': 'Box', 'access': []}, {'name': 'Doom Castle B2 - Hook Platform Box', 'object_id': 31, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 231, 'entrance': 1, 'teleporter': [1, 6], 'access': []}, {'target_room': 5, 'entrance': 0, 'teleporter': [0, 0], 'access': ['DragonClaw', 'MegaGrenade']}]}, {'name': 'Doom Castle - Aero Room', 'id': 2, 'game_objects': [{'name': 'Doom Castle B2 - Sun Door Chest', 'object_id': 0, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 4, 'entrance': 2, 'teleporter': [1, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Main Loop', 'id': 3, 'game_objects': [], 'links': [{'target_room': 220, 'entrance': 3, 'teleporter': [2, 6], 'access': []}, {'target_room': 6, 'entrance': 4, 'teleporter': [4, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Aero Corridor', 'id': 4, 'game_objects': [], 'links': [{'target_room': 9, 'entrance': 5, 'teleporter': [5, 0], 'access': []}, {'target_room': 2, 'entrance': 6, 'teleporter': [8, 0], 'access': []}]}, {'name': 'Focus Tower B1 - Inner Loop', 'id': 5, 'game_objects': [], 'links': [{'target_room': 1, 'entrance': 8, 'teleporter': [7, 0], 'access': []}, {'target_room': 201, 'entrance': 7, 'teleporter': [6, 0], 'access': []}]}, {'name': 'Focus Tower 1F Main Lobby', 'id': 6, 'game_objects': [{'name': 'Focus Tower 1F - Main Lobby Box', 'object_id': 33, 'type': 'Box', 'access': []}], 'links': [{'target_room': 3, 'entrance': 11, 'teleporter': [11, 0], 'access': []}, {'target_room': 7, 'access': ['SandCoin']}, {'target_room': 8, 'access': ['RiverCoin']}, {'target_room': 9, 'access': ['SunCoin']}]}, {'name': 'Focus Tower 1F SandCoin Room', 'id': 7, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SandCoin']}, {'target_room': 10, 'entrance': 10, 'teleporter': [10, 0], 'access': []}]}, {'name': 'Focus Tower 1F RiverCoin Room', 'id': 8, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['RiverCoin']}, {'target_room': 11, 'entrance': 14, 'teleporter': [14, 0], 'access': []}]}, {'name': 'Focus Tower 1F SunCoin Room', 'id': 9, 'game_objects': [], 'links': [{'target_room': 6, 'access': ['SunCoin']}, {'target_room': 4, 'entrance': 12, 'teleporter': [12, 0], 'access': []}, {'target_room': 226, 'entrance': 9, 'teleporter': [3, 6], 'access': []}]}, {'name': 'Focus Tower 1F SkyCoin Room', 'id': 201, 'game_objects': [], 'links': [{'target_room': 195, 'entrance': 13, 'teleporter': [13, 0], 'access': ['SkyCoin', 'FlamerusRex', 'IceGolem', 'DualheadHydra', 'Pazuzu']}, {'target_room': 5, 'entrance': 15, 'teleporter': [15, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Sand Coin Passage', 'id': 10, 'game_objects': [{'name': 'Focus Tower 2F - Sand Door Chest', 'object_id': 3, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 221, 'entrance': 16, 'teleporter': [4, 6], 'access': []}, {'target_room': 7, 'entrance': 17, 'teleporter': [17, 0], 'access': []}]}, {'name': 'Focus Tower 2F - River Coin Passage', 'id': 11, 'game_objects': [], 'links': [{'target_room': 8, 'entrance': 18, 'teleporter': [18, 0], 'access': []}, {'target_room': 13, 'entrance': 19, 'teleporter': [20, 0], 'access': []}]}, {'name': 'Focus Tower 2F - Venus Chest Room', 'id': 12, 'game_objects': [{'name': 'Focus Tower 2F - Back Door Chest', 'object_id': 2, 'type': 'Chest', 'access': []}, {'name': 'Focus Tower 2F - Venus Chest', 'object_id': 9, 'type': 'NPC', 'access': ['Bomb', 'VenusKey']}], 'links': [{'target_room': 14, 'entrance': 20, 'teleporter': [19, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Lower Floor', 'id': 13, 'game_objects': [{'name': 'Focus Tower 3F - River Door Box', 'object_id': 34, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 22, 'teleporter': [6, 6], 'access': []}, {'target_room': 11, 'entrance': 23, 'teleporter': [24, 0], 'access': []}]}, {'name': 'Focus Tower 3F - Upper Floor', 'id': 14, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 24, 'teleporter': [5, 6], 'access': []}, {'target_room': 12, 'entrance': 25, 'teleporter': [23, 0], 'access': []}]}, {'name': 'Level Forest', 'id': 15, 'game_objects': [{'name': 'Level Forest - Northwest Box', 'object_id': 40, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Northeast Box', 'object_id': 41, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Middle Box', 'object_id': 42, 'type': 'Box', 'access': []}, {'name': 'Level Forest - Southwest Box', 'object_id': 43, 'type': 'Box', 'access': ['Axe']}, {'name': 'Level Forest - Southeast Box', 'object_id': 44, 'type': 'Box', 'access': ['Axe']}, {'name': 'Minotaur', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Minotaur'], 'access': ['Kaeli1']}, {'name': 'Level Forest - Old Man', 'object_id': 0, 'type': 'NPC', 'access': []}, {'name': 'Level Forest - Kaeli', 'object_id': 1, 'type': 'NPC', 'access': ['Kaeli1', 'Minotaur']}], 'links': [{'target_room': 220, 'entrance': 28, 'teleporter': [25, 0], 'access': []}]}, {'name': 'Foresta', 'id': 16, 'game_objects': [{'name': 'Foresta - Outside Box', 'object_id': 45, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 220, 'entrance': 38, 'teleporter': [31, 0], 'access': []}, {'target_room': 17, 'entrance': 44, 'teleporter': [0, 5], 'access': []}, {'target_room': 18, 'entrance': 42, 'teleporter': [32, 4], 'access': []}, {'target_room': 19, 'entrance': 43, 'teleporter': [33, 0], 'access': []}, {'target_room': 20, 'entrance': 45, 'teleporter': [1, 5], 'access': []}]}, {'name': "Kaeli's House", 'id': 17, 'game_objects': [{'name': "Foresta - Kaeli's House Box", 'object_id': 46, 'type': 'Box', 'access': []}, {'name': 'Kaeli Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli1'], 'access': ['TreeWither']}, {'name': 'Kaeli 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Kaeli2'], 'access': ['Kaeli1', 'Minotaur', 'Elixir']}], 'links': [{'target_room': 16, 'entrance': 46, 'teleporter': [86, 3], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Main", 'id': 18, 'game_objects': [], 'links': [{'target_room': 19, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 47, 'teleporter': [34, 0], 'access': []}]}, {'name': "Foresta Houses - Old Man's House Back", 'id': 19, 'game_objects': [{'name': 'Foresta - Old Man House Chest', 'object_id': 5, 'type': 'Chest', 'access': []}, {'name': 'Old Man Barrel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['BarrelPushed'], 'access': []}], 'links': [{'target_room': 18, 'access': ['BarrelPushed']}, {'target_room': 16, 'entrance': 48, 'teleporter': [35, 0], 'access': []}]}, {'name': 'Foresta Houses - Rest House', 'id': 20, 'game_objects': [{'name': 'Foresta - Rest House Box', 'object_id': 47, 'type': 'Box', 'access': []}], 'links': [{'target_room': 16, 'entrance': 50, 'teleporter': [87, 3], 'access': []}]}, {'name': 'Libra Treehouse', 'id': 21, 'game_objects': [{'name': 'Alive Forest - Libra Treehouse Box', 'object_id': 50, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 51, 'teleporter': [67, 8], 'access': ['LibraCrest']}]}, {'name': 'Gemini Treehouse', 'id': 22, 'game_objects': [{'name': 'Alive Forest - Gemini Treehouse Box', 'object_id': 51, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 52, 'teleporter': [68, 8], 'access': ['GeminiCrest']}]}, {'name': 'Mobius Treehouse', 'id': 23, 'game_objects': [{'name': 'Alive Forest - Mobius Treehouse West Box', 'object_id': 48, 'type': 'Box', 'access': []}, {'name': 'Alive Forest - Mobius Treehouse East Box', 'object_id': 49, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 53, 'teleporter': [69, 8], 'access': ['MobiusCrest']}]}, {'name': 'Sand Temple', 'id': 24, 'game_objects': [{'name': 'Tristam Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Tristam'], 'access': []}], 'links': [{'target_room': 220, 'entrance': 54, 'teleporter': [36, 0], 'access': []}]}, {'name': 'Bone Dungeon 1F', 'id': 25, 'game_objects': [{'name': 'Bone Dungeon 1F - Entrance Room West Box', 'object_id': 53, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room Middle Box', 'object_id': 54, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon 1F - Entrance Room East Box', 'object_id': 55, 'type': 'Box', 'access': []}], 'links': [{'target_room': 220, 'entrance': 55, 'teleporter': [37, 0], 'access': []}, {'target_room': 26, 'entrance': 56, 'teleporter': [2, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Waterway', 'id': 26, 'game_objects': [{'name': 'Bone Dungeon B1 - Skull Chest', 'object_id': 6, 'type': 'Chest', 'access': ['Bomb']}, {'name': 'Bone Dungeon B1 - Tristam', 'object_id': 2, 'type': 'NPC', 'access': ['Tristam']}, {'name': 'Tristam Bone Dungeon Item Given', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TristamBoneItemGiven'], 'access': ['Tristam']}], 'links': [{'target_room': 25, 'entrance': 59, 'teleporter': [88, 3], 'access': []}, {'target_room': 28, 'entrance': 57, 'teleporter': [3, 2], 'access': ['Bomb']}]}, {'name': 'Bone Dungeon B1 - Checker Room', 'id': 28, 'game_objects': [{'name': 'Bone Dungeon B1 - Checker Room Box', 'object_id': 56, 'type': 'Box', 'access': ['Bomb']}], 'links': [{'target_room': 26, 'entrance': 61, 'teleporter': [89, 3], 'access': []}, {'target_room': 30, 'entrance': 60, 'teleporter': [4, 2], 'access': []}]}, {'name': 'Bone Dungeon B1 - Hidden Room', 'id': 29, 'game_objects': [{'name': 'Bone Dungeon B1 - Ribcage Waterway Box', 'object_id': 57, 'type': 'Box', 'access': []}], 'links': [{'target_room': 31, 'entrance': 62, 'teleporter': [91, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - First Room', 'id': 30, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Alcove Box', 'object_id': 59, 'type': 'Box', 'access': []}, {'name': 'Long Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LongSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 28, 'entrance': 65, 'teleporter': [90, 3], 'access': []}, {'target_room': 31, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Second Room', 'id': 31, 'game_objects': [{'name': 'Bone Dungeon B2 - Spines Room Looped Hallway Box', 'object_id': 58, 'type': 'Box', 'access': []}, {'name': 'Short Spine', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShortSpineBombed'], 'access': ['Bomb']}], 'links': [{'target_room': 29, 'entrance': 63, 'teleporter': [5, 2], 'access': ['LongSpineBombed']}, {'target_room': 32, 'access': ['ShortSpineBombed']}, {'target_room': 30, 'access': ['LongSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Exploding Skull Room - Third Room', 'id': 32, 'game_objects': [], 'links': [{'target_room': 35, 'entrance': 64, 'teleporter': [6, 2], 'access': []}, {'target_room': 31, 'access': ['ShortSpineBombed']}]}, {'name': 'Bone Dungeon B2 - Box Room', 'id': 33, 'game_objects': [{'name': 'Bone Dungeon B2 - Lone Room Box', 'object_id': 61, 'type': 'Box', 'access': []}], 'links': [{'target_room': 36, 'entrance': 66, 'teleporter': [93, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Quake Room', 'id': 34, 'game_objects': [{'name': 'Bone Dungeon B2 - Penultimate Room Chest', 'object_id': 7, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 37, 'entrance': 67, 'teleporter': [94, 3], 'access': []}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - First Room', 'id': 35, 'game_objects': [{'name': 'Bone Dungeon B2 - Two Skulls Room Box', 'object_id': 60, 'type': 'Box', 'access': []}, {'name': 'Skull 1', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull1Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 32, 'entrance': 71, 'teleporter': [92, 3], 'access': []}, {'target_room': 36, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Second Room', 'id': 36, 'game_objects': [{'name': 'Skull 2', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Skull2Bombed'], 'access': ['Bomb']}], 'links': [{'target_room': 33, 'entrance': 68, 'teleporter': [7, 2], 'access': []}, {'target_room': 37, 'access': ['Skull2Bombed']}, {'target_room': 35, 'access': ['Skull1Bombed']}]}, {'name': 'Bone Dungeon B2 - Two Skulls Room - Third Room', 'id': 37, 'game_objects': [], 'links': [{'target_room': 34, 'entrance': 69, 'teleporter': [8, 2], 'access': []}, {'target_room': 38, 'entrance': 70, 'teleporter': [9, 2], 'access': ['Bomb']}, {'target_room': 36, 'access': ['Skull2Bombed']}]}, {'name': 'Bone Dungeon B2 - Boss Room', 'id': 38, 'game_objects': [{'name': 'Bone Dungeon B2 - North Box', 'object_id': 62, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - South Box', 'object_id': 63, 'type': 'Box', 'access': []}, {'name': 'Bone Dungeon B2 - Flamerus Rex Chest', 'object_id': 8, 'type': 'Chest', 'access': []}, {'name': "Bone Dungeon B2 - Tristam's Treasure Chest", 'object_id': 4, 'type': 'Chest', 'access': []}, {'name': 'Flamerus Rex', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FlamerusRex'], 'access': []}], 'links': [{'target_room': 37, 'entrance': 74, 'teleporter': [95, 3], 'access': []}]}, {'name': 'Libra Temple', 'id': 39, 'game_objects': [{'name': 'Libra Temple - Box', 'object_id': 64, 'type': 'Box', 'access': []}, {'name': 'Phoebe Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phoebe1'], 'access': []}], 'links': [{'target_room': 221, 'entrance': 75, 'teleporter': [13, 6], 'access': []}, {'target_room': 51, 'entrance': 76, 'teleporter': [59, 8], 'access': ['LibraCrest']}]}, {'name': 'Aquaria', 'id': 40, 'game_objects': [{'name': 'Summer Aquaria', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SummerAquaria'], 'access': ['WakeWater']}], 'links': [{'target_room': 221, 'entrance': 77, 'teleporter': [8, 6], 'access': []}, {'target_room': 41, 'entrance': 81, 'teleporter': [10, 5], 'access': []}, {'target_room': 42, 'entrance': 82, 'teleporter': [44, 4], 'access': []}, {'target_room': 44, 'entrance': 83, 'teleporter': [11, 5], 'access': []}, {'target_room': 71, 'entrance': 89, 'teleporter': [42, 0], 'access': ['SummerAquaria']}, {'target_room': 71, 'entrance': 90, 'teleporter': [43, 0], 'access': ['SummerAquaria']}]}, {'name': "Phoebe's House", 'id': 41, 'game_objects': [{'name': "Aquaria - Phoebe's House Chest", 'object_id': 65, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 93, 'teleporter': [5, 8], 'access': []}]}, {'name': 'Aquaria Vendor House', 'id': 42, 'game_objects': [{'name': 'Aquaria - Vendor', 'object_id': 4, 'type': 'NPC', 'access': []}, {'name': 'Aquaria - Vendor House Box', 'object_id': 66, 'type': 'Box', 'access': []}], 'links': [{'target_room': 40, 'entrance': 94, 'teleporter': [40, 8], 'access': []}, {'target_room': 43, 'entrance': 95, 'teleporter': [47, 0], 'access': []}]}, {'name': 'Aquaria Gemini Room', 'id': 43, 'game_objects': [], 'links': [{'target_room': 42, 'entrance': 97, 'teleporter': [48, 0], 'access': []}, {'target_room': 81, 'entrance': 96, 'teleporter': [72, 8], 'access': ['GeminiCrest']}]}, {'name': 'Aquaria INN', 'id': 44, 'game_objects': [], 'links': [{'target_room': 40, 'entrance': 98, 'teleporter': [75, 8], 'access': []}]}, {'name': 'Wintry Cave 1F - East Ledge', 'id': 45, 'game_objects': [{'name': 'Wintry Cave 1F - North Box', 'object_id': 67, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Entrance Box', 'object_id': 70, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 1F - Slippery Cliff Box', 'object_id': 68, 'type': 'Box', 'access': ['Claw']}, {'name': 'Wintry Cave 1F - Phoebe', 'object_id': 5, 'type': 'NPC', 'access': ['Phoebe1']}], 'links': [{'target_room': 221, 'entrance': 99, 'teleporter': [49, 0], 'access': []}, {'target_room': 49, 'entrance': 100, 'teleporter': [14, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - Central Space', 'id': 46, 'game_objects': [{'name': 'Wintry Cave 1F - Scenic Overlook Box', 'object_id': 69, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 45, 'access': ['Claw']}, {'target_room': 47, 'access': ['Claw']}]}, {'name': 'Wintry Cave 1F - West Ledge', 'id': 47, 'game_objects': [], 'links': [{'target_room': 48, 'entrance': 101, 'teleporter': [15, 2], 'access': ['Bomb']}, {'target_room': 46, 'access': ['Claw']}]}, {'name': 'Wintry Cave 2F', 'id': 48, 'game_objects': [{'name': 'Wintry Cave 2F - West Left Box', 'object_id': 71, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - West Right Box', 'object_id': 72, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Left Box', 'object_id': 73, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 2F - East Right Box', 'object_id': 74, 'type': 'Box', 'access': []}], 'links': [{'target_room': 47, 'entrance': 104, 'teleporter': [97, 3], 'access': []}, {'target_room': 50, 'entrance': 103, 'teleporter': [50, 0], 'access': []}]}, {'name': 'Wintry Cave 3F Top', 'id': 49, 'game_objects': [{'name': 'Wintry Cave 3F - West Box', 'object_id': 75, 'type': 'Box', 'access': []}, {'name': 'Wintry Cave 3F - East Box', 'object_id': 76, 'type': 'Box', 'access': []}], 'links': [{'target_room': 45, 'entrance': 105, 'teleporter': [96, 3], 'access': []}]}, {'name': 'Wintry Cave 3F Bottom', 'id': 50, 'game_objects': [{'name': 'Wintry Cave 3F - Squidite Chest', 'object_id': 9, 'type': 'Chest', 'access': ['Phanquid']}, {'name': 'Phanquid', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Phanquid'], 'access': []}, {'name': 'Wintry Cave 3F - Before Boss Box', 'object_id': 77, 'type': 'Box', 'access': []}], 'links': [{'target_room': 48, 'entrance': 106, 'teleporter': [51, 0], 'access': []}]}, {'name': 'Life Temple', 'id': 51, 'game_objects': [{'name': 'Life Temple - Box', 'object_id': 78, 'type': 'Box', 'access': []}, {'name': 'Life Temple - Mysterious Man', 'object_id': 6, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 222, 'entrance': 107, 'teleporter': [14, 6], 'access': []}, {'target_room': 39, 'entrance': 108, 'teleporter': [60, 8], 'access': ['LibraCrest']}]}, {'name': 'Fall Basin', 'id': 52, 'game_objects': [{'name': 'Falls Basin - Snow Crab Chest', 'object_id': 10, 'type': 'Chest', 'access': ['FreezerCrab']}, {'name': 'Freezer Crab', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['FreezerCrab'], 'access': []}, {'name': 'Falls Basin - Box', 'object_id': 79, 'type': 'Box', 'access': []}], 'links': [{'target_room': 221, 'entrance': 111, 'teleporter': [53, 0], 'access': []}]}, {'name': 'Ice Pyramid B1 Taunt Room', 'id': 53, 'game_objects': [{'name': 'Ice Pyramid B1 - Chest', 'object_id': 11, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid B1 - West Box', 'object_id': 80, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - North Box', 'object_id': 81, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid B1 - East Box', 'object_id': 82, 'type': 'Box', 'access': []}], 'links': [{'target_room': 68, 'entrance': 113, 'teleporter': [55, 0], 'access': []}]}, {'name': 'Ice Pyramid 1F Maze Lobby', 'id': 54, 'game_objects': [{'name': 'Ice Pyramid 1F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid1FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 221, 'entrance': 114, 'teleporter': [56, 0], 'access': []}, {'target_room': 55, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 1F Maze', 'id': 55, 'game_objects': [{'name': 'Ice Pyramid 1F - East Alcove Chest', 'object_id': 13, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 1F - Sandwiched Alcove Box', 'object_id': 83, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Left Box', 'object_id': 84, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 1F - Southwest Right Box', 'object_id': 85, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 116, 'teleporter': [57, 0], 'access': []}, {'target_room': 57, 'entrance': 117, 'teleporter': [58, 0], 'access': []}, {'target_room': 58, 'entrance': 118, 'teleporter': [59, 0], 'access': []}, {'target_room': 59, 'entrance': 119, 'teleporter': [60, 0], 'access': []}, {'target_room': 60, 'entrance': 120, 'teleporter': [61, 0], 'access': []}, {'target_room': 54, 'access': ['IcePyramid1FStatue']}]}, {'name': 'Ice Pyramid 2F South Tiled Room', 'id': 56, 'game_objects': [{'name': 'Ice Pyramid 2F - South Side Glass Door Box', 'object_id': 87, 'type': 'Box', 'access': ['Sword']}, {'name': 'Ice Pyramid 2F - South Side East Box', 'object_id': 91, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 122, 'teleporter': [62, 0], 'access': []}, {'target_room': 61, 'entrance': 123, 'teleporter': [67, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F West Room', 'id': 57, 'game_objects': [{'name': 'Ice Pyramid 2F - Northwest Room Box', 'object_id': 90, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 124, 'teleporter': [63, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Center Room', 'id': 58, 'game_objects': [{'name': 'Ice Pyramid 2F - Center Room Box', 'object_id': 86, 'type': 'Box', 'access': []}], 'links': [{'target_room': 55, 'entrance': 125, 'teleporter': [64, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F Small North Room', 'id': 59, 'game_objects': [{'name': 'Ice Pyramid 2F - North Room Glass Door Box', 'object_id': 88, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 126, 'teleporter': [65, 0], 'access': []}]}, {'name': 'Ice Pyramid 2F North Corridor', 'id': 60, 'game_objects': [{'name': 'Ice Pyramid 2F - North Corridor Glass Door Box', 'object_id': 89, 'type': 'Box', 'access': ['Sword']}], 'links': [{'target_room': 55, 'entrance': 127, 'teleporter': [66, 0], 'access': []}, {'target_room': 62, 'entrance': 128, 'teleporter': [68, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Two Boxes Room', 'id': 61, 'game_objects': [{'name': 'Ice Pyramid 3F - Staircase Dead End Left Box', 'object_id': 94, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Staircase Dead End Right Box', 'object_id': 95, 'type': 'Box', 'access': []}], 'links': [{'target_room': 56, 'entrance': 129, 'teleporter': [69, 0], 'access': []}]}, {'name': 'Ice Pyramid 3F Main Loop', 'id': 62, 'game_objects': [{'name': 'Ice Pyramid 3F - Inner Room North Box', 'object_id': 92, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Inner Room South Box', 'object_id': 93, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - East Alcove Box', 'object_id': 96, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F - Leapfrog Box', 'object_id': 97, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 3F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid3FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 60, 'entrance': 130, 'teleporter': [70, 0], 'access': []}, {'target_room': 63, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 3F Blocked Room', 'id': 63, 'game_objects': [], 'links': [{'target_room': 64, 'entrance': 131, 'teleporter': [71, 0], 'access': []}, {'target_room': 62, 'access': ['IcePyramid3FStatue']}]}, {'name': 'Ice Pyramid 4F Main Loop', 'id': 64, 'game_objects': [], 'links': [{'target_room': 66, 'entrance': 133, 'teleporter': [73, 0], 'access': []}, {'target_room': 63, 'entrance': 132, 'teleporter': [72, 0], 'access': []}, {'target_room': 65, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 4F Treasure Room', 'id': 65, 'game_objects': [{'name': 'Ice Pyramid 4F - Chest', 'object_id': 12, 'type': 'Chest', 'access': []}, {'name': 'Ice Pyramid 4F - Northwest Box', 'object_id': 98, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Left Box', 'object_id': 99, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - West Right Box', 'object_id': 100, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Left Box', 'object_id': 101, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - South Right Box', 'object_id': 102, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Left Box', 'object_id': 103, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F - East Right Box', 'object_id': 104, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 4F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid4FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 64, 'access': ['IcePyramid4FStatue']}]}, {'name': 'Ice Pyramid 5F Leap of Faith Room', 'id': 66, 'game_objects': [{'name': 'Ice Pyramid 5F - Glass Door Left Box', 'object_id': 105, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - West Ledge Box', 'object_id': 106, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Shelf Box', 'object_id': 107, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - South Leapfrog Box', 'object_id': 108, 'type': 'Box', 'access': []}, {'name': 'Ice Pyramid 5F - Glass Door Right Box', 'object_id': 109, 'type': 'Box', 'access': ['IcePyramid5FStatue']}, {'name': 'Ice Pyramid 5F - North Box', 'object_id': 110, 'type': 'Box', 'access': []}], 'links': [{'target_room': 64, 'entrance': 134, 'teleporter': [74, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 53, 'access': ['Bomb', 'Claw', 'Sword']}]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem', 'id': 67, 'game_objects': [{'name': 'Ice Pyramid 5F Statue', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IcePyramid5FStatue'], 'access': ['Sword']}], 'links': [{'target_room': 69, 'entrance': 137, 'teleporter': [76, 0], 'access': []}, {'target_room': 65, 'access': []}, {'target_room': 70, 'entrance': 136, 'teleporter': [75, 0], 'access': []}]}, {'name': 'Ice Pyramid Climbing Wall Room Lower Space', 'id': 68, 'game_objects': [], 'links': [{'target_room': 53, 'entrance': 139, 'teleporter': [78, 0], 'access': []}, {'target_room': 69, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Climbing Wall Room Upper Space', 'id': 69, 'game_objects': [], 'links': [{'target_room': 67, 'entrance': 140, 'teleporter': [79, 0], 'access': []}, {'target_room': 68, 'access': ['Claw']}]}, {'name': 'Ice Pyramid Ice Golem Room', 'id': 70, 'game_objects': [{'name': 'Ice Pyramid 6F - Ice Golem Chest', 'object_id': 14, 'type': 'Chest', 'access': ['IceGolem']}, {'name': 'Ice Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['IceGolem'], 'access': []}], 'links': [{'target_room': 67, 'entrance': 141, 'teleporter': [80, 0], 'access': []}, {'target_room': 66, 'access': []}]}, {'name': 'Spencer Waterfall', 'id': 71, 'game_objects': [], 'links': [{'target_room': 72, 'entrance': 143, 'teleporter': [81, 0], 'access': []}, {'target_room': 40, 'entrance': 145, 'teleporter': [82, 0], 'access': []}, {'target_room': 40, 'entrance': 148, 'teleporter': [83, 0], 'access': []}]}, {'name': 'Spencer Cave Normal Main', 'id': 72, 'game_objects': [{'name': "Spencer's Cave - Box", 'object_id': 111, 'type': 'Box', 'access': ['Claw']}, {'name': "Spencer's Cave - Spencer", 'object_id': 8, 'type': 'NPC', 'access': []}, {'name': "Spencer's Cave - Locked Chest", 'object_id': 13, 'type': 'NPC', 'access': ['VenusKey']}], 'links': [{'target_room': 71, 'entrance': 150, 'teleporter': [85, 0], 'access': []}]}, {'name': 'Spencer Cave Normal South Ledge', 'id': 73, 'game_objects': [{'name': "Collapse Spencer's Cave", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLiberated'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 227, 'entrance': 151, 'teleporter': [7, 6], 'access': []}, {'target_room': 203, 'access': ['MegaGrenade']}]}, {'name': 'Spencer Cave Caved In Main Loop', 'id': 203, 'game_objects': [], 'links': [{'target_room': 73, 'access': []}, {'target_room': 207, 'entrance': 156, 'teleporter': [36, 8], 'access': ['MobiusCrest']}, {'target_room': 204, 'access': ['Claw']}, {'target_room': 205, 'access': ['Bomb']}]}, {'name': 'Spencer Cave Caved In Waters', 'id': 204, 'game_objects': [{'name': 'Bomb Libra Block', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['SpencerCaveLibraBlockBombed'], 'access': ['MegaGrenade', 'Claw']}], 'links': [{'target_room': 203, 'access': ['Claw']}]}, {'name': 'Spencer Cave Caved In Libra Nook', 'id': 205, 'game_objects': [], 'links': [{'target_room': 206, 'entrance': 153, 'teleporter': [33, 8], 'access': ['LibraCrest']}]}, {'name': 'Spencer Cave Caved In Libra Corridor', 'id': 206, 'game_objects': [], 'links': [{'target_room': 205, 'entrance': 154, 'teleporter': [34, 8], 'access': ['LibraCrest']}, {'target_room': 207, 'access': ['SpencerCaveLibraBlockBombed']}]}, {'name': 'Spencer Cave Caved In Mobius Chest', 'id': 207, 'game_objects': [{'name': "Spencer's Cave - Mobius Chest", 'object_id': 15, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 203, 'entrance': 155, 'teleporter': [35, 8], 'access': ['MobiusCrest']}, {'target_room': 206, 'access': ['Bomb']}]}, {'name': 'Wintry Temple Outer Room', 'id': 74, 'game_objects': [], 'links': [{'target_room': 223, 'entrance': 157, 'teleporter': [15, 6], 'access': []}]}, {'name': 'Wintry Temple Inner Room', 'id': 75, 'game_objects': [{'name': 'Wintry Temple - West Box', 'object_id': 112, 'type': 'Box', 'access': []}, {'name': 'Wintry Temple - North Box', 'object_id': 113, 'type': 'Box', 'access': []}], 'links': [{'target_room': 92, 'entrance': 158, 'teleporter': [62, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Upper Plaza', 'id': 76, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 159, 'teleporter': [9, 6], 'access': []}, {'target_room': 80, 'entrance': 163, 'teleporter': [91, 0], 'access': []}, {'target_room': 77, 'entrance': 164, 'teleporter': [98, 8], 'access': []}, {'target_room': 82, 'entrance': 165, 'teleporter': [96, 8], 'access': []}, {'target_room': 208, 'access': ['Claw']}]}, {'name': 'Fireburg Lower Plaza', 'id': 208, 'game_objects': [{'name': 'Fireburg - Hidden Tunnel Box', 'object_id': 116, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'access': ['Claw']}, {'target_room': 78, 'entrance': 166, 'teleporter': [11, 8], 'access': ['MultiKey']}]}, {'name': "Reuben's House", 'id': 77, 'game_objects': [{'name': "Fireburg - Reuben's House Arion", 'object_id': 14, 'type': 'NPC', 'access': ['ReubenDadSaved']}, {'name': 'Reuben Companion', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Reuben1'], 'access': []}, {'name': "Fireburg - Reuben's House Box", 'object_id': 117, 'type': 'Box', 'access': []}], 'links': [{'target_room': 76, 'entrance': 167, 'teleporter': [98, 3], 'access': []}]}, {'name': "GrenadeMan's House", 'id': 78, 'game_objects': [{'name': 'Fireburg - Locked House Man', 'object_id': 12, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 208, 'entrance': 168, 'teleporter': [9, 8], 'access': ['MultiKey']}, {'target_room': 79, 'entrance': 169, 'teleporter': [93, 0], 'access': []}]}, {'name': "GrenadeMan's Mobius Room", 'id': 79, 'game_objects': [], 'links': [{'target_room': 78, 'entrance': 170, 'teleporter': [94, 0], 'access': []}, {'target_room': 161, 'entrance': 171, 'teleporter': [54, 8], 'access': ['MobiusCrest']}]}, {'name': 'Fireburg Vendor House', 'id': 80, 'game_objects': [{'name': 'Fireburg - Vendor', 'object_id': 11, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 76, 'entrance': 172, 'teleporter': [95, 0], 'access': []}, {'target_room': 81, 'entrance': 173, 'teleporter': [96, 0], 'access': []}]}, {'name': 'Fireburg Gemini Room', 'id': 81, 'game_objects': [], 'links': [{'target_room': 80, 'entrance': 174, 'teleporter': [97, 0], 'access': []}, {'target_room': 43, 'entrance': 175, 'teleporter': [45, 8], 'access': ['GeminiCrest']}]}, {'name': 'Fireburg Hotel Lobby', 'id': 82, 'game_objects': [{'name': 'Fireburg - Tristam', 'object_id': 10, 'type': 'NPC', 'access': ['Tristam', 'TristamBoneItemGiven']}], 'links': [{'target_room': 76, 'entrance': 177, 'teleporter': [99, 3], 'access': []}, {'target_room': 83, 'entrance': 176, 'teleporter': [213, 0], 'access': []}]}, {'name': 'Fireburg Hotel Beds', 'id': 83, 'game_objects': [], 'links': [{'target_room': 82, 'entrance': 178, 'teleporter': [214, 0], 'access': []}]}, {'name': 'Mine Exterior North West Platforms', 'id': 84, 'game_objects': [], 'links': [{'target_room': 224, 'entrance': 179, 'teleporter': [98, 0], 'access': []}, {'target_room': 88, 'entrance': 181, 'teleporter': [20, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}, {'target_room': 86, 'access': ['Claw']}, {'target_room': 87, 'access': ['Claw']}]}, {'name': 'Mine Exterior Central Ledge', 'id': 85, 'game_objects': [], 'links': [{'target_room': 90, 'entrance': 183, 'teleporter': [22, 2], 'access': ['Bomb']}, {'target_room': 84, 'access': ['Claw']}]}, {'name': 'Mine Exterior North Ledge', 'id': 86, 'game_objects': [], 'links': [{'target_room': 89, 'entrance': 182, 'teleporter': [21, 2], 'access': ['Bomb']}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Exterior South East Platforms', 'id': 87, 'game_objects': [{'name': 'Jinn', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Jinn'], 'access': []}], 'links': [{'target_room': 91, 'entrance': 180, 'teleporter': [99, 0], 'access': ['Jinn']}, {'target_room': 86, 'access': []}, {'target_room': 85, 'access': ['Claw']}]}, {'name': 'Mine Parallel Room', 'id': 88, 'game_objects': [{'name': 'Mine - Parallel Room West Box', 'object_id': 119, 'type': 'Box', 'access': ['Claw']}, {'name': 'Mine - Parallel Room East Box', 'object_id': 120, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 84, 'entrance': 185, 'teleporter': [100, 3], 'access': []}]}, {'name': 'Mine Crescent Room', 'id': 89, 'game_objects': [{'name': 'Mine - Crescent Room Chest', 'object_id': 16, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 86, 'entrance': 186, 'teleporter': [101, 3], 'access': []}]}, {'name': 'Mine Climbing Room', 'id': 90, 'game_objects': [{'name': 'Mine - Glitchy Collision Cave Box', 'object_id': 118, 'type': 'Box', 'access': ['Claw']}], 'links': [{'target_room': 85, 'entrance': 187, 'teleporter': [102, 3], 'access': []}]}, {'name': 'Mine Cliff', 'id': 91, 'game_objects': [{'name': 'Mine - Cliff Southwest Box', 'object_id': 121, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northwest Box', 'object_id': 122, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Northeast Box', 'object_id': 123, 'type': 'Box', 'access': []}, {'name': 'Mine - Cliff Southeast Box', 'object_id': 124, 'type': 'Box', 'access': []}, {'name': 'Mine - Reuben', 'object_id': 7, 'type': 'NPC', 'access': ['Reuben1']}, {'name': "Reuben's dad Saved", 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ReubenDadSaved'], 'access': ['MegaGrenade']}], 'links': [{'target_room': 87, 'entrance': 188, 'teleporter': [100, 0], 'access': []}]}, {'name': 'Sealed Temple', 'id': 92, 'game_objects': [{'name': 'Sealed Temple - West Box', 'object_id': 125, 'type': 'Box', 'access': []}, {'name': 'Sealed Temple - East Box', 'object_id': 126, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 190, 'teleporter': [16, 6], 'access': []}, {'target_room': 75, 'entrance': 191, 'teleporter': [63, 8], 'access': ['GeminiCrest']}]}, {'name': 'Volcano Base', 'id': 93, 'game_objects': [{'name': 'Volcano - Base Chest', 'object_id': 17, 'type': 'Chest', 'access': []}, {'name': 'Volcano - Base West Box', 'object_id': 127, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Left Box', 'object_id': 128, 'type': 'Box', 'access': []}, {'name': 'Volcano - Base East Right Box', 'object_id': 129, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 192, 'teleporter': [103, 0], 'access': []}, {'target_room': 98, 'entrance': 196, 'teleporter': [31, 8], 'access': []}, {'target_room': 96, 'entrance': 197, 'teleporter': [30, 8], 'access': []}]}, {'name': 'Volcano Top Left', 'id': 94, 'game_objects': [{'name': 'Volcano - Medusa Chest', 'object_id': 18, 'type': 'Chest', 'access': ['Medusa']}, {'name': 'Medusa', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Medusa'], 'access': []}, {'name': 'Volcano - Behind Medusa Box', 'object_id': 130, 'type': 'Box', 'access': []}], 'links': [{'target_room': 209, 'entrance': 199, 'teleporter': [26, 8], 'access': []}]}, {'name': 'Volcano Top Right', 'id': 95, 'game_objects': [{'name': 'Volcano - Top of the Volcano Left Box', 'object_id': 131, 'type': 'Box', 'access': []}, {'name': 'Volcano - Top of the Volcano Right Box', 'object_id': 132, 'type': 'Box', 'access': []}], 'links': [{'target_room': 99, 'entrance': 200, 'teleporter': [79, 8], 'access': []}]}, {'name': 'Volcano Right Path', 'id': 96, 'game_objects': [{'name': 'Volcano - Right Path Box', 'object_id': 135, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 201, 'teleporter': [15, 8], 'access': []}]}, {'name': 'Volcano Left Path', 'id': 98, 'game_objects': [{'name': 'Volcano - Left Path Box', 'object_id': 134, 'type': 'Box', 'access': []}], 'links': [{'target_room': 93, 'entrance': 204, 'teleporter': [27, 8], 'access': []}, {'target_room': 99, 'entrance': 202, 'teleporter': [25, 2], 'access': []}, {'target_room': 209, 'entrance': 203, 'teleporter': [26, 2], 'access': []}]}, {'name': 'Volcano Cross Left-Right', 'id': 99, 'game_objects': [], 'links': [{'target_room': 95, 'entrance': 206, 'teleporter': [29, 8], 'access': []}, {'target_room': 98, 'entrance': 205, 'teleporter': [103, 3], 'access': []}]}, {'name': 'Volcano Cross Right-Left', 'id': 209, 'game_objects': [{'name': 'Volcano - Crossover Section Box', 'object_id': 133, 'type': 'Box', 'access': []}], 'links': [{'target_room': 98, 'entrance': 208, 'teleporter': [104, 3], 'access': []}, {'target_room': 94, 'entrance': 207, 'teleporter': [28, 8], 'access': []}]}, {'name': 'Lava Dome Inner Ring Main Loop', 'id': 100, 'game_objects': [{'name': 'Lava Dome - Exterior Caldera Near Switch Cliff Box', 'object_id': 136, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Exterior South Cliff Box', 'object_id': 137, 'type': 'Box', 'access': []}], 'links': [{'target_room': 224, 'entrance': 209, 'teleporter': [104, 0], 'access': []}, {'target_room': 113, 'entrance': 211, 'teleporter': [105, 0], 'access': []}, {'target_room': 114, 'entrance': 212, 'teleporter': [106, 0], 'access': []}, {'target_room': 116, 'entrance': 213, 'teleporter': [108, 0], 'access': []}, {'target_room': 118, 'entrance': 214, 'teleporter': [111, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Center Ledge', 'id': 101, 'game_objects': [{'name': 'Lava Dome - Exterior Center Dropoff Ledge Box', 'object_id': 138, 'type': 'Box', 'access': []}], 'links': [{'target_room': 115, 'entrance': 215, 'teleporter': [107, 0], 'access': []}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Plate Ledge', 'id': 102, 'game_objects': [{'name': 'Lava Dome Plate', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['LavaDomePlate'], 'access': []}], 'links': [{'target_room': 119, 'entrance': 216, 'teleporter': [109, 0], 'access': []}]}, {'name': 'Lava Dome Inner Ring Upper Ledge West', 'id': 103, 'game_objects': [], 'links': [{'target_room': 111, 'entrance': 219, 'teleporter': [112, 0], 'access': []}, {'target_room': 108, 'entrance': 220, 'teleporter': [113, 0], 'access': []}, {'target_room': 104, 'access': ['Claw']}, {'target_room': 100, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Upper Ledge East', 'id': 104, 'game_objects': [], 'links': [{'target_room': 110, 'entrance': 218, 'teleporter': [110, 0], 'access': []}, {'target_room': 103, 'access': ['Claw']}]}, {'name': 'Lava Dome Inner Ring Big Door Ledge', 'id': 105, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 221, 'teleporter': [114, 0], 'access': []}, {'target_room': 121, 'entrance': 222, 'teleporter': [29, 2], 'access': ['LavaDomePlate']}]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge', 'id': 106, 'game_objects': [{'name': 'Lava Dome - Exterior Dead End Caldera Box', 'object_id': 139, 'type': 'Box', 'access': []}], 'links': [{'target_room': 120, 'entrance': 226, 'teleporter': [115, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze II', 'id': 107, 'game_objects': [{'name': 'Lava Dome - Gold Maze Northwest Box', 'object_id': 140, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southwest Box', 'object_id': 246, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Northeast Box', 'object_id': 247, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze North Box', 'object_id': 248, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Center Box', 'object_id': 249, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Maze Southeast Box', 'object_id': 250, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 227, 'teleporter': [116, 0], 'access': []}, {'target_room': 108, 'entrance': 228, 'teleporter': [119, 0], 'access': []}, {'target_room': 120, 'entrance': 229, 'teleporter': [120, 0], 'access': []}]}, {'name': 'Lava Dome Up-Down Corridor', 'id': 108, 'game_objects': [], 'links': [{'target_room': 107, 'entrance': 231, 'teleporter': [118, 0], 'access': []}, {'target_room': 103, 'entrance': 230, 'teleporter': [117, 0], 'access': []}]}, {'name': 'Lava Dome Jump Maze I', 'id': 109, 'game_objects': [{'name': 'Lava Dome - Bare Maze Leapfrog Alcove North Box', 'object_id': 141, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Leapfrog Alcove South Box', 'object_id': 142, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Center Box', 'object_id': 143, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Bare Maze Southwest Box', 'object_id': 144, 'type': 'Box', 'access': []}], 'links': [{'target_room': 118, 'entrance': 232, 'teleporter': [121, 0], 'access': []}, {'target_room': 111, 'entrance': 233, 'teleporter': [122, 0], 'access': []}]}, {'name': 'Lava Dome Pointless Room', 'id': 110, 'game_objects': [], 'links': [{'target_room': 104, 'entrance': 234, 'teleporter': [123, 0], 'access': []}]}, {'name': 'Lava Dome Lower Moon Helm Room', 'id': 111, 'game_objects': [{'name': 'Lava Dome - U-Bend Room North Box', 'object_id': 146, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - U-Bend Room South Box', 'object_id': 147, 'type': 'Box', 'access': []}], 'links': [{'target_room': 103, 'entrance': 235, 'teleporter': [124, 0], 'access': []}, {'target_room': 109, 'entrance': 236, 'teleporter': [125, 0], 'access': []}]}, {'name': 'Lava Dome Moon Helm Room', 'id': 112, 'game_objects': [{'name': 'Lava Dome - Beyond River Room Chest', 'object_id': 19, 'type': 'Chest', 'access': []}, {'name': 'Lava Dome - Beyond River Room Box', 'object_id': 145, 'type': 'Box', 'access': []}], 'links': [{'target_room': 117, 'entrance': 237, 'teleporter': [126, 0], 'access': []}]}, {'name': 'Lava Dome Three Jumps Room', 'id': 113, 'game_objects': [{'name': 'Lava Dome - Three Jumps Room Box', 'object_id': 150, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 238, 'teleporter': [127, 0], 'access': []}]}, {'name': 'Lava Dome Life Chest Room Lower Ledge', 'id': 114, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Boulder Chest', 'object_id': 28, 'type': 'Chest', 'access': ['MegaGrenade']}], 'links': [{'target_room': 100, 'entrance': 239, 'teleporter': [128, 0], 'access': []}, {'target_room': 115, 'access': ['Claw']}]}, {'name': 'Lava Dome Life Chest Room Upper Ledge', 'id': 115, 'game_objects': [{'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box West', 'object_id': 148, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Gold Bar Room Leapfrog Alcove Box East', 'object_id': 149, 'type': 'Box', 'access': []}], 'links': [{'target_room': 101, 'entrance': 240, 'teleporter': [129, 0], 'access': []}, {'target_room': 114, 'access': ['Claw']}]}, {'name': 'Lava Dome Big Jump Room Main Area', 'id': 116, 'game_objects': [{'name': 'Lava Dome - Lava River Room North Box', 'object_id': 152, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room East Box', 'object_id': 153, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Lava River Room South Box', 'object_id': 154, 'type': 'Box', 'access': []}], 'links': [{'target_room': 100, 'entrance': 241, 'teleporter': [133, 0], 'access': []}, {'target_room': 119, 'entrance': 243, 'teleporter': [132, 0], 'access': []}, {'target_room': 117, 'access': ['MegaGrenade']}]}, {'name': 'Lava Dome Big Jump Room MegaGrenade Area', 'id': 117, 'game_objects': [], 'links': [{'target_room': 112, 'entrance': 242, 'teleporter': [131, 0], 'access': []}, {'target_room': 116, 'access': ['Bomb']}]}, {'name': 'Lava Dome Split Corridor', 'id': 118, 'game_objects': [{'name': 'Lava Dome - Split Corridor Box', 'object_id': 151, 'type': 'Box', 'access': []}], 'links': [{'target_room': 109, 'entrance': 244, 'teleporter': [130, 0], 'access': []}, {'target_room': 100, 'entrance': 245, 'teleporter': [134, 0], 'access': []}]}, {'name': 'Lava Dome Plate Corridor', 'id': 119, 'game_objects': [], 'links': [{'target_room': 102, 'entrance': 246, 'teleporter': [135, 0], 'access': []}, {'target_room': 116, 'entrance': 247, 'teleporter': [137, 0], 'access': []}]}, {'name': 'Lava Dome Four Boxes Stairs', 'id': 120, 'game_objects': [{'name': 'Lava Dome - Caldera Stairway West Left Box', 'object_id': 155, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway West Right Box', 'object_id': 156, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Left Box', 'object_id': 157, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Caldera Stairway East Right Box', 'object_id': 158, 'type': 'Box', 'access': []}], 'links': [{'target_room': 107, 'entrance': 248, 'teleporter': [136, 0], 'access': []}, {'target_room': 106, 'entrance': 249, 'teleporter': [16, 0], 'access': []}]}, {'name': 'Lava Dome Hydra Room', 'id': 121, 'game_objects': [{'name': 'Lava Dome - Dualhead Hydra Chest', 'object_id': 20, 'type': 'Chest', 'access': ['DualheadHydra']}, {'name': 'Dualhead Hydra', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['DualheadHydra'], 'access': []}, {'name': 'Lava Dome - Hydra Room Northwest Box', 'object_id': 159, 'type': 'Box', 'access': []}, {'name': 'Lava Dome - Hydra Room Southweast Box', 'object_id': 160, 'type': 'Box', 'access': []}], 'links': [{'target_room': 105, 'entrance': 250, 'teleporter': [105, 3], 'access': []}, {'target_room': 122, 'entrance': 251, 'teleporter': [138, 0], 'access': ['DualheadHydra']}]}, {'name': 'Lava Dome Escape Corridor', 'id': 122, 'game_objects': [], 'links': [{'target_room': 121, 'entrance': 253, 'teleporter': [139, 0], 'access': []}]}, {'name': 'Rope Bridge', 'id': 123, 'game_objects': [{'name': 'Rope Bridge - West Box', 'object_id': 163, 'type': 'Box', 'access': []}, {'name': 'Rope Bridge - East Box', 'object_id': 164, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 255, 'teleporter': [140, 0], 'access': []}]}, {'name': 'Alive Forest', 'id': 124, 'game_objects': [{'name': 'Alive Forest - Tree Stump Chest', 'object_id': 21, 'type': 'Chest', 'access': ['Axe']}, {'name': 'Alive Forest - Near Entrance Box', 'object_id': 165, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - After Bridge Box', 'object_id': 166, 'type': 'Box', 'access': ['Axe']}, {'name': 'Alive Forest - Gemini Stump Box', 'object_id': 167, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 226, 'entrance': 272, 'teleporter': [142, 0], 'access': ['Axe']}, {'target_room': 21, 'entrance': 275, 'teleporter': [64, 8], 'access': ['LibraCrest', 'Axe']}, {'target_room': 22, 'entrance': 276, 'teleporter': [65, 8], 'access': ['GeminiCrest', 'Axe']}, {'target_room': 23, 'entrance': 277, 'teleporter': [66, 8], 'access': ['MobiusCrest', 'Axe']}, {'target_room': 125, 'entrance': 274, 'teleporter': [143, 0], 'access': ['Axe']}]}, {'name': 'Giant Tree 1F Main Area', 'id': 125, 'game_objects': [{'name': 'Giant Tree 1F - Northwest Box', 'object_id': 168, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Southwest Box', 'object_id': 169, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - Center Box', 'object_id': 170, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 1F - East Box', 'object_id': 171, 'type': 'Box', 'access': []}], 'links': [{'target_room': 124, 'entrance': 278, 'teleporter': [56, 1], 'access': []}, {'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F North Island', 'id': 202, 'game_objects': [], 'links': [{'target_room': 127, 'entrance': 280, 'teleporter': [144, 0], 'access': []}, {'target_room': 125, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 1F Central Island', 'id': 126, 'game_objects': [], 'links': [{'target_room': 202, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Main Lobby', 'id': 127, 'game_objects': [{'name': 'Giant Tree 2F - North Box', 'object_id': 172, 'type': 'Box', 'access': []}], 'links': [{'target_room': 126, 'access': ['DragonClaw']}, {'target_room': 125, 'entrance': 281, 'teleporter': [145, 0], 'access': []}, {'target_room': 133, 'entrance': 283, 'teleporter': [149, 0], 'access': []}, {'target_room': 129, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F West Ledge', 'id': 128, 'game_objects': [{'name': 'Giant Tree 2F - Dropdown Ledge Box', 'object_id': 174, 'type': 'Box', 'access': []}], 'links': [{'target_room': 140, 'entrance': 284, 'teleporter': [147, 0], 'access': ['Sword']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Lower Area', 'id': 129, 'game_objects': [{'name': 'Giant Tree 2F - South Box', 'object_id': 173, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'access': ['Claw']}, {'target_room': 131, 'access': ['Claw']}]}, {'name': 'Giant Tree 2F Central Island', 'id': 130, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 135, 'entrance': 282, 'teleporter': [146, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 2F East Ledge', 'id': 131, 'game_objects': [], 'links': [{'target_room': 129, 'access': ['Claw']}, {'target_room': 130, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 2F Meteor Chest Room', 'id': 132, 'game_objects': [{'name': 'Giant Tree 2F - Gidrah Chest', 'object_id': 22, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 133, 'entrance': 285, 'teleporter': [148, 0], 'access': []}]}, {'name': 'Giant Tree 2F Mushroom Room', 'id': 133, 'game_objects': [{'name': 'Giant Tree 2F - Mushroom Tunnel West Box', 'object_id': 175, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 2F - Mushroom Tunnel East Box', 'object_id': 176, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 127, 'entrance': 286, 'teleporter': [150, 0], 'access': ['Axe']}, {'target_room': 132, 'entrance': 287, 'teleporter': [151, 0], 'access': ['Axe', 'Gidrah']}]}, {'name': 'Giant Tree 3F Central Island', 'id': 135, 'game_objects': [{'name': 'Giant Tree 3F - Central Island Box', 'object_id': 179, 'type': 'Box', 'access': []}], 'links': [{'target_room': 130, 'entrance': 288, 'teleporter': [152, 0], 'access': []}, {'target_room': 136, 'access': ['Claw']}, {'target_room': 137, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 3F Central Area', 'id': 136, 'game_objects': [{'name': 'Giant Tree 3F - Center North Box', 'object_id': 177, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 3F - Center West Box', 'object_id': 178, 'type': 'Box', 'access': []}], 'links': [{'target_room': 135, 'access': ['Claw']}, {'target_room': 127, 'access': []}, {'target_room': 131, 'access': []}]}, {'name': 'Giant Tree 3F Lower Ledge', 'id': 137, 'game_objects': [], 'links': [{'target_room': 135, 'access': ['DragonClaw']}, {'target_room': 142, 'entrance': 289, 'teleporter': [153, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 3F West Area', 'id': 138, 'game_objects': [{'name': 'Giant Tree 3F - West Side Box', 'object_id': 180, 'type': 'Box', 'access': []}], 'links': [{'target_room': 128, 'access': []}, {'target_room': 210, 'entrance': 290, 'teleporter': [154, 0], 'access': []}]}, {'name': 'Giant Tree 3F Middle Up Island', 'id': 139, 'game_objects': [], 'links': [{'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree 3F West Platform', 'id': 140, 'game_objects': [], 'links': [{'target_room': 139, 'access': ['Claw']}, {'target_room': 141, 'access': ['Claw']}, {'target_room': 128, 'entrance': 291, 'teleporter': [155, 0], 'access': []}]}, {'name': 'Giant Tree 3F North Ledge', 'id': 141, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 292, 'teleporter': [156, 0], 'access': ['Sword']}, {'target_room': 139, 'access': ['Claw']}, {'target_room': 136, 'access': ['Claw']}]}, {'name': 'Giant Tree Worm Room Upper Ledge', 'id': 142, 'game_objects': [{'name': 'Giant Tree 3F - Worm Room North Box', 'object_id': 181, 'type': 'Box', 'access': ['Axe']}, {'name': 'Giant Tree 3F - Worm Room South Box', 'object_id': 182, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 137, 'entrance': 293, 'teleporter': [157, 0], 'access': ['Axe']}, {'target_room': 210, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree Worm Room Lower Ledge', 'id': 210, 'game_objects': [], 'links': [{'target_room': 138, 'entrance': 294, 'teleporter': [158, 0], 'access': []}]}, {'name': 'Giant Tree 4F Lower Floor', 'id': 143, 'game_objects': [], 'links': [{'target_room': 141, 'entrance': 295, 'teleporter': [159, 0], 'access': []}, {'target_room': 148, 'entrance': 296, 'teleporter': [160, 0], 'access': []}, {'target_room': 148, 'entrance': 297, 'teleporter': [161, 0], 'access': []}, {'target_room': 147, 'entrance': 298, 'teleporter': [162, 0], 'access': ['Sword']}]}, {'name': 'Giant Tree 4F Middle Floor', 'id': 144, 'game_objects': [{'name': 'Giant Tree 4F - Highest Platform North Box', 'object_id': 183, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Highest Platform South Box', 'object_id': 184, 'type': 'Box', 'access': []}], 'links': [{'target_room': 149, 'entrance': 299, 'teleporter': [163, 0], 'access': []}, {'target_room': 145, 'access': ['Claw']}, {'target_room': 146, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Upper Floor', 'id': 145, 'game_objects': [], 'links': [{'target_room': 150, 'entrance': 300, 'teleporter': [164, 0], 'access': ['Sword']}, {'target_room': 144, 'access': ['Claw']}]}, {'name': 'Giant Tree 4F South Ledge', 'id': 146, 'game_objects': [{'name': 'Giant Tree 4F - Hook Ledge Northeast Box', 'object_id': 185, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 4F - Hook Ledge Southwest Box', 'object_id': 186, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'access': ['DragonClaw']}]}, {'name': 'Giant Tree 4F Slime Room East Area', 'id': 147, 'game_objects': [{'name': 'Giant Tree 4F - East Slime Room Box', 'object_id': 188, 'type': 'Box', 'access': ['Axe']}], 'links': [{'target_room': 143, 'entrance': 304, 'teleporter': [168, 0], 'access': []}]}, {'name': 'Giant Tree 4F Slime Room West Area', 'id': 148, 'game_objects': [], 'links': [{'target_room': 143, 'entrance': 303, 'teleporter': [167, 0], 'access': ['Axe']}, {'target_room': 143, 'entrance': 302, 'teleporter': [166, 0], 'access': ['Axe']}, {'target_room': 149, 'access': ['Axe', 'Claw']}]}, {'name': 'Giant Tree 4F Slime Room Platform', 'id': 149, 'game_objects': [{'name': 'Giant Tree 4F - West Slime Room Box', 'object_id': 187, 'type': 'Box', 'access': []}], 'links': [{'target_room': 144, 'entrance': 301, 'teleporter': [165, 0], 'access': []}, {'target_room': 148, 'access': ['Claw']}]}, {'name': 'Giant Tree 5F Lower Area', 'id': 150, 'game_objects': [{'name': 'Giant Tree 5F - Northwest Left Box', 'object_id': 189, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - Northwest Right Box', 'object_id': 190, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Left Box', 'object_id': 191, 'type': 'Box', 'access': []}, {'name': 'Giant Tree 5F - South Right Box', 'object_id': 192, 'type': 'Box', 'access': []}], 'links': [{'target_room': 145, 'entrance': 305, 'teleporter': [169, 0], 'access': []}, {'target_room': 151, 'access': ['Claw']}, {'target_room': 143, 'access': []}]}, {'name': 'Giant Tree 5F Gidrah Platform', 'id': 151, 'game_objects': [{'name': 'Gidrah', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Gidrah'], 'access': []}], 'links': [{'target_room': 150, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Lower Ledge', 'id': 152, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 307, 'teleporter': [18, 6], 'access': []}, {'target_room': 153, 'access': ['Claw']}]}, {'name': 'Kaidge Temple Upper Ledge', 'id': 153, 'game_objects': [{'name': 'Kaidge Temple - Box', 'object_id': 193, 'type': 'Box', 'access': []}], 'links': [{'target_room': 185, 'entrance': 308, 'teleporter': [71, 8], 'access': ['MobiusCrest']}, {'target_room': 152, 'access': ['Claw']}]}, {'name': 'Windhole Temple', 'id': 154, 'game_objects': [{'name': 'Windhole Temple - Box', 'object_id': 194, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 309, 'teleporter': [173, 0], 'access': []}]}, {'name': 'Mount Gale', 'id': 155, 'game_objects': [{'name': 'Mount Gale - Dullahan Chest', 'object_id': 23, 'type': 'Chest', 'access': ['DragonClaw', 'Dullahan']}, {'name': 'Dullahan', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Dullahan'], 'access': ['DragonClaw']}, {'name': 'Mount Gale - East Box', 'object_id': 195, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Mount Gale - West Box', 'object_id': 196, 'type': 'Box', 'access': []}], 'links': [{'target_room': 226, 'entrance': 310, 'teleporter': [174, 0], 'access': []}]}, {'name': 'Windia', 'id': 156, 'game_objects': [], 'links': [{'target_room': 226, 'entrance': 312, 'teleporter': [10, 6], 'access': []}, {'target_room': 157, 'entrance': 320, 'teleporter': [30, 5], 'access': []}, {'target_room': 163, 'entrance': 321, 'teleporter': [97, 8], 'access': []}, {'target_room': 165, 'entrance': 322, 'teleporter': [32, 5], 'access': []}, {'target_room': 159, 'entrance': 323, 'teleporter': [176, 4], 'access': []}, {'target_room': 160, 'entrance': 324, 'teleporter': [177, 4], 'access': []}]}, {'name': "Otto's House", 'id': 157, 'game_objects': [{'name': 'Otto', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['RainbowBridge'], 'access': ['ThunderRock']}], 'links': [{'target_room': 156, 'entrance': 327, 'teleporter': [106, 3], 'access': []}, {'target_room': 158, 'entrance': 326, 'teleporter': [33, 2], 'access': []}]}, {'name': "Otto's Attic", 'id': 158, 'game_objects': [{'name': "Windia - Otto's Attic Box", 'object_id': 197, 'type': 'Box', 'access': []}], 'links': [{'target_room': 157, 'entrance': 328, 'teleporter': [107, 3], 'access': []}]}, {'name': 'Windia Kid House', 'id': 159, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 329, 'teleporter': [178, 0], 'access': []}, {'target_room': 161, 'entrance': 330, 'teleporter': [180, 0], 'access': []}]}, {'name': 'Windia Old People House', 'id': 160, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 331, 'teleporter': [179, 0], 'access': []}, {'target_room': 162, 'entrance': 332, 'teleporter': [181, 0], 'access': []}]}, {'name': 'Windia Kid House Basement', 'id': 161, 'game_objects': [], 'links': [{'target_room': 159, 'entrance': 333, 'teleporter': [182, 0], 'access': []}, {'target_room': 79, 'entrance': 334, 'teleporter': [44, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Old People House Basement', 'id': 162, 'game_objects': [{'name': 'Windia - Mobius Basement West Box', 'object_id': 200, 'type': 'Box', 'access': []}, {'name': 'Windia - Mobius Basement East Box', 'object_id': 201, 'type': 'Box', 'access': []}], 'links': [{'target_room': 160, 'entrance': 335, 'teleporter': [183, 0], 'access': []}, {'target_room': 186, 'entrance': 336, 'teleporter': [43, 8], 'access': ['MobiusCrest']}]}, {'name': 'Windia Inn Lobby', 'id': 163, 'game_objects': [], 'links': [{'target_room': 156, 'entrance': 338, 'teleporter': [135, 3], 'access': []}, {'target_room': 164, 'entrance': 337, 'teleporter': [102, 8], 'access': []}]}, {'name': 'Windia Inn Beds', 'id': 164, 'game_objects': [{'name': 'Windia - Inn Bedroom North Box', 'object_id': 198, 'type': 'Box', 'access': []}, {'name': 'Windia - Inn Bedroom South Box', 'object_id': 199, 'type': 'Box', 'access': []}, {'name': 'Windia - Kaeli', 'object_id': 15, 'type': 'NPC', 'access': ['Kaeli2']}], 'links': [{'target_room': 163, 'entrance': 339, 'teleporter': [216, 0], 'access': []}]}, {'name': 'Windia Vendor House', 'id': 165, 'game_objects': [{'name': 'Windia - Vendor', 'object_id': 16, 'type': 'NPC', 'access': []}], 'links': [{'target_room': 156, 'entrance': 340, 'teleporter': [108, 3], 'access': []}]}, {'name': 'Pazuzu Tower 1F Main Lobby', 'id': 166, 'game_objects': [{'name': 'Pazuzu 1F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu1F'], 'access': []}], 'links': [{'target_room': 226, 'entrance': 341, 'teleporter': [184, 0], 'access': []}, {'target_room': 180, 'entrance': 345, 'teleporter': [185, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Boxes Room', 'id': 167, 'game_objects': [{'name': "Pazuzu's Tower 1F - Descent Bomb Wall West Box", 'object_id': 202, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall Center Box", 'object_id': 203, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Bomb Wall East Box", 'object_id': 204, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 1F - Descent Box", 'object_id': 205, 'type': 'Box', 'access': []}], 'links': [{'target_room': 169, 'entrance': 349, 'teleporter': [187, 0], 'access': []}]}, {'name': 'Pazuzu Tower 1F Southern Platform', 'id': 168, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 346, 'teleporter': [186, 0], 'access': []}, {'target_room': 166, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 2F', 'id': 169, 'game_objects': [{'name': "Pazuzu's Tower 2F - East Room West Box", 'object_id': 206, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 2F - East Room East Box", 'object_id': 207, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 2F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 2F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu2F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 350, 'teleporter': [188, 0], 'access': []}, {'target_room': 168, 'entrance': 351, 'teleporter': [189, 0], 'access': []}, {'target_room': 167, 'entrance': 352, 'teleporter': [190, 0], 'access': []}, {'target_room': 171, 'entrance': 353, 'teleporter': [191, 0], 'access': []}]}, {'name': 'Pazuzu 3F Main Room', 'id': 170, 'game_objects': [{'name': "Pazuzu's Tower 3F - Guest Room West Box", 'object_id': 208, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 3F - Guest Room East Box", 'object_id': 209, 'type': 'Box', 'access': []}, {'name': 'Pazuzu 3F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu3F'], 'access': []}], 'links': [{'target_room': 180, 'entrance': 356, 'teleporter': [192, 0], 'access': []}, {'target_room': 181, 'entrance': 357, 'teleporter': [193, 0], 'access': []}]}, {'name': 'Pazuzu 3F Central Island', 'id': 171, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 360, 'teleporter': [194, 0], 'access': []}, {'target_room': 170, 'access': ['DragonClaw']}, {'target_room': 172, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 3F Southern Island', 'id': 172, 'game_objects': [{'name': "Pazuzu's Tower 3F - South Ledge Box", 'object_id': 210, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 361, 'teleporter': [195, 0], 'access': []}, {'target_room': 171, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 4F', 'id': 173, 'game_objects': [{'name': "Pazuzu's Tower 4F - Elevator West Box", 'object_id': 211, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - Elevator East Box", 'object_id': 212, 'type': 'Box', 'access': ['Bomb']}, {'name': "Pazuzu's Tower 4F - East Storage Room Chest", 'object_id': 24, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 4F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4FLock'], 'access': ['Axe']}, {'name': 'Pazuzu 4F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu4F'], 'access': ['Bomb']}], 'links': [{'target_room': 183, 'entrance': 362, 'teleporter': [196, 0], 'access': []}, {'target_room': 184, 'entrance': 363, 'teleporter': [197, 0], 'access': []}, {'target_room': 172, 'entrance': 364, 'teleporter': [198, 0], 'access': []}, {'target_room': 175, 'entrance': 365, 'teleporter': [199, 0], 'access': []}]}, {'name': 'Pazuzu 5F Pazuzu Loop', 'id': 174, 'game_objects': [{'name': 'Pazuzu 5F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu5F'], 'access': []}], 'links': [{'target_room': 181, 'entrance': 368, 'teleporter': [200, 0], 'access': []}, {'target_room': 182, 'entrance': 369, 'teleporter': [201, 0], 'access': []}]}, {'name': 'Pazuzu 5F Upper Loop', 'id': 175, 'game_objects': [{'name': "Pazuzu's Tower 5F - North Box", 'object_id': 213, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 5F - South Box", 'object_id': 214, 'type': 'Box', 'access': []}], 'links': [{'target_room': 173, 'entrance': 370, 'teleporter': [202, 0], 'access': []}, {'target_room': 176, 'entrance': 371, 'teleporter': [203, 0], 'access': []}]}, {'name': 'Pazuzu 6F', 'id': 176, 'game_objects': [{'name': "Pazuzu's Tower 6F - Box", 'object_id': 215, 'type': 'Box', 'access': []}, {'name': "Pazuzu's Tower 6F - Chest", 'object_id': 25, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu 6F Lock', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6FLock'], 'access': ['Bomb', 'Axe']}, {'name': 'Pazuzu 6F', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu6F'], 'access': ['Bomb']}], 'links': [{'target_room': 184, 'entrance': 374, 'teleporter': [204, 0], 'access': []}, {'target_room': 175, 'entrance': 375, 'teleporter': [205, 0], 'access': []}, {'target_room': 178, 'entrance': 376, 'teleporter': [206, 0], 'access': []}, {'target_room': 178, 'entrance': 377, 'teleporter': [207, 0], 'access': []}]}, {'name': 'Pazuzu 7F Southwest Area', 'id': 177, 'game_objects': [], 'links': [{'target_room': 182, 'entrance': 380, 'teleporter': [26, 0], 'access': []}, {'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 7F Rest of the Area', 'id': 178, 'game_objects': [], 'links': [{'target_room': 177, 'access': ['DragonClaw']}, {'target_room': 176, 'entrance': 381, 'teleporter': [27, 0], 'access': []}, {'target_room': 176, 'entrance': 382, 'teleporter': [28, 0], 'access': []}, {'target_room': 179, 'access': ['DragonClaw', 'Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}]}, {'name': 'Pazuzu 7F Sky Room', 'id': 179, 'game_objects': [{'name': "Pazuzu's Tower 7F - Pazuzu Chest", 'object_id': 26, 'type': 'Chest', 'access': []}, {'name': 'Pazuzu', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Pazuzu'], 'access': ['Pazuzu2FLock', 'Pazuzu4FLock', 'Pazuzu6FLock', 'Pazuzu1F', 'Pazuzu2F', 'Pazuzu3F', 'Pazuzu4F', 'Pazuzu5F', 'Pazuzu6F']}], 'links': [{'target_room': 178, 'access': ['DragonClaw']}]}, {'name': 'Pazuzu 1F to 3F', 'id': 180, 'game_objects': [], 'links': [{'target_room': 166, 'entrance': 385, 'teleporter': [29, 0], 'access': []}, {'target_room': 170, 'entrance': 386, 'teleporter': [30, 0], 'access': []}]}, {'name': 'Pazuzu 3F to 5F', 'id': 181, 'game_objects': [], 'links': [{'target_room': 170, 'entrance': 387, 'teleporter': [40, 0], 'access': []}, {'target_room': 174, 'entrance': 388, 'teleporter': [41, 0], 'access': []}]}, {'name': 'Pazuzu 5F to 7F', 'id': 182, 'game_objects': [], 'links': [{'target_room': 174, 'entrance': 389, 'teleporter': [38, 0], 'access': []}, {'target_room': 177, 'entrance': 390, 'teleporter': [39, 0], 'access': []}]}, {'name': 'Pazuzu 2F to 4F', 'id': 183, 'game_objects': [], 'links': [{'target_room': 169, 'entrance': 391, 'teleporter': [21, 0], 'access': []}, {'target_room': 173, 'entrance': 392, 'teleporter': [22, 0], 'access': []}]}, {'name': 'Pazuzu 4F to 6F', 'id': 184, 'game_objects': [], 'links': [{'target_room': 173, 'entrance': 393, 'teleporter': [2, 0], 'access': []}, {'target_room': 176, 'entrance': 394, 'teleporter': [3, 0], 'access': []}]}, {'name': 'Light Temple', 'id': 185, 'game_objects': [{'name': 'Light Temple - Box', 'object_id': 216, 'type': 'Box', 'access': []}], 'links': [{'target_room': 230, 'entrance': 395, 'teleporter': [19, 6], 'access': []}, {'target_room': 153, 'entrance': 396, 'teleporter': [70, 8], 'access': ['MobiusCrest']}]}, {'name': 'Ship Dock', 'id': 186, 'game_objects': [{'name': 'Ship Dock Access', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipDockAccess'], 'access': []}], 'links': [{'target_room': 228, 'entrance': 399, 'teleporter': [17, 6], 'access': []}, {'target_room': 162, 'entrance': 397, 'teleporter': [61, 8], 'access': ['MobiusCrest']}]}, {'name': 'Mac Ship Deck', 'id': 187, 'game_objects': [{'name': 'Mac Ship Steering Wheel', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipSteeringWheel'], 'access': []}, {'name': "Mac's Ship Deck - North Box", 'object_id': 217, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - Center Box", 'object_id': 218, 'type': 'Box', 'access': []}, {'name': "Mac's Ship Deck - South Box", 'object_id': 219, 'type': 'Box', 'access': []}], 'links': [{'target_room': 229, 'entrance': 400, 'teleporter': [37, 8], 'access': []}, {'target_room': 188, 'entrance': 401, 'teleporter': [50, 8], 'access': []}, {'target_room': 188, 'entrance': 402, 'teleporter': [51, 8], 'access': []}, {'target_room': 188, 'entrance': 403, 'teleporter': [52, 8], 'access': []}, {'target_room': 189, 'entrance': 404, 'teleporter': [53, 8], 'access': []}]}, {'name': 'Mac Ship B1 Outer Ring', 'id': 188, 'game_objects': [{'name': "Mac's Ship B1 - Northwest Hook Platform Box", 'object_id': 228, 'type': 'Box', 'access': ['DragonClaw']}, {'name': "Mac's Ship B1 - Center Hook Platform Box", 'object_id': 229, 'type': 'Box', 'access': ['DragonClaw']}], 'links': [{'target_room': 187, 'entrance': 405, 'teleporter': [208, 0], 'access': []}, {'target_room': 187, 'entrance': 406, 'teleporter': [175, 0], 'access': []}, {'target_room': 187, 'entrance': 407, 'teleporter': [172, 0], 'access': []}, {'target_room': 193, 'entrance': 408, 'teleporter': [88, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B1 Square Room', 'id': 189, 'game_objects': [], 'links': [{'target_room': 187, 'entrance': 409, 'teleporter': [141, 0], 'access': []}, {'target_room': 192, 'entrance': 410, 'teleporter': [87, 0], 'access': []}]}, {'name': 'Mac Ship B1 Central Corridor', 'id': 190, 'game_objects': [{'name': "Mac's Ship B1 - Central Corridor Box", 'object_id': 230, 'type': 'Box', 'access': []}], 'links': [{'target_room': 192, 'entrance': 413, 'teleporter': [86, 0], 'access': []}, {'target_room': 191, 'entrance': 412, 'teleporter': [102, 0], 'access': []}, {'target_room': 193, 'access': []}]}, {'name': 'Mac Ship B2 South Corridor', 'id': 191, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 415, 'teleporter': [55, 8], 'access': []}, {'target_room': 194, 'entrance': 414, 'teleporter': [57, 1], 'access': []}]}, {'name': 'Mac Ship B2 North Corridor', 'id': 192, 'game_objects': [], 'links': [{'target_room': 190, 'entrance': 416, 'teleporter': [56, 8], 'access': []}, {'target_room': 189, 'entrance': 417, 'teleporter': [57, 8], 'access': []}]}, {'name': 'Mac Ship B2 Outer Ring', 'id': 193, 'game_objects': [{'name': "Mac's Ship B2 - Barrel Room South Box", 'object_id': 223, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Barrel Room North Box", 'object_id': 224, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southwest Room Box", 'object_id': 225, 'type': 'Box', 'access': []}, {'name': "Mac's Ship B2 - Southeast Room Box", 'object_id': 226, 'type': 'Box', 'access': []}], 'links': [{'target_room': 188, 'entrance': 418, 'teleporter': [58, 8], 'access': []}]}, {'name': 'Mac Ship B1 Mac Room', 'id': 194, 'game_objects': [{'name': "Mac's Ship B1 - Mac Room Chest", 'object_id': 27, 'type': 'Chest', 'access': []}, {'name': 'Captain Mac', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['ShipLoaned'], 'access': ['CaptainCap']}], 'links': [{'target_room': 191, 'entrance': 424, 'teleporter': [101, 0], 'access': []}]}, {'name': 'Doom Castle Corridor of Destiny', 'id': 195, 'game_objects': [], 'links': [{'target_room': 201, 'entrance': 428, 'teleporter': [84, 0], 'access': []}, {'target_room': 196, 'entrance': 429, 'teleporter': [35, 2], 'access': []}, {'target_room': 197, 'entrance': 430, 'teleporter': [209, 0], 'access': ['StoneGolem']}, {'target_room': 198, 'entrance': 431, 'teleporter': [211, 0], 'access': ['StoneGolem', 'TwinheadWyvern']}, {'target_room': 199, 'entrance': 432, 'teleporter': [13, 2], 'access': ['StoneGolem', 'TwinheadWyvern', 'Zuh']}]}, {'name': 'Doom Castle Ice Floor', 'id': 196, 'game_objects': [{'name': 'Doom Castle 4F - Northwest Room Box', 'object_id': 231, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Southwest Room Box', 'object_id': 232, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Doom Castle 4F - Northeast Room Box', 'object_id': 233, 'type': 'Box', 'access': ['Sword']}, {'name': 'Doom Castle 4F - Southeast Room Box', 'object_id': 234, 'type': 'Box', 'access': ['Sword', 'DragonClaw']}, {'name': 'Stone Golem', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['StoneGolem'], 'access': ['Sword', 'DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 433, 'teleporter': [109, 3], 'access': []}]}, {'name': 'Doom Castle Lava Floor', 'id': 197, 'game_objects': [{'name': 'Doom Castle 5F - North Left Box', 'object_id': 235, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - North Right Box', 'object_id': 236, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Left Box', 'object_id': 237, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Doom Castle 5F - South Right Box', 'object_id': 238, 'type': 'Box', 'access': ['DragonClaw']}, {'name': 'Twinhead Wyvern', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['TwinheadWyvern'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 434, 'teleporter': [210, 0], 'access': []}]}, {'name': 'Doom Castle Sky Floor', 'id': 198, 'game_objects': [{'name': 'Doom Castle 6F - West Box', 'object_id': 239, 'type': 'Box', 'access': []}, {'name': 'Doom Castle 6F - East Box', 'object_id': 240, 'type': 'Box', 'access': []}, {'name': 'Zuh', 'object_id': 0, 'type': 'Trigger', 'on_trigger': ['Zuh'], 'access': ['DragonClaw']}], 'links': [{'target_room': 195, 'entrance': 435, 'teleporter': [212, 0], 'access': []}, {'target_room': 197, 'access': []}]}, {'name': 'Doom Castle Hero Room', 'id': 199, 'game_objects': [{'name': 'Doom Castle Hero Chest 01', 'object_id': 242, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 02', 'object_id': 243, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 03', 'object_id': 244, 'type': 'Chest', 'access': []}, {'name': 'Doom Castle Hero Chest 04', 'object_id': 245, 'type': 'Chest', 'access': []}], 'links': [{'target_room': 200, 'entrance': 436, 'teleporter': [54, 0], 'access': []}, {'target_room': 195, 'entrance': 441, 'teleporter': [110, 3], 'access': []}]}, {'name': 'Doom Castle Dark King Room', 'id': 200, 'game_objects': [], 'links': [{'target_room': 199, 'entrance': 442, 'teleporter': [52, 0], 'access': []}]}] +entrances = [{'name': 'Doom Castle - Sand Floor - To Sky Door - Sand Floor', 'id': 0, 'area': 7, 'coordinates': [24, 19], 'teleporter': [0, 0]}, {'name': 'Doom Castle - Sand Floor - Main Entrance - Sand Floor', 'id': 1, 'area': 7, 'coordinates': [19, 43], 'teleporter': [1, 6]}, {'name': 'Doom Castle - Aero Room - Aero Room Entrance', 'id': 2, 'area': 7, 'coordinates': [27, 39], 'teleporter': [1, 0]}, {'name': 'Focus Tower B1 - Main Loop - South Entrance', 'id': 3, 'area': 8, 'coordinates': [43, 60], 'teleporter': [2, 6]}, {'name': 'Focus Tower B1 - Main Loop - To Focus Tower 1F - Main Hall', 'id': 4, 'area': 8, 'coordinates': [37, 41], 'teleporter': [4, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Focus Tower 1F - Sun Coin Room', 'id': 5, 'area': 8, 'coordinates': [59, 35], 'teleporter': [5, 0]}, {'name': 'Focus Tower B1 - Aero Corridor - To Sand Floor - Aero Chest', 'id': 6, 'area': 8, 'coordinates': [57, 59], 'teleporter': [8, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Focus Tower 1F - Sky Door', 'id': 7, 'area': 8, 'coordinates': [51, 49], 'teleporter': [6, 0]}, {'name': 'Focus Tower B1 - Inner Loop - To Doom Castle Sand Floor', 'id': 8, 'area': 8, 'coordinates': [51, 45], 'teleporter': [7, 0]}, {'name': 'Focus Tower 1F - Focus Tower West Entrance', 'id': 9, 'area': 9, 'coordinates': [25, 29], 'teleporter': [3, 6]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From SandCoin', 'id': 10, 'area': 9, 'coordinates': [16, 4], 'teleporter': [10, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - Main Hall', 'id': 11, 'area': 9, 'coordinates': [4, 23], 'teleporter': [11, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - To Aero Chest', 'id': 12, 'area': 9, 'coordinates': [26, 17], 'teleporter': [12, 0]}, {'name': 'Focus Tower 1F - Sky Door', 'id': 13, 'area': 9, 'coordinates': [16, 24], 'teleporter': [13, 0]}, {'name': 'Focus Tower 1F - To Focus Tower 2F - From RiverCoin', 'id': 14, 'area': 9, 'coordinates': [16, 10], 'teleporter': [14, 0]}, {'name': 'Focus Tower 1F - To Focus Tower B1 - From Sky Door', 'id': 15, 'area': 9, 'coordinates': [16, 29], 'teleporter': [15, 0]}, {'name': 'Focus Tower 2F - Sand Coin Passage - North Entrance', 'id': 16, 'area': 10, 'coordinates': [49, 30], 'teleporter': [4, 6]}, {'name': 'Focus Tower 2F - Sand Coin Passage - To Focus Tower 1F - To SandCoin', 'id': 17, 'area': 10, 'coordinates': [47, 33], 'teleporter': [17, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 1F - To RiverCoin', 'id': 18, 'area': 10, 'coordinates': [47, 41], 'teleporter': [18, 0]}, {'name': 'Focus Tower 2F - River Coin Passage - To Focus Tower 3F - Lower Floor', 'id': 19, 'area': 10, 'coordinates': [38, 40], 'teleporter': [20, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - To Focus Tower 3F - Upper Floor', 'id': 20, 'area': 10, 'coordinates': [56, 40], 'teleporter': [19, 0]}, {'name': 'Focus Tower 2F - Venus Chest Room - Pillar Script', 'id': 21, 'area': 10, 'coordinates': [48, 53], 'teleporter': [13, 8]}, {'name': 'Focus Tower 3F - Lower Floor - To Fireburg Entrance', 'id': 22, 'area': 11, 'coordinates': [11, 39], 'teleporter': [6, 6]}, {'name': 'Focus Tower 3F - Lower Floor - To Focus Tower 2F - Jump on Pillar', 'id': 23, 'area': 11, 'coordinates': [6, 47], 'teleporter': [24, 0]}, {'name': 'Focus Tower 3F - Upper Floor - To Aquaria Entrance', 'id': 24, 'area': 11, 'coordinates': [21, 38], 'teleporter': [5, 6]}, {'name': 'Focus Tower 3F - Upper Floor - To Focus Tower 2F - Venus Chest Room', 'id': 25, 'area': 11, 'coordinates': [24, 47], 'teleporter': [23, 0]}, {'name': 'Level Forest - Boulder Script', 'id': 26, 'area': 14, 'coordinates': [52, 15], 'teleporter': [0, 8]}, {'name': 'Level Forest - Rotten Tree Script', 'id': 27, 'area': 14, 'coordinates': [47, 6], 'teleporter': [2, 8]}, {'name': 'Level Forest - Exit Level Forest 1', 'id': 28, 'area': 14, 'coordinates': [46, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 2', 'id': 29, 'area': 14, 'coordinates': [46, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 3', 'id': 30, 'area': 14, 'coordinates': [47, 25], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 4', 'id': 31, 'area': 14, 'coordinates': [47, 26], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 5', 'id': 32, 'area': 14, 'coordinates': [60, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 6', 'id': 33, 'area': 14, 'coordinates': [61, 14], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 7', 'id': 34, 'area': 14, 'coordinates': [46, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 8', 'id': 35, 'area': 14, 'coordinates': [46, 3], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest 9', 'id': 36, 'area': 14, 'coordinates': [47, 4], 'teleporter': [25, 0]}, {'name': 'Level Forest - Exit Level Forest A', 'id': 37, 'area': 14, 'coordinates': [47, 3], 'teleporter': [25, 0]}, {'name': 'Foresta - Exit Foresta 1', 'id': 38, 'area': 15, 'coordinates': [10, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 2', 'id': 39, 'area': 15, 'coordinates': [10, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 3', 'id': 40, 'area': 15, 'coordinates': [11, 25], 'teleporter': [31, 0]}, {'name': 'Foresta - Exit Foresta 4', 'id': 41, 'area': 15, 'coordinates': [11, 26], 'teleporter': [31, 0]}, {'name': 'Foresta - Old Man House - Front Door', 'id': 42, 'area': 15, 'coordinates': [25, 17], 'teleporter': [32, 4]}, {'name': 'Foresta - Old Man House - Back Door', 'id': 43, 'area': 15, 'coordinates': [25, 14], 'teleporter': [33, 0]}, {'name': "Foresta - Kaeli's House", 'id': 44, 'area': 15, 'coordinates': [7, 21], 'teleporter': [0, 5]}, {'name': 'Foresta - Rest House', 'id': 45, 'area': 15, 'coordinates': [23, 23], 'teleporter': [1, 5]}, {'name': "Kaeli's House - Kaeli's House Entrance", 'id': 46, 'area': 16, 'coordinates': [11, 20], 'teleporter': [86, 3]}, {'name': "Foresta Houses - Old Man's House - Old Man Front Exit", 'id': 47, 'area': 17, 'coordinates': [35, 44], 'teleporter': [34, 0]}, {'name': "Foresta Houses - Old Man's House - Old Man Back Exit", 'id': 48, 'area': 17, 'coordinates': [35, 27], 'teleporter': [35, 0]}, {'name': 'Foresta - Old Man House - Barrel Tile Script', 'id': 483, 'area': 17, 'coordinates': [35, 30], 'teleporter': [13, 8]}, {'name': 'Foresta Houses - Rest House - Bed Script', 'id': 49, 'area': 17, 'coordinates': [30, 6], 'teleporter': [1, 8]}, {'name': 'Foresta Houses - Rest House - Rest House Exit', 'id': 50, 'area': 17, 'coordinates': [35, 20], 'teleporter': [87, 3]}, {'name': 'Foresta Houses - Libra House - Libra House Script', 'id': 51, 'area': 17, 'coordinates': [8, 49], 'teleporter': [67, 8]}, {'name': 'Foresta Houses - Gemini House - Gemini House Script', 'id': 52, 'area': 17, 'coordinates': [26, 55], 'teleporter': [68, 8]}, {'name': 'Foresta Houses - Mobius House - Mobius House Script', 'id': 53, 'area': 17, 'coordinates': [14, 33], 'teleporter': [69, 8]}, {'name': 'Sand Temple - Sand Temple Entrance', 'id': 54, 'area': 18, 'coordinates': [56, 27], 'teleporter': [36, 0]}, {'name': 'Bone Dungeon 1F - Bone Dungeon Entrance', 'id': 55, 'area': 19, 'coordinates': [13, 60], 'teleporter': [37, 0]}, {'name': 'Bone Dungeon 1F - To Bone Dungeon B1', 'id': 56, 'area': 19, 'coordinates': [13, 39], 'teleporter': [2, 2]}, {'name': 'Bone Dungeon B1 - Waterway - Exit Waterway', 'id': 57, 'area': 20, 'coordinates': [27, 39], 'teleporter': [3, 2]}, {'name': "Bone Dungeon B1 - Waterway - Tristam's Script", 'id': 58, 'area': 20, 'coordinates': [27, 45], 'teleporter': [3, 8]}, {'name': 'Bone Dungeon B1 - Waterway - To Bone Dungeon 1F', 'id': 59, 'area': 20, 'coordinates': [54, 61], 'teleporter': [88, 3]}, {'name': 'Bone Dungeon B1 - Checker Room - Exit Checker Room', 'id': 60, 'area': 20, 'coordinates': [23, 40], 'teleporter': [4, 2]}, {'name': 'Bone Dungeon B1 - Checker Room - To Waterway', 'id': 61, 'area': 20, 'coordinates': [39, 49], 'teleporter': [89, 3]}, {'name': 'Bone Dungeon B1 - Hidden Room - To B2 - Exploding Skull Room', 'id': 62, 'area': 20, 'coordinates': [5, 33], 'teleporter': [91, 3]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Hidden Passage', 'id': 63, 'area': 21, 'coordinates': [19, 13], 'teleporter': [5, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Two Skulls Room', 'id': 64, 'area': 21, 'coordinates': [29, 15], 'teleporter': [6, 2]}, {'name': 'Bonne Dungeon B2 - Exploding Skull Room - To Checker Room', 'id': 65, 'area': 21, 'coordinates': [8, 25], 'teleporter': [90, 3]}, {'name': 'Bonne Dungeon B2 - Box Room - To B2 - Two Skulls Room', 'id': 66, 'area': 21, 'coordinates': [59, 12], 'teleporter': [93, 3]}, {'name': 'Bonne Dungeon B2 - Quake Room - To B2 - Two Skulls Room', 'id': 67, 'area': 21, 'coordinates': [59, 28], 'teleporter': [94, 3]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Box Room', 'id': 68, 'area': 21, 'coordinates': [53, 7], 'teleporter': [7, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Quake Room', 'id': 69, 'area': 21, 'coordinates': [41, 3], 'teleporter': [8, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To Boss Room', 'id': 70, 'area': 21, 'coordinates': [47, 57], 'teleporter': [9, 2]}, {'name': 'Bonne Dungeon B2 - Two Skulls Room - To B2 - Exploding Skull Room', 'id': 71, 'area': 21, 'coordinates': [54, 23], 'teleporter': [92, 3]}, {'name': 'Bone Dungeon B2 - Boss Room - Flamerus Rex Script', 'id': 72, 'area': 22, 'coordinates': [29, 19], 'teleporter': [4, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - Tristam Leave Script', 'id': 73, 'area': 22, 'coordinates': [29, 23], 'teleporter': [75, 8]}, {'name': 'Bone Dungeon B2 - Boss Room - To B2 - Two Skulls Room', 'id': 74, 'area': 22, 'coordinates': [30, 27], 'teleporter': [95, 3]}, {'name': 'Libra Temple - Entrance', 'id': 75, 'area': 23, 'coordinates': [10, 15], 'teleporter': [13, 6]}, {'name': 'Libra Temple - Libra Tile Script', 'id': 76, 'area': 23, 'coordinates': [9, 8], 'teleporter': [59, 8]}, {'name': 'Aquaria Winter - Winter Entrance 1', 'id': 77, 'area': 24, 'coordinates': [25, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 2', 'id': 78, 'area': 24, 'coordinates': [25, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 3', 'id': 79, 'area': 24, 'coordinates': [26, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Winter - Winter Entrance 4', 'id': 80, 'area': 24, 'coordinates': [26, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Winter - Winter Phoebe's House Entrance Script", 'id': 81, 'area': 24, 'coordinates': [8, 19], 'teleporter': [10, 5]}, {'name': 'Aquaria Winter - Winter Vendor House Entrance', 'id': 82, 'area': 24, 'coordinates': [8, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Winter - Winter INN Entrance', 'id': 83, 'area': 24, 'coordinates': [26, 17], 'teleporter': [11, 5]}, {'name': 'Aquaria Summer - Summer Entrance 1', 'id': 84, 'area': 25, 'coordinates': [57, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 2', 'id': 85, 'area': 25, 'coordinates': [57, 26], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 3', 'id': 86, 'area': 25, 'coordinates': [58, 25], 'teleporter': [8, 6]}, {'name': 'Aquaria Summer - Summer Entrance 4', 'id': 87, 'area': 25, 'coordinates': [58, 26], 'teleporter': [8, 6]}, {'name': "Aquaria Summer - Summer Phoebe's House Entrance", 'id': 88, 'area': 25, 'coordinates': [40, 19], 'teleporter': [10, 5]}, {'name': "Aquaria Summer - Spencer's Place Entrance Top", 'id': 89, 'area': 25, 'coordinates': [40, 16], 'teleporter': [42, 0]}, {'name': "Aquaria Summer - Spencer's Place Entrance Side", 'id': 90, 'area': 25, 'coordinates': [41, 18], 'teleporter': [43, 0]}, {'name': 'Aquaria Summer - Summer Vendor House Entrance', 'id': 91, 'area': 25, 'coordinates': [40, 5], 'teleporter': [44, 4]}, {'name': 'Aquaria Summer - Summer INN Entrance', 'id': 92, 'area': 25, 'coordinates': [58, 17], 'teleporter': [11, 5]}, {'name': "Phoebe's House - Entrance", 'id': 93, 'area': 26, 'coordinates': [29, 14], 'teleporter': [5, 8]}, {'name': "Aquaria Vendor House - Vendor House Entrance's Script", 'id': 94, 'area': 27, 'coordinates': [7, 10], 'teleporter': [40, 8]}, {'name': 'Aquaria Vendor House - Vendor House Stairs', 'id': 95, 'area': 27, 'coordinates': [1, 4], 'teleporter': [47, 0]}, {'name': 'Aquaria Gemini Room - Gemini Script', 'id': 96, 'area': 27, 'coordinates': [2, 40], 'teleporter': [72, 8]}, {'name': 'Aquaria Gemini Room - Gemini Room Stairs', 'id': 97, 'area': 27, 'coordinates': [4, 39], 'teleporter': [48, 0]}, {'name': 'Aquaria INN - Aquaria INN entrance', 'id': 98, 'area': 27, 'coordinates': [51, 46], 'teleporter': [75, 8]}, {'name': 'Wintry Cave 1F - Main Entrance', 'id': 99, 'area': 28, 'coordinates': [50, 58], 'teleporter': [49, 0]}, {'name': 'Wintry Cave 1F - To 3F Top', 'id': 100, 'area': 28, 'coordinates': [40, 25], 'teleporter': [14, 2]}, {'name': 'Wintry Cave 1F - To 2F', 'id': 101, 'area': 28, 'coordinates': [10, 43], 'teleporter': [15, 2]}, {'name': "Wintry Cave 1F - Phoebe's Script", 'id': 102, 'area': 28, 'coordinates': [44, 37], 'teleporter': [6, 8]}, {'name': 'Wintry Cave 2F - To 3F Bottom', 'id': 103, 'area': 29, 'coordinates': [58, 5], 'teleporter': [50, 0]}, {'name': 'Wintry Cave 2F - To 1F', 'id': 104, 'area': 29, 'coordinates': [38, 18], 'teleporter': [97, 3]}, {'name': 'Wintry Cave 3F Top - Exit from 3F Top', 'id': 105, 'area': 30, 'coordinates': [24, 6], 'teleporter': [96, 3]}, {'name': 'Wintry Cave 3F Bottom - Exit to 2F', 'id': 106, 'area': 31, 'coordinates': [4, 29], 'teleporter': [51, 0]}, {'name': 'Life Temple - Entrance', 'id': 107, 'area': 32, 'coordinates': [9, 60], 'teleporter': [14, 6]}, {'name': 'Life Temple - Libra Tile Script', 'id': 108, 'area': 32, 'coordinates': [3, 55], 'teleporter': [60, 8]}, {'name': 'Life Temple - Mysterious Man Script', 'id': 109, 'area': 32, 'coordinates': [9, 44], 'teleporter': [78, 8]}, {'name': 'Fall Basin - Back Exit Script', 'id': 110, 'area': 33, 'coordinates': [17, 5], 'teleporter': [9, 0]}, {'name': 'Fall Basin - Main Exit', 'id': 111, 'area': 33, 'coordinates': [15, 26], 'teleporter': [53, 0]}, {'name': "Fall Basin - Phoebe's Script", 'id': 112, 'area': 33, 'coordinates': [17, 6], 'teleporter': [9, 8]}, {'name': 'Ice Pyramid B1 Taunt Room - To Climbing Wall Room', 'id': 113, 'area': 34, 'coordinates': [43, 6], 'teleporter': [55, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 1', 'id': 114, 'area': 35, 'coordinates': [18, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - Main Entrance 2', 'id': 115, 'area': 35, 'coordinates': [19, 36], 'teleporter': [56, 0]}, {'name': 'Ice Pyramid 1F Maze - West Stairs To 2F South Tiled Room', 'id': 116, 'area': 35, 'coordinates': [3, 27], 'teleporter': [57, 0]}, {'name': 'Ice Pyramid 1F Maze - West Center Stairs to 2F West Room', 'id': 117, 'area': 35, 'coordinates': [11, 15], 'teleporter': [58, 0]}, {'name': 'Ice Pyramid 1F Maze - East Center Stairs to 2F Center Room', 'id': 118, 'area': 35, 'coordinates': [25, 16], 'teleporter': [59, 0]}, {'name': 'Ice Pyramid 1F Maze - Upper Stairs to 2F Small North Room', 'id': 119, 'area': 35, 'coordinates': [31, 1], 'teleporter': [60, 0]}, {'name': 'Ice Pyramid 1F Maze - East Stairs to 2F North Corridor', 'id': 120, 'area': 35, 'coordinates': [34, 9], 'teleporter': [61, 0]}, {'name': "Ice Pyramid 1F Maze - Statue's Script", 'id': 121, 'area': 35, 'coordinates': [21, 32], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 1F', 'id': 122, 'area': 36, 'coordinates': [4, 26], 'teleporter': [62, 0]}, {'name': 'Ice Pyramid 2F South Tiled Room - To 3F Two Boxes Room', 'id': 123, 'area': 36, 'coordinates': [22, 17], 'teleporter': [67, 0]}, {'name': 'Ice Pyramid 2F West Room - To 1F', 'id': 124, 'area': 36, 'coordinates': [9, 10], 'teleporter': [63, 0]}, {'name': 'Ice Pyramid 2F Center Room - To 1F', 'id': 125, 'area': 36, 'coordinates': [22, 14], 'teleporter': [64, 0]}, {'name': 'Ice Pyramid 2F Small North Room - To 1F', 'id': 126, 'area': 36, 'coordinates': [26, 4], 'teleporter': [65, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 1F', 'id': 127, 'area': 36, 'coordinates': [32, 8], 'teleporter': [66, 0]}, {'name': 'Ice Pyramid 2F North Corridor - To 3F Main Loop', 'id': 128, 'area': 36, 'coordinates': [12, 7], 'teleporter': [68, 0]}, {'name': 'Ice Pyramid 3F Two Boxes Room - To 2F South Tiled Room', 'id': 129, 'area': 37, 'coordinates': [24, 54], 'teleporter': [69, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 2F Corridor', 'id': 130, 'area': 37, 'coordinates': [16, 45], 'teleporter': [70, 0]}, {'name': 'Ice Pyramid 3F Main Loop - To 4F', 'id': 131, 'area': 37, 'coordinates': [19, 43], 'teleporter': [71, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 3F Main Loop', 'id': 132, 'area': 38, 'coordinates': [52, 5], 'teleporter': [72, 0]}, {'name': 'Ice Pyramid 4F Treasure Room - To 5F Leap of Faith Room', 'id': 133, 'area': 38, 'coordinates': [62, 19], 'teleporter': [73, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - To 4F Treasure Room', 'id': 134, 'area': 39, 'coordinates': [54, 63], 'teleporter': [74, 0]}, {'name': 'Ice Pyramid 5F Leap of Faith Room - Bombed Ice Plate', 'id': 135, 'area': 39, 'coordinates': [47, 54], 'teleporter': [77, 8]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Ice Golem Room', 'id': 136, 'area': 39, 'coordinates': [39, 43], 'teleporter': [75, 0]}, {'name': 'Ice Pyramid 5F Stairs to Ice Golem - To Climbing Wall Room', 'id': 137, 'area': 39, 'coordinates': [39, 60], 'teleporter': [76, 0]}, {'name': 'Ice Pyramid - Duplicate Ice Golem Room', 'id': 138, 'area': 40, 'coordinates': [44, 43], 'teleporter': [77, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To Taunt Room', 'id': 139, 'area': 41, 'coordinates': [4, 59], 'teleporter': [78, 0]}, {'name': 'Ice Pyramid Climbing Wall Room - To 5F Stairs', 'id': 140, 'area': 41, 'coordinates': [4, 45], 'teleporter': [79, 0]}, {'name': 'Ice Pyramid Ice Golem Room - To 5F Stairs', 'id': 141, 'area': 42, 'coordinates': [44, 43], 'teleporter': [80, 0]}, {'name': 'Ice Pyramid Ice Golem Room - Ice Golem Script', 'id': 142, 'area': 42, 'coordinates': [53, 32], 'teleporter': [10, 8]}, {'name': 'Spencer Waterfall - To Spencer Cave', 'id': 143, 'area': 43, 'coordinates': [48, 57], 'teleporter': [81, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 1', 'id': 144, 'area': 43, 'coordinates': [40, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 2', 'id': 145, 'area': 43, 'coordinates': [40, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 3', 'id': 146, 'area': 43, 'coordinates': [41, 5], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Upper Exit to Aquaria 4', 'id': 147, 'area': 43, 'coordinates': [41, 6], 'teleporter': [82, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 1', 'id': 148, 'area': 43, 'coordinates': [46, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Waterfall - Right Exit to Aquaria 2', 'id': 149, 'area': 43, 'coordinates': [47, 8], 'teleporter': [83, 0]}, {'name': 'Spencer Cave Normal Main - To Waterfall', 'id': 150, 'area': 44, 'coordinates': [14, 39], 'teleporter': [85, 0]}, {'name': 'Spencer Cave Normal From Overworld - Exit to Overworld', 'id': 151, 'area': 44, 'coordinates': [15, 57], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Exit to Overworld', 'id': 152, 'area': 45, 'coordinates': [40, 29], 'teleporter': [7, 6]}, {'name': 'Spencer Cave Unplug - Libra Teleporter Start Script', 'id': 153, 'area': 45, 'coordinates': [28, 21], 'teleporter': [33, 8]}, {'name': 'Spencer Cave Unplug - Libra Teleporter End Script', 'id': 154, 'area': 45, 'coordinates': [46, 4], 'teleporter': [34, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Chest Script', 'id': 155, 'area': 45, 'coordinates': [21, 9], 'teleporter': [35, 8]}, {'name': 'Spencer Cave Unplug - Mobius Teleporter Start Script', 'id': 156, 'area': 45, 'coordinates': [29, 28], 'teleporter': [36, 8]}, {'name': 'Wintry Temple Outer Room - Main Entrance', 'id': 157, 'area': 46, 'coordinates': [8, 31], 'teleporter': [15, 6]}, {'name': 'Wintry Temple Inner Room - Gemini Tile to Sealed temple', 'id': 158, 'area': 46, 'coordinates': [9, 24], 'teleporter': [62, 8]}, {'name': 'Fireburg - To Overworld', 'id': 159, 'area': 47, 'coordinates': [4, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 160, 'area': 47, 'coordinates': [5, 13], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 161, 'area': 47, 'coordinates': [28, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - To Overworld', 'id': 162, 'area': 47, 'coordinates': [27, 15], 'teleporter': [9, 6]}, {'name': 'Fireburg - Vendor House', 'id': 163, 'area': 47, 'coordinates': [10, 24], 'teleporter': [91, 0]}, {'name': 'Fireburg - Reuben House', 'id': 164, 'area': 47, 'coordinates': [14, 6], 'teleporter': [98, 8]}, {'name': 'Fireburg - Hotel', 'id': 165, 'area': 47, 'coordinates': [20, 8], 'teleporter': [96, 8]}, {'name': 'Fireburg - GrenadeMan House Script', 'id': 166, 'area': 47, 'coordinates': [12, 18], 'teleporter': [11, 8]}, {'name': 'Reuben House - Main Entrance', 'id': 167, 'area': 48, 'coordinates': [33, 46], 'teleporter': [98, 3]}, {'name': 'GrenadeMan House - Entrance Script', 'id': 168, 'area': 49, 'coordinates': [55, 60], 'teleporter': [9, 8]}, {'name': 'GrenadeMan House - To Mobius Crest Room', 'id': 169, 'area': 49, 'coordinates': [57, 52], 'teleporter': [93, 0]}, {'name': 'GrenadeMan Mobius Room - Stairs to House', 'id': 170, 'area': 49, 'coordinates': [39, 26], 'teleporter': [94, 0]}, {'name': 'GrenadeMan Mobius Room - Mobius Teleporter Script', 'id': 171, 'area': 49, 'coordinates': [39, 23], 'teleporter': [54, 8]}, {'name': 'Fireburg Vendor House - Entrance Script', 'id': 172, 'area': 49, 'coordinates': [7, 10], 'teleporter': [95, 0]}, {'name': 'Fireburg Vendor House - Stairs to Gemini Room', 'id': 173, 'area': 49, 'coordinates': [1, 4], 'teleporter': [96, 0]}, {'name': 'Fireburg Gemini Room - Stairs to Vendor House', 'id': 174, 'area': 49, 'coordinates': [4, 39], 'teleporter': [97, 0]}, {'name': 'Fireburg Gemini Room - Gemini Teleporter Script', 'id': 175, 'area': 49, 'coordinates': [2, 40], 'teleporter': [45, 8]}, {'name': 'Fireburg Hotel Lobby - Stairs to beds', 'id': 176, 'area': 49, 'coordinates': [4, 50], 'teleporter': [213, 0]}, {'name': 'Fireburg Hotel Lobby - Entrance', 'id': 177, 'area': 49, 'coordinates': [17, 56], 'teleporter': [99, 3]}, {'name': 'Fireburg Hotel Beds - Stairs to Hotel Lobby', 'id': 178, 'area': 49, 'coordinates': [45, 59], 'teleporter': [214, 0]}, {'name': 'Mine Exterior - Main Entrance', 'id': 179, 'area': 50, 'coordinates': [5, 28], 'teleporter': [98, 0]}, {'name': 'Mine Exterior - To Cliff', 'id': 180, 'area': 50, 'coordinates': [58, 29], 'teleporter': [99, 0]}, {'name': 'Mine Exterior - To Parallel Room', 'id': 181, 'area': 50, 'coordinates': [8, 7], 'teleporter': [20, 2]}, {'name': 'Mine Exterior - To Crescent Room', 'id': 182, 'area': 50, 'coordinates': [26, 15], 'teleporter': [21, 2]}, {'name': 'Mine Exterior - To Climbing Room', 'id': 183, 'area': 50, 'coordinates': [21, 35], 'teleporter': [22, 2]}, {'name': 'Mine Exterior - Jinn Fight Script', 'id': 184, 'area': 50, 'coordinates': [58, 31], 'teleporter': [74, 8]}, {'name': 'Mine Parallel Room - To Mine Exterior', 'id': 185, 'area': 51, 'coordinates': [7, 60], 'teleporter': [100, 3]}, {'name': 'Mine Crescent Room - To Mine Exterior', 'id': 186, 'area': 51, 'coordinates': [22, 61], 'teleporter': [101, 3]}, {'name': 'Mine Climbing Room - To Mine Exterior', 'id': 187, 'area': 51, 'coordinates': [56, 21], 'teleporter': [102, 3]}, {'name': 'Mine Cliff - Entrance', 'id': 188, 'area': 52, 'coordinates': [9, 5], 'teleporter': [100, 0]}, {'name': 'Mine Cliff - Reuben Grenade Script', 'id': 189, 'area': 52, 'coordinates': [15, 7], 'teleporter': [12, 8]}, {'name': 'Sealed Temple - To Overworld', 'id': 190, 'area': 53, 'coordinates': [58, 43], 'teleporter': [16, 6]}, {'name': 'Sealed Temple - Gemini Tile Script', 'id': 191, 'area': 53, 'coordinates': [56, 38], 'teleporter': [63, 8]}, {'name': 'Volcano Base - Main Entrance 1', 'id': 192, 'area': 54, 'coordinates': [23, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 2', 'id': 193, 'area': 54, 'coordinates': [23, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 3', 'id': 194, 'area': 54, 'coordinates': [24, 25], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Main Entrance 4', 'id': 195, 'area': 54, 'coordinates': [24, 26], 'teleporter': [103, 0]}, {'name': 'Volcano Base - Left Stairs Script', 'id': 196, 'area': 54, 'coordinates': [20, 5], 'teleporter': [31, 8]}, {'name': 'Volcano Base - Right Stairs Script', 'id': 197, 'area': 54, 'coordinates': [32, 5], 'teleporter': [30, 8]}, {'name': 'Volcano Top Right - Top Exit', 'id': 198, 'area': 55, 'coordinates': [44, 8], 'teleporter': [9, 0]}, {'name': 'Volcano Top Left - To Right-Left Path Script', 'id': 199, 'area': 55, 'coordinates': [40, 24], 'teleporter': [26, 8]}, {'name': 'Volcano Top Right - To Left-Right Path Script', 'id': 200, 'area': 55, 'coordinates': [52, 24], 'teleporter': [79, 8]}, {'name': 'Volcano Right Path - To Volcano Base Script', 'id': 201, 'area': 56, 'coordinates': [48, 42], 'teleporter': [15, 8]}, {'name': 'Volcano Left Path - To Volcano Cross Left-Right', 'id': 202, 'area': 56, 'coordinates': [40, 31], 'teleporter': [25, 2]}, {'name': 'Volcano Left Path - To Volcano Cross Right-Left', 'id': 203, 'area': 56, 'coordinates': [52, 29], 'teleporter': [26, 2]}, {'name': 'Volcano Left Path - To Volcano Base Script', 'id': 204, 'area': 56, 'coordinates': [36, 42], 'teleporter': [27, 8]}, {'name': 'Volcano Cross Left-Right - To Volcano Left Path', 'id': 205, 'area': 56, 'coordinates': [10, 42], 'teleporter': [103, 3]}, {'name': 'Volcano Cross Left-Right - To Volcano Top Right Script', 'id': 206, 'area': 56, 'coordinates': [16, 24], 'teleporter': [29, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Top Left Script', 'id': 207, 'area': 56, 'coordinates': [8, 22], 'teleporter': [28, 8]}, {'name': 'Volcano Cross Right-Left - To Volcano Left Path', 'id': 208, 'area': 56, 'coordinates': [16, 42], 'teleporter': [104, 3]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 1', 'id': 209, 'area': 57, 'coordinates': [32, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - Main Entrance 2', 'id': 210, 'area': 57, 'coordinates': [33, 5], 'teleporter': [104, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Three Steps Room', 'id': 211, 'area': 57, 'coordinates': [14, 5], 'teleporter': [105, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Life Chest Room Lower', 'id': 212, 'area': 57, 'coordinates': [40, 17], 'teleporter': [106, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Big Jump Room Left', 'id': 213, 'area': 57, 'coordinates': [8, 11], 'teleporter': [108, 0]}, {'name': 'Lava Dome Inner Ring Main Loop - To Split Corridor Room', 'id': 214, 'area': 57, 'coordinates': [11, 19], 'teleporter': [111, 0]}, {'name': 'Lava Dome Inner Ring Center Ledge - To Life Chest Room Higher', 'id': 215, 'area': 57, 'coordinates': [32, 11], 'teleporter': [107, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - To Plate Corridor', 'id': 216, 'area': 57, 'coordinates': [12, 23], 'teleporter': [109, 0]}, {'name': 'Lava Dome Inner Ring Plate Ledge - Plate Script', 'id': 217, 'area': 57, 'coordinates': [5, 23], 'teleporter': [47, 8]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Pointless Room', 'id': 218, 'area': 57, 'coordinates': [0, 9], 'teleporter': [110, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Lower Moon Helm Room', 'id': 219, 'area': 57, 'coordinates': [0, 15], 'teleporter': [112, 0]}, {'name': 'Lava Dome Inner Ring Upper Ledges - To Up-Down Corridor', 'id': 220, 'area': 57, 'coordinates': [54, 5], 'teleporter': [113, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - To Jumping Maze II', 'id': 221, 'area': 57, 'coordinates': [54, 21], 'teleporter': [114, 0]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 1', 'id': 222, 'area': 57, 'coordinates': [62, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 2', 'id': 223, 'area': 57, 'coordinates': [63, 20], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 3', 'id': 224, 'area': 57, 'coordinates': [62, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Big Door Ledge - Hydra Gate 4', 'id': 225, 'area': 57, 'coordinates': [63, 21], 'teleporter': [29, 2]}, {'name': 'Lava Dome Inner Ring Tiny Bottom Ledge - To Four Boxes Corridor', 'id': 226, 'area': 57, 'coordinates': [50, 25], 'teleporter': [115, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Right Entrance', 'id': 227, 'area': 58, 'coordinates': [55, 28], 'teleporter': [116, 0]}, {'name': 'Lava Dome Jump Maze II - Upper Entrance', 'id': 228, 'area': 58, 'coordinates': [35, 3], 'teleporter': [119, 0]}, {'name': 'Lava Dome Jump Maze II - Lower Left Entrance', 'id': 229, 'area': 58, 'coordinates': [34, 27], 'teleporter': [120, 0]}, {'name': 'Lava Dome Up-Down Corridor - Upper Entrance', 'id': 230, 'area': 58, 'coordinates': [29, 8], 'teleporter': [117, 0]}, {'name': 'Lava Dome Up-Down Corridor - Lower Entrance', 'id': 231, 'area': 58, 'coordinates': [28, 25], 'teleporter': [118, 0]}, {'name': 'Lava Dome Jump Maze I - South Entrance', 'id': 232, 'area': 59, 'coordinates': [20, 27], 'teleporter': [121, 0]}, {'name': 'Lava Dome Jump Maze I - North Entrance', 'id': 233, 'area': 59, 'coordinates': [7, 3], 'teleporter': [122, 0]}, {'name': 'Lava Dome Pointless Room - Entrance', 'id': 234, 'area': 60, 'coordinates': [2, 7], 'teleporter': [123, 0]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 1', 'id': 490, 'area': 60, 'coordinates': [4, 4], 'teleporter': [99, 8]}, {'name': 'Lava Dome Pointless Room - Visit Quest Script 2', 'id': 491, 'area': 60, 'coordinates': [4, 5], 'teleporter': [99, 8]}, {'name': 'Lava Dome Lower Moon Helm Room - Left Entrance', 'id': 235, 'area': 60, 'coordinates': [2, 19], 'teleporter': [124, 0]}, {'name': 'Lava Dome Lower Moon Helm Room - Right Entrance', 'id': 236, 'area': 60, 'coordinates': [11, 21], 'teleporter': [125, 0]}, {'name': 'Lava Dome Moon Helm Room - Entrance', 'id': 237, 'area': 60, 'coordinates': [15, 23], 'teleporter': [126, 0]}, {'name': 'Lava Dome Three Jumps Room - To Main Loop', 'id': 238, 'area': 61, 'coordinates': [58, 15], 'teleporter': [127, 0]}, {'name': 'Lava Dome Life Chest Room - Lower South Entrance', 'id': 239, 'area': 61, 'coordinates': [38, 27], 'teleporter': [128, 0]}, {'name': 'Lava Dome Life Chest Room - Upper South Entrance', 'id': 240, 'area': 61, 'coordinates': [28, 23], 'teleporter': [129, 0]}, {'name': 'Lava Dome Big Jump Room - Left Entrance', 'id': 241, 'area': 62, 'coordinates': [42, 51], 'teleporter': [133, 0]}, {'name': 'Lava Dome Big Jump Room - North Entrance', 'id': 242, 'area': 62, 'coordinates': [30, 29], 'teleporter': [131, 0]}, {'name': 'Lava Dome Big Jump Room - Lower Right Stairs', 'id': 243, 'area': 62, 'coordinates': [61, 59], 'teleporter': [132, 0]}, {'name': 'Lava Dome Split Corridor - Upper Stairs', 'id': 244, 'area': 62, 'coordinates': [30, 43], 'teleporter': [130, 0]}, {'name': 'Lava Dome Split Corridor - Lower Stairs', 'id': 245, 'area': 62, 'coordinates': [36, 61], 'teleporter': [134, 0]}, {'name': 'Lava Dome Plate Corridor - Right Entrance', 'id': 246, 'area': 63, 'coordinates': [19, 29], 'teleporter': [135, 0]}, {'name': 'Lava Dome Plate Corridor - Left Entrance', 'id': 247, 'area': 63, 'coordinates': [60, 21], 'teleporter': [137, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Upper Entrance', 'id': 248, 'area': 63, 'coordinates': [22, 3], 'teleporter': [136, 0]}, {'name': 'Lava Dome Four Boxes Stairs - Lower Entrance', 'id': 249, 'area': 63, 'coordinates': [22, 17], 'teleporter': [16, 0]}, {'name': 'Lava Dome Hydra Room - South Entrance', 'id': 250, 'area': 64, 'coordinates': [14, 59], 'teleporter': [105, 3]}, {'name': 'Lava Dome Hydra Room - North Exit', 'id': 251, 'area': 64, 'coordinates': [25, 31], 'teleporter': [138, 0]}, {'name': 'Lava Dome Hydra Room - Hydra Script', 'id': 252, 'area': 64, 'coordinates': [14, 36], 'teleporter': [14, 8]}, {'name': 'Lava Dome Escape Corridor - South Entrance', 'id': 253, 'area': 65, 'coordinates': [22, 17], 'teleporter': [139, 0]}, {'name': 'Lava Dome Escape Corridor - North Entrance', 'id': 254, 'area': 65, 'coordinates': [22, 3], 'teleporter': [9, 0]}, {'name': 'Rope Bridge - West Entrance 1', 'id': 255, 'area': 66, 'coordinates': [3, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 2', 'id': 256, 'area': 66, 'coordinates': [3, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 3', 'id': 257, 'area': 66, 'coordinates': [3, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 4', 'id': 258, 'area': 66, 'coordinates': [3, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 5', 'id': 259, 'area': 66, 'coordinates': [4, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 6', 'id': 260, 'area': 66, 'coordinates': [4, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 7', 'id': 261, 'area': 66, 'coordinates': [4, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - West Entrance 8', 'id': 262, 'area': 66, 'coordinates': [4, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 1', 'id': 263, 'area': 66, 'coordinates': [59, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 2', 'id': 264, 'area': 66, 'coordinates': [59, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 3', 'id': 265, 'area': 66, 'coordinates': [59, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 4', 'id': 266, 'area': 66, 'coordinates': [59, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 5', 'id': 267, 'area': 66, 'coordinates': [60, 10], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 6', 'id': 268, 'area': 66, 'coordinates': [60, 11], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 7', 'id': 269, 'area': 66, 'coordinates': [60, 12], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - East Entrance 8', 'id': 270, 'area': 66, 'coordinates': [60, 13], 'teleporter': [140, 0]}, {'name': 'Rope Bridge - Reuben Fall Script', 'id': 271, 'area': 66, 'coordinates': [13, 12], 'teleporter': [15, 8]}, {'name': 'Alive Forest - West Entrance 1', 'id': 272, 'area': 67, 'coordinates': [8, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - West Entrance 2', 'id': 273, 'area': 67, 'coordinates': [9, 13], 'teleporter': [142, 0]}, {'name': 'Alive Forest - Giant Tree Entrance', 'id': 274, 'area': 67, 'coordinates': [42, 42], 'teleporter': [143, 0]}, {'name': 'Alive Forest - Libra Teleporter Script', 'id': 275, 'area': 67, 'coordinates': [8, 52], 'teleporter': [64, 8]}, {'name': 'Alive Forest - Gemini Teleporter Script', 'id': 276, 'area': 67, 'coordinates': [57, 49], 'teleporter': [65, 8]}, {'name': 'Alive Forest - Mobius Teleporter Script', 'id': 277, 'area': 67, 'coordinates': [24, 10], 'teleporter': [66, 8]}, {'name': 'Giant Tree 1F - Entrance Script 1', 'id': 278, 'area': 68, 'coordinates': [18, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - Entrance Script 2', 'id': 279, 'area': 68, 'coordinates': [19, 31], 'teleporter': [56, 1]}, {'name': 'Giant Tree 1F - North Entrance To 2F', 'id': 280, 'area': 68, 'coordinates': [16, 1], 'teleporter': [144, 0]}, {'name': 'Giant Tree 2F Main Lobby - North Entrance to 1F', 'id': 281, 'area': 69, 'coordinates': [44, 33], 'teleporter': [145, 0]}, {'name': 'Giant Tree 2F Main Lobby - Central Entrance to 3F', 'id': 282, 'area': 69, 'coordinates': [42, 47], 'teleporter': [146, 0]}, {'name': 'Giant Tree 2F Main Lobby - West Entrance to Mushroom Room', 'id': 283, 'area': 69, 'coordinates': [58, 49], 'teleporter': [149, 0]}, {'name': 'Giant Tree 2F West Ledge - To 3F Northwest Ledge', 'id': 284, 'area': 69, 'coordinates': [34, 37], 'teleporter': [147, 0]}, {'name': 'Giant Tree 2F Fall From Vine Script', 'id': 482, 'area': 69, 'coordinates': [46, 51], 'teleporter': [76, 8]}, {'name': 'Giant Tree Meteor Chest Room - To 2F Mushroom Room', 'id': 285, 'area': 69, 'coordinates': [58, 44], 'teleporter': [148, 0]}, {'name': 'Giant Tree 2F Mushroom Room - Entrance', 'id': 286, 'area': 70, 'coordinates': [55, 18], 'teleporter': [150, 0]}, {'name': 'Giant Tree 2F Mushroom Room - North Face to Meteor', 'id': 287, 'area': 70, 'coordinates': [56, 7], 'teleporter': [151, 0]}, {'name': 'Giant Tree 3F Central Room - Central Entrance to 2F', 'id': 288, 'area': 71, 'coordinates': [46, 53], 'teleporter': [152, 0]}, {'name': 'Giant Tree 3F Central Room - East Entrance to Worm Room', 'id': 289, 'area': 71, 'coordinates': [58, 39], 'teleporter': [153, 0]}, {'name': 'Giant Tree 3F Lower Corridor - Entrance from Worm Room', 'id': 290, 'area': 71, 'coordinates': [45, 39], 'teleporter': [154, 0]}, {'name': 'Giant Tree 3F West Platform - Lower Entrance', 'id': 291, 'area': 71, 'coordinates': [33, 43], 'teleporter': [155, 0]}, {'name': 'Giant Tree 3F West Platform - Top Entrance', 'id': 292, 'area': 71, 'coordinates': [52, 25], 'teleporter': [156, 0]}, {'name': 'Giant Tree Worm Room - East Entrance', 'id': 293, 'area': 72, 'coordinates': [20, 58], 'teleporter': [157, 0]}, {'name': 'Giant Tree Worm Room - West Entrance', 'id': 294, 'area': 72, 'coordinates': [6, 56], 'teleporter': [158, 0]}, {'name': 'Giant Tree 4F Lower Floor - Entrance', 'id': 295, 'area': 73, 'coordinates': [20, 7], 'teleporter': [159, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower West Mouth', 'id': 296, 'area': 73, 'coordinates': [8, 23], 'teleporter': [160, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower Central Mouth', 'id': 297, 'area': 73, 'coordinates': [14, 25], 'teleporter': [161, 0]}, {'name': 'Giant Tree 4F Lower Floor - Lower East Mouth', 'id': 298, 'area': 73, 'coordinates': [20, 25], 'teleporter': [162, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper West Mouth', 'id': 299, 'area': 73, 'coordinates': [8, 19], 'teleporter': [163, 0]}, {'name': 'Giant Tree 4F Upper Floor - Upper Central Mouth', 'id': 300, 'area': 73, 'coordinates': [12, 17], 'teleporter': [164, 0]}, {'name': 'Giant Tree 4F Slime Room - Exit', 'id': 301, 'area': 74, 'coordinates': [47, 10], 'teleporter': [165, 0]}, {'name': 'Giant Tree 4F Slime Room - West Entrance', 'id': 302, 'area': 74, 'coordinates': [45, 24], 'teleporter': [166, 0]}, {'name': 'Giant Tree 4F Slime Room - Central Entrance', 'id': 303, 'area': 74, 'coordinates': [50, 24], 'teleporter': [167, 0]}, {'name': 'Giant Tree 4F Slime Room - East Entrance', 'id': 304, 'area': 74, 'coordinates': [57, 28], 'teleporter': [168, 0]}, {'name': 'Giant Tree 5F - Entrance', 'id': 305, 'area': 75, 'coordinates': [14, 51], 'teleporter': [169, 0]}, {'name': 'Giant Tree 5F - Giant Tree Face', 'id': 306, 'area': 75, 'coordinates': [14, 37], 'teleporter': [170, 0]}, {'name': 'Kaidge Temple - Entrance', 'id': 307, 'area': 77, 'coordinates': [44, 63], 'teleporter': [18, 6]}, {'name': 'Kaidge Temple - Mobius Teleporter Script', 'id': 308, 'area': 77, 'coordinates': [35, 57], 'teleporter': [71, 8]}, {'name': 'Windhole Temple - Entrance', 'id': 309, 'area': 78, 'coordinates': [10, 29], 'teleporter': [173, 0]}, {'name': 'Mount Gale - Entrance 1', 'id': 310, 'area': 79, 'coordinates': [1, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Entrance 2', 'id': 311, 'area': 79, 'coordinates': [2, 45], 'teleporter': [174, 0]}, {'name': 'Mount Gale - Visit Quest', 'id': 494, 'area': 79, 'coordinates': [44, 7], 'teleporter': [101, 8]}, {'name': 'Windia - Main Entrance 1', 'id': 312, 'area': 80, 'coordinates': [12, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 2', 'id': 313, 'area': 80, 'coordinates': [13, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 3', 'id': 314, 'area': 80, 'coordinates': [14, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 4', 'id': 315, 'area': 80, 'coordinates': [15, 40], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 5', 'id': 316, 'area': 80, 'coordinates': [12, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 6', 'id': 317, 'area': 80, 'coordinates': [13, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 7', 'id': 318, 'area': 80, 'coordinates': [14, 41], 'teleporter': [10, 6]}, {'name': 'Windia - Main Entrance 8', 'id': 319, 'area': 80, 'coordinates': [15, 41], 'teleporter': [10, 6]}, {'name': "Windia - Otto's House", 'id': 320, 'area': 80, 'coordinates': [21, 39], 'teleporter': [30, 5]}, {'name': "Windia - INN's Script", 'id': 321, 'area': 80, 'coordinates': [18, 34], 'teleporter': [97, 8]}, {'name': 'Windia - Vendor House', 'id': 322, 'area': 80, 'coordinates': [8, 36], 'teleporter': [32, 5]}, {'name': 'Windia - Kid House', 'id': 323, 'area': 80, 'coordinates': [7, 23], 'teleporter': [176, 4]}, {'name': 'Windia - Old People House', 'id': 324, 'area': 80, 'coordinates': [19, 21], 'teleporter': [177, 4]}, {'name': 'Windia - Rainbow Bridge Script', 'id': 325, 'area': 80, 'coordinates': [21, 9], 'teleporter': [10, 6]}, {'name': "Otto's House - Attic Stairs", 'id': 326, 'area': 81, 'coordinates': [2, 19], 'teleporter': [33, 2]}, {'name': "Otto's House - Entrance", 'id': 327, 'area': 81, 'coordinates': [9, 30], 'teleporter': [106, 3]}, {'name': "Otto's Attic - Stairs", 'id': 328, 'area': 81, 'coordinates': [26, 23], 'teleporter': [107, 3]}, {'name': 'Windia Kid House - Entrance Script', 'id': 329, 'area': 82, 'coordinates': [7, 10], 'teleporter': [178, 0]}, {'name': 'Windia Kid House - Basement Stairs', 'id': 330, 'area': 82, 'coordinates': [1, 4], 'teleporter': [180, 0]}, {'name': 'Windia Old People House - Entrance', 'id': 331, 'area': 82, 'coordinates': [55, 12], 'teleporter': [179, 0]}, {'name': 'Windia Old People House - Basement Stairs', 'id': 332, 'area': 82, 'coordinates': [60, 5], 'teleporter': [181, 0]}, {'name': 'Windia Kid House Basement - Stairs', 'id': 333, 'area': 82, 'coordinates': [43, 8], 'teleporter': [182, 0]}, {'name': 'Windia Kid House Basement - Mobius Teleporter', 'id': 334, 'area': 82, 'coordinates': [41, 9], 'teleporter': [44, 8]}, {'name': 'Windia Old People House Basement - Stairs', 'id': 335, 'area': 82, 'coordinates': [39, 26], 'teleporter': [183, 0]}, {'name': 'Windia Old People House Basement - Mobius Teleporter Script', 'id': 336, 'area': 82, 'coordinates': [39, 23], 'teleporter': [43, 8]}, {'name': 'Windia Inn Lobby - Stairs to Beds', 'id': 337, 'area': 82, 'coordinates': [45, 24], 'teleporter': [102, 8]}, {'name': 'Windia Inn Lobby - Exit', 'id': 338, 'area': 82, 'coordinates': [53, 30], 'teleporter': [135, 3]}, {'name': 'Windia Inn Beds - Stairs to Lobby', 'id': 339, 'area': 82, 'coordinates': [33, 59], 'teleporter': [216, 0]}, {'name': 'Windia Vendor House - Entrance', 'id': 340, 'area': 82, 'coordinates': [29, 14], 'teleporter': [108, 3]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 1', 'id': 341, 'area': 83, 'coordinates': [47, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 2', 'id': 342, 'area': 83, 'coordinates': [47, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 3', 'id': 343, 'area': 83, 'coordinates': [48, 29], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Main Entrance 4', 'id': 344, 'area': 83, 'coordinates': [48, 30], 'teleporter': [184, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - East Entrance', 'id': 345, 'area': 83, 'coordinates': [55, 12], 'teleporter': [185, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - South Stairs', 'id': 346, 'area': 83, 'coordinates': [51, 25], 'teleporter': [186, 0]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 1', 'id': 347, 'area': 83, 'coordinates': [47, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Main Lobby - Pazuzu Script 2', 'id': 348, 'area': 83, 'coordinates': [48, 8], 'teleporter': [16, 8]}, {'name': 'Pazuzu Tower 1F Boxes Room - West Stairs', 'id': 349, 'area': 83, 'coordinates': [38, 17], 'teleporter': [187, 0]}, {'name': 'Pazuzu 2F - West Upper Stairs', 'id': 350, 'area': 84, 'coordinates': [7, 11], 'teleporter': [188, 0]}, {'name': 'Pazuzu 2F - South Stairs', 'id': 351, 'area': 84, 'coordinates': [20, 24], 'teleporter': [189, 0]}, {'name': 'Pazuzu 2F - West Lower Stairs', 'id': 352, 'area': 84, 'coordinates': [6, 17], 'teleporter': [190, 0]}, {'name': 'Pazuzu 2F - Central Stairs', 'id': 353, 'area': 84, 'coordinates': [15, 15], 'teleporter': [191, 0]}, {'name': 'Pazuzu 2F - Pazuzu 1', 'id': 354, 'area': 84, 'coordinates': [15, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 2F - Pazuzu 2', 'id': 355, 'area': 84, 'coordinates': [16, 8], 'teleporter': [17, 8]}, {'name': 'Pazuzu 3F Main Room - North Stairs', 'id': 356, 'area': 85, 'coordinates': [23, 11], 'teleporter': [192, 0]}, {'name': 'Pazuzu 3F Main Room - West Stairs', 'id': 357, 'area': 85, 'coordinates': [7, 15], 'teleporter': [193, 0]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 1', 'id': 358, 'area': 85, 'coordinates': [15, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Main Room - Pazuzu Script 2', 'id': 359, 'area': 85, 'coordinates': [16, 8], 'teleporter': [18, 8]}, {'name': 'Pazuzu 3F Central Island - Central Stairs', 'id': 360, 'area': 85, 'coordinates': [15, 14], 'teleporter': [194, 0]}, {'name': 'Pazuzu 3F Central Island - South Stairs', 'id': 361, 'area': 85, 'coordinates': [17, 25], 'teleporter': [195, 0]}, {'name': 'Pazuzu 4F - Northwest Stairs', 'id': 362, 'area': 86, 'coordinates': [39, 12], 'teleporter': [196, 0]}, {'name': 'Pazuzu 4F - Southwest Stairs', 'id': 363, 'area': 86, 'coordinates': [39, 19], 'teleporter': [197, 0]}, {'name': 'Pazuzu 4F - South Stairs', 'id': 364, 'area': 86, 'coordinates': [47, 24], 'teleporter': [198, 0]}, {'name': 'Pazuzu 4F - Northeast Stairs', 'id': 365, 'area': 86, 'coordinates': [54, 9], 'teleporter': [199, 0]}, {'name': 'Pazuzu 4F - Pazuzu Script 1', 'id': 366, 'area': 86, 'coordinates': [47, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 4F - Pazuzu Script 2', 'id': 367, 'area': 86, 'coordinates': [48, 8], 'teleporter': [19, 8]}, {'name': 'Pazuzu 5F Pazuzu Loop - West Stairs', 'id': 368, 'area': 87, 'coordinates': [9, 49], 'teleporter': [200, 0]}, {'name': 'Pazuzu 5F Pazuzu Loop - South Stairs', 'id': 369, 'area': 87, 'coordinates': [16, 55], 'teleporter': [201, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northeast Stairs', 'id': 370, 'area': 87, 'coordinates': [22, 40], 'teleporter': [202, 0]}, {'name': 'Pazuzu 5F Upper Loop - Northwest Stairs', 'id': 371, 'area': 87, 'coordinates': [9, 40], 'teleporter': [203, 0]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 1', 'id': 372, 'area': 87, 'coordinates': [15, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 5F Upper Loop - Pazuzu Script 2', 'id': 373, 'area': 87, 'coordinates': [16, 40], 'teleporter': [20, 8]}, {'name': 'Pazuzu 6F - West Stairs', 'id': 374, 'area': 88, 'coordinates': [41, 47], 'teleporter': [204, 0]}, {'name': 'Pazuzu 6F - Northwest Stairs', 'id': 375, 'area': 88, 'coordinates': [41, 40], 'teleporter': [205, 0]}, {'name': 'Pazuzu 6F - Northeast Stairs', 'id': 376, 'area': 88, 'coordinates': [54, 40], 'teleporter': [206, 0]}, {'name': 'Pazuzu 6F - South Stairs', 'id': 377, 'area': 88, 'coordinates': [52, 56], 'teleporter': [207, 0]}, {'name': 'Pazuzu 6F - Pazuzu Script 1', 'id': 378, 'area': 88, 'coordinates': [47, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 6F - Pazuzu Script 2', 'id': 379, 'area': 88, 'coordinates': [48, 40], 'teleporter': [21, 8]}, {'name': 'Pazuzu 7F Main Room - Southwest Stairs', 'id': 380, 'area': 89, 'coordinates': [15, 54], 'teleporter': [26, 0]}, {'name': 'Pazuzu 7F Main Room - Northeast Stairs', 'id': 381, 'area': 89, 'coordinates': [21, 40], 'teleporter': [27, 0]}, {'name': 'Pazuzu 7F Main Room - Southeast Stairs', 'id': 382, 'area': 89, 'coordinates': [21, 56], 'teleporter': [28, 0]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 1', 'id': 383, 'area': 89, 'coordinates': [15, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Pazuzu Script 2', 'id': 384, 'area': 89, 'coordinates': [16, 44], 'teleporter': [22, 8]}, {'name': 'Pazuzu 7F Main Room - Crystal Script', 'id': 480, 'area': 89, 'coordinates': [15, 40], 'teleporter': [38, 8]}, {'name': 'Pazuzu 1F to 3F - South Stairs', 'id': 385, 'area': 90, 'coordinates': [43, 60], 'teleporter': [29, 0]}, {'name': 'Pazuzu 1F to 3F - North Stairs', 'id': 386, 'area': 90, 'coordinates': [43, 36], 'teleporter': [30, 0]}, {'name': 'Pazuzu 3F to 5F - South Stairs', 'id': 387, 'area': 91, 'coordinates': [43, 60], 'teleporter': [40, 0]}, {'name': 'Pazuzu 3F to 5F - North Stairs', 'id': 388, 'area': 91, 'coordinates': [43, 36], 'teleporter': [41, 0]}, {'name': 'Pazuzu 5F to 7F - South Stairs', 'id': 389, 'area': 92, 'coordinates': [43, 60], 'teleporter': [38, 0]}, {'name': 'Pazuzu 5F to 7F - North Stairs', 'id': 390, 'area': 92, 'coordinates': [43, 36], 'teleporter': [39, 0]}, {'name': 'Pazuzu 2F to 4F - South Stairs', 'id': 391, 'area': 93, 'coordinates': [43, 60], 'teleporter': [21, 0]}, {'name': 'Pazuzu 2F to 4F - North Stairs', 'id': 392, 'area': 93, 'coordinates': [43, 36], 'teleporter': [22, 0]}, {'name': 'Pazuzu 4F to 6F - South Stairs', 'id': 393, 'area': 94, 'coordinates': [43, 60], 'teleporter': [2, 0]}, {'name': 'Pazuzu 4F to 6F - North Stairs', 'id': 394, 'area': 94, 'coordinates': [43, 36], 'teleporter': [3, 0]}, {'name': 'Light Temple - Entrance', 'id': 395, 'area': 95, 'coordinates': [28, 57], 'teleporter': [19, 6]}, {'name': 'Light Temple - Mobius Teleporter Script', 'id': 396, 'area': 95, 'coordinates': [29, 37], 'teleporter': [70, 8]}, {'name': 'Light Temple - Visit Quest Script 1', 'id': 492, 'area': 95, 'coordinates': [34, 39], 'teleporter': [100, 8]}, {'name': 'Light Temple - Visit Quest Script 2', 'id': 493, 'area': 95, 'coordinates': [35, 39], 'teleporter': [100, 8]}, {'name': 'Ship Dock - Mobius Teleporter Script', 'id': 397, 'area': 96, 'coordinates': [15, 18], 'teleporter': [61, 8]}, {'name': 'Ship Dock - From Overworld', 'id': 398, 'area': 96, 'coordinates': [15, 11], 'teleporter': [73, 0]}, {'name': 'Ship Dock - Entrance', 'id': 399, 'area': 96, 'coordinates': [15, 23], 'teleporter': [17, 6]}, {'name': 'Mac Ship Deck - East Entrance Script', 'id': 400, 'area': 97, 'coordinates': [26, 40], 'teleporter': [37, 8]}, {'name': 'Mac Ship Deck - Central Stairs Script', 'id': 401, 'area': 97, 'coordinates': [16, 47], 'teleporter': [50, 8]}, {'name': 'Mac Ship Deck - West Stairs Script', 'id': 402, 'area': 97, 'coordinates': [8, 34], 'teleporter': [51, 8]}, {'name': 'Mac Ship Deck - East Stairs Script', 'id': 403, 'area': 97, 'coordinates': [24, 36], 'teleporter': [52, 8]}, {'name': 'Mac Ship Deck - North Stairs Script', 'id': 404, 'area': 97, 'coordinates': [12, 9], 'teleporter': [53, 8]}, {'name': 'Mac Ship B1 Outer Ring - South Stairs', 'id': 405, 'area': 98, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring - West Stairs', 'id': 406, 'area': 98, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring - East Stairs', 'id': 407, 'area': 98, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Outer Ring - Northwest Stairs', 'id': 408, 'area': 98, 'coordinates': [10, 23], 'teleporter': [88, 0]}, {'name': 'Mac Ship B1 Square Room - North Stairs', 'id': 409, 'area': 98, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room - South Stairs', 'id': 410, 'area': 98, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room - Stairs', 'id': 411, 'area': 98, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor - South Stairs', 'id': 412, 'area': 98, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor - North Stairs', 'id': 413, 'area': 98, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B2 South Corridor - South Stairs', 'id': 414, 'area': 99, 'coordinates': [48, 51], 'teleporter': [57, 1]}, {'name': 'Mac Ship B2 South Corridor - North Stairs Script', 'id': 415, 'area': 99, 'coordinates': [48, 38], 'teleporter': [55, 8]}, {'name': 'Mac Ship B2 North Corridor - South Stairs Script', 'id': 416, 'area': 99, 'coordinates': [48, 27], 'teleporter': [56, 8]}, {'name': 'Mac Ship B2 North Corridor - North Stairs Script', 'id': 417, 'area': 99, 'coordinates': [48, 12], 'teleporter': [57, 8]}, {'name': 'Mac Ship B2 Outer Ring - Northwest Stairs Script', 'id': 418, 'area': 99, 'coordinates': [55, 11], 'teleporter': [58, 8]}, {'name': 'Mac Ship B1 Outer Ring Cleared - South Stairs', 'id': 419, 'area': 100, 'coordinates': [16, 45], 'teleporter': [208, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - West Stairs', 'id': 420, 'area': 100, 'coordinates': [8, 35], 'teleporter': [175, 0]}, {'name': 'Mac Ship B1 Outer Ring Cleared - East Stairs', 'id': 421, 'area': 100, 'coordinates': [25, 37], 'teleporter': [172, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - North Stairs', 'id': 422, 'area': 100, 'coordinates': [14, 9], 'teleporter': [141, 0]}, {'name': 'Mac Ship B1 Square Room Cleared - South Stairs', 'id': 423, 'area': 100, 'coordinates': [16, 12], 'teleporter': [87, 0]}, {'name': 'Mac Ship B1 Mac Room Cleared - Main Stairs', 'id': 424, 'area': 100, 'coordinates': [16, 51], 'teleporter': [101, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - South Stairs', 'id': 425, 'area': 100, 'coordinates': [16, 38], 'teleporter': [102, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - North Stairs', 'id': 426, 'area': 100, 'coordinates': [16, 26], 'teleporter': [86, 0]}, {'name': 'Mac Ship B1 Central Corridor Cleared - Northwest Stairs', 'id': 427, 'area': 100, 'coordinates': [23, 10], 'teleporter': [88, 0]}, {'name': 'Doom Castle Corridor of Destiny - South Entrance', 'id': 428, 'area': 101, 'coordinates': [59, 29], 'teleporter': [84, 0]}, {'name': 'Doom Castle Corridor of Destiny - Ice Floor Entrance', 'id': 429, 'area': 101, 'coordinates': [59, 21], 'teleporter': [35, 2]}, {'name': 'Doom Castle Corridor of Destiny - Lava Floor Entrance', 'id': 430, 'area': 101, 'coordinates': [59, 13], 'teleporter': [209, 0]}, {'name': 'Doom Castle Corridor of Destiny - Sky Floor Entrance', 'id': 431, 'area': 101, 'coordinates': [59, 5], 'teleporter': [211, 0]}, {'name': 'Doom Castle Corridor of Destiny - Hero Room Entrance', 'id': 432, 'area': 101, 'coordinates': [59, 61], 'teleporter': [13, 2]}, {'name': 'Doom Castle Ice Floor - Entrance', 'id': 433, 'area': 102, 'coordinates': [23, 42], 'teleporter': [109, 3]}, {'name': 'Doom Castle Lava Floor - Entrance', 'id': 434, 'area': 103, 'coordinates': [23, 40], 'teleporter': [210, 0]}, {'name': 'Doom Castle Sky Floor - Entrance', 'id': 435, 'area': 104, 'coordinates': [24, 41], 'teleporter': [212, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 1', 'id': 436, 'area': 106, 'coordinates': [15, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 2', 'id': 437, 'area': 106, 'coordinates': [16, 5], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 3', 'id': 438, 'area': 106, 'coordinates': [15, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Dark King Entrance 4', 'id': 439, 'area': 106, 'coordinates': [16, 4], 'teleporter': [54, 0]}, {'name': 'Doom Castle Hero Room - Hero Statue Script', 'id': 440, 'area': 106, 'coordinates': [15, 17], 'teleporter': [24, 8]}, {'name': 'Doom Castle Hero Room - Entrance', 'id': 441, 'area': 106, 'coordinates': [15, 24], 'teleporter': [110, 3]}, {'name': 'Doom Castle Dark King Room - Entrance', 'id': 442, 'area': 107, 'coordinates': [14, 26], 'teleporter': [52, 0]}, {'name': 'Doom Castle Dark King Room - Dark King Script', 'id': 443, 'area': 107, 'coordinates': [14, 15], 'teleporter': [25, 8]}, {'name': 'Doom Castle Dark King Room - Unknown', 'id': 444, 'area': 107, 'coordinates': [47, 54], 'teleporter': [77, 0]}, {'name': 'Overworld - Level Forest', 'id': 445, 'area': 0, 'type': 'Overworld', 'teleporter': [46, 8]}, {'name': 'Overworld - Foresta', 'id': 446, 'area': 0, 'type': 'Overworld', 'teleporter': [2, 1]}, {'name': 'Overworld - Sand Temple', 'id': 447, 'area': 0, 'type': 'Overworld', 'teleporter': [3, 1]}, {'name': 'Overworld - Bone Dungeon', 'id': 448, 'area': 0, 'type': 'Overworld', 'teleporter': [4, 1]}, {'name': 'Overworld - Focus Tower Foresta', 'id': 449, 'area': 0, 'type': 'Overworld', 'teleporter': [5, 1]}, {'name': 'Overworld - Focus Tower Aquaria', 'id': 450, 'area': 0, 'type': 'Overworld', 'teleporter': [19, 1]}, {'name': 'Overworld - Libra Temple', 'id': 451, 'area': 0, 'type': 'Overworld', 'teleporter': [7, 1]}, {'name': 'Overworld - Aquaria', 'id': 452, 'area': 0, 'type': 'Overworld', 'teleporter': [8, 8]}, {'name': 'Overworld - Wintry Cave', 'id': 453, 'area': 0, 'type': 'Overworld', 'teleporter': [10, 1]}, {'name': 'Overworld - Life Temple', 'id': 454, 'area': 0, 'type': 'Overworld', 'teleporter': [11, 1]}, {'name': 'Overworld - Falls Basin', 'id': 455, 'area': 0, 'type': 'Overworld', 'teleporter': [12, 1]}, {'name': 'Overworld - Ice Pyramid', 'id': 456, 'area': 0, 'type': 'Overworld', 'teleporter': [13, 1]}, {'name': "Overworld - Spencer's Place", 'id': 457, 'area': 0, 'type': 'Overworld', 'teleporter': [48, 8]}, {'name': 'Overworld - Wintry Temple', 'id': 458, 'area': 0, 'type': 'Overworld', 'teleporter': [16, 1]}, {'name': 'Overworld - Focus Tower Frozen Strip', 'id': 459, 'area': 0, 'type': 'Overworld', 'teleporter': [17, 1]}, {'name': 'Overworld - Focus Tower Fireburg', 'id': 460, 'area': 0, 'type': 'Overworld', 'teleporter': [18, 1]}, {'name': 'Overworld - Fireburg', 'id': 461, 'area': 0, 'type': 'Overworld', 'teleporter': [20, 1]}, {'name': 'Overworld - Mine', 'id': 462, 'area': 0, 'type': 'Overworld', 'teleporter': [21, 1]}, {'name': 'Overworld - Sealed Temple', 'id': 463, 'area': 0, 'type': 'Overworld', 'teleporter': [22, 1]}, {'name': 'Overworld - Volcano', 'id': 464, 'area': 0, 'type': 'Overworld', 'teleporter': [23, 1]}, {'name': 'Overworld - Lava Dome', 'id': 465, 'area': 0, 'type': 'Overworld', 'teleporter': [24, 1]}, {'name': 'Overworld - Focus Tower Windia', 'id': 466, 'area': 0, 'type': 'Overworld', 'teleporter': [6, 1]}, {'name': 'Overworld - Rope Bridge', 'id': 467, 'area': 0, 'type': 'Overworld', 'teleporter': [25, 1]}, {'name': 'Overworld - Alive Forest', 'id': 468, 'area': 0, 'type': 'Overworld', 'teleporter': [26, 1]}, {'name': 'Overworld - Giant Tree', 'id': 469, 'area': 0, 'type': 'Overworld', 'teleporter': [27, 1]}, {'name': 'Overworld - Kaidge Temple', 'id': 470, 'area': 0, 'type': 'Overworld', 'teleporter': [28, 1]}, {'name': 'Overworld - Windia', 'id': 471, 'area': 0, 'type': 'Overworld', 'teleporter': [29, 1]}, {'name': 'Overworld - Windhole Temple', 'id': 472, 'area': 0, 'type': 'Overworld', 'teleporter': [30, 1]}, {'name': 'Overworld - Mount Gale', 'id': 473, 'area': 0, 'type': 'Overworld', 'teleporter': [31, 1]}, {'name': 'Overworld - Pazuzu Tower', 'id': 474, 'area': 0, 'type': 'Overworld', 'teleporter': [32, 1]}, {'name': 'Overworld - Ship Dock', 'id': 475, 'area': 0, 'type': 'Overworld', 'teleporter': [62, 1]}, {'name': 'Overworld - Doom Castle', 'id': 476, 'area': 0, 'type': 'Overworld', 'teleporter': [33, 1]}, {'name': 'Overworld - Light Temple', 'id': 477, 'area': 0, 'type': 'Overworld', 'teleporter': [34, 1]}, {'name': 'Overworld - Mac Ship', 'id': 478, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Overworld - Mac Ship Doom', 'id': 479, 'area': 0, 'type': 'Overworld', 'teleporter': [36, 1]}, {'name': 'Dummy House - Bed Script', 'id': 480, 'area': 17, 'coordinates': [40, 56], 'teleporter': [1, 8]}, {'name': 'Dummy House - Entrance', 'id': 481, 'area': 17, 'coordinates': [41, 59], 'teleporter': [0, 10]}] \ No newline at end of file diff --git a/worlds/ffmq/data/rooms.yaml b/worlds/ffmq/data/rooms.yaml deleted file mode 100644 index e0c2e8d7f9fc..000000000000 --- a/worlds/ffmq/data/rooms.yaml +++ /dev/null @@ -1,4026 +0,0 @@ -- name: Overworld - id: 0 - type: "Overworld" - game_objects: [] - links: - - target_room: 220 # To Forest Subregion - access: [] -- name: Subregion Foresta - id: 220 - type: "Subregion" - region: "Foresta" - game_objects: - - name: "Foresta South Battlefield" - object_id: 0x01 - location: "ForestaSouthBattlefield" - location_slot: "ForestaSouthBattlefield" - type: "BattlefieldXp" - access: [] - - name: "Foresta West Battlefield" - object_id: 0x02 - location: "ForestaWestBattlefield" - location_slot: "ForestaWestBattlefield" - type: "BattlefieldItem" - access: [] - - name: "Foresta East Battlefield" - object_id: 0x03 - location: "ForestaEastBattlefield" - location_slot: "ForestaEastBattlefield" - type: "BattlefieldGp" - access: [] - links: - - target_room: 15 # Level Forest - location: "LevelForest" - location_slot: "LevelForest" - entrance: 445 - teleporter: [0x2E, 8] - access: [] - - target_room: 16 # Foresta - location: "Foresta" - location_slot: "Foresta" - entrance: 446 - teleporter: [0x02, 1] - access: [] - - target_room: 24 # Sand Temple - location: "SandTemple" - location_slot: "SandTemple" - entrance: 447 - teleporter: [0x03, 1] - access: [] - - target_room: 25 # Bone Dungeon - location: "BoneDungeon" - location_slot: "BoneDungeon" - entrance: 448 - teleporter: [0x04, 1] - access: [] - - target_room: 3 # Focus Tower Foresta - location: "FocusTowerForesta" - location_slot: "FocusTowerForesta" - entrance: 449 - teleporter: [0x05, 1] - access: [] - - target_room: 221 - access: ["SandCoin"] - - target_room: 224 - access: ["RiverCoin"] - - target_room: 226 - access: ["SunCoin"] -- name: Subregion Aquaria - id: 221 - type: "Subregion" - region: "Aquaria" - game_objects: - - name: "South of Libra Temple Battlefield" - object_id: 0x04 - location: "AquariaBattlefield01" - location_slot: "AquariaBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "East of Libra Temple Battlefield" - object_id: 0x05 - location: "AquariaBattlefield02" - location_slot: "AquariaBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "South of Aquaria Battlefield" - object_id: 0x06 - location: "AquariaBattlefield03" - location_slot: "AquariaBattlefield03" - type: "BattlefieldItem" - access: [] - - name: "South of Wintry Cave Battlefield" - object_id: 0x07 - location: "WintryBattlefield01" - location_slot: "WintryBattlefield01" - type: "BattlefieldXp" - access: [] - - name: "West of Wintry Cave Battlefield" - object_id: 0x08 - location: "WintryBattlefield02" - location_slot: "WintryBattlefield02" - type: "BattlefieldGp" - access: [] - - name: "Ice Pyramid Battlefield" - object_id: 0x09 - location: "PyramidBattlefield01" - location_slot: "PyramidBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 10 # Focus Tower Aquaria - location: "FocusTowerAquaria" - location_slot: "FocusTowerAquaria" - entrance: 450 - teleporter: [0x13, 1] - access: [] - - target_room: 39 # Libra Temple - location: "LibraTemple" - location_slot: "LibraTemple" - entrance: 451 - teleporter: [0x07, 1] - access: [] - - target_room: 40 # Aquaria - location: "Aquaria" - location_slot: "Aquaria" - entrance: 452 - teleporter: [0x08, 8] - access: [] - - target_room: 45 # Wintry Cave - location: "WintryCave" - location_slot: "WintryCave" - entrance: 453 - teleporter: [0x0A, 1] - access: [] - - target_room: 52 # Falls Basin - location: "FallsBasin" - location_slot: "FallsBasin" - entrance: 455 - teleporter: [0x0C, 1] - access: [] - - target_room: 54 # Ice Pyramid - location: "IcePyramid" - location_slot: "IcePyramid" - entrance: 456 - teleporter: [0x0D, 1] # Will be switched to a script - access: [] - - target_room: 220 - access: ["SandCoin"] - - target_room: 224 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["SandCoin", "SunCoin"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Life Temple - id: 222 - type: "Subregion" - region: "LifeTemple" - game_objects: [] - links: - - target_room: 51 # Life Temple - location: "LifeTemple" - location_slot: "LifeTemple" - entrance: 454 - teleporter: [0x0B, 1] - access: [] -- name: Subregion Frozen Fields - id: 223 - type: "Subregion" - region: "AquariaFrozenField" - game_objects: - - name: "North of Libra Temple Battlefield" - object_id: 0x0A - location: "LibraBattlefield01" - location_slot: "LibraBattlefield01" - type: "BattlefieldItem" - access: [] - - name: "Aquaria Frozen Field Battlefield" - object_id: 0x0B - location: "LibraBattlefield02" - location_slot: "LibraBattlefield02" - type: "BattlefieldXp" - access: [] - links: - - target_room: 74 # Wintry Temple - location: "WintryTemple" - location_slot: "WintryTemple" - entrance: 458 - teleporter: [0x10, 1] - access: [] - - target_room: 14 # Focus Tower Frozen Strip - location: "FocusTowerFrozen" - location_slot: "FocusTowerFrozen" - entrance: 459 - teleporter: [0x11, 1] - access: [] - - target_room: 221 - access: [] - - target_room: 225 - access: ["SummerAquaria", "DualheadHydra"] -- name: Subregion Fireburg - id: 224 - type: "Subregion" - region: "Fireburg" - game_objects: - - name: "Path to Fireburg Southern Battlefield" - object_id: 0x0C - location: "FireburgBattlefield01" - location_slot: "FireburgBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Path to Fireburg Central Battlefield" - object_id: 0x0D - location: "FireburgBattlefield02" - location_slot: "FireburgBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Path to Fireburg Northern Battlefield" - object_id: 0x0E - location: "FireburgBattlefield03" - location_slot: "FireburgBattlefield03" - type: "BattlefieldXp" - access: [] - - name: "Sealed Temple Battlefield" - object_id: 0x0F - location: "MineBattlefield01" - location_slot: "MineBattlefield01" - type: "BattlefieldGp" - access: [] - - name: "Mine Battlefield" - object_id: 0x10 - location: "MineBattlefield02" - location_slot: "MineBattlefield02" - type: "BattlefieldItem" - access: [] - - name: "Boulder Battlefield" - object_id: 0x11 - location: "MineBattlefield03" - location_slot: "MineBattlefield03" - type: "BattlefieldXp" - access: [] - links: - - target_room: 13 # Focus Tower Fireburg - location: "FocusTowerFireburg" - location_slot: "FocusTowerFireburg" - entrance: 460 - teleporter: [0x12, 1] - access: [] - - target_room: 76 # Fireburg - location: "Fireburg" - location_slot: "Fireburg" - entrance: 461 - teleporter: [0x14, 1] - access: [] - - target_room: 84 # Mine - location: "Mine" - location_slot: "Mine" - entrance: 462 - teleporter: [0x15, 1] - access: [] - - target_room: 92 # Sealed Temple - location: "SealedTemple" - location_slot: "SealedTemple" - entrance: 463 - teleporter: [0x16, 1] - access: [] - - target_room: 93 # Volcano - location: "Volcano" - location_slot: "Volcano" - entrance: 464 - teleporter: [0x17, 1] # Also this one / 0x0F, 8 - access: [] - - target_room: 100 # Lava Dome - location: "LavaDome" - location_slot: "LavaDome" - entrance: 465 - teleporter: [0x18, 1] - access: [] - - target_room: 220 - access: ["RiverCoin"] - - target_room: 221 - access: ["SandCoin", "RiverCoin"] - - target_room: 226 - access: ["RiverCoin", "SunCoin"] - - target_room: 225 - access: ["DualheadHydra"] -- name: Subregion Volcano Battlefield - id: 225 - type: "Subregion" - region: "VolcanoBattlefield" - game_objects: - - name: "Volcano Battlefield" - object_id: 0x12 - location: "VolcanoBattlefield01" - location_slot: "VolcanoBattlefield01" - type: "BattlefieldXp" - access: [] - links: - - target_room: 224 - access: ["DualheadHydra"] - - target_room: 223 - access: ["SummerAquaria"] -- name: Subregion Windia - id: 226 - type: "Subregion" - region: "Windia" - game_objects: - - name: "Kaidge Temple Battlefield" - object_id: 0x13 - location: "WindiaBattlefield01" - location_slot: "WindiaBattlefield01" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - - name: "South of Windia Battlefield" - object_id: 0x14 - location: "WindiaBattlefield02" - location_slot: "WindiaBattlefield02" - type: "BattlefieldXp" - access: ["SandCoin", "RiverCoin"] - links: - - target_room: 9 # Focus Tower Windia - location: "FocusTowerWindia" - location_slot: "FocusTowerWindia" - entrance: 466 - teleporter: [0x06, 1] - access: [] - - target_room: 123 # Rope Bridge - location: "RopeBridge" - location_slot: "RopeBridge" - entrance: 467 - teleporter: [0x19, 1] - access: [] - - target_room: 124 # Alive Forest - location: "AliveForest" - location_slot: "AliveForest" - entrance: 468 - teleporter: [0x1A, 1] - access: [] - - target_room: 125 # Giant Tree - location: "GiantTree" - location_slot: "GiantTree" - entrance: 469 - teleporter: [0x1B, 1] - access: ["Barred"] - - target_room: 152 # Kaidge Temple - location: "KaidgeTemple" - location_slot: "KaidgeTemple" - entrance: 470 - teleporter: [0x1C, 1] - access: [] - - target_room: 156 # Windia - location: "Windia" - location_slot: "Windia" - entrance: 471 - teleporter: [0x1D, 1] - access: [] - - target_room: 154 # Windhole Temple - location: "WindholeTemple" - location_slot: "WindholeTemple" - entrance: 472 - teleporter: [0x1E, 1] - access: [] - - target_room: 155 # Mount Gale - location: "MountGale" - location_slot: "MountGale" - entrance: 473 - teleporter: [0x1F, 1] - access: [] - - target_room: 166 # Pazuzu Tower - location: "PazuzusTower" - location_slot: "PazuzusTower" - entrance: 474 - teleporter: [0x20, 1] - access: [] - - target_room: 220 - access: ["SunCoin"] - - target_room: 221 - access: ["SandCoin", "SunCoin"] - - target_room: 224 - access: ["RiverCoin", "SunCoin"] - - target_room: 227 - access: ["RainbowBridge"] -- name: Subregion Spencer's Cave - id: 227 - type: "Subregion" - region: "SpencerCave" - game_objects: [] - links: - - target_room: 73 # Spencer's Place - location: "SpencersPlace" - location_slot: "SpencersPlace" - entrance: 457 - teleporter: [0x30, 8] - access: [] - - target_room: 226 - access: ["RainbowBridge"] -- name: Subregion Ship Dock - id: 228 - type: "Subregion" - region: "ShipDock" - game_objects: [] - links: - - target_room: 186 # Ship Dock - location: "ShipDock" - location_slot: "ShipDock" - entrance: 475 - teleporter: [0x3E, 1] - access: [] - - target_room: 229 - access: ["ShipLiberated", "ShipDockAccess"] -- name: Subregion Mac's Ship - id: 229 - type: "Subregion" - region: "MacShip" - game_objects: [] - links: - - target_room: 187 # Mac Ship - location: "MacsShip" - location_slot: "MacsShip" - entrance: 478 - teleporter: [0x24, 1] - access: [] - - target_room: 228 - access: ["ShipLiberated", "ShipDockAccess"] - - target_room: 231 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Subregion Light Temple - id: 230 - type: "Subregion" - region: "LightTemple" - game_objects: [] - links: - - target_room: 185 # Light Temple - location: "LightTemple" - location_slot: "LightTemple" - entrance: 477 - teleporter: [0x23, 1] - access: [] -- name: Subregion Doom Castle - id: 231 - type: "Subregion" - region: "DoomCastle" - game_objects: [] - links: - - target_room: 1 # Doom Castle - location: "DoomCastle" - location_slot: "DoomCastle" - entrance: 476 - teleporter: [0x21, 1] - access: [] - - target_room: 187 # Mac Ship Doom - location: "MacsShipDoom" - location_slot: "MacsShipDoom" - entrance: 479 - teleporter: [0x24, 1] - access: ["Barred"] - - target_room: 229 - access: ["ShipLoaned", "ShipDockAccess", "ShipSteeringWheel"] -- name: Doom Castle - Sand Floor - id: 1 - game_objects: - - name: "Doom Castle B2 - Southeast Chest" - object_id: 0x01 - type: "Chest" - access: ["Bomb"] - - name: "Doom Castle B2 - Bone Ledge Box" - object_id: 0x1E - type: "Box" - access: [] - - name: "Doom Castle B2 - Hook Platform Box" - object_id: 0x1F - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 231 - entrance: 1 - teleporter: [1, 6] - access: [] - - target_room: 5 - entrance: 0 - teleporter: [0, 0] - access: ["DragonClaw", "MegaGrenade"] -- name: Doom Castle - Aero Room - id: 2 - game_objects: - - name: "Doom Castle B2 - Sun Door Chest" - object_id: 0x00 - type: "Chest" - access: [] - links: - - target_room: 4 - entrance: 2 - teleporter: [1, 0] - access: [] -- name: Focus Tower B1 - Main Loop - id: 3 - game_objects: [] - links: - - target_room: 220 - entrance: 3 - teleporter: [2, 6] - access: [] - - target_room: 6 - entrance: 4 - teleporter: [4, 0] - access: [] -- name: Focus Tower B1 - Aero Corridor - id: 4 - game_objects: [] - links: - - target_room: 9 - entrance: 5 - teleporter: [5, 0] - access: [] - - target_room: 2 - entrance: 6 - teleporter: [8, 0] - access: [] -- name: Focus Tower B1 - Inner Loop - id: 5 - game_objects: [] - links: - - target_room: 1 - entrance: 8 - teleporter: [7, 0] - access: [] - - target_room: 201 - entrance: 7 - teleporter: [6, 0] - access: [] -- name: Focus Tower 1F Main Lobby - id: 6 - game_objects: - - name: "Focus Tower 1F - Main Lobby Box" - object_id: 0x21 - type: "Box" - access: [] - links: - - target_room: 3 - entrance: 11 - teleporter: [11, 0] - access: [] - - target_room: 7 - access: ["SandCoin"] - - target_room: 8 - access: ["RiverCoin"] - - target_room: 9 - access: ["SunCoin"] -- name: Focus Tower 1F SandCoin Room - id: 7 - game_objects: [] - links: - - target_room: 6 - access: ["SandCoin"] - - target_room: 10 - entrance: 10 - teleporter: [10, 0] - access: [] -- name: Focus Tower 1F RiverCoin Room - id: 8 - game_objects: [] - links: - - target_room: 6 - access: ["RiverCoin"] - - target_room: 11 - entrance: 14 - teleporter: [14, 0] - access: [] -- name: Focus Tower 1F SunCoin Room - id: 9 - game_objects: [] - links: - - target_room: 6 - access: ["SunCoin"] - - target_room: 4 - entrance: 12 - teleporter: [12, 0] - access: [] - - target_room: 226 - entrance: 9 - teleporter: [3, 6] - access: [] -- name: Focus Tower 1F SkyCoin Room - id: 201 - game_objects: [] - links: - - target_room: 195 - entrance: 13 - teleporter: [13, 0] - access: ["SkyCoin", "FlamerusRex", "IceGolem", "DualheadHydra", "Pazuzu"] - - target_room: 5 - entrance: 15 - teleporter: [15, 0] - access: [] -- name: Focus Tower 2F - Sand Coin Passage - id: 10 - game_objects: - - name: "Focus Tower 2F - Sand Door Chest" - object_id: 0x03 - type: "Chest" - access: [] - links: - - target_room: 221 - entrance: 16 - teleporter: [4, 6] - access: [] - - target_room: 7 - entrance: 17 - teleporter: [17, 0] - access: [] -- name: Focus Tower 2F - River Coin Passage - id: 11 - game_objects: [] - links: - - target_room: 8 - entrance: 18 - teleporter: [18, 0] - access: [] - - target_room: 13 - entrance: 19 - teleporter: [20, 0] - access: [] -- name: Focus Tower 2F - Venus Chest Room - id: 12 - game_objects: - - name: "Focus Tower 2F - Back Door Chest" - object_id: 0x02 - type: "Chest" - access: [] - - name: "Focus Tower 2F - Venus Chest" - object_id: 9 - type: "NPC" - access: ["Bomb", "VenusKey"] - links: - - target_room: 14 - entrance: 20 - teleporter: [19, 0] - access: [] -- name: Focus Tower 3F - Lower Floor - id: 13 - game_objects: - - name: "Focus Tower 3F - River Door Box" - object_id: 0x22 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 22 - teleporter: [6, 6] - access: [] - - target_room: 11 - entrance: 23 - teleporter: [24, 0] - access: [] -- name: Focus Tower 3F - Upper Floor - id: 14 - game_objects: [] - links: - - target_room: 223 - entrance: 24 - teleporter: [5, 6] - access: [] - - target_room: 12 - entrance: 25 - teleporter: [23, 0] - access: [] -- name: Level Forest - id: 15 - game_objects: - - name: "Level Forest - Northwest Box" - object_id: 0x28 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Northeast Box" - object_id: 0x29 - type: "Box" - access: ["Axe"] - - name: "Level Forest - Middle Box" - object_id: 0x2A - type: "Box" - access: [] - - name: "Level Forest - Southwest Box" - object_id: 0x2B - type: "Box" - access: ["Axe"] - - name: "Level Forest - Southeast Box" - object_id: 0x2C - type: "Box" - access: ["Axe"] - - name: "Minotaur" - object_id: 0 - type: "Trigger" - on_trigger: ["Minotaur"] - access: ["Kaeli1"] - - name: "Level Forest - Old Man" - object_id: 0 - type: "NPC" - access: [] - - name: "Level Forest - Kaeli" - object_id: 1 - type: "NPC" - access: ["Kaeli1", "Minotaur"] - links: - - target_room: 220 - entrance: 28 - teleporter: [25, 0] - access: [] -- name: Foresta - id: 16 - game_objects: - - name: "Foresta - Outside Box" - object_id: 0x2D - type: "Box" - access: ["Axe"] - links: - - target_room: 220 - entrance: 38 - teleporter: [31, 0] - access: [] - - target_room: 17 - entrance: 44 - teleporter: [0, 5] - access: [] - - target_room: 18 - entrance: 42 - teleporter: [32, 4] - access: [] - - target_room: 19 - entrance: 43 - teleporter: [33, 0] - access: [] - - target_room: 20 - entrance: 45 - teleporter: [1, 5] - access: [] -- name: Kaeli's House - id: 17 - game_objects: - - name: "Foresta - Kaeli's House Box" - object_id: 0x2E - type: "Box" - access: [] - - name: "Kaeli Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli1"] - access: ["TreeWither"] - - name: "Kaeli 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Kaeli2"] - access: ["Kaeli1", "Minotaur", "Elixir"] - links: - - target_room: 16 - entrance: 46 - teleporter: [86, 3] - access: [] -- name: Foresta Houses - Old Man's House Main - id: 18 - game_objects: [] - links: - - target_room: 19 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 47 - teleporter: [34, 0] - access: [] -- name: Foresta Houses - Old Man's House Back - id: 19 - game_objects: - - name: "Foresta - Old Man House Chest" - object_id: 0x05 - type: "Chest" - access: [] - - name: "Old Man Barrel" - object_id: 0 - type: "Trigger" - on_trigger: ["BarrelPushed"] - access: [] - links: - - target_room: 18 - access: ["BarrelPushed"] - - target_room: 16 - entrance: 48 - teleporter: [35, 0] - access: [] -- name: Foresta Houses - Rest House - id: 20 - game_objects: - - name: "Foresta - Rest House Box" - object_id: 0x2F - type: "Box" - access: [] - links: - - target_room: 16 - entrance: 50 - teleporter: [87, 3] - access: [] -- name: Libra Treehouse - id: 21 - game_objects: - - name: "Alive Forest - Libra Treehouse Box" - object_id: 0x32 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 51 - teleporter: [67, 8] - access: ["LibraCrest"] -- name: Gemini Treehouse - id: 22 - game_objects: - - name: "Alive Forest - Gemini Treehouse Box" - object_id: 0x33 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 52 - teleporter: [68, 8] - access: ["GeminiCrest"] -- name: Mobius Treehouse - id: 23 - game_objects: - - name: "Alive Forest - Mobius Treehouse West Box" - object_id: 0x30 - type: "Box" - access: [] - - name: "Alive Forest - Mobius Treehouse East Box" - object_id: 0x31 - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 53 - teleporter: [69, 8] - access: ["MobiusCrest"] -- name: Sand Temple - id: 24 - game_objects: - - name: "Tristam Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Tristam"] - access: [] - links: - - target_room: 220 - entrance: 54 - teleporter: [36, 0] - access: [] -- name: Bone Dungeon 1F - id: 25 - game_objects: - - name: "Bone Dungeon 1F - Entrance Room West Box" - object_id: 0x35 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room Middle Box" - object_id: 0x36 - type: "Box" - access: [] - - name: "Bone Dungeon 1F - Entrance Room East Box" - object_id: 0x37 - type: "Box" - access: [] - links: - - target_room: 220 - entrance: 55 - teleporter: [37, 0] - access: [] - - target_room: 26 - entrance: 56 - teleporter: [2, 2] - access: [] -- name: Bone Dungeon B1 - Waterway - id: 26 - game_objects: - - name: "Bone Dungeon B1 - Skull Chest" - object_id: 0x06 - type: "Chest" - access: ["Bomb"] - - name: "Bone Dungeon B1 - Tristam" - object_id: 2 - type: "NPC" - access: ["Tristam"] - - name: "Tristam Bone Dungeon Item Given" - object_id: 0 - type: "Trigger" - on_trigger: ["TristamBoneItemGiven"] - access: ["Tristam"] - links: - - target_room: 25 - entrance: 59 - teleporter: [88, 3] - access: [] - - target_room: 28 - entrance: 57 - teleporter: [3, 2] - access: ["Bomb"] -- name: Bone Dungeon B1 - Checker Room - id: 28 - game_objects: - - name: "Bone Dungeon B1 - Checker Room Box" - object_id: 0x38 - type: "Box" - access: ["Bomb"] - links: - - target_room: 26 - entrance: 61 - teleporter: [89, 3] - access: [] - - target_room: 30 - entrance: 60 - teleporter: [4, 2] - access: [] -- name: Bone Dungeon B1 - Hidden Room - id: 29 - game_objects: - - name: "Bone Dungeon B1 - Ribcage Waterway Box" - object_id: 0x39 - type: "Box" - access: [] - links: - - target_room: 31 - entrance: 62 - teleporter: [91, 3] - access: [] -- name: Bone Dungeon B2 - Exploding Skull Room - First Room - id: 30 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Alcove Box" - object_id: 0x3B - type: "Box" - access: [] - - name: "Long Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["LongSpineBombed"] - access: ["Bomb"] - links: - - target_room: 28 - entrance: 65 - teleporter: [90, 3] - access: [] - - target_room: 31 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Second Room - id: 31 - game_objects: - - name: "Bone Dungeon B2 - Spines Room Looped Hallway Box" - object_id: 0x3A - type: "Box" - access: [] - - name: "Short Spine" - object_id: 0 - type: "Trigger" - on_trigger: ["ShortSpineBombed"] - access: ["Bomb"] - links: - - target_room: 29 - entrance: 63 - teleporter: [5, 2] - access: ["LongSpineBombed"] - - target_room: 32 - access: ["ShortSpineBombed"] - - target_room: 30 - access: ["LongSpineBombed"] -- name: Bone Dungeon B2 - Exploding Skull Room - Third Room - id: 32 - game_objects: [] - links: - - target_room: 35 - entrance: 64 - teleporter: [6, 2] - access: [] - - target_room: 31 - access: ["ShortSpineBombed"] -- name: Bone Dungeon B2 - Box Room - id: 33 - game_objects: - - name: "Bone Dungeon B2 - Lone Room Box" - object_id: 0x3D - type: "Box" - access: [] - links: - - target_room: 36 - entrance: 66 - teleporter: [93, 3] - access: [] -- name: Bone Dungeon B2 - Quake Room - id: 34 - game_objects: - - name: "Bone Dungeon B2 - Penultimate Room Chest" - object_id: 0x07 - type: "Chest" - access: [] - links: - - target_room: 37 - entrance: 67 - teleporter: [94, 3] - access: [] -- name: Bone Dungeon B2 - Two Skulls Room - First Room - id: 35 - game_objects: - - name: "Bone Dungeon B2 - Two Skulls Room Box" - object_id: 0x3C - type: "Box" - access: [] - - name: "Skull 1" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull1Bombed"] - access: ["Bomb"] - links: - - target_room: 32 - entrance: 71 - teleporter: [92, 3] - access: [] - - target_room: 36 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Second Room - id: 36 - game_objects: - - name: "Skull 2" - object_id: 0 - type: "Trigger" - on_trigger: ["Skull2Bombed"] - access: ["Bomb"] - links: - - target_room: 33 - entrance: 68 - teleporter: [7, 2] - access: [] - - target_room: 37 - access: ["Skull2Bombed"] - - target_room: 35 - access: ["Skull1Bombed"] -- name: Bone Dungeon B2 - Two Skulls Room - Third Room - id: 37 - game_objects: [] - links: - - target_room: 34 - entrance: 69 - teleporter: [8, 2] - access: [] - - target_room: 38 - entrance: 70 - teleporter: [9, 2] - access: ["Bomb"] - - target_room: 36 - access: ["Skull2Bombed"] -- name: Bone Dungeon B2 - Boss Room - id: 38 - game_objects: - - name: "Bone Dungeon B2 - North Box" - object_id: 0x3E - type: "Box" - access: [] - - name: "Bone Dungeon B2 - South Box" - object_id: 0x3F - type: "Box" - access: [] - - name: "Bone Dungeon B2 - Flamerus Rex Chest" - object_id: 0x08 - type: "Chest" - access: [] - - name: "Bone Dungeon B2 - Tristam's Treasure Chest" - object_id: 0x04 - type: "Chest" - access: [] - - name: "Flamerus Rex" - object_id: 0 - type: "Trigger" - on_trigger: ["FlamerusRex"] - access: [] - links: - - target_room: 37 - entrance: 74 - teleporter: [95, 3] - access: [] -- name: Libra Temple - id: 39 - game_objects: - - name: "Libra Temple - Box" - object_id: 0x40 - type: "Box" - access: [] - - name: "Phoebe Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Phoebe1"] - access: [] - links: - - target_room: 221 - entrance: 75 - teleporter: [13, 6] - access: [] - - target_room: 51 - entrance: 76 - teleporter: [59, 8] - access: ["LibraCrest"] -- name: Aquaria - id: 40 - game_objects: - - name: "Summer Aquaria" - object_id: 0 - type: "Trigger" - on_trigger: ["SummerAquaria"] - access: ["WakeWater"] - links: - - target_room: 221 - entrance: 77 - teleporter: [8, 6] - access: [] - - target_room: 41 - entrance: 81 - teleporter: [10, 5] - access: [] - - target_room: 42 - entrance: 82 - teleporter: [44, 4] - access: [] - - target_room: 44 - entrance: 83 - teleporter: [11, 5] - access: [] - - target_room: 71 - entrance: 89 - teleporter: [42, 0] - access: ["SummerAquaria"] - - target_room: 71 - entrance: 90 - teleporter: [43, 0] - access: ["SummerAquaria"] -- name: Phoebe's House - id: 41 - game_objects: - - name: "Aquaria - Phoebe's House Chest" - object_id: 0x41 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 93 - teleporter: [5, 8] - access: [] -- name: Aquaria Vendor House - id: 42 - game_objects: - - name: "Aquaria - Vendor" - object_id: 4 - type: "NPC" - access: [] - - name: "Aquaria - Vendor House Box" - object_id: 0x42 - type: "Box" - access: [] - links: - - target_room: 40 - entrance: 94 - teleporter: [40, 8] - access: [] - - target_room: 43 - entrance: 95 - teleporter: [47, 0] - access: [] -- name: Aquaria Gemini Room - id: 43 - game_objects: [] - links: - - target_room: 42 - entrance: 97 - teleporter: [48, 0] - access: [] - - target_room: 81 - entrance: 96 - teleporter: [72, 8] - access: ["GeminiCrest"] -- name: Aquaria INN - id: 44 - game_objects: [] - links: - - target_room: 40 - entrance: 98 - teleporter: [75, 8] - access: [] -- name: Wintry Cave 1F - East Ledge - id: 45 - game_objects: - - name: "Wintry Cave 1F - North Box" - object_id: 0x43 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Entrance Box" - object_id: 0x46 - type: "Box" - access: [] - - name: "Wintry Cave 1F - Slippery Cliff Box" - object_id: 0x44 - type: "Box" - access: ["Claw"] - - name: "Wintry Cave 1F - Phoebe" - object_id: 5 - type: "NPC" - access: ["Phoebe1"] - links: - - target_room: 221 - entrance: 99 - teleporter: [49, 0] - access: [] - - target_room: 49 - entrance: 100 - teleporter: [14, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 1F - Central Space - id: 46 - game_objects: - - name: "Wintry Cave 1F - Scenic Overlook Box" - object_id: 0x45 - type: "Box" - access: ["Claw"] - links: - - target_room: 45 - access: ["Claw"] - - target_room: 47 - access: ["Claw"] -- name: Wintry Cave 1F - West Ledge - id: 47 - game_objects: [] - links: - - target_room: 48 - entrance: 101 - teleporter: [15, 2] - access: ["Bomb"] - - target_room: 46 - access: ["Claw"] -- name: Wintry Cave 2F - id: 48 - game_objects: - - name: "Wintry Cave 2F - West Left Box" - object_id: 0x47 - type: "Box" - access: [] - - name: "Wintry Cave 2F - West Right Box" - object_id: 0x48 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Left Box" - object_id: 0x49 - type: "Box" - access: [] - - name: "Wintry Cave 2F - East Right Box" - object_id: 0x4A - type: "Box" - access: [] - links: - - target_room: 47 - entrance: 104 - teleporter: [97, 3] - access: [] - - target_room: 50 - entrance: 103 - teleporter: [50, 0] - access: [] -- name: Wintry Cave 3F Top - id: 49 - game_objects: - - name: "Wintry Cave 3F - West Box" - object_id: 0x4B - type: "Box" - access: [] - - name: "Wintry Cave 3F - East Box" - object_id: 0x4C - type: "Box" - access: [] - links: - - target_room: 45 - entrance: 105 - teleporter: [96, 3] - access: [] -- name: Wintry Cave 3F Bottom - id: 50 - game_objects: - - name: "Wintry Cave 3F - Squidite Chest" - object_id: 0x09 - type: "Chest" - access: ["Phanquid"] - - name: "Phanquid" - object_id: 0 - type: "Trigger" - on_trigger: ["Phanquid"] - access: [] - - name: "Wintry Cave 3F - Before Boss Box" - object_id: 0x4D - type: "Box" - access: [] - links: - - target_room: 48 - entrance: 106 - teleporter: [51, 0] - access: [] -- name: Life Temple - id: 51 - game_objects: - - name: "Life Temple - Box" - object_id: 0x4E - type: "Box" - access: [] - - name: "Life Temple - Mysterious Man" - object_id: 6 - type: "NPC" - access: [] - links: - - target_room: 222 - entrance: 107 - teleporter: [14, 6] - access: [] - - target_room: 39 - entrance: 108 - teleporter: [60, 8] - access: ["LibraCrest"] -- name: Fall Basin - id: 52 - game_objects: - - name: "Falls Basin - Snow Crab Chest" - object_id: 0x0A - type: "Chest" - access: ["FreezerCrab"] - - name: "Freezer Crab" - object_id: 0 - type: "Trigger" - on_trigger: ["FreezerCrab"] - access: [] - - name: "Falls Basin - Box" - object_id: 0x4F - type: "Box" - access: [] - links: - - target_room: 221 - entrance: 111 - teleporter: [53, 0] - access: [] -- name: Ice Pyramid B1 Taunt Room - id: 53 - game_objects: - - name: "Ice Pyramid B1 - Chest" - object_id: 0x0B - type: "Chest" - access: [] - - name: "Ice Pyramid B1 - West Box" - object_id: 0x50 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - North Box" - object_id: 0x51 - type: "Box" - access: [] - - name: "Ice Pyramid B1 - East Box" - object_id: 0x52 - type: "Box" - access: [] - links: - - target_room: 68 - entrance: 113 - teleporter: [55, 0] - access: [] -- name: Ice Pyramid 1F Maze Lobby - id: 54 - game_objects: - - name: "Ice Pyramid 1F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid1FStatue"] - access: ["Sword"] - links: - - target_room: 221 - entrance: 114 - teleporter: [56, 0] - access: [] - - target_room: 55 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 1F Maze - id: 55 - game_objects: - - name: "Ice Pyramid 1F - East Alcove Chest" - object_id: 0x0D - type: "Chest" - access: [] - - name: "Ice Pyramid 1F - Sandwiched Alcove Box" - object_id: 0x53 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Left Box" - object_id: 0x54 - type: "Box" - access: [] - - name: "Ice Pyramid 1F - Southwest Right Box" - object_id: 0x55 - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 116 - teleporter: [57, 0] - access: [] - - target_room: 57 - entrance: 117 - teleporter: [58, 0] - access: [] - - target_room: 58 - entrance: 118 - teleporter: [59, 0] - access: [] - - target_room: 59 - entrance: 119 - teleporter: [60, 0] - access: [] - - target_room: 60 - entrance: 120 - teleporter: [61, 0] - access: [] - - target_room: 54 - access: ["IcePyramid1FStatue"] -- name: Ice Pyramid 2F South Tiled Room - id: 56 - game_objects: - - name: "Ice Pyramid 2F - South Side Glass Door Box" - object_id: 0x57 - type: "Box" - access: ["Sword"] - - name: "Ice Pyramid 2F - South Side East Box" - object_id: 0x5B - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 122 - teleporter: [62, 0] - access: [] - - target_room: 61 - entrance: 123 - teleporter: [67, 0] - access: [] -- name: Ice Pyramid 2F West Room - id: 57 - game_objects: - - name: "Ice Pyramid 2F - Northwest Room Box" - object_id: 0x5A - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 124 - teleporter: [63, 0] - access: [] -- name: Ice Pyramid 2F Center Room - id: 58 - game_objects: - - name: "Ice Pyramid 2F - Center Room Box" - object_id: 0x56 - type: "Box" - access: [] - links: - - target_room: 55 - entrance: 125 - teleporter: [64, 0] - access: [] -- name: Ice Pyramid 2F Small North Room - id: 59 - game_objects: - - name: "Ice Pyramid 2F - North Room Glass Door Box" - object_id: 0x58 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 126 - teleporter: [65, 0] - access: [] -- name: Ice Pyramid 2F North Corridor - id: 60 - game_objects: - - name: "Ice Pyramid 2F - North Corridor Glass Door Box" - object_id: 0x59 - type: "Box" - access: ["Sword"] - links: - - target_room: 55 - entrance: 127 - teleporter: [66, 0] - access: [] - - target_room: 62 - entrance: 128 - teleporter: [68, 0] - access: [] -- name: Ice Pyramid 3F Two Boxes Room - id: 61 - game_objects: - - name: "Ice Pyramid 3F - Staircase Dead End Left Box" - object_id: 0x5E - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Staircase Dead End Right Box" - object_id: 0x5F - type: "Box" - access: [] - links: - - target_room: 56 - entrance: 129 - teleporter: [69, 0] - access: [] -- name: Ice Pyramid 3F Main Loop - id: 62 - game_objects: - - name: "Ice Pyramid 3F - Inner Room North Box" - object_id: 0x5C - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Inner Room South Box" - object_id: 0x5D - type: "Box" - access: [] - - name: "Ice Pyramid 3F - East Alcove Box" - object_id: 0x60 - type: "Box" - access: [] - - name: "Ice Pyramid 3F - Leapfrog Box" - object_id: 0x61 - type: "Box" - access: [] - - name: "Ice Pyramid 3F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid3FStatue"] - access: ["Sword"] - links: - - target_room: 60 - entrance: 130 - teleporter: [70, 0] - access: [] - - target_room: 63 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 3F Blocked Room - id: 63 - game_objects: [] - links: - - target_room: 64 - entrance: 131 - teleporter: [71, 0] - access: [] - - target_room: 62 - access: ["IcePyramid3FStatue"] -- name: Ice Pyramid 4F Main Loop - id: 64 - game_objects: [] - links: - - target_room: 66 - entrance: 133 - teleporter: [73, 0] - access: [] - - target_room: 63 - entrance: 132 - teleporter: [72, 0] - access: [] - - target_room: 65 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 4F Treasure Room - id: 65 - game_objects: - - name: "Ice Pyramid 4F - Chest" - object_id: 0x0C - type: "Chest" - access: [] - - name: "Ice Pyramid 4F - Northwest Box" - object_id: 0x62 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Left Box" - object_id: 0x63 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - West Right Box" - object_id: 0x64 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Left Box" - object_id: 0x65 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - South Right Box" - object_id: 0x66 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Left Box" - object_id: 0x67 - type: "Box" - access: [] - - name: "Ice Pyramid 4F - East Right Box" - object_id: 0x68 - type: "Box" - access: [] - - name: "Ice Pyramid 4F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid4FStatue"] - access: ["Sword"] - links: - - target_room: 64 - access: ["IcePyramid4FStatue"] -- name: Ice Pyramid 5F Leap of Faith Room - id: 66 - game_objects: - - name: "Ice Pyramid 5F - Glass Door Left Box" - object_id: 0x69 - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - West Ledge Box" - object_id: 0x6A - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Shelf Box" - object_id: 0x6B - type: "Box" - access: [] - - name: "Ice Pyramid 5F - South Leapfrog Box" - object_id: 0x6C - type: "Box" - access: [] - - name: "Ice Pyramid 5F - Glass Door Right Box" - object_id: 0x6D - type: "Box" - access: ["IcePyramid5FStatue"] - - name: "Ice Pyramid 5F - North Box" - object_id: 0x6E - type: "Box" - access: [] - links: - - target_room: 64 - entrance: 134 - teleporter: [74, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 53 - access: ["Bomb", "Claw", "Sword"] -- name: Ice Pyramid 5F Stairs to Ice Golem - id: 67 - game_objects: - - name: "Ice Pyramid 5F Statue" - object_id: 0 - type: "Trigger" - on_trigger: ["IcePyramid5FStatue"] - access: ["Sword"] - links: - - target_room: 69 - entrance: 137 - teleporter: [76, 0] - access: [] - - target_room: 65 - access: [] - - target_room: 70 - entrance: 136 - teleporter: [75, 0] - access: [] -- name: Ice Pyramid Climbing Wall Room Lower Space - id: 68 - game_objects: [] - links: - - target_room: 53 - entrance: 139 - teleporter: [78, 0] - access: [] - - target_room: 69 - access: ["Claw"] -- name: Ice Pyramid Climbing Wall Room Upper Space - id: 69 - game_objects: [] - links: - - target_room: 67 - entrance: 140 - teleporter: [79, 0] - access: [] - - target_room: 68 - access: ["Claw"] -- name: Ice Pyramid Ice Golem Room - id: 70 - game_objects: - - name: "Ice Pyramid 6F - Ice Golem Chest" - object_id: 0x0E - type: "Chest" - access: ["IceGolem"] - - name: "Ice Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["IceGolem"] - access: [] - links: - - target_room: 67 - entrance: 141 - teleporter: [80, 0] - access: [] - - target_room: 66 - access: [] -- name: Spencer Waterfall - id: 71 - game_objects: [] - links: - - target_room: 72 - entrance: 143 - teleporter: [81, 0] - access: [] - - target_room: 40 - entrance: 145 - teleporter: [82, 0] - access: [] - - target_room: 40 - entrance: 148 - teleporter: [83, 0] - access: [] -- name: Spencer Cave Normal Main - id: 72 - game_objects: - - name: "Spencer's Cave - Box" - object_id: 0x6F - type: "Box" - access: ["Claw"] - - name: "Spencer's Cave - Spencer" - object_id: 8 - type: "NPC" - access: [] - - name: "Spencer's Cave - Locked Chest" - object_id: 13 - type: "NPC" - access: ["VenusKey"] - links: - - target_room: 71 - entrance: 150 - teleporter: [85, 0] - access: [] -- name: Spencer Cave Normal South Ledge - id: 73 - game_objects: - - name: "Collapse Spencer's Cave" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLiberated"] - access: ["MegaGrenade"] - links: - - target_room: 227 - entrance: 151 - teleporter: [7, 6] - access: [] - - target_room: 203 - access: ["MegaGrenade"] -# - target_room: 72 # access to spencer? -# access: ["MegaGrenade"] -- name: Spencer Cave Caved In Main Loop - id: 203 - game_objects: [] - links: - - target_room: 73 - access: [] - - target_room: 207 - entrance: 156 - teleporter: [36, 8] - access: ["MobiusCrest"] - - target_room: 204 - access: ["Claw"] - - target_room: 205 - access: ["Bomb"] -- name: Spencer Cave Caved In Waters - id: 204 - game_objects: - - name: "Bomb Libra Block" - object_id: 0 - type: "Trigger" - on_trigger: ["SpencerCaveLibraBlockBombed"] - access: ["MegaGrenade", "Claw"] - links: - - target_room: 203 - access: ["Claw"] -- name: Spencer Cave Caved In Libra Nook - id: 205 - game_objects: [] - links: - - target_room: 206 - entrance: 153 - teleporter: [33, 8] - access: ["LibraCrest"] -- name: Spencer Cave Caved In Libra Corridor - id: 206 - game_objects: [] - links: - - target_room: 205 - entrance: 154 - teleporter: [34, 8] - access: ["LibraCrest"] - - target_room: 207 - access: ["SpencerCaveLibraBlockBombed"] -- name: Spencer Cave Caved In Mobius Chest - id: 207 - game_objects: - - name: "Spencer's Cave - Mobius Chest" - object_id: 0x0F - type: "Chest" - access: [] - links: - - target_room: 203 - entrance: 155 - teleporter: [35, 8] - access: ["MobiusCrest"] - - target_room: 206 - access: ["Bomb"] -- name: Wintry Temple Outer Room - id: 74 - game_objects: [] - links: - - target_room: 223 - entrance: 157 - teleporter: [15, 6] - access: [] -- name: Wintry Temple Inner Room - id: 75 - game_objects: - - name: "Wintry Temple - West Box" - object_id: 0x70 - type: "Box" - access: [] - - name: "Wintry Temple - North Box" - object_id: 0x71 - type: "Box" - access: [] - links: - - target_room: 92 - entrance: 158 - teleporter: [62, 8] - access: ["GeminiCrest"] -- name: Fireburg Upper Plaza - id: 76 - game_objects: [] - links: - - target_room: 224 - entrance: 159 - teleporter: [9, 6] - access: [] - - target_room: 80 - entrance: 163 - teleporter: [91, 0] - access: [] - - target_room: 77 - entrance: 164 - teleporter: [98, 8] # original value [16, 2] - access: [] - - target_room: 82 - entrance: 165 - teleporter: [96, 8] # original value [17, 2] - access: [] - - target_room: 208 - access: ["Claw"] -- name: Fireburg Lower Plaza - id: 208 - game_objects: - - name: "Fireburg - Hidden Tunnel Box" - object_id: 0x74 - type: "Box" - access: [] - links: - - target_room: 76 - access: ["Claw"] - - target_room: 78 - entrance: 166 - teleporter: [11, 8] - access: ["MultiKey"] -- name: Reuben's House - id: 77 - game_objects: - - name: "Fireburg - Reuben's House Arion" - object_id: 14 - type: "NPC" - access: ["ReubenDadSaved"] - - name: "Reuben Companion" - object_id: 0 - type: "Trigger" - on_trigger: ["Reuben1"] - access: [] - - name: "Fireburg - Reuben's House Box" - object_id: 0x75 - type: "Box" - access: [] - links: - - target_room: 76 - entrance: 167 - teleporter: [98, 3] - access: [] -- name: GrenadeMan's House - id: 78 - game_objects: - - name: "Fireburg - Locked House Man" - object_id: 12 - type: "NPC" - access: [] - links: - - target_room: 208 - entrance: 168 - teleporter: [9, 8] - access: ["MultiKey"] - - target_room: 79 - entrance: 169 - teleporter: [93, 0] - access: [] -- name: GrenadeMan's Mobius Room - id: 79 - game_objects: [] - links: - - target_room: 78 - entrance: 170 - teleporter: [94, 0] - access: [] - - target_room: 161 - entrance: 171 - teleporter: [54, 8] - access: ["MobiusCrest"] -- name: Fireburg Vendor House - id: 80 - game_objects: - - name: "Fireburg - Vendor" - object_id: 11 - type: "NPC" - access: [] - links: - - target_room: 76 - entrance: 172 - teleporter: [95, 0] - access: [] - - target_room: 81 - entrance: 173 - teleporter: [96, 0] - access: [] -- name: Fireburg Gemini Room - id: 81 - game_objects: [] - links: - - target_room: 80 - entrance: 174 - teleporter: [97, 0] - access: [] - - target_room: 43 - entrance: 175 - teleporter: [45, 8] - access: ["GeminiCrest"] -- name: Fireburg Hotel Lobby - id: 82 - game_objects: - - name: "Fireburg - Tristam" - object_id: 10 - type: "NPC" - access: ["Tristam", "TristamBoneItemGiven"] - links: - - target_room: 76 - entrance: 177 - teleporter: [99, 3] - access: [] - - target_room: 83 - entrance: 176 - teleporter: [213, 0] - access: [] -- name: Fireburg Hotel Beds - id: 83 - game_objects: [] - links: - - target_room: 82 - entrance: 178 - teleporter: [214, 0] - access: [] -- name: Mine Exterior North West Platforms - id: 84 - game_objects: [] - links: - - target_room: 224 - entrance: 179 - teleporter: [98, 0] - access: [] - - target_room: 88 - entrance: 181 - teleporter: [20, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] - - target_room: 86 - access: ["Claw"] - - target_room: 87 - access: ["Claw"] -- name: Mine Exterior Central Ledge - id: 85 - game_objects: [] - links: - - target_room: 90 - entrance: 183 - teleporter: [22, 2] - access: ["Bomb"] - - target_room: 84 - access: ["Claw"] -- name: Mine Exterior North Ledge - id: 86 - game_objects: [] - links: - - target_room: 89 - entrance: 182 - teleporter: [21, 2] - access: ["Bomb"] - - target_room: 85 - access: ["Claw"] -- name: Mine Exterior South East Platforms - id: 87 - game_objects: - - name: "Jinn" - object_id: 0 - type: "Trigger" - on_trigger: ["Jinn"] - access: [] - links: - - target_room: 91 - entrance: 180 - teleporter: [99, 0] - access: ["Jinn"] - - target_room: 86 - access: [] - - target_room: 85 - access: ["Claw"] -- name: Mine Parallel Room - id: 88 - game_objects: - - name: "Mine - Parallel Room West Box" - object_id: 0x77 - type: "Box" - access: ["Claw"] - - name: "Mine - Parallel Room East Box" - object_id: 0x78 - type: "Box" - access: ["Claw"] - links: - - target_room: 84 - entrance: 185 - teleporter: [100, 3] - access: [] -- name: Mine Crescent Room - id: 89 - game_objects: - - name: "Mine - Crescent Room Chest" - object_id: 0x10 - type: "Chest" - access: [] - links: - - target_room: 86 - entrance: 186 - teleporter: [101, 3] - access: [] -- name: Mine Climbing Room - id: 90 - game_objects: - - name: "Mine - Glitchy Collision Cave Box" - object_id: 0x76 - type: "Box" - access: ["Claw"] - links: - - target_room: 85 - entrance: 187 - teleporter: [102, 3] - access: [] -- name: Mine Cliff - id: 91 - game_objects: - - name: "Mine - Cliff Southwest Box" - object_id: 0x79 - type: "Box" - access: [] - - name: "Mine - Cliff Northwest Box" - object_id: 0x7A - type: "Box" - access: [] - - name: "Mine - Cliff Northeast Box" - object_id: 0x7B - type: "Box" - access: [] - - name: "Mine - Cliff Southeast Box" - object_id: 0x7C - type: "Box" - access: [] - - name: "Mine - Reuben" - object_id: 7 - type: "NPC" - access: ["Reuben1"] - - name: "Reuben's dad Saved" - object_id: 0 - type: "Trigger" - on_trigger: ["ReubenDadSaved"] - access: ["MegaGrenade"] - links: - - target_room: 87 - entrance: 188 - teleporter: [100, 0] - access: [] -- name: Sealed Temple - id: 92 - game_objects: - - name: "Sealed Temple - West Box" - object_id: 0x7D - type: "Box" - access: [] - - name: "Sealed Temple - East Box" - object_id: 0x7E - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 190 - teleporter: [16, 6] - access: [] - - target_room: 75 - entrance: 191 - teleporter: [63, 8] - access: ["GeminiCrest"] -- name: Volcano Base - id: 93 - game_objects: - - name: "Volcano - Base Chest" - object_id: 0x11 - type: "Chest" - access: [] - - name: "Volcano - Base West Box" - object_id: 0x7F - type: "Box" - access: [] - - name: "Volcano - Base East Left Box" - object_id: 0x80 - type: "Box" - access: [] - - name: "Volcano - Base East Right Box" - object_id: 0x81 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 192 - teleporter: [103, 0] - access: [] - - target_room: 98 - entrance: 196 - teleporter: [31, 8] - access: [] - - target_room: 96 - entrance: 197 - teleporter: [30, 8] - access: [] -- name: Volcano Top Left - id: 94 - game_objects: - - name: "Volcano - Medusa Chest" - object_id: 0x12 - type: "Chest" - access: ["Medusa"] - - name: "Medusa" - object_id: 0 - type: "Trigger" - on_trigger: ["Medusa"] - access: [] - - name: "Volcano - Behind Medusa Box" - object_id: 0x82 - type: "Box" - access: [] - links: - - target_room: 209 - entrance: 199 - teleporter: [26, 8] - access: [] -- name: Volcano Top Right - id: 95 - game_objects: - - name: "Volcano - Top of the Volcano Left Box" - object_id: 0x83 - type: "Box" - access: [] - - name: "Volcano - Top of the Volcano Right Box" - object_id: 0x84 - type: "Box" - access: [] - links: - - target_room: 99 - entrance: 200 - teleporter: [79, 8] - access: [] -- name: Volcano Right Path - id: 96 - game_objects: - - name: "Volcano - Right Path Box" - object_id: 0x87 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 201 - teleporter: [15, 8] - access: [] -- name: Volcano Left Path - id: 98 - game_objects: - - name: "Volcano - Left Path Box" - object_id: 0x86 - type: "Box" - access: [] - links: - - target_room: 93 - entrance: 204 - teleporter: [27, 8] - access: [] - - target_room: 99 - entrance: 202 - teleporter: [25, 2] - access: [] - - target_room: 209 - entrance: 203 - teleporter: [26, 2] - access: [] -- name: Volcano Cross Left-Right - id: 99 - game_objects: [] - links: - - target_room: 95 - entrance: 206 - teleporter: [29, 8] - access: [] - - target_room: 98 - entrance: 205 - teleporter: [103, 3] - access: [] -- name: Volcano Cross Right-Left - id: 209 - game_objects: - - name: "Volcano - Crossover Section Box" - object_id: 0x85 - type: "Box" - access: [] - links: - - target_room: 98 - entrance: 208 - teleporter: [104, 3] - access: [] - - target_room: 94 - entrance: 207 - teleporter: [28, 8] - access: [] -- name: Lava Dome Inner Ring Main Loop - id: 100 - game_objects: - - name: "Lava Dome - Exterior Caldera Near Switch Cliff Box" - object_id: 0x88 - type: "Box" - access: [] - - name: "Lava Dome - Exterior South Cliff Box" - object_id: 0x89 - type: "Box" - access: [] - links: - - target_room: 224 - entrance: 209 - teleporter: [104, 0] - access: [] - - target_room: 113 - entrance: 211 - teleporter: [105, 0] - access: [] - - target_room: 114 - entrance: 212 - teleporter: [106, 0] - access: [] - - target_room: 116 - entrance: 213 - teleporter: [108, 0] - access: [] - - target_room: 118 - entrance: 214 - teleporter: [111, 0] - access: [] -- name: Lava Dome Inner Ring Center Ledge - id: 101 - game_objects: - - name: "Lava Dome - Exterior Center Dropoff Ledge Box" - object_id: 0x8A - type: "Box" - access: [] - links: - - target_room: 115 - entrance: 215 - teleporter: [107, 0] - access: [] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Plate Ledge - id: 102 - game_objects: - - name: "Lava Dome Plate" - object_id: 0 - type: "Trigger" - on_trigger: ["LavaDomePlate"] - access: [] - links: - - target_room: 119 - entrance: 216 - teleporter: [109, 0] - access: [] -- name: Lava Dome Inner Ring Upper Ledge West - id: 103 - game_objects: [] - links: - - target_room: 111 - entrance: 219 - teleporter: [112, 0] - access: [] - - target_room: 108 - entrance: 220 - teleporter: [113, 0] - access: [] - - target_room: 104 - access: ["Claw"] - - target_room: 100 - access: ["Claw"] -- name: Lava Dome Inner Ring Upper Ledge East - id: 104 - game_objects: [] - links: - - target_room: 110 - entrance: 218 - teleporter: [110, 0] - access: [] - - target_room: 103 - access: ["Claw"] -- name: Lava Dome Inner Ring Big Door Ledge - id: 105 - game_objects: [] - links: - - target_room: 107 - entrance: 221 - teleporter: [114, 0] - access: [] - - target_room: 121 - entrance: 222 - teleporter: [29, 2] - access: ["LavaDomePlate"] -- name: Lava Dome Inner Ring Tiny Bottom Ledge - id: 106 - game_objects: - - name: "Lava Dome - Exterior Dead End Caldera Box" - object_id: 0x8B - type: "Box" - access: [] - links: - - target_room: 120 - entrance: 226 - teleporter: [115, 0] - access: [] -- name: Lava Dome Jump Maze II - id: 107 - game_objects: - - name: "Lava Dome - Gold Maze Northwest Box" - object_id: 0x8C - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southwest Box" - object_id: 0xF6 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Northeast Box" - object_id: 0xF7 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze North Box" - object_id: 0xF8 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Center Box" - object_id: 0xF9 - type: "Box" - access: [] - - name: "Lava Dome - Gold Maze Southeast Box" - object_id: 0xFA - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 227 - teleporter: [116, 0] - access: [] - - target_room: 108 - entrance: 228 - teleporter: [119, 0] - access: [] - - target_room: 120 - entrance: 229 - teleporter: [120, 0] - access: [] -- name: Lava Dome Up-Down Corridor - id: 108 - game_objects: [] - links: - - target_room: 107 - entrance: 231 - teleporter: [118, 0] - access: [] - - target_room: 103 - entrance: 230 - teleporter: [117, 0] - access: [] -- name: Lava Dome Jump Maze I - id: 109 - game_objects: - - name: "Lava Dome - Bare Maze Leapfrog Alcove North Box" - object_id: 0x8D - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Leapfrog Alcove South Box" - object_id: 0x8E - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Center Box" - object_id: 0x8F - type: "Box" - access: [] - - name: "Lava Dome - Bare Maze Southwest Box" - object_id: 0x90 - type: "Box" - access: [] - links: - - target_room: 118 - entrance: 232 - teleporter: [121, 0] - access: [] - - target_room: 111 - entrance: 233 - teleporter: [122, 0] - access: [] -- name: Lava Dome Pointless Room - id: 110 - game_objects: [] - links: - - target_room: 104 - entrance: 234 - teleporter: [123, 0] - access: [] -- name: Lava Dome Lower Moon Helm Room - id: 111 - game_objects: - - name: "Lava Dome - U-Bend Room North Box" - object_id: 0x92 - type: "Box" - access: [] - - name: "Lava Dome - U-Bend Room South Box" - object_id: 0x93 - type: "Box" - access: [] - links: - - target_room: 103 - entrance: 235 - teleporter: [124, 0] - access: [] - - target_room: 109 - entrance: 236 - teleporter: [125, 0] - access: [] -- name: Lava Dome Moon Helm Room - id: 112 - game_objects: - - name: "Lava Dome - Beyond River Room Chest" - object_id: 0x13 - type: "Chest" - access: [] - - name: "Lava Dome - Beyond River Room Box" - object_id: 0x91 - type: "Box" - access: [] - links: - - target_room: 117 - entrance: 237 - teleporter: [126, 0] - access: [] -- name: Lava Dome Three Jumps Room - id: 113 - game_objects: - - name: "Lava Dome - Three Jumps Room Box" - object_id: 0x96 - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 238 - teleporter: [127, 0] - access: [] -- name: Lava Dome Life Chest Room Lower Ledge - id: 114 - game_objects: - - name: "Lava Dome - Gold Bar Room Boulder Chest" - object_id: 0x1C - type: "Chest" - access: ["MegaGrenade"] - links: - - target_room: 100 - entrance: 239 - teleporter: [128, 0] - access: [] - - target_room: 115 - access: ["Claw"] -- name: Lava Dome Life Chest Room Upper Ledge - id: 115 - game_objects: - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box West" - object_id: 0x94 - type: "Box" - access: [] - - name: "Lava Dome - Gold Bar Room Leapfrog Alcove Box East" - object_id: 0x95 - type: "Box" - access: [] - links: - - target_room: 101 - entrance: 240 - teleporter: [129, 0] - access: [] - - target_room: 114 - access: ["Claw"] -- name: Lava Dome Big Jump Room Main Area - id: 116 - game_objects: - - name: "Lava Dome - Lava River Room North Box" - object_id: 0x98 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room East Box" - object_id: 0x99 - type: "Box" - access: [] - - name: "Lava Dome - Lava River Room South Box" - object_id: 0x9A - type: "Box" - access: [] - links: - - target_room: 100 - entrance: 241 - teleporter: [133, 0] - access: [] - - target_room: 119 - entrance: 243 - teleporter: [132, 0] - access: [] - - target_room: 117 - access: ["MegaGrenade"] -- name: Lava Dome Big Jump Room MegaGrenade Area - id: 117 - game_objects: [] - links: - - target_room: 112 - entrance: 242 - teleporter: [131, 0] - access: [] - - target_room: 116 - access: ["Bomb"] -- name: Lava Dome Split Corridor - id: 118 - game_objects: - - name: "Lava Dome - Split Corridor Box" - object_id: 0x97 - type: "Box" - access: [] - links: - - target_room: 109 - entrance: 244 - teleporter: [130, 0] - access: [] - - target_room: 100 - entrance: 245 - teleporter: [134, 0] - access: [] -- name: Lava Dome Plate Corridor - id: 119 - game_objects: [] - links: - - target_room: 102 - entrance: 246 - teleporter: [135, 0] - access: [] - - target_room: 116 - entrance: 247 - teleporter: [137, 0] - access: [] -- name: Lava Dome Four Boxes Stairs - id: 120 - game_objects: - - name: "Lava Dome - Caldera Stairway West Left Box" - object_id: 0x9B - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway West Right Box" - object_id: 0x9C - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Left Box" - object_id: 0x9D - type: "Box" - access: [] - - name: "Lava Dome - Caldera Stairway East Right Box" - object_id: 0x9E - type: "Box" - access: [] - links: - - target_room: 107 - entrance: 248 - teleporter: [136, 0] - access: [] - - target_room: 106 - entrance: 249 - teleporter: [16, 0] - access: [] -- name: Lava Dome Hydra Room - id: 121 - game_objects: - - name: "Lava Dome - Dualhead Hydra Chest" - object_id: 0x14 - type: "Chest" - access: ["DualheadHydra"] - - name: "Dualhead Hydra" - object_id: 0 - type: "Trigger" - on_trigger: ["DualheadHydra"] - access: [] - - name: "Lava Dome - Hydra Room Northwest Box" - object_id: 0x9F - type: "Box" - access: [] - - name: "Lava Dome - Hydra Room Southweast Box" - object_id: 0xA0 - type: "Box" - access: [] - links: - - target_room: 105 - entrance: 250 - teleporter: [105, 3] - access: [] - - target_room: 122 - entrance: 251 - teleporter: [138, 0] - access: ["DualheadHydra"] -- name: Lava Dome Escape Corridor - id: 122 - game_objects: [] - links: - - target_room: 121 - entrance: 253 - teleporter: [139, 0] - access: [] -- name: Rope Bridge - id: 123 - game_objects: - - name: "Rope Bridge - West Box" - object_id: 0xA3 - type: "Box" - access: [] - - name: "Rope Bridge - East Box" - object_id: 0xA4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 255 - teleporter: [140, 0] - access: [] -- name: Alive Forest - id: 124 - game_objects: - - name: "Alive Forest - Tree Stump Chest" - object_id: 0x15 - type: "Chest" - access: ["Axe"] - - name: "Alive Forest - Near Entrance Box" - object_id: 0xA5 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - After Bridge Box" - object_id: 0xA6 - type: "Box" - access: ["Axe"] - - name: "Alive Forest - Gemini Stump Box" - object_id: 0xA7 - type: "Box" - access: ["Axe"] - links: - - target_room: 226 - entrance: 272 - teleporter: [142, 0] - access: ["Axe"] - - target_room: 21 - entrance: 275 - teleporter: [64, 8] - access: ["LibraCrest", "Axe"] - - target_room: 22 - entrance: 276 - teleporter: [65, 8] - access: ["GeminiCrest", "Axe"] - - target_room: 23 - entrance: 277 - teleporter: [66, 8] - access: ["MobiusCrest", "Axe"] - - target_room: 125 - entrance: 274 - teleporter: [143, 0] - access: ["Axe"] -- name: Giant Tree 1F Main Area - id: 125 - game_objects: - - name: "Giant Tree 1F - Northwest Box" - object_id: 0xA8 - type: "Box" - access: [] - - name: "Giant Tree 1F - Southwest Box" - object_id: 0xA9 - type: "Box" - access: [] - - name: "Giant Tree 1F - Center Box" - object_id: 0xAA - type: "Box" - access: [] - - name: "Giant Tree 1F - East Box" - object_id: 0xAB - type: "Box" - access: [] - links: - - target_room: 124 - entrance: 278 - teleporter: [56, 1] # [49, 8] script restored if no map shuffling - access: [] - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 1F North Island - id: 202 - game_objects: [] - links: - - target_room: 127 - entrance: 280 - teleporter: [144, 0] - access: [] - - target_room: 125 - access: ["DragonClaw"] -- name: Giant Tree 1F Central Island - id: 126 - game_objects: [] - links: - - target_room: 202 - access: ["DragonClaw"] -- name: Giant Tree 2F Main Lobby - id: 127 - game_objects: - - name: "Giant Tree 2F - North Box" - object_id: 0xAC - type: "Box" - access: [] - links: - - target_room: 126 - access: ["DragonClaw"] - - target_room: 125 - entrance: 281 - teleporter: [145, 0] - access: [] - - target_room: 133 - entrance: 283 - teleporter: [149, 0] - access: [] - - target_room: 129 - access: ["DragonClaw"] -- name: Giant Tree 2F West Ledge - id: 128 - game_objects: - - name: "Giant Tree 2F - Dropdown Ledge Box" - object_id: 0xAE - type: "Box" - access: [] - links: - - target_room: 140 - entrance: 284 - teleporter: [147, 0] - access: ["Sword"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Lower Area - id: 129 - game_objects: - - name: "Giant Tree 2F - South Box" - object_id: 0xAD - type: "Box" - access: [] - links: - - target_room: 130 - access: ["Claw"] - - target_room: 131 - access: ["Claw"] -- name: Giant Tree 2F Central Island - id: 130 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 135 - entrance: 282 - teleporter: [146, 0] - access: ["Sword"] -- name: Giant Tree 2F East Ledge - id: 131 - game_objects: [] - links: - - target_room: 129 - access: ["Claw"] - - target_room: 130 - access: ["DragonClaw"] -- name: Giant Tree 2F Meteor Chest Room - id: 132 - game_objects: - - name: "Giant Tree 2F - Gidrah Chest" - object_id: 0x16 - type: "Chest" - access: [] - links: - - target_room: 133 - entrance: 285 - teleporter: [148, 0] - access: [] -- name: Giant Tree 2F Mushroom Room - id: 133 - game_objects: - - name: "Giant Tree 2F - Mushroom Tunnel West Box" - object_id: 0xAF - type: "Box" - access: ["Axe"] - - name: "Giant Tree 2F - Mushroom Tunnel East Box" - object_id: 0xB0 - type: "Box" - access: ["Axe"] - links: - - target_room: 127 - entrance: 286 - teleporter: [150, 0] - access: ["Axe"] - - target_room: 132 - entrance: 287 - teleporter: [151, 0] - access: ["Axe", "Gidrah"] -- name: Giant Tree 3F Central Island - id: 135 - game_objects: - - name: "Giant Tree 3F - Central Island Box" - object_id: 0xB3 - type: "Box" - access: [] - links: - - target_room: 130 - entrance: 288 - teleporter: [152, 0] - access: [] - - target_room: 136 - access: ["Claw"] - - target_room: 137 - access: ["DragonClaw"] -- name: Giant Tree 3F Central Area - id: 136 - game_objects: - - name: "Giant Tree 3F - Center North Box" - object_id: 0xB1 - type: "Box" - access: [] - - name: "Giant Tree 3F - Center West Box" - object_id: 0xB2 - type: "Box" - access: [] - links: - - target_room: 135 - access: ["Claw"] - - target_room: 127 - access: [] - - target_room: 131 - access: [] -- name: Giant Tree 3F Lower Ledge - id: 137 - game_objects: [] - links: - - target_room: 135 - access: ["DragonClaw"] - - target_room: 142 - entrance: 289 - teleporter: [153, 0] - access: ["Sword"] -- name: Giant Tree 3F West Area - id: 138 - game_objects: - - name: "Giant Tree 3F - West Side Box" - object_id: 0xB4 - type: "Box" - access: [] - links: - - target_room: 128 - access: [] - - target_room: 210 - entrance: 290 - teleporter: [154, 0] - access: [] -- name: Giant Tree 3F Middle Up Island - id: 139 - game_objects: [] - links: - - target_room: 136 - access: ["Claw"] -- name: Giant Tree 3F West Platform - id: 140 - game_objects: [] - links: - - target_room: 139 - access: ["Claw"] - - target_room: 141 - access: ["Claw"] - - target_room: 128 - entrance: 291 - teleporter: [155, 0] - access: [] -- name: Giant Tree 3F North Ledge - id: 141 - game_objects: [] - links: - - target_room: 143 - entrance: 292 - teleporter: [156, 0] - access: ["Sword"] - - target_room: 139 - access: ["Claw"] - - target_room: 136 - access: ["Claw"] -- name: Giant Tree Worm Room Upper Ledge - id: 142 - game_objects: - - name: "Giant Tree 3F - Worm Room North Box" - object_id: 0xB5 - type: "Box" - access: ["Axe"] - - name: "Giant Tree 3F - Worm Room South Box" - object_id: 0xB6 - type: "Box" - access: ["Axe"] - links: - - target_room: 137 - entrance: 293 - teleporter: [157, 0] - access: ["Axe"] - - target_room: 210 - access: ["Axe", "Claw"] -- name: Giant Tree Worm Room Lower Ledge - id: 210 - game_objects: [] - links: - - target_room: 138 - entrance: 294 - teleporter: [158, 0] - access: [] -- name: Giant Tree 4F Lower Floor - id: 143 - game_objects: [] - links: - - target_room: 141 - entrance: 295 - teleporter: [159, 0] - access: [] - - target_room: 148 - entrance: 296 - teleporter: [160, 0] - access: [] - - target_room: 148 - entrance: 297 - teleporter: [161, 0] - access: [] - - target_room: 147 - entrance: 298 - teleporter: [162, 0] - access: ["Sword"] -- name: Giant Tree 4F Middle Floor - id: 144 - game_objects: - - name: "Giant Tree 4F - Highest Platform North Box" - object_id: 0xB7 - type: "Box" - access: [] - - name: "Giant Tree 4F - Highest Platform South Box" - object_id: 0xB8 - type: "Box" - access: [] - links: - - target_room: 149 - entrance: 299 - teleporter: [163, 0] - access: [] - - target_room: 145 - access: ["Claw"] - - target_room: 146 - access: ["DragonClaw"] -- name: Giant Tree 4F Upper Floor - id: 145 - game_objects: [] - links: - - target_room: 150 - entrance: 300 - teleporter: [164, 0] - access: ["Sword"] - - target_room: 144 - access: ["Claw"] -- name: Giant Tree 4F South Ledge - id: 146 - game_objects: - - name: "Giant Tree 4F - Hook Ledge Northeast Box" - object_id: 0xB9 - type: "Box" - access: [] - - name: "Giant Tree 4F - Hook Ledge Southwest Box" - object_id: 0xBA - type: "Box" - access: [] - links: - - target_room: 144 - access: ["DragonClaw"] -- name: Giant Tree 4F Slime Room East Area - id: 147 - game_objects: - - name: "Giant Tree 4F - East Slime Room Box" - object_id: 0xBC - type: "Box" - access: ["Axe"] - links: - - target_room: 143 - entrance: 304 - teleporter: [168, 0] - access: [] -- name: Giant Tree 4F Slime Room West Area - id: 148 - game_objects: [] - links: - - target_room: 143 - entrance: 303 - teleporter: [167, 0] - access: ["Axe"] - - target_room: 143 - entrance: 302 - teleporter: [166, 0] - access: ["Axe"] - - target_room: 149 - access: ["Axe", "Claw"] -- name: Giant Tree 4F Slime Room Platform - id: 149 - game_objects: - - name: "Giant Tree 4F - West Slime Room Box" - object_id: 0xBB - type: "Box" - access: [] - links: - - target_room: 144 - entrance: 301 - teleporter: [165, 0] - access: [] - - target_room: 148 - access: ["Claw"] -- name: Giant Tree 5F Lower Area - id: 150 - game_objects: - - name: "Giant Tree 5F - Northwest Left Box" - object_id: 0xBD - type: "Box" - access: [] - - name: "Giant Tree 5F - Northwest Right Box" - object_id: 0xBE - type: "Box" - access: [] - - name: "Giant Tree 5F - South Left Box" - object_id: 0xBF - type: "Box" - access: [] - - name: "Giant Tree 5F - South Right Box" - object_id: 0xC0 - type: "Box" - access: [] - links: - - target_room: 145 - entrance: 305 - teleporter: [169, 0] - access: [] - - target_room: 151 - access: ["Claw"] - - target_room: 143 - access: [] -- name: Giant Tree 5F Gidrah Platform - id: 151 - game_objects: - - name: "Gidrah" - object_id: 0 - type: "Trigger" - on_trigger: ["Gidrah"] - access: [] - links: - - target_room: 150 - access: ["Claw"] -- name: Kaidge Temple Lower Ledge - id: 152 - game_objects: [] - links: - - target_room: 226 - entrance: 307 - teleporter: [18, 6] - access: [] - - target_room: 153 - access: ["Claw"] -- name: Kaidge Temple Upper Ledge - id: 153 - game_objects: - - name: "Kaidge Temple - Box" - object_id: 0xC1 - type: "Box" - access: [] - links: - - target_room: 185 - entrance: 308 - teleporter: [71, 8] - access: ["MobiusCrest"] - - target_room: 152 - access: ["Claw"] -- name: Windhole Temple - id: 154 - game_objects: - - name: "Windhole Temple - Box" - object_id: 0xC2 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 309 - teleporter: [173, 0] - access: [] -- name: Mount Gale - id: 155 - game_objects: - - name: "Mount Gale - Dullahan Chest" - object_id: 0x17 - type: "Chest" - access: ["DragonClaw", "Dullahan"] - - name: "Dullahan" - object_id: 0 - type: "Trigger" - on_trigger: ["Dullahan"] - access: ["DragonClaw"] - - name: "Mount Gale - East Box" - object_id: 0xC3 - type: "Box" - access: ["DragonClaw"] - - name: "Mount Gale - West Box" - object_id: 0xC4 - type: "Box" - access: [] - links: - - target_room: 226 - entrance: 310 - teleporter: [174, 0] - access: [] -- name: Windia - id: 156 - game_objects: [] - links: - - target_room: 226 - entrance: 312 - teleporter: [10, 6] - access: [] - - target_room: 157 - entrance: 320 - teleporter: [30, 5] - access: [] - - target_room: 163 - entrance: 321 - teleporter: [97, 8] - access: [] - - target_room: 165 - entrance: 322 - teleporter: [32, 5] - access: [] - - target_room: 159 - entrance: 323 - teleporter: [176, 4] - access: [] - - target_room: 160 - entrance: 324 - teleporter: [177, 4] - access: [] -- name: Otto's House - id: 157 - game_objects: - - name: "Otto" - object_id: 0 - type: "Trigger" - on_trigger: ["RainbowBridge"] - access: ["ThunderRock"] - links: - - target_room: 156 - entrance: 327 - teleporter: [106, 3] - access: [] - - target_room: 158 - entrance: 326 - teleporter: [33, 2] - access: [] -- name: Otto's Attic - id: 158 - game_objects: - - name: "Windia - Otto's Attic Box" - object_id: 0xC5 - type: "Box" - access: [] - links: - - target_room: 157 - entrance: 328 - teleporter: [107, 3] - access: [] -- name: Windia Kid House - id: 159 - game_objects: [] - links: - - target_room: 156 - entrance: 329 - teleporter: [178, 0] - access: [] - - target_room: 161 - entrance: 330 - teleporter: [180, 0] - access: [] -- name: Windia Old People House - id: 160 - game_objects: [] - links: - - target_room: 156 - entrance: 331 - teleporter: [179, 0] - access: [] - - target_room: 162 - entrance: 332 - teleporter: [181, 0] - access: [] -- name: Windia Kid House Basement - id: 161 - game_objects: [] - links: - - target_room: 159 - entrance: 333 - teleporter: [182, 0] - access: [] - - target_room: 79 - entrance: 334 - teleporter: [44, 8] - access: ["MobiusCrest"] -- name: Windia Old People House Basement - id: 162 - game_objects: - - name: "Windia - Mobius Basement West Box" - object_id: 0xC8 - type: "Box" - access: [] - - name: "Windia - Mobius Basement East Box" - object_id: 0xC9 - type: "Box" - access: [] - links: - - target_room: 160 - entrance: 335 - teleporter: [183, 0] - access: [] - - target_room: 186 - entrance: 336 - teleporter: [43, 8] - access: ["MobiusCrest"] -- name: Windia Inn Lobby - id: 163 - game_objects: [] - links: - - target_room: 156 - entrance: 338 - teleporter: [135, 3] - access: [] - - target_room: 164 - entrance: 337 - teleporter: [102, 8] - access: [] -- name: Windia Inn Beds - id: 164 - game_objects: - - name: "Windia - Inn Bedroom North Box" - object_id: 0xC6 - type: "Box" - access: [] - - name: "Windia - Inn Bedroom South Box" - object_id: 0xC7 - type: "Box" - access: [] - - name: "Windia - Kaeli" - object_id: 15 - type: "NPC" - access: ["Kaeli2"] - links: - - target_room: 163 - entrance: 339 - teleporter: [216, 0] - access: [] -- name: Windia Vendor House - id: 165 - game_objects: - - name: "Windia - Vendor" - object_id: 16 - type: "NPC" - access: [] - links: - - target_room: 156 - entrance: 340 - teleporter: [108, 3] - access: [] -- name: Pazuzu Tower 1F Main Lobby - id: 166 - game_objects: - - name: "Pazuzu 1F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu1F"] - access: [] - links: - - target_room: 226 - entrance: 341 - teleporter: [184, 0] - access: [] - - target_room: 180 - entrance: 345 - teleporter: [185, 0] - access: [] -- name: Pazuzu Tower 1F Boxes Room - id: 167 - game_objects: - - name: "Pazuzu's Tower 1F - Descent Bomb Wall West Box" - object_id: 0xCA - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall Center Box" - object_id: 0xCB - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Bomb Wall East Box" - object_id: 0xCC - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 1F - Descent Box" - object_id: 0xCD - type: "Box" - access: [] - links: - - target_room: 169 - entrance: 349 - teleporter: [187, 0] - access: [] -- name: Pazuzu Tower 1F Southern Platform - id: 168 - game_objects: [] - links: - - target_room: 169 - entrance: 346 - teleporter: [186, 0] - access: [] - - target_room: 166 - access: ["DragonClaw"] -- name: Pazuzu 2F - id: 169 - game_objects: - - name: "Pazuzu's Tower 2F - East Room West Box" - object_id: 0xCE - type: "Box" - access: [] - - name: "Pazuzu's Tower 2F - East Room East Box" - object_id: 0xCF - type: "Box" - access: [] - - name: "Pazuzu 2F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2FLock"] - access: ["Axe"] - - name: "Pazuzu 2F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu2F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 350 - teleporter: [188, 0] - access: [] - - target_room: 168 - entrance: 351 - teleporter: [189, 0] - access: [] - - target_room: 167 - entrance: 352 - teleporter: [190, 0] - access: [] - - target_room: 171 - entrance: 353 - teleporter: [191, 0] - access: [] -- name: Pazuzu 3F Main Room - id: 170 - game_objects: - - name: "Pazuzu's Tower 3F - Guest Room West Box" - object_id: 0xD0 - type: "Box" - access: [] - - name: "Pazuzu's Tower 3F - Guest Room East Box" - object_id: 0xD1 - type: "Box" - access: [] - - name: "Pazuzu 3F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu3F"] - access: [] - links: - - target_room: 180 - entrance: 356 - teleporter: [192, 0] - access: [] - - target_room: 181 - entrance: 357 - teleporter: [193, 0] - access: [] -- name: Pazuzu 3F Central Island - id: 171 - game_objects: [] - links: - - target_room: 169 - entrance: 360 - teleporter: [194, 0] - access: [] - - target_room: 170 - access: ["DragonClaw"] - - target_room: 172 - access: ["DragonClaw"] -- name: Pazuzu 3F Southern Island - id: 172 - game_objects: - - name: "Pazuzu's Tower 3F - South Ledge Box" - object_id: 0xD2 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 361 - teleporter: [195, 0] - access: [] - - target_room: 171 - access: ["DragonClaw"] -- name: Pazuzu 4F - id: 173 - game_objects: - - name: "Pazuzu's Tower 4F - Elevator West Box" - object_id: 0xD3 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - Elevator East Box" - object_id: 0xD4 - type: "Box" - access: ["Bomb"] - - name: "Pazuzu's Tower 4F - East Storage Room Chest" - object_id: 0x18 - type: "Chest" - access: [] - - name: "Pazuzu 4F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4FLock"] - access: ["Axe"] - - name: "Pazuzu 4F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu4F"] - access: ["Bomb"] - links: - - target_room: 183 - entrance: 362 - teleporter: [196, 0] - access: [] - - target_room: 184 - entrance: 363 - teleporter: [197, 0] - access: [] - - target_room: 172 - entrance: 364 - teleporter: [198, 0] - access: [] - - target_room: 175 - entrance: 365 - teleporter: [199, 0] - access: [] -- name: Pazuzu 5F Pazuzu Loop - id: 174 - game_objects: - - name: "Pazuzu 5F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu5F"] - access: [] - links: - - target_room: 181 - entrance: 368 - teleporter: [200, 0] - access: [] - - target_room: 182 - entrance: 369 - teleporter: [201, 0] - access: [] -- name: Pazuzu 5F Upper Loop - id: 175 - game_objects: - - name: "Pazuzu's Tower 5F - North Box" - object_id: 0xD5 - type: "Box" - access: [] - - name: "Pazuzu's Tower 5F - South Box" - object_id: 0xD6 - type: "Box" - access: [] - links: - - target_room: 173 - entrance: 370 - teleporter: [202, 0] - access: [] - - target_room: 176 - entrance: 371 - teleporter: [203, 0] - access: [] -- name: Pazuzu 6F - id: 176 - game_objects: - - name: "Pazuzu's Tower 6F - Box" - object_id: 0xD7 - type: "Box" - access: [] - - name: "Pazuzu's Tower 6F - Chest" - object_id: 0x19 - type: "Chest" - access: [] - - name: "Pazuzu 6F Lock" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6FLock"] - access: ["Bomb", "Axe"] - - name: "Pazuzu 6F" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu6F"] - access: ["Bomb"] - links: - - target_room: 184 - entrance: 374 - teleporter: [204, 0] - access: [] - - target_room: 175 - entrance: 375 - teleporter: [205, 0] - access: [] - - target_room: 178 - entrance: 376 - teleporter: [206, 0] - access: [] - - target_room: 178 - entrance: 377 - teleporter: [207, 0] - access: [] -- name: Pazuzu 7F Southwest Area - id: 177 - game_objects: [] - links: - - target_room: 182 - entrance: 380 - teleporter: [26, 0] - access: [] - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 7F Rest of the Area - id: 178 - game_objects: [] - links: - - target_room: 177 - access: ["DragonClaw"] - - target_room: 176 - entrance: 381 - teleporter: [27, 0] - access: [] - - target_room: 176 - entrance: 382 - teleporter: [28, 0] - access: [] - - target_room: 179 - access: ["DragonClaw", "Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] -- name: Pazuzu 7F Sky Room - id: 179 - game_objects: - - name: "Pazuzu's Tower 7F - Pazuzu Chest" - object_id: 0x1A - type: "Chest" - access: [] - - name: "Pazuzu" - object_id: 0 - type: "Trigger" - on_trigger: ["Pazuzu"] - access: ["Pazuzu2FLock", "Pazuzu4FLock", "Pazuzu6FLock", "Pazuzu1F", "Pazuzu2F", "Pazuzu3F", "Pazuzu4F", "Pazuzu5F", "Pazuzu6F"] - links: - - target_room: 178 - access: ["DragonClaw"] -- name: Pazuzu 1F to 3F - id: 180 - game_objects: [] - links: - - target_room: 166 - entrance: 385 - teleporter: [29, 0] - access: [] - - target_room: 170 - entrance: 386 - teleporter: [30, 0] - access: [] -- name: Pazuzu 3F to 5F - id: 181 - game_objects: [] - links: - - target_room: 170 - entrance: 387 - teleporter: [40, 0] - access: [] - - target_room: 174 - entrance: 388 - teleporter: [41, 0] - access: [] -- name: Pazuzu 5F to 7F - id: 182 - game_objects: [] - links: - - target_room: 174 - entrance: 389 - teleporter: [38, 0] - access: [] - - target_room: 177 - entrance: 390 - teleporter: [39, 0] - access: [] -- name: Pazuzu 2F to 4F - id: 183 - game_objects: [] - links: - - target_room: 169 - entrance: 391 - teleporter: [21, 0] - access: [] - - target_room: 173 - entrance: 392 - teleporter: [22, 0] - access: [] -- name: Pazuzu 4F to 6F - id: 184 - game_objects: [] - links: - - target_room: 173 - entrance: 393 - teleporter: [2, 0] - access: [] - - target_room: 176 - entrance: 394 - teleporter: [3, 0] - access: [] -- name: Light Temple - id: 185 - game_objects: - - name: "Light Temple - Box" - object_id: 0xD8 - type: "Box" - access: [] - links: - - target_room: 230 - entrance: 395 - teleporter: [19, 6] - access: [] - - target_room: 153 - entrance: 396 - teleporter: [70, 8] - access: ["MobiusCrest"] -- name: Ship Dock - id: 186 - game_objects: - - name: "Ship Dock Access" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipDockAccess"] - access: [] - links: - - target_room: 228 - entrance: 399 - teleporter: [17, 6] - access: [] - - target_room: 162 - entrance: 397 - teleporter: [61, 8] - access: ["MobiusCrest"] -- name: Mac Ship Deck - id: 187 - game_objects: - - name: "Mac Ship Steering Wheel" - object_id: 00 - type: "Trigger" - on_trigger: ["ShipSteeringWheel"] - access: [] - - name: "Mac's Ship Deck - North Box" - object_id: 0xD9 - type: "Box" - access: [] - - name: "Mac's Ship Deck - Center Box" - object_id: 0xDA - type: "Box" - access: [] - - name: "Mac's Ship Deck - South Box" - object_id: 0xDB - type: "Box" - access: [] - links: - - target_room: 229 - entrance: 400 - teleporter: [37, 8] - access: [] - - target_room: 188 - entrance: 401 - teleporter: [50, 8] - access: [] - - target_room: 188 - entrance: 402 - teleporter: [51, 8] - access: [] - - target_room: 188 - entrance: 403 - teleporter: [52, 8] - access: [] - - target_room: 189 - entrance: 404 - teleporter: [53, 8] - access: [] -- name: Mac Ship B1 Outer Ring - id: 188 - game_objects: - - name: "Mac's Ship B1 - Northwest Hook Platform Box" - object_id: 0xE4 - type: "Box" - access: ["DragonClaw"] - - name: "Mac's Ship B1 - Center Hook Platform Box" - object_id: 0xE5 - type: "Box" - access: ["DragonClaw"] - links: - - target_room: 187 - entrance: 405 - teleporter: [208, 0] - access: [] - - target_room: 187 - entrance: 406 - teleporter: [175, 0] - access: [] - - target_room: 187 - entrance: 407 - teleporter: [172, 0] - access: [] - - target_room: 193 - entrance: 408 - teleporter: [88, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B1 Square Room - id: 189 - game_objects: [] - links: - - target_room: 187 - entrance: 409 - teleporter: [141, 0] - access: [] - - target_room: 192 - entrance: 410 - teleporter: [87, 0] - access: [] -- name: Mac Ship B1 Central Corridor - id: 190 - game_objects: - - name: "Mac's Ship B1 - Central Corridor Box" - object_id: 0xE6 - type: "Box" - access: [] - links: - - target_room: 192 - entrance: 413 - teleporter: [86, 0] - access: [] - - target_room: 191 - entrance: 412 - teleporter: [102, 0] - access: [] - - target_room: 193 - access: [] -- name: Mac Ship B2 South Corridor - id: 191 - game_objects: [] - links: - - target_room: 190 - entrance: 415 - teleporter: [55, 8] - access: [] - - target_room: 194 - entrance: 414 - teleporter: [57, 1] - access: [] -- name: Mac Ship B2 North Corridor - id: 192 - game_objects: [] - links: - - target_room: 190 - entrance: 416 - teleporter: [56, 8] - access: [] - - target_room: 189 - entrance: 417 - teleporter: [57, 8] - access: [] -- name: Mac Ship B2 Outer Ring - id: 193 - game_objects: - - name: "Mac's Ship B2 - Barrel Room South Box" - object_id: 0xDF - type: "Box" - access: [] - - name: "Mac's Ship B2 - Barrel Room North Box" - object_id: 0xE0 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southwest Room Box" - object_id: 0xE1 - type: "Box" - access: [] - - name: "Mac's Ship B2 - Southeast Room Box" - object_id: 0xE2 - type: "Box" - access: [] - links: - - target_room: 188 - entrance: 418 - teleporter: [58, 8] - access: [] -- name: Mac Ship B1 Mac Room - id: 194 - game_objects: - - name: "Mac's Ship B1 - Mac Room Chest" - object_id: 0x1B - type: "Chest" - access: [] - - name: "Captain Mac" - object_id: 0 - type: "Trigger" - on_trigger: ["ShipLoaned"] - access: ["CaptainCap"] - links: - - target_room: 191 - entrance: 424 - teleporter: [101, 0] - access: [] -- name: Doom Castle Corridor of Destiny - id: 195 - game_objects: [] - links: - - target_room: 201 - entrance: 428 - teleporter: [84, 0] - access: [] - - target_room: 196 - entrance: 429 - teleporter: [35, 2] - access: [] - - target_room: 197 - entrance: 430 - teleporter: [209, 0] - access: ["StoneGolem"] - - target_room: 198 - entrance: 431 - teleporter: [211, 0] - access: ["StoneGolem", "TwinheadWyvern"] - - target_room: 199 - entrance: 432 - teleporter: [13, 2] - access: ["StoneGolem", "TwinheadWyvern", "Zuh"] -- name: Doom Castle Ice Floor - id: 196 - game_objects: - - name: "Doom Castle 4F - Northwest Room Box" - object_id: 0xE7 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Southwest Room Box" - object_id: 0xE8 - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Doom Castle 4F - Northeast Room Box" - object_id: 0xE9 - type: "Box" - access: ["Sword"] - - name: "Doom Castle 4F - Southeast Room Box" - object_id: 0xEA - type: "Box" - access: ["Sword", "DragonClaw"] - - name: "Stone Golem" - object_id: 0 - type: "Trigger" - on_trigger: ["StoneGolem"] - access: ["Sword", "DragonClaw"] - links: - - target_room: 195 - entrance: 433 - teleporter: [109, 3] - access: [] -- name: Doom Castle Lava Floor - id: 197 - game_objects: - - name: "Doom Castle 5F - North Left Box" - object_id: 0xEB - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - North Right Box" - object_id: 0xEC - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Left Box" - object_id: 0xED - type: "Box" - access: ["DragonClaw"] - - name: "Doom Castle 5F - South Right Box" - object_id: 0xEE - type: "Box" - access: ["DragonClaw"] - - name: "Twinhead Wyvern" - object_id: 0 - type: "Trigger" - on_trigger: ["TwinheadWyvern"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 434 - teleporter: [210, 0] - access: [] -- name: Doom Castle Sky Floor - id: 198 - game_objects: - - name: "Doom Castle 6F - West Box" - object_id: 0xEF - type: "Box" - access: [] - - name: "Doom Castle 6F - East Box" - object_id: 0xF0 - type: "Box" - access: [] - - name: "Zuh" - object_id: 0 - type: "Trigger" - on_trigger: ["Zuh"] - access: ["DragonClaw"] - links: - - target_room: 195 - entrance: 435 - teleporter: [212, 0] - access: [] - - target_room: 197 - access: [] -- name: Doom Castle Hero Room - id: 199 - game_objects: - - name: "Doom Castle Hero Chest 01" - object_id: 0xF2 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 02" - object_id: 0xF3 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 03" - object_id: 0xF4 - type: "Chest" - access: [] - - name: "Doom Castle Hero Chest 04" - object_id: 0xF5 - type: "Chest" - access: [] - links: - - target_room: 200 - entrance: 436 - teleporter: [54, 0] - access: [] - - target_room: 195 - entrance: 441 - teleporter: [110, 3] - access: [] -- name: Doom Castle Dark King Room - id: 200 - game_objects: [] - links: - - target_room: 199 - entrance: 442 - teleporter: [52, 0] - access: [] From 29a0b013cb3ecfb186939c558bbc608ff00c8447 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:47:19 -0400 Subject: [PATCH 41/78] KH2: Hotfix update for game verison 1.0.0.9 (#3534) * update the addresses hopefully * todo * update address for steam and epic * oops * leftover hard address * made auto tracking say which version of the game * not needed anymore since they were updated --- worlds/kh2/Client.py | 67 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index 513d85257b97..e2d2338b7651 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -116,12 +116,19 @@ def __init__(self, server_address, password): # self.inBattle = 0x2A0EAC4 + 0x40 # self.onDeath = 0xAB9078 # PC Address anchors - self.Now = 0x0714DB8 - self.Save = 0x09A70B0 + # self.Now = 0x0714DB8 old address + # epic addresses + self.Now = 0x0716DF8 + self.Save = 0x09A92F0 + self.Journal = 0x743260 + self.Shop = 0x743350 + self.Slot1 = 0x2A22FD8 # self.Sys3 = 0x2A59DF0 # self.Bt10 = 0x2A74880 # self.BtlEnd = 0x2A0D3E0 - self.Slot1 = 0x2A20C98 + # self.Slot1 = 0x2A20C98 old address + + self.kh2_game_version = None # can be egs or steam self.chest_set = set(exclusion_table["Chests"]) self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"]) @@ -228,6 +235,9 @@ def kh2_read_int(self, address): def kh2_write_int(self, address, value): self.kh2.write_int(self.kh2.base_address + address, value) + def kh2_read_string(self, address, length): + return self.kh2.read_string(self.kh2.base_address + address, length) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -367,10 +377,26 @@ def on_package(self, cmd: str, args: dict): for weapon_location in all_weapon_slot: all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location]) self.all_weapon_location_id = set(all_weapon_location_id) + try: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") - logger.info("You are now auto-tracking") - self.kh2connected = True + if self.kh2_game_version is None: + if self.kh2_read_string(0x09A9830, 4) == "KH2J": + self.kh2_game_version = "STEAM" + self.Now = 0x0717008 + self.Save = 0x09A9830 + self.Slot1 = 0x2A23518 + self.Journal = 0x7434E0 + self.Shop = 0x7435D0 + + elif self.kh2_read_string(0x09A92F0, 4) == "KH2J": + self.kh2_game_version = "EGS" + else: + self.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if self.kh2_game_version is not None: + logger.info(f"You are now auto-tracking. {self.kh2_game_version}") + self.kh2connected = True except Exception as e: if self.kh2connected: @@ -589,8 +615,8 @@ async def IsInShop(self, sellable): # if journal=-1 and shop = 5 then in shop # if journal !=-1 and shop = 10 then journal - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) if (journal == -1 and shop == 5) or (journal != -1 and shop == 10): # print("your in the shop") sellable_dict = {} @@ -599,8 +625,8 @@ async def IsInShop(self, sellable): amount = self.kh2_read_byte(self.Save + itemdata.memaddr) sellable_dict[itemName] = amount while (journal == -1 and shop == 5) or (journal != -1 and shop == 10): - journal = self.kh2_read_short(0x741230) - shop = self.kh2_read_short(0x741320) + journal = self.kh2_read_short(self.Journal) + shop = self.kh2_read_short(self.Shop) await asyncio.sleep(0.5) for item, amount in sellable_dict.items(): itemdata = self.item_name_to_data[item] @@ -750,7 +776,7 @@ async def verifyItems(self): item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["Magic"][item_name] - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(0x741320) in {10, 8}: + if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items and self.kh2_read_byte(self.Shop) in {10, 8}: self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: @@ -802,7 +828,7 @@ async def verifyItems(self): self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) elif self.base_item_slots + amount_of_items < 8: self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) - + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: @@ -905,8 +931,23 @@ async def kh2_watcher(ctx: KH2Context): await asyncio.sleep(15) ctx.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") if ctx.kh2 is not None: - logger.info("You are now auto-tracking") - ctx.kh2connected = True + if ctx.kh2_game_version is None: + if ctx.kh2_read_string(0x09A9830, 4) == "KH2J": + ctx.kh2_game_version = "STEAM" + ctx.Now = 0x0717008 + ctx.Save = 0x09A9830 + ctx.Slot1 = 0x2A23518 + ctx.Journal = 0x7434E0 + ctx.Shop = 0x7435D0 + + elif ctx.kh2_read_string(0x09A92F0, 4) == "KH2J": + ctx.kh2_game_version = "EGS" + else: + ctx.kh2_game_version = None + logger.info("Your game version is out of date. Please update your game via The Epic Games Store or Steam.") + if ctx.kh2_game_version is not None: + logger.info(f"You are now auto-tracking {ctx.kh2_game_version}") + ctx.kh2connected = True except Exception as e: if ctx.kh2connected: ctx.kh2connected = False From ff680b26cc20d7f3392c1f683f5217702e56e326 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 24 Jul 2024 07:49:28 -0400 Subject: [PATCH 42/78] DLC Quest: Add options presets to DLC Quest (#3676) * - Add options presets to DLC Quest * - Removed unused import --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/dlcquest/__init__.py | 2 ++ worlds/dlcquest/presets.py | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 worlds/dlcquest/presets.py diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 2fc0da075d22..b8f2aad6ff94 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,12 +8,14 @@ from .Options import DLCQuestOptions from .Regions import create_regions from .Rules import set_rules +from .presets import dlcq_options_presets from .option_groups import dlcq_option_groups client_version = 0 class DLCqwebworld(WebWorld): + options_presets = dlcq_options_presets option_groups = dlcq_option_groups setup_en = Tutorial( "Multiworld Setup Guide", diff --git a/worlds/dlcquest/presets.py b/worlds/dlcquest/presets.py new file mode 100644 index 000000000000..ccfd79399521 --- /dev/null +++ b/worlds/dlcquest/presets.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +from .Options import DoubleJumpGlitch, CoinSanity, CoinSanityRange, PermanentCoins, TimeIsMoney, EndingChoice, Campaign, ItemShuffle + +all_random_settings = { + DoubleJumpGlitch.internal_name: "random", + CoinSanity.internal_name: "random", + CoinSanityRange.internal_name: "random", + PermanentCoins.internal_name: "random", + TimeIsMoney.internal_name: "random", + EndingChoice.internal_name: "random", + Campaign.internal_name: "random", + ItemShuffle.internal_name: "random", + "death_link": "random", +} + +main_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_basic, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +lfod_campaign_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_live_freemium_or_die, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +easy_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_none, + CoinSanity.internal_name: CoinSanity.option_none, + CoinSanityRange.internal_name: 40, + PermanentCoins.internal_name: PermanentCoins.option_true, + TimeIsMoney.internal_name: TimeIsMoney.option_required, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + +hard_settings = { + DoubleJumpGlitch.internal_name: DoubleJumpGlitch.option_simple, + CoinSanity.internal_name: CoinSanity.option_coin, + CoinSanityRange.internal_name: 30, + PermanentCoins.internal_name: PermanentCoins.option_false, + TimeIsMoney.internal_name: TimeIsMoney.option_optional, + EndingChoice.internal_name: EndingChoice.option_true, + Campaign.internal_name: Campaign.option_both, + ItemShuffle.internal_name: ItemShuffle.option_shuffled, +} + + +dlcq_options_presets: Dict[str, Dict[str, Any]] = { + "All random": all_random_settings, + "Main campaign": main_campaign_settings, + "LFOD campaign": lfod_campaign_settings, + "Both easy": easy_settings, + "Both hard": hard_settings, +} From 8756f48e46f0da7685453890b4eccac7f84372d2 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:00:16 -0400 Subject: [PATCH 43/78] [TLOZ]: Fix determinism / Add Location Name Groups / Remove Level 9 Junk Fill (#3670) * [TLOZ]: Fix determinism / Add Location Name Groups / Remove Level 9 Junk Fill Axing the final uses of world.multiworld.random that were missed before, hopefully fixing the determinism issue brought up in Issue #3664 (at least on TLOZ's end, leaving SMZ3 alone). Also adding location name groups finally, as well as axing the Level 9 Junk Fill because with the new location name groups players can choose to exclude Level 9 with exclude locations instead. * location name groups * add take any item and sword cave location name groups * use sets like you're supposed to, silly --- worlds/tloz/ItemPool.py | 10 +--------- worlds/tloz/Locations.py | 8 ++++++++ worlds/tloz/__init__.py | 20 ++++++++++++++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 5b90e99722df..4acda4ef41fc 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -80,7 +80,7 @@ def generate_itempool(tlozworld): location.item.classification = ItemClassification.progression def get_pool_core(world): - random = world.multiworld.random + random = world.random pool = [] placed_items = {} @@ -132,14 +132,6 @@ def get_pool_core(world): else: pool.append(fragment) - # Level 9 junk fill - if world.options.ExpandedPool > 0: - spots = random.sample(level_locations[8], len(level_locations[8]) // 2) - for spot in spots: - junk = random.choice(list(minor_items.keys())) - placed_items[spot] = junk - minor_items[junk] -= 1 - # Finish Pool final_pool = basic_pool if world.options.ExpandedPool: diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py index 5b30357c940c..9715cc684291 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -99,6 +99,14 @@ "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" ] +take_any_locations = [ + "Take Any Item Left", "Take Any Item Middle", "Take Any Item Right" +] + +sword_cave_locations = [ + "Starting Sword Cave", "White Sword Pond", "Magical Sword Grave" +] + food_locations = [ "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index a1f9081418e4..8ea5f3e18ca1 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -12,7 +12,8 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations from .Items import item_table, item_prices, item_game_ids from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ - standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations, \ + take_any_locations, sword_cave_locations from .Options import TlozOptions from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late from .Rules import set_rules @@ -87,6 +88,21 @@ class TLoZWorld(World): } } + location_name_groups = { + "Shops": set(shop_locations), + "Take Any": set(take_any_locations), + "Sword Caves": set(sword_cave_locations), + "Level 1": set(level_locations[0]), + "Level 2": set(level_locations[1]), + "Level 3": set(level_locations[2]), + "Level 4": set(level_locations[3]), + "Level 5": set(level_locations[4]), + "Level 6": set(level_locations[5]), + "Level 7": set(level_locations[6]), + "Level 8": set(level_locations[7]), + "Level 9": set(level_locations[8]) + } + for k, v in item_name_to_id.items(): item_name_to_id[k] = v + base_id @@ -307,7 +323,7 @@ def modify_multidata(self, multidata: dict): def get_filler_item_name(self) -> str: if self.filler_items is None: self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] - return self.multiworld.random.choice(self.filler_items) + return self.random.choice(self.filler_items) def fill_slot_data(self) -> Dict[str, Any]: if self.options.ExpandedPool: From 1852287c913cf751b219b1f3552aef57744ccc61 Mon Sep 17 00:00:00 2001 From: Ladybunne Date: Wed, 24 Jul 2024 22:07:07 +1000 Subject: [PATCH 44/78] LADX: Add an item group for instruments (#3666) * Add an item group for LADX instruments * Update worlds/ladx/__init__.py Co-authored-by: Scipio Wright * Fix indent depth --------- Co-authored-by: Scipio Wright Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/ladx/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 21876ed671e2..c958ef212fe4 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -98,9 +98,12 @@ class LinksAwakeningWorld(World): # Items can be grouped using their names to allow easy checking if any item # from that group has been collected. Group names can also be used for !hint - #item_name_groups = { - # "weapons": {"sword", "lance"} - #} + item_name_groups = { + "Instruments": { + "Full Moon Cello", "Conch Horn", "Sea Lily's Bell", "Surf Harp", + "Wind Marimba", "Coral Triangle", "Organ of Evening Calm", "Thunder Drum" + }, + } prefill_dungeon_items = None From 878d5141ce2c96aeb9565c4b8eadeedfe2b62242 Mon Sep 17 00:00:00 2001 From: JKLeckr <11635283+JKLeckr@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:08:16 -0400 Subject: [PATCH 45/78] Project: Add .code-workspace wildcard to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5686f43de380..791f7b1bb7fe 100644 --- a/.gitignore +++ b/.gitignore @@ -150,7 +150,7 @@ venv/ ENV/ env.bak/ venv.bak/ -.code-workspace +*.code-workspace shell.nix # Spyder project settings From e714d2e129bdb940f923ab6469f289ae9a4a7e58 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 24 Jul 2024 08:34:51 -0400 Subject: [PATCH 46/78] Lingo: Add option to prevent shuffling postgame (#3350) * Lingo: Add option to prevent shuffling postgame * Allow roof access on door shuffle * Fix broken unit test * Simplified THE END edge case * Revert unnecessary change * Review comments * Fix mastery unit test * Update generated.dat * Added player's name to error message --- worlds/lingo/__init__.py | 33 +++++++++++++++- worlds/lingo/data/LL1.yaml | 16 +++++++- worlds/lingo/data/generated.dat | Bin 136277 -> 136563 bytes worlds/lingo/data/ids.yaml | 3 +- worlds/lingo/options.py | 6 +++ worlds/lingo/player_logic.py | 34 +++++++++------- worlds/lingo/rules.py | 3 ++ worlds/lingo/test/TestMastery.py | 6 ++- worlds/lingo/test/TestPostgame.py | 62 ++++++++++++++++++++++++++++++ 9 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 worlds/lingo/test/TestPostgame.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 8d6a7fc4ebee..3b67617873c7 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -3,7 +3,7 @@ """ from logging import warning -from BaseClasses import Item, ItemClassification, Tutorial +from BaseClasses import CollectionState, Item, ItemClassification, Tutorial from Options import OptionError from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance @@ -68,6 +68,37 @@ def generate_early(self): def create_regions(self): create_regions(self) + if not self.options.shuffle_postgame: + state = CollectionState(self.multiworld) + state.collect(LingoItem("Prevent Victory", ItemClassification.progression, None, self.player), True) + + # Note: relies on the assumption that real_items is a definitive list of real progression items in this + # world, and is not modified after being created. + for item in self.player_logic.real_items: + state.collect(self.create_item(item), True) + + # Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway. + if self.player_logic.forced_good_item != "": + state.collect(self.create_item(self.player_logic.forced_good_item), True) + + all_locations = self.multiworld.get_locations(self.player) + state.sweep_for_events(locations=all_locations) + + unreachable_locations = [location for location in all_locations + if not state.can_reach_location(location.name, self.player)] + + for location in unreachable_locations: + if location.name in self.player_logic.event_loc_to_item.keys(): + continue + + self.player_logic.real_locations.remove(location.name) + location.parent_region.locations.remove(location) + + if len(self.player_logic.real_items) > len(self.player_logic.real_locations): + raise OptionError(f"{self.player_name}'s Lingo world does not have enough locations to fit the number" + f" of required items without shuffling the postgame. Either enable postgame" + f" shuffling, or choose different options.") + def create_items(self): pool = [self.create_item(name) for name in self.player_logic.real_items] diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index e12ca022973b..3035446ef793 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -879,6 +879,8 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + - room: Ending Area + panel: THE END paintings: - id: eye_painting disable: True @@ -2322,7 +2324,7 @@ orientation: east - id: hi_solved_painting orientation: west - Orange Tower Seventh Floor: + Ending Area: entrances: Orange Tower Sixth Floor: room: Orange Tower @@ -2334,6 +2336,18 @@ check: True tag: forbid non_counting: True + location_name: Orange Tower Seventh Floor - THE END + doors: + End: + event: True + panels: + - THE END + Orange Tower Seventh Floor: + entrances: + Ending Area: + room: Ending Area + door: End + panels: THE MASTER: # We will set up special rules for this in code. id: Countdown Panels/Panel_master_master diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 3ed6cb24f7d289c38189932fe5ccf705bdaf0262..4a751b25ec5f143b6b055fd2043a6543754ef5b1 100644 GIT binary patch delta 27407 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 delta 27456 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%0D?iOrPle5Pi@mE?)2&I^!3< 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 Date: Wed, 24 Jul 2024 08:37:18 -0400 Subject: [PATCH 47/78] TUNIC: Missing slot data bugfix (#3628) * Fix certain items not being added to slot data * Change where items get added to slot data --- worlds/tunic/__init__.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 9b28d1d451a8..b3aa1e6a3479 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -160,9 +160,9 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: if new_cxn: cls.seed_groups[group]["plando"].value.append(cxn) - def create_item(self, name: str) -> TunicItem: + def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] - return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) + return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player) def create_items(self) -> None: @@ -192,14 +192,12 @@ def create_items(self) -> None: self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) elif self.options.laurels_location == "10_fairies": self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) - self.slot_data_items.append(laurels) items_to_create["Hero's Laurels"] = 0 if self.options.keys_behind_bosses: for rgb_hexagon, location in hexagon_locations.items(): hex_item = self.create_item(gold_hexagon if self.options.hexagon_quest else rgb_hexagon) self.multiworld.get_location(location, self.player).place_locked_item(hex_item) - self.slot_data_items.append(hex_item) items_to_create[rgb_hexagon] = 0 items_to_create[gold_hexagon] -= 3 @@ -245,33 +243,30 @@ def remove_filler(amount: int) -> None: remove_filler(items_to_create[gold_hexagon]) for hero_relic in item_name_groups["Hero Relics"]: - relic_item = TunicItem(hero_relic, ItemClassification.useful, self.item_name_to_id[hero_relic], self.player) - tunic_items.append(relic_item) + tunic_items.append(self.create_item(hero_relic, ItemClassification.useful)) items_to_create[hero_relic] = 0 if not self.options.ability_shuffling: for page in item_name_groups["Abilities"]: if items_to_create[page] > 0: - page_item = TunicItem(page, ItemClassification.useful, self.item_name_to_id[page], self.player) - tunic_items.append(page_item) + tunic_items.append(self.create_item(page, ItemClassification.useful)) items_to_create[page] = 0 if self.options.maskless: - mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) - tunic_items.append(mask_item) + tunic_items.append(self.create_item("Scavenger Mask", ItemClassification.useful)) items_to_create["Scavenger Mask"] = 0 if self.options.lanternless: - lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) - tunic_items.append(lantern_item) + tunic_items.append(self.create_item("Lantern", ItemClassification.useful)) items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): for _ in range(quantity): - tunic_item: TunicItem = self.create_item(item) - if item in slot_data_item_names: - self.slot_data_items.append(tunic_item) - tunic_items.append(tunic_item) + tunic_items.append(self.create_item(item)) + + for tunic_item in tunic_items: + if tunic_item.name in slot_data_item_names: + self.slot_data_items.append(tunic_item) self.multiworld.itempool += tunic_items From b23c1202582cbdf9a8e882d6b65cb580c5c5a382 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:17:43 -0400 Subject: [PATCH 48/78] Subnautica: Fix deprecated option getting (#3685) --- worlds/subnautica/rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/rules.py b/worlds/subnautica/rules.py index 3b6c5cd4dd68..ea9ec6a8058f 100644 --- a/worlds/subnautica/rules.py +++ b/worlds/subnautica/rules.py @@ -150,7 +150,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: def get_max_swim_depth(state: "CollectionState", player: int) -> int: - swim_rule: SwimRule = state.multiworld.swim_rule[player] + swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule depth: int = swim_rule.base_depth if swim_rule.consider_items: if has_seaglide(state, player): @@ -296,7 +296,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = multiworld.creature_scan_logic[player] + option = multiworld.worlds[player].options.creature_scan_logic for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(multiworld, player, creature_name) From 2307694012f3d49dbacbcdb059473f52c87aedea Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 24 Jul 2024 20:08:58 -0500 Subject: [PATCH 49/78] HK: fix remove issues failing collect/remove test (#3667) --- worlds/hk/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 78287305df5f..fbc6461f6aab 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -554,7 +554,8 @@ def remove(self, state, item: HKItem) -> bool: for effect_name, effect_value in item_effects.get(item.name, {}).items(): if state.prog_items[item.player][effect_name] == effect_value: del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + else: + state.prog_items[item.player][effect_name] -= effect_value return change From 697f7495184bbc7791ab3ad93bd2d7f4fa468ea5 Mon Sep 17 00:00:00 2001 From: Silent <110704408+silent-destroyer@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:06:45 -0400 Subject: [PATCH 50/78] TUNIC: Missing slot data bugfix (#3628) * Fix certain items not being added to slot data * Change where items get added to slot data From 94e6e978f330c25eeca37692f03b52cbfeb4c386 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:07:20 -0400 Subject: [PATCH 51/78] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Also=20fix=20Rt=204?= =?UTF-8?q?=20Hidden=20Item=20(#3668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: alchav --- worlds/pokemon_rb/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index 251beb59cc18..6aee25df2637 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -427,7 +427,7 @@ def __init__(self, flag): LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items), LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items), LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), - LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), + LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), From f34da74012ba0f69433b158a68e008dc36a258d7 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 25 Jul 2024 00:13:16 -0400 Subject: [PATCH 52/78] Stardew Valley: Make Fairy Dust a Ginger Island only item and location (#3650) --- worlds/stardew_valley/data/items.csv | 2 +- worlds/stardew_valley/data/locations.csv | 2 +- worlds/stardew_valley/items.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 2604ad2c46bd..e026090f8659 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -307,7 +307,7 @@ id,name,classification,groups,mod_name 322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", -325,Fairy Dust Recipe,progression,, +325,Fairy Dust Recipe,progression,"GINGER_ISLAND", 326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 6e30d2b8c858..242d00b4455b 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2088,7 +2088,7 @@ id,region,name,tags,mod_name 3472,Farm,Craft Life Elixir,CRAFTSANITY, 3473,Farm,Craft Oil of Garlic,CRAFTSANITY, 3474,Farm,Craft Monster Musk,CRAFTSANITY, -3475,Farm,Craft Fairy Dust,CRAFTSANITY, +3475,Farm,Craft Fairy Dust,"CRAFTSANITY,GINGER_ISLAND", 3476,Farm,Craft Warp Totem: Beach,CRAFTSANITY, 3477,Farm,Craft Warp Totem: Mountains,CRAFTSANITY, 3478,Farm,Craft Warp Totem: Farm,CRAFTSANITY, diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index cb6102016942..31c7da5e3ade 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -409,8 +409,9 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star else: items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE items.append(item_factory(Wallet.iridium_snake_milk)) - items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: + items.append(item_factory("Fairy Dust Recipe")) def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): From 496f0e09afe7e863cf1bcce948d48bf591086274 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 01:21:51 -0500 Subject: [PATCH 53/78] CommonClient: forget password when disconnecting (#3641) * makes the kivy connect button do the same username forgetting that /connect does to fix an issue where losing connection would make you unable to connect to a different server * extract duplicate code * per request, adds handling on any disconnect to forget the saved password as to not leak it to other servers --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 2 ++ kvui.py | 1 + 2 files changed, 3 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index f8d1fcb7a221..09937e4b9ab8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -61,6 +61,7 @@ def _cmd_connect(self, address: str = "") -> bool: if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -514,6 +515,7 @@ def update_permissions(self, permissions: typing.Dict[str, int]): async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() diff --git a/kvui.py b/kvui.py index a63d636960a7..f83590a819d5 100644 --- a/kvui.py +++ b/kvui.py @@ -596,6 +596,7 @@ def command_button_action(self, button): def connect_button_action(self, button): self.ctx.username = None + self.ctx.password = None if self.ctx.server: async_start(self.ctx.disconnect()) else: From deae524e9ba9f1bab1c48642f11108799d7b3b3a Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 02:05:04 -0500 Subject: [PATCH 54/78] Docs: add a living faq document for sharing dev solutions (#3156) * adding one faq :) * adding another faq that links to the relevant file * add lined line breaks between questions and lower the heading size of the question so sub-divisions can be added later * missed some newlines * updating best practice filler method * add note about get_filler_item_name() * updates to wording from review * add section to CODEOWNERS for maintainers of this doc * use underscores to reference the file easier in CODEOWNERS * update link to be direct and filter to function name --- docs/CODEOWNERS | 14 +++++++++++--- docs/apworld_dev_faq.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 docs/apworld_dev_faq.md diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 3b40d7e77a73..ab841e65ee4c 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,8 +1,8 @@ # Archipelago World Code Owners / Maintainers Document # -# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull -# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to -# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer. +# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as +# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in +# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly. # # All usernames must be GitHub usernames (and are case sensitive). @@ -226,3 +226,11 @@ # Ori and the Blind Forest # /worlds_disabled/oribf/ + +################### +## Documentation ## +################### + +# Apworld Dev Faq +/docs/apworld_dev_faq.md @qwint @ScipioWright + diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md new file mode 100644 index 000000000000..059c33844f27 --- /dev/null +++ b/docs/apworld_dev_faq.md @@ -0,0 +1,32 @@ +# APWorld Dev FAQ + +This document is meant as a reference tool to show solutions to common problems when developing an apworld. + +--- + +### My game has a restrictive start that leads to fill errors + +Hint to the Generator that an item needs to be in sphere one with local_early_items +```py +early_item_name = "Sword" +self.multiworld.local_early_items[self.player][early_item_name] = 1 +``` + +--- + +### I have multiple settings that change the item/location pool counts and need to balance them out + +In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible. + +If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit + +Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names +```py +total_locations = len(self.multiworld.get_unfilled_locations(self.player)) +item_pool = self.create_non_filler_items() + +while len(item_pool) < total_locations: + item_pool.append(self.create_filler()) + +self.multiworld.itempool += item_pool +``` From 8949e215654c29f3e46e254bd28fa8572c0acdbc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:10:36 +0200 Subject: [PATCH 55/78] settings: safer writing (#3644) * settings: clean up imports * settings: try to use atomic rename * settings: flush, sync and validate new yaml before replacing the old one * settings: add test for Settings.save --- settings.py | 18 +++++++++++++----- test/general/test_host_yaml.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/settings.py b/settings.py index 7ab618c344d8..792770521459 100644 --- a/settings.py +++ b/settings.py @@ -3,6 +3,7 @@ This is different from player options. """ +import os import os.path import shutil import sys @@ -11,7 +12,6 @@ from enum import IntEnum from threading import Lock from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar -import os __all__ = [ "get_settings", "fmt_doc", "no_gui", @@ -798,6 +798,7 @@ def autosave() -> None: atexit.register(autosave) def save(self, location: Optional[str] = None) -> None: # as above + from Utils import parse_yaml location = location or self._filename assert location, "No file specified" temp_location = location + ".tmp" # not using tempfile to test expected file access @@ -807,10 +808,18 @@ def save(self, location: Optional[str] = None) -> None: # as above # can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM with open(temp_location, "w", encoding="utf-8") as f: self.dump(f) - # replace old with new - if os.path.exists(location): + f.flush() + if hasattr(os, "fsync"): + os.fsync(f.fileno()) + # validate new file is valid yaml + with open(temp_location, encoding="utf-8") as f: + parse_yaml(f.read()) + # replace old with new, try atomic operation first + try: + os.rename(temp_location, location) + except (OSError, FileExistsError): os.unlink(location) - os.rename(temp_location, location) + os.rename(temp_location, location) self._filename = location def dump(self, f: TextIO, level: int = 0) -> None: @@ -832,7 +841,6 @@ def get_settings() -> Settings: with _lock: # make sure we only have one instance res = getattr(get_settings, "_cache", None) if not res: - import os from Utils import user_path, local_path filenames = ("options.yaml", "host.yaml") locations: List[str] = [] diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 7174befca428..3edbd34a51c5 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -1,11 +1,12 @@ import os +import os.path import unittest from io import StringIO -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from typing import Any, Dict, List, cast import Utils -from settings import Settings, Group +from settings import Group, Settings, ServerOptions class TestIDs(unittest.TestCase): @@ -80,3 +81,27 @@ class AGroup(Group): self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list self.assertGreater(value_spaces[3], value_spaces[0], f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}") + + +class TestSettingsSave(unittest.TestCase): + def test_save(self) -> None: + """Test that saving and updating works""" + with TemporaryDirectory() as d: + filename = os.path.join(d, "host.yaml") + new_release_mode = ServerOptions.ReleaseMode("enabled") + # create default host.yaml + settings = Settings(None) + settings.save(filename) + self.assertTrue(os.path.exists(filename), + "Default settings could not be saved") + self.assertNotEqual(settings.server_options.release_mode, new_release_mode, + "Unexpected default release mode") + # update host.yaml + settings.server_options.release_mode = new_release_mode + settings.save(filename) + self.assertFalse(os.path.exists(filename + ".tmp"), + "Temp file was not removed during save") + # read back host.yaml + settings = Settings(filename) + self.assertEqual(settings.server_options.release_mode, new_release_mode, + "Settings were not overwritten") From 205ca7fa37cbcfc1515eaace6239a1acbb34f6a2 Mon Sep 17 00:00:00 2001 From: Witchybun <96719127+Witchybun@users.noreply.github.com> Date: Thu, 25 Jul 2024 02:22:46 -0500 Subject: [PATCH 56/78] Stardew Valley: Fix Daggerfish, Cropsanity; Move Some Rules to Content Packs; Add Missing Shipsanity Location (#3626) * Fix logic bug on daggerfish * Make new region for pond. * Fix SVE logic for crops * Fix Distant Lands Cropsanity * Fix failing tests. * Reverting removing these for now. * Fix bugs, add combat requirement * convert str into tuple directly * add ginger island to mod tests * Move a lot of mod item logic to content pack * Gut the rules from DL while we're at it. * Import nuke * Fix alecto * Move back some rules for now. * Move archaeology rules * Add some comments why its done. * Clean up archaeology and fix sve * Moved dulse to water item class * Remove digging like worms for now * fix * Add missing shipsanity location * Move background names around or something idk * Revert ArchaeologyTrash for now --------- Co-authored-by: Jouramie --- worlds/stardew_valley/content/mods/alecto.py | 33 +++++ .../stardew_valley/content/mods/archeology.py | 36 ++++-- .../content/mods/distant_lands.py | 31 ++++- .../stardew_valley/content/mods/npc_mods.py | 7 -- worlds/stardew_valley/content/mods/sve.py | 102 ++++++++++++++-- .../content/vanilla/pelican_town.py | 6 +- worlds/stardew_valley/data/fish_data.py | 7 +- worlds/stardew_valley/data/locations.csv | 3 +- worlds/stardew_valley/data/recipe_data.py | 4 +- worlds/stardew_valley/data/requirement.py | 21 ++++ worlds/stardew_valley/data/shop.py | 4 +- .../stardew_valley/logic/requirement_logic.py | 27 +++- .../stardew_valley/mods/logic/item_logic.py | 115 +----------------- worlds/stardew_valley/mods/mod_regions.py | 4 +- worlds/stardew_valley/rules.py | 1 + worlds/stardew_valley/strings/book_names.py | 4 - .../stardew_valley/strings/entrance_names.py | 1 + worlds/stardew_valley/strings/fish_names.py | 5 +- worlds/stardew_valley/strings/food_names.py | 1 + worlds/stardew_valley/strings/region_names.py | 1 + worlds/stardew_valley/test/mods/TestMods.py | 8 +- 21 files changed, 258 insertions(+), 163 deletions(-) create mode 100644 worlds/stardew_valley/content/mods/alecto.py diff --git a/worlds/stardew_valley/content/mods/alecto.py b/worlds/stardew_valley/content/mods/alecto.py new file mode 100644 index 000000000000..c05c936de3c0 --- /dev/null +++ b/worlds/stardew_valley/content/mods/alecto.py @@ -0,0 +1,33 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.seed_names import DistantLandsSeed + + +class AlectoContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + if ModNames.distant_lands in content.registered_packs: + content.game_items.pop(DistantLandsSeed.void_mint) + content.game_items.pop(DistantLandsSeed.vile_ancient_fruit) + content.source_item(DistantLandsSeed.void_mint, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),), + content.source_item(DistantLandsSeed.vile_ancient_fruit, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ), + + +register_mod_content_pack(ContentPack( + ModNames.alecto, + weak_dependencies=( + ModNames.distant_lands, # For Witch's order + ), + villagers=( + villagers_data.alecto, + ) + +)) diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py index 97d38085d3b2..5eb8af4cfc38 100644 --- a/worlds/stardew_valley/content/mods/archeology.py +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -1,20 +1,34 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack -from ...data.game_item import ItemTag, Tag -from ...data.shop import ShopSource +from ...data.artisan import MachineSource from ...data.skill import Skill from ...mods.mod_data import ModNames -from ...strings.book_names import ModBook -from ...strings.region_names import LogicRegion +from ...strings.craftable_names import ModMachine +from ...strings.fish_names import ModTrash +from ...strings.metal_names import all_artifacts, all_fossils from ...strings.skill_names import ModSkill -register_mod_content_pack(ContentPack( + +class ArchaeologyContentPack(ContentPack): + def artisan_good_hook(self, content: StardewContent): + # Done as honestly there are too many display items to put into the initial registration traditionally. + display_items = all_artifacts + all_fossils + for item in display_items: + self.source_display_items(item, content) + content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts)) + + def source_display_items(self, item: str, content: StardewContent): + wood_display = f"Wooden Display: {item}" + hardwood_display = f"Hardwood Display: {item}" + if item == "Trilobite": + wood_display = f"Wooden Display: Trilobite Fossil" + hardwood_display = f"Hardwood Display: Trilobite Fossil" + content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber)) + content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber)) + + +register_mod_content_pack(ArchaeologyContentPack( ModNames.archaeology, - shop_sources={ - ModBook.digging_like_worms: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=500, shop_region=LogicRegion.bookseller_1),), - }, skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), )) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py index 19380d4ff565..c5614d130250 100644 --- a/worlds/stardew_valley/content/mods/distant_lands.py +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -1,9 +1,26 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import QuestRequirement from ...mods.mod_data import ModNames +from ...strings.crop_names import DistantLandsCrop +from ...strings.forageable_names import DistantLandsForageable +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import DistantLandsSeed -register_mod_content_pack(ContentPack( + +class DistantLandsContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED) + + +register_mod_content_pack(DistantLandsContentPack( ModNames.distant_lands, fishes=( fish_data.void_minnow, @@ -13,5 +30,13 @@ ), villagers=( villagers_data.zic, - ) + ), + harvest_sources={ + DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),), + DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),) + } )) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py index 3172a55dbf32..52d97d5c52b7 100644 --- a/worlds/stardew_valley/content/mods/npc_mods.py +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -73,13 +73,6 @@ ) )) -register_mod_content_pack(ContentPack( - ModNames.alecto, - villagers=( - villagers_data.alecto, - ) -)) - register_mod_content_pack(ContentPack( ModNames.lacey, villagers=( diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py index f74b80948c96..a68d4ae9c097 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -3,15 +3,27 @@ from ..override import override from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack from ...data import villagers_data, fish_data -from ...data.harvest import ForagingSource -from ...data.requirement import YearRequirement +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement +from ...data.shop import ShopSource from ...mods.mod_data import ModNames -from ...strings.crop_names import Fruit -from ...strings.fish_names import WaterItem +from ...strings.craftable_names import ModEdible +from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit +from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem from ...strings.flower_names import Flower -from ...strings.forageable_names import Mushroom, Forageable -from ...strings.region_names import Region, SVERegion +from ...strings.food_names import SVEMeal, SVEBeverage +from ...strings.forageable_names import Mushroom, Forageable, SVEForage +from ...strings.gift_names import SVEGift +from ...strings.metal_names import Ore +from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.performance_names import Performance +from ...strings.region_names import Region, SVERegion, LogicRegion from ...strings.season_names import Season +from ...strings.seed_names import SVESeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial +from ...strings.villager_names import ModNPC class SVEContentPack(ContentPack): @@ -38,6 +50,24 @@ def villager_hook(self, content: StardewContent): # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge content.villagers.pop(villagers_data.lance.name) + def harvest_source_hook(self, content: StardewContent): + content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED) + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Highlands seeds as these are behind Lance existing. + content.game_items.pop(SVESeed.void) + content.game_items.pop(SVEVegetable.void_root) + content.game_items.pop(SVESeed.stalk) + content.game_items.pop(SVEFruit.monster_fruit) + content.game_items.pop(SVESeed.fungus) + content.game_items.pop(SVEVegetable.monster_mushroom) + content.game_items.pop(SVESeed.slime) + content.game_items.pop(SVEFruit.slime_berry) + register_mod_content_pack(SVEContentPack( ModNames.sve, @@ -45,12 +75,24 @@ def villager_hook(self, content: StardewContent): ginger_island_content_pack.name, ModNames.jasper, # To override Marlon and Gunther ), + shop_sources={ + SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),), + SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),), + ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), + SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), + ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), + SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), + SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),), + }, harvest_sources={ Mushroom.red: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) ), Mushroom.purple: ( - ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) ), Mushroom.morel: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) @@ -64,17 +106,59 @@ def villager_hook(self, content: StardewContent): Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), Fruit.ancient_fruit: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ForagingSource(regions=(SVERegion.sprite_spring_cave,)), ), Fruit.sweet_gem_berry: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ), + # New items + + ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), + ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), + other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), + ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), + SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),), + SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),), + SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),), + SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),), + SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),), + SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),), + SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),), + SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),), + SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),), + SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),), + SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),), + SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),), + SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), + ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), + other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), + # Fable Reef WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + + # Crops + SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),), + SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),), + SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),), + SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),), + SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),), + SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),), + SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),), + }, fishes=( fish_data.baby_lunaloo, # Removed when no ginger island diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 917e8cca220a..220b46eae2a4 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -229,7 +229,7 @@ ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.mapping_cave_systems: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.adventurer_guild_bedroom), + GenericSource(regions=(Region.adventurer_guild_bedroom,)), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), @@ -243,12 +243,12 @@ ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), Book.the_alleyway_buffet: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.town, + GenericSource(regions=(Region.town,), other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.the_art_o_crabbing: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.beach, + GenericSource(regions=(Region.beach,), other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), SkillRequirement(Skill.fishing, 6), SeasonRequirement(Season.winter))), diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index c6f0c30d41ff..26b1a0d58a81 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -46,7 +46,8 @@ def __repr__(self): crimson_badlands = (SVERegion.crimson_badlands,) shearwater = (SVERegion.shearwater,) -highlands = (SVERegion.highlands_outside,) +highlands_pond = (SVERegion.highlands_pond,) +highlands_cave = (SVERegion.highlands_cavern,) sprite_spring = (SVERegion.sprite_spring,) fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) @@ -133,9 +134,9 @@ def create_fish(name: str, locations: Tuple[str, ...], seasons: Union[str, Tuple bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve) butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve) clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve) -daggerfish = create_fish(SVEFish.daggerfish, highlands, season.all_seasons, 50, mod_name=ModNames.sve) +daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve) frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve) -gemfish = create_fish(SVEFish.gemfish, highlands, season.all_seasons, 100, mod_name=ModNames.sve) +gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve) goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve) grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve) king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 242d00b4455b..0d7a10f95496 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2900,7 +2900,6 @@ id,region,name,tags,mod_name 7055,Abandoned Mines - 3,Abandoned Treasure - Floor 3,MANDATORY,Boarding House and Bus Stop Extension 7056,Abandoned Mines - 4,Abandoned Treasure - Floor 4,MANDATORY,Boarding House and Bus Stop Extension 7057,Abandoned Mines - 5,Abandoned Treasure - Floor 5,MANDATORY,Boarding House and Bus Stop Extension -7351,Farm,Read Digging Like Worms,"BOOKSANITY,BOOKSANITY_SKILL",Archaeology 7401,Farm,Cook Magic Elixir,COOKSANITY,Magic 7402,Farm,Craft Travel Core,CRAFTSANITY,Magic 7403,Farm,Craft Haste Elixir,CRAFTSANITY,Stardew Valley Expanded @@ -3280,10 +3279,10 @@ id,region,name,tags,mod_name 8237,Shipping,Shipsanity: Pterodactyl R Wing Bone,SHIPSANITY,Boarding House and Bus Stop Extension 8238,Shipping,Shipsanity: Scrap Rust,SHIPSANITY,Archaeology 8239,Shipping,Shipsanity: Rusty Path,SHIPSANITY,Archaeology -8240,Shipping,Shipsanity: Digging Like Worms,SHIPSANITY,Archaeology 8241,Shipping,Shipsanity: Digger's Delight,SHIPSANITY,Archaeology 8242,Shipping,Shipsanity: Rocky Root Coffee,SHIPSANITY,Archaeology 8243,Shipping,Shipsanity: Ancient Jello,SHIPSANITY,Archaeology 8244,Shipping,Shipsanity: Bone Fence,SHIPSANITY,Archaeology 8245,Shipping,Shipsanity: Grilled Cheese,SHIPSANITY,Binning Skill 8246,Shipping,Shipsanity: Fish Casserole,SHIPSANITY,Binning Skill +8247,Shipping,Shipsanity: Snatcher Worm,SHIPSANITY,Stardew Valley Expanded diff --git a/worlds/stardew_valley/data/recipe_data.py b/worlds/stardew_valley/data/recipe_data.py index b48246876271..3123bb924307 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -5,7 +5,7 @@ from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop -from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish +from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient @@ -195,7 +195,7 @@ def create_recipe(name: str, ingredients: Dict[str, int], source: RecipeSource, ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEFish.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 4744f9dffdfe..b2416d8d0b72 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -31,6 +31,27 @@ class YearRequirement(Requirement): year: int +@dataclass(frozen=True) +class CombatRequirement(Requirement): + level: str + + +@dataclass(frozen=True) +class QuestRequirement(Requirement): + quest: str + + +@dataclass(frozen=True) +class RelationshipRequirement(Requirement): + npc: str + hearts: int + + +@dataclass(frozen=True) +class FishingRequirement(Requirement): + region: str + + @dataclass(frozen=True) class WalnutRequirement(Requirement): amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index ca54d35e14f2..f14dbac82131 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -16,8 +16,8 @@ class ShopSource(ItemSource): other_requirements: Tuple[Requirement, ...] = () def __post_init__(self): - assert self.money_price or self.items_price, "At least money price or items price need to be defined." - assert self.items_price is None or all(type(p) == tuple for p in self.items_price), "Items price should be a tuple." + assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." + assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." @dataclass(frozen=True, **kw_only) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 9356440ac6a8..6a5adf4890c9 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -3,15 +3,20 @@ from .base_logic import BaseLogicMixin, BaseLogic from .book_logic import BookLogicMixin +from .combat_logic import CombatLogicMixin +from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin +from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin +from .relationship_logic import RelationshipLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ + RelationshipRequirement, FishingRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -21,7 +26,7 @@ def __init__(self, *args, **kwargs): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -55,3 +60,21 @@ def _(self, requirement: YearRequirement): @meet_requirement.register def _(self, requirement: WalnutRequirement): return self.logic.walnut.has_walnut(requirement.amount) + + @meet_requirement.register + def _(self, requirement: CombatRequirement): + return self.logic.combat.can_fight_at_level(requirement.level) + + @meet_requirement.register + def _(self, requirement: QuestRequirement): + return self.logic.quest.can_complete_quest(requirement.quest) + + @meet_requirement.register + def _(self, requirement: RelationshipRequirement): + return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts) + + @meet_requirement.register + def _(self, requirement: FishingRequirement): + return self.logic.fishing.can_fish_at(requirement.region) + + diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index cfafc88e83f5..ef5eab0134d1 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -23,24 +23,15 @@ from ...options import Cropsanity from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood -from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop -from ...strings.fish_names import ModTrash, SVEFish -from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable -from ...strings.gift_names import SVEGift +from ...strings.craftable_names import ModCraftable, ModMachine +from ...strings.fish_names import ModTrash from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import Loot from ...strings.performance_names import Performance -from ...strings.quest_names import ModQuest -from ...strings.region_names import Region, SVERegion, DeepWoodsRegion, BoardingHouseRegion -from ...strings.season_names import Season -from ...strings.seed_names import SVESeed, DistantLandsSeed -from ...strings.skill_names import Skill +from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion from ...strings.tool_names import Tool, ToolMaterial -from ...strings.villager_names import ModNPC display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display] display_items = all_artifacts + all_fossils @@ -58,12 +49,6 @@ class ModItemLogic(BaseLogic[Union[CombatLogicMixin, ReceivedLogicMixin, Cooking def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() - if ModNames.sve in self.options.mods: - items.update(self.get_sve_item_rules()) - if ModNames.archaeology in self.options.mods: - items.update(self.get_archaeology_item_rules()) - if ModNames.distant_lands in self.options.mods: - items.update(self.get_distant_lands_item_rules()) if ModNames.boarding_house in self.options.mods: items.update(self.get_boarding_house_item_rules()) return items @@ -75,61 +60,6 @@ def modify_vanilla_item_rules_with_mod_additions(self, item_rule: Dict[str, Star item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule)) return item_rule - def get_sve_item_rules(self): - return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), - SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus), - ModLoot.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime), - SVESeed.slime: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - ModLoot.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void), - SVESeed.void: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.void_soul: self.logic.region.can_reach( - SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), - SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), - SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), - SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), - SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), - SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( - self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.farming.can_plant_and_grow_item((Season.spring, Season.summer)) & self.logic.has(SVESeed.shrub), - ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), - ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), - ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), - ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_fern: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.farming.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_fern), - SVEForage.conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), - SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), - SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), - SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), - ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), - SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & - self.logic.season.has(Season.fall), - SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.rafflesia: self.logic.region.can_reach(Region.secret_woods), - SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), - "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), - SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - ModLoot.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & - self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three - } - # @formatter:on - def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): return { Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( @@ -141,7 +71,7 @@ def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), - SVEFish.dulse_seaweed: self.logic.fishing.can_fish_at(Region.beach) & self.logic.season.has_any([Season.spring, Season.summer, Season.winter]) + } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): @@ -160,36 +90,6 @@ def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): return options_to_update - def get_archaeology_item_rules(self): - archaeology_item_rules = {} - preservation_chamber_rule = self.logic.has(ModMachine.preservation_chamber) - hardwood_preservation_chamber_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) - for item in display_items: - for display_type in display_types: - if item == "Trilobite": - location_name = f"{display_type}: Trilobite Fossil" - else: - location_name = f"{display_type}: {item}" - display_item_rule = self.logic.crafting.can_craft(all_crafting_recipes_by_name[display_type]) & self.logic.has(item) - if "Wooden" in display_type: - archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule - else: - archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule - archaeology_item_rules[ModTrash.rusty_scrap] = self.logic.has(ModMachine.grinder) & self.logic.has_any(*all_artifacts) - return archaeology_item_rules - - def get_distant_lands_item_rules(self): - return { - DistantLandsForageable.swamp_herb: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsForageable.brown_amanita: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsSeed.vile_ancient_fruit: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsSeed.void_mint: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsCrop.void_mint: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.void_mint), - DistantLandsCrop.vile_ancient_fruit: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.vile_ancient_fruit), - } - def get_boarding_house_item_rules(self): return { # Mob Drops from lost valley enemies @@ -251,8 +151,3 @@ def get_boarding_house_item_rules(self): BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level( Performance.great), } - - def has_seed_unlocked(self, seed_name: str): - if self.options.cropsanity == Cropsanity.option_disabled: - return True_() - return self.logic.received(seed_name) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index c075bd4d106f..a402ba606868 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -183,7 +183,8 @@ RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave], is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True), + RegionData(SVERegion.highlands_pond, is_ginger_island=True), RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), RegionData(SVERegion.dwarf_prison, is_ginger_island=True), RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), @@ -276,6 +277,7 @@ ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ] alecto_regions = [ diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7c1fdbda3cf4..89b1cf87c3c1 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1031,6 +1031,7 @@ def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, pla set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py index 3c32cd81b326..6c271f42ae9c 100644 --- a/worlds/stardew_valley/strings/book_names.py +++ b/worlds/stardew_valley/strings/book_names.py @@ -27,10 +27,6 @@ class Book: the_diamond_hunter = "The Diamond Hunter" -class ModBook: - digging_like_worms = "Digging Like Worms" - - ordered_lost_books = [] all_lost_books = set() diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 9b651f42760a..58a919f2a8a4 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -358,6 +358,7 @@ class SVEEntrance: sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave" fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom" museum_to_gunther_bedroom = "Museum to Gunther's Bedroom" + highlands_to_pond = "Highlands to Highlands Pond" class AlectoEntrance: diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index d94f9e2fd403..d4ee81430eb4 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -137,7 +137,6 @@ class SVEFish: void_eel = "Void Eel" water_grub = "Water Grub" sea_sponge = "Sea Sponge" - dulse_seaweed = "Dulse Seaweed" class DistantLandsFish: @@ -147,6 +146,10 @@ class DistantLandsFish: giant_horsehoe_crab = "Giant Horsehoe Crab" +class SVEWaterItem: + dulse_seaweed = "Dulse Seaweed" + + class ModTrash: rusty_scrap = "Scrap Rust" diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 5555316f8314..03784336d19c 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -102,6 +102,7 @@ class SVEMeal: void_delight = "Void Delight" void_salmon_sushi = "Void Salmon Sushi" grampleton_orange_chicken = "Grampleton Orange Chicken" + stamina_capsule = "Stamina Capsule" class TrashyMeal: diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 9cedb6b8ef32..58763b6fcb80 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -296,6 +296,7 @@ class SVERegion: sprite_spring_cave = "Sprite Spring Cave" willy_bedroom = "Willy's Bedroom" gunther_bedroom = "Gunther's Bedroom" + highlands_pond = "Highlands Pond" class AlectoRegion: diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 5e7e9d4143bd..97184b1338b8 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -14,7 +14,8 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in options.Mods.valid_keys: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): + world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false} + with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) @@ -22,8 +23,9 @@ def test_given_mod_names_when_generate_paired_with_entrance_randomizer_then_basi for option in options.EntranceRandomization.options: for mod in options.Mods.valid_keys: world_options = { - options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], - options.Mods: mod + options.EntranceRandomization: options.EntranceRandomization.options[option], + options.Mods: mod, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false } with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) From b019485944543e8b1cb440c572f27b6a592a0dd8 Mon Sep 17 00:00:00 2001 From: CookieCat <81494827+CookieCat45@users.noreply.github.com> Date: Thu, 25 Jul 2024 03:27:22 -0400 Subject: [PATCH 57/78] AHIT: Update Setup Guide (#3647) --- worlds/ahit/docs/setup_en.md | 63 ++++++++---------------------------- 1 file changed, 13 insertions(+), 50 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 509869fc256a..23b34907071c 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -12,41 +12,29 @@ ## Instructions -1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) -This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, -paste the link into the box, and hit Enter. +1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!** + Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place. + **This is important! Changing the game version CAN and WILL break your existing save files!!!** -2. In the Steam console, enter the following command: -`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, -**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** -or else the download may potentially become corrupted (see first FAQ issue below). +2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**. -3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. +3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. + While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) -4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. +4. Once the game finishes downloading, start it up. + In Game Settings, make sure **Enable Developer Console** is checked. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. -In this new text file, input the number **253230** on the first line. - - -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. -You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. - - -7. Start up the game using your new shortcut. To confirm if you are on the correct version, -go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running -the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. +5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** -(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher +and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. @@ -61,33 +49,8 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t ## FAQ/Common Issues -### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! -If you receive an error message such as -**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or -**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot -download was likely corrupted. The only way to fix this is to start the entire download all over again. -Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this -from happening is to ensure that your connection is not interrupted or slowed while downloading. - -### The game keeps crashing on startup after the splash screen! -This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, -try the following: - -- Close Steam **entirely**. -- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. -- Close the game, and then open Steam again. -- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. - -### I followed the setup, but "Live Game Events" still shows up in the options menu! -The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by -default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file -extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. -To show file extensions in Windows 10, open any folder, click the View tab at the top, and check -"File name extensions". Then you can correct the name of the file. If the name of the file is correct, -and you're still running into the issue, re-read the setup guide again in case you missed a step. -If you still can't get it to work, ask for help in the Discord thread. - -### The game is running on the older version, but it's not connecting when starting a new save! + +### The game is not connecting when starting a new save! For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu (rocket icon) in-game, and re-enable the mod. From 5fb1ebdcfd4a9adaaf7d655492069649c9de0482 Mon Sep 17 00:00:00 2001 From: Tsukino <16899482+Tsukino-uwu@users.noreply.github.com> Date: Thu, 25 Jul 2024 09:30:23 +0200 Subject: [PATCH 58/78] Docs: Add Swedish Guide for Pokemon Emerald (#3252) * Docs: Add Swedish Guide for Pokemon Emerald Swedish Translation * v2 some proof reading & clarification changes * v3 * v4 * v5 typo * v6 * Update worlds/pokemon_emerald/docs/setup_sv.md Co-authored-by: Bryce Wilson * Update worlds/pokemon_emerald/docs/setup_sv.md Co-authored-by: Bryce Wilson * v7 Tried to reduce the length of lines, this should still convey the same message/meaning * typo * v8 Removed Leading/Trailing Spaces * typo v2 * Added a couple of full stops. * lowercase typos * Update setup_sv.md * Apply suggestions from code review Co-authored-by: Bryce Wilson --------- Co-authored-by: Bryce Wilson Co-authored-by: bittersweetrin --- worlds/pokemon_emerald/__init__.py | 11 +++- worlds/pokemon_emerald/docs/setup_sv.md | 78 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 worlds/pokemon_emerald/docs/setup_sv.md diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index aa4f6ccf7519..abdee26f572f 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -52,8 +52,17 @@ class PokemonEmeraldWebWorld(WebWorld): "setup/es", ["nachocua"] ) + + setup_sv = Tutorial( + "Multivärld Installations Guide", + "En guide för att kunna spela Pokémon Emerald med Archipelago.", + "Svenska", + "setup_sv.md", + "setup/sv", + ["Tsukino"] + ) - tutorials = [setup_en, setup_es] + tutorials = [setup_en, setup_es, setup_sv] class PokemonEmeraldSettings(settings.Group): diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md new file mode 100644 index 000000000000..88b1d384096b --- /dev/null +++ b/worlds/pokemon_emerald/docs/setup_sv.md @@ -0,0 +1,78 @@ +# Pokémon Emerald Installationsguide + +## Programvara som behövs + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Ett engelskt Pokémon Emerald ROM, Archipelago kan inte hjälpa dig med detta. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare + +### Konfigurera BizHawk + +När du har installerat BizHawk, öppna `EmuHawk.exe` och ändra följande inställningar: + +- Om du använder BizHawk 2.7 eller 2.8, gå till `Config > Customize`. På "Advanced Tab", byt Lua core från +`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efteråt. (Använder du BizHawk 2.9, kan du skippa detta steg.) +- Gå till `Config > Customize`. Markera "Run in background" inställningen för att förhindra bortkoppling från +klienten om du alt-tabbar bort från EmuHawk. +- Öppna en `.gba` fil i EmuHawk och gå till `Config > Controllers…` för att konfigurera dina inputs. +Om du inte hittar `Controllers…`, starta ett valfritt `.gba` ROM först. +- Överväg att rensa keybinds i `Config > Hotkeys…` som du inte tänkt använda. Välj en keybind och tryck på ESC +för att rensa bort den. + +## Extra programvara + +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), +används tillsammans med +[PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Generera och patcha ett spel + +1. Skapa din konfigurationsfil (YAML). Du kan göra en via att använda +[Pokémon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options). +2. Följ de allmänna Archipelago instruktionerna för att +[Generera ett spel](../../Archipelago/setup/en#generating-a-game). +Detta kommer generera en fil för dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg. +3. Öppna `ArchipelagoLauncher.exe` +4. Välj "Open Patch" på vänstra sidan, och välj din patchfil. +5. Om detta är första gången du patchar, så kommer du behöva välja var ditt ursprungliga ROM är. +6. En patchad `.gba` fil kommer skapas på samma plats som patchfilen. +7. Första gången du öppnar en patch med BizHawk-klienten, kommer du också behöva bekräfta var `EmuHawk.exe` filen är +installerad i din BizHawk-mapp. + +Om du bara tänkt spela själv och du inte bryr dig om automatisk spårning eller ledtrådar, så kan du stanna här, stänga +av klienten, och starta ditt patchade ROM med valfri emulator. Dock, för multvärldsfunktionen eller andra +Archipelago-funktioner, fortsätt nedanför med BizHawk. + +## Anslut till en server + +Om du vanligtsvis öppnar en patchad fil så görs steg 1-5 automatiskt åt dig. Även om det är så, kom ihåg dessa steg +ifall du till exempel behöver stänga ner och starta om något medans du spelar. + +1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel, +så kan du bara öppna den igen från launchern. +2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen. +3. I EmuHawk, gå till `Tools > Lua Console`. Luakonsolen måste vara igång medans du spelar. +4. I Luakonsolen, Tryck på `Script > Open Script…`. +5. Leta reda på din Archipelago-mapp och i den öppna `data/lua/connector_bizhawk_generic.lua`. +6. Emulatorn och klienten kommer så småningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är +anslutet och att Pokemon Emerald är igenkänt. +7. För att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex. +`archipelago.gg:38281` +längst upp i din klient och tryck sen på "Connect". + +Du borde nu kunna ta emot och skicka föremål. Du behöver göra dom här stegen varje gång du vill ansluta igen. Det är +helt okej att göra saker offline utan att behöva oroa sig; allt kommer att synkronisera när du ansluter till servern +igen. + +## Automatisk Spårning + +Pokémon Emerald har en fullt fungerande spårare med stöd för automatisk spårning. + +1. Ladda ner [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) +och +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat. +3. Öppna PopTracker, och välj Pokemon Emerald. +4. För att automatiskt spåra, tryck på "AP" symbolen längst upp. +5. Skriv in Archipelago-serverns uppgifter (Samma som du använde för att ansluta med klienten), "Slot"-namn samt +lösenord. From 79843803cf3a2547390f6e139be9c229a77d370b Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 16:01:22 -0500 Subject: [PATCH 59/78] Docs: Add header to FAQ doc referencing other relevant docs (#3692) * Add header to FAQ doc referencing other relevant docs * Update docs/apworld_dev_faq.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update docs/apworld_dev_faq.md Co-authored-by: Scipio Wright --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Scipio Wright --- docs/apworld_dev_faq.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md index 059c33844f27..71e2e4152e71 100644 --- a/docs/apworld_dev_faq.md +++ b/docs/apworld_dev_faq.md @@ -1,6 +1,8 @@ # APWorld Dev FAQ This document is meant as a reference tool to show solutions to common problems when developing an apworld. +It is not intended to answer every question about Archipelago and it assumes you have read the other docs, +including [Contributing](contributing.md), [Adding Games](), and [World API](). --- From b6e5223aa27bd77217897bcad41645b6645a6969 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:02:25 -0400 Subject: [PATCH 60/78] Docs: Expanding on the answers in the FAQ (#3690) * Expand on some existing answers * Oops * Sphere "one" * Removing while * Update docs/apworld_dev_faq.md Co-authored-by: Scipio Wright --------- Co-authored-by: Scipio Wright --- docs/apworld_dev_faq.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md index 71e2e4152e71..8d9429afa321 100644 --- a/docs/apworld_dev_faq.md +++ b/docs/apworld_dev_faq.md @@ -8,12 +8,18 @@ including [Contributing](contributing.md), [Adding Games](), an ### My game has a restrictive start that leads to fill errors -Hint to the Generator that an item needs to be in sphere one with local_early_items +Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one. ```py early_item_name = "Sword" self.multiworld.local_early_items[self.player][early_item_name] = 1 ``` +Some alternative ways to try to fix this problem are: +* Add more locations to sphere one of your world, potentially only when there would be a restrictive start +* Pre-place items yourself, such as during `create_items` +* Put items into the player's starting inventory using `push_precollected` +* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start + --- ### I have multiple settings that change the item/location pool counts and need to balance them out @@ -27,8 +33,13 @@ Note: to use self.create_filler(), self.get_filler_item_name() should be defined total_locations = len(self.multiworld.get_unfilled_locations(self.player)) item_pool = self.create_non_filler_items() -while len(item_pool) < total_locations: +for _ in range(total_locations - len(item_pool)): item_pool.append(self.create_filler()) self.multiworld.itempool += item_pool ``` + +A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions): +```py +item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))] +``` From d030a698a6824ec960fdcba40383d38991f82812 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 25 Jul 2024 17:09:37 -0400 Subject: [PATCH 61/78] Lingo: Changed minimum progression requirement (#3672) --- worlds/lingo/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index 3b67617873c7..a1b8b7c1d439 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,7 +9,7 @@ from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions, lingo_option_groups +from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -54,14 +54,17 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): - if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps): + if not (self.options.shuffle_doors or self.options.shuffle_colors or + (self.options.sunwarp_access >= SunwarpAccess.option_unlock and + self.options.victory_condition == VictoryCondition.option_pilgrimage)): if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" - f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem" - f" right.") + warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door" + f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition" + f" if that doesn't seem right.") else: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" - f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.") + raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on" + f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage" + f" victory condition.") self.player_logic = LingoPlayerLogic(self) From cc2216164489f89d78ac1e53aaa71b9dce04ac28 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 26 Jul 2024 04:53:11 -0400 Subject: [PATCH 62/78] Lingo: Add panels mode door shuffle (#3163) * Created panels mode door shuffle * Added some panel door item names * Remove RUNT TURN panel door Not really useful. * Fix logic with First SIX related stuff * Add group_doors to slot data * Fix LEVEL 2 behavior with panels mode * Fixed unit tests * Fixed duplicate IDs from merge * Just regenerated new IDs * Fixed duplication of color and door group items * Removed unnecessary unit test option * Fix The Seeker being achievable without entrance door * Fix The Observant being achievable without locked panels * Added some more panel doors * Added Progressive Suits Area * Lingo: Fix Basement access with THE MASTER * Added indirect conditions for MASTER-blocked entrances * Fixed Incomparable achievement access * Fix STAIRS panel logic * Fix merge error with good items * Is this clearer? * DREAD and TURN LEARN * Allow a weird edge case for reduced locations Panels mode door shuffle + grouped doors + color shuffle + pilgrimage enabled is exactly the right number of items for reduced locations. Removing color shuffle also allows for disabling pilgrimage, adding sunwarp locking, or both, with a couple of locations left over. * Prevent small sphere one on panels mode * Added shuffle_doors aliases for old options * Fixed a unit test * Updated datafile * Tweaked requirements for reduced locations * Added player name to OptionError messages * Update generated.dat --- worlds/lingo/__init__.py | 3 +- worlds/lingo/data/LL1.yaml | 694 +++++++++++++++++++++-- worlds/lingo/data/generated.dat | Bin 136563 -> 148903 bytes worlds/lingo/data/ids.yaml | 142 +++++ worlds/lingo/datatypes.py | 11 + worlds/lingo/items.py | 17 +- worlds/lingo/options.py | 29 +- worlds/lingo/player_logic.py | 121 +++- worlds/lingo/rules.py | 10 +- worlds/lingo/static_logic.py | 32 +- worlds/lingo/test/TestDoors.py | 56 +- worlds/lingo/test/TestOptions.py | 17 +- worlds/lingo/test/TestOrangeTower.py | 2 +- worlds/lingo/test/TestPanelsanity.py | 2 +- worlds/lingo/test/TestPilgrimage.py | 8 +- worlds/lingo/test/TestProgressive.py | 7 +- worlds/lingo/test/TestSunwarps.py | 21 +- worlds/lingo/utils/assign_ids.rb | 40 ++ worlds/lingo/utils/pickle_static_data.py | 124 +++- worlds/lingo/utils/validate_config.rb | 88 ++- 20 files changed, 1274 insertions(+), 150 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index a1b8b7c1d439..9853be73fa9b 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -170,7 +170,8 @@ def fill_slot_data(self): slot_options = [ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks", - "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps" + "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps", + "group_doors" ] slot_data = { diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 3035446ef793..1c9f4e551df1 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -1,6 +1,13 @@ --- # This file is an associative array where the keys are region names. Rooms - # have four properties: entrances, panels, doors, and paintings. + # have a number of properties: + # - entrances + # - panels + # - doors + # - panel_doors + # - paintings + # - progression + # - sunwarps # # entrances is an array of regions from which this room can be accessed. The # key of each entry is the room that can access this one. The value is a list @@ -13,7 +20,7 @@ # room that the door is in. The room name may be omitted if the door is # located in the current room. # - # panels is an array of panels in the room. The key of the array is an + # panels is a named array of panels in the room. The key of the array is an # arbitrary name for the panel. Panels can have the following fields: # - id: The internal ID of the panel in the LINGO map # - required_room: In addition to having access to this room, the player must @@ -45,7 +52,7 @@ # - hunt: If True, the tracker will show this panel even when it is # not a check. Used for hunts like the Number Hunt. # - # doors is an array of doors associated with this room. When door + # doors is a named array of doors associated with this room. When door # randomization is enabled, each of these is an item. The key is a name that # will be displayed as part of the item's name. Doors can have the following # fields: @@ -78,6 +85,18 @@ # - event: Denotes that the door is event only. This is similar to # setting both skip_location and skip_item. # + # panel_doors is a named array of "panel doors" associated with this room. + # When panel door shuffle is enabled, each of these becomes an item, and those + # items block access to the listed panels. The key is a name for internal + # reference only. Panel doors can have the following fields: + # - panels: Required. This is the set of panels that are blocked by this + # panel door. + # - item_name: Overrides the name of the item generated for this panel + # door. If not specified, the item name will be generated from + # the room name and the name(s) of the panel(s). + # - panel_group: When region grouping is enabled, all panel doors with the + # same group will be covered by a single item. + # # paintings is an array of paintings in the room. This is used for painting # shuffling. # - id: The internal painting ID from the LINGO map. @@ -105,6 +124,14 @@ # fine in door shuffle mode. # - move: Denotes that the painting is able to move. # + # progression is a named array of items that define an ordered set of items. + # progression items do not have any true connection to the rooms that they + # are defined in, but it is best to place them in a thematically appropriate + # room. The key for a progression entry is the name of the item that will be + # created. A progression entry is a dictionary with one or both of a "doors" + # key and a "panel_doors" key. These fields should be lists of doors or + # panel doors that will be contained in this progressive item. + # # sunwarps is an array of sunwarps in the room. This is used for sunwarp # shuffling. # - dots: The number of dots on this sunwarp. @@ -193,6 +220,10 @@ panel: RACECAR (Black) - room: The Tenacious panel: SOLOS (Black) + panel_doors: + HIDDEN: + panels: + - HIDDEN paintings: - id: arrows_painting exit_only: True @@ -303,6 +334,10 @@ panel: SOLOS (Black) - room: Hub Room panel: RAT + panel_doors: + OPEN: + panels: + - OPEN paintings: - id: owl_painting orientation: north @@ -317,7 +352,13 @@ panels: Achievement: id: Countdown Panels/Panel_seeker_seeker - required_room: Hidden Room + # The Seeker uniquely has the property that 1) it can be entered (through the Pilgrim Room) without opening the + # front door in panels mode door shuffle, and 2) the front door panel is part of the CDP. This necessitates this + # required_panel clause, because the entrance panel needs to be solvable for the achievement even if an + # alternate entrance to the room is used. + required_panel: + room: Hidden Room + panel: OPEN tag: forbid check: True achievement: The Seeker @@ -537,6 +578,23 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + ORDER: + panels: + - ORDER + SLAUGHTER: + panel_group: Tenacious Entrance Panels + panels: + - SLAUGHTER + TRACE: + panels: + - TRACE + RAT: + panels: + - RAT + OPEN: + panels: + - OPEN paintings: - id: maze_painting orientation: west @@ -608,12 +666,13 @@ item_name: "6 Sunwarp" progression: Progressive Pilgrimage: - - 1 Sunwarp - - 2 Sunwarp - - 3 Sunwarp - - 4 Sunwarp - - 5 Sunwarp - - 6 Sunwarp + doors: + - 1 Sunwarp + - 2 Sunwarp + - 3 Sunwarp + - 4 Sunwarp + - 5 Sunwarp + - 6 Sunwarp Pilgrim Antechamber: # The entrances to this room are special. When pilgrimage is enabled, we use a special access rule to determine # whether a pilgrimage can succeed. When pilgrimage is disabled, the sun painting will be added to the pool. @@ -881,6 +940,24 @@ panel: READS + RUST - room: Ending Area panel: THE END + panel_doors: + DECAY: + panel_group: Tenacious Entrance Panels + panels: + - DECAY + NOPE: + panels: + - NOPE + WE ROT: + panels: + - WE ROT + WORDS SWORD: + panels: + - WORDS + - SWORD + BEND HI: + panels: + - BEND HI paintings: - id: eye_painting disable: True @@ -895,6 +972,14 @@ direction: exit entrance_indicator_pos: [ -17, 2.5, -41.01 ] orientation: north + progression: + Progressive Suits Area: + panel_doors: + - WORDS SWORD + - room: Lost Area + panel_door: LOST + - room: Amen Name Area + panel_door: AMEN NAME Lost Area: entrances: Outside The Agreeable: @@ -920,6 +1005,11 @@ panels: - LOST (1) - LOST (2) + panel_doors: + LOST: + panels: + - LOST (1) + - LOST (2) Amen Name Area: entrances: Crossroads: @@ -953,6 +1043,11 @@ panels: - AMEN - NAME + panel_doors: + AMEN NAME: + panels: + - AMEN + - NAME Suits Area: entrances: Amen Name Area: @@ -1056,6 +1151,13 @@ - LEVEL (White) - RACECAR (White) - SOLOS (White) + panel_doors: + Black Palindromes: + item_name: The Tenacious - Black Palindromes (Panels) + panels: + - LEVEL (Black) + - RACECAR (Black) + - SOLOS (Black) Near Far Area: entrances: Hub Room: True @@ -1081,6 +1183,21 @@ panels: - NEAR - FAR + panel_doors: + NEAR FAR: + item_name: Symmetry Room - NEAR, FAR (Panels) + panel_group: Symmetry Room Panels + panels: + - NEAR + - FAR + progression: + Progressive Symmetry Room: + panel_doors: + - NEAR FAR + - room: Warts Straw Area + panel_door: WARTS STRAW + - room: Leaf Feel Area + panel_door: LEAF FEEL Warts Straw Area: entrances: Near Far Area: @@ -1108,6 +1225,13 @@ panels: - WARTS - STRAW + panel_doors: + WARTS STRAW: + item_name: Symmetry Room - WARTS, STRAW (Panels) + panel_group: Symmetry Room Panels + panels: + - WARTS + - STRAW Leaf Feel Area: entrances: Warts Straw Area: @@ -1135,6 +1259,13 @@ panels: - LEAF - FEEL + panel_doors: + LEAF FEEL: + item_name: Symmetry Room - LEAF, FEEL (Panels) + panel_group: Symmetry Room Panels + panels: + - LEAF + - FEEL Outside The Agreeable: entrances: Crossroads: @@ -1243,6 +1374,20 @@ panels: - room: Color Hunt panel: PURPLE + panel_doors: + MASSACRED: + panel_group: Tenacious Entrance Panels + panels: + - MASSACRED + BLACK: + panels: + - BLACK + CLOSE: + panels: + - CLOSE + RIGHT: + panels: + - RIGHT paintings: - id: eyes_yellow_painting orientation: east @@ -1294,6 +1439,14 @@ - WINTER - DIAMONDS - FIRE + panel_doors: + Lookout: + item_name: Compass Room Panels + panels: + - NORTH + - WINTER + - DIAMONDS + - FIRE paintings: - id: pencil_painting7 orientation: north @@ -1510,6 +1663,10 @@ - HIDE (3) - room: Outside The Agreeable panel: HIDE + panel_doors: + DOWN: + panels: + - DOWN The Perceptive: entrances: Starting Room: @@ -1531,6 +1688,10 @@ check: True exclude_reduce: True tag: botwhite + panel_doors: + GAZE: + panels: + - GAZE paintings: - id: garden_painting_tower orientation: north @@ -1572,9 +1733,10 @@ - EAT progression: Progressive Fearless: - - Second Floor - - room: The Fearless (Second Floor) - door: Third Floor + doors: + - Second Floor + - room: The Fearless (Second Floor) + door: Third Floor The Fearless (Second Floor): entrances: The Fearless (First Floor): @@ -1669,6 +1831,10 @@ tag: forbid required_door: door: Stairs + required_panel: + - panel: FOUR (1) + - panel: FOUR (2) + - panel: SIX achievement: The Observant FOUR (1): id: Look Room/Panel_four_back @@ -1782,6 +1948,16 @@ door_group: Observant Doors panels: - SIX + panel_doors: + BACKSIDE: + item_name: The Observant - Backside Entrance Panels + panel_group: Backside Entrance Panels + panels: + - FOUR (1) + - FOUR (2) + STAIRS: + panels: + - SIX The Incomparable: entrances: The Observant: @@ -1798,9 +1974,12 @@ check: True tag: forbid required_room: - - Elements Area - - Courtyard - Eight Room + required_panel: + - room: Courtyard + panel: I + - room: Elements Area + panel: A achievement: The Incomparable A (One): id: Strand Room/Panel_blank_a @@ -1865,6 +2044,15 @@ panel: I - room: Elements Area panel: A + panel_doors: + Giant Sevens: + item_name: Giant Seven Panels + panels: + - I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A paintings: - id: crown_painting orientation: east @@ -1972,14 +2160,31 @@ panel: DRAWL + RUNS - room: Owl Hallway panel: READS + RUST + panel_doors: + Access: + item_name: Orange Tower Panels + panels: + - room: Orange Tower First Floor + panel: DADS + ALE + - room: Outside The Undeterred + panel: ART + ART + - room: Orange Tower Third Floor + panel: DEER + WREN + - room: Orange Tower Fourth Floor + panel: LEARNS + UNSEW + - room: Orange Tower Fifth Floor + panel: DRAWL + RUNS + - room: Owl Hallway + panel: READS + RUST progression: Progressive Orange Tower: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor - - Sixth Floor - - Seventh Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor + - Sixth Floor + - Seventh Floor Orange Tower First Floor: entrances: Hub Room: @@ -2022,6 +2227,10 @@ - SALT - room: Directional Gallery panel: PEPPER + panel_doors: + SECRET: + panels: + - SECRET sunwarps: - dots: 4 direction: enter @@ -2174,6 +2383,10 @@ id: Shuffle Room Area Doors/Door_hotcrust_shortcuts panels: - HOT CRUSTS + panel_doors: + HOT CRUSTS: + panels: + - HOT CRUSTS sunwarps: - dots: 5 direction: enter @@ -2288,6 +2501,12 @@ panels: - SIZE (Small) - SIZE (Big) + panel_doors: + SIZE: + item_name: Orange Tower Fifth Floor - SIZE Panels + panels: + - SIZE (Small) + - SIZE (Big) paintings: - id: hi_solved_painting3 orientation: south @@ -2631,6 +2850,15 @@ - SECOND - THIRD - FOURTH + panel_doors: + FIRST SECOND THIRD FOURTH: + item_name: Courtyard - Ordinal Panels + panel_group: Backside Entrance Panels + panels: + - FIRST + - SECOND + - THIRD + - FOURTH The Colorful (White): entrances: Courtyard: True @@ -2648,6 +2876,12 @@ location_name: The Colorful - White panels: - BEGIN + panel_doors: + BEGIN: + item_name: The Colorful - BEGIN (Panel) + panel_group: Colorful Panels + panels: + - BEGIN The Colorful (Black): entrances: The Colorful (White): @@ -2668,6 +2902,12 @@ door_group: Colorful Doors panels: - FOUND + panel_doors: + FOUND: + item_name: The Colorful - FOUND (Panel) + panel_group: Colorful Panels + panels: + - FOUND The Colorful (Red): entrances: The Colorful (Black): @@ -2688,6 +2928,12 @@ door_group: Colorful Doors panels: - LOAF + panel_doors: + LOAF: + item_name: The Colorful - LOAF (Panel) + panel_group: Colorful Panels + panels: + - LOAF The Colorful (Yellow): entrances: The Colorful (Red): @@ -2708,6 +2954,12 @@ door_group: Colorful Doors panels: - CREAM + panel_doors: + CREAM: + item_name: The Colorful - CREAM (Panel) + panel_group: Colorful Panels + panels: + - CREAM The Colorful (Blue): entrances: The Colorful (Yellow): @@ -2728,6 +2980,12 @@ door_group: Colorful Doors panels: - SUN + panel_doors: + SUN: + item_name: The Colorful - SUN (Panel) + panel_group: Colorful Panels + panels: + - SUN The Colorful (Purple): entrances: The Colorful (Blue): @@ -2748,6 +3006,12 @@ door_group: Colorful Doors panels: - SPOON + panel_doors: + SPOON: + item_name: The Colorful - SPOON (Panel) + panel_group: Colorful Panels + panels: + - SPOON The Colorful (Orange): entrances: The Colorful (Purple): @@ -2768,6 +3032,12 @@ door_group: Colorful Doors panels: - LETTERS + panel_doors: + LETTERS: + item_name: The Colorful - LETTERS (Panel) + panel_group: Colorful Panels + panels: + - LETTERS The Colorful (Green): entrances: The Colorful (Orange): @@ -2788,6 +3058,12 @@ door_group: Colorful Doors panels: - WALLS + panel_doors: + WALLS: + item_name: The Colorful - WALLS (Panel) + panel_group: Colorful Panels + panels: + - WALLS The Colorful (Brown): entrances: The Colorful (Green): @@ -2808,6 +3084,12 @@ door_group: Colorful Doors panels: - IRON + panel_doors: + IRON: + item_name: The Colorful - IRON (Panel) + panel_group: Colorful Panels + panels: + - IRON The Colorful (Gray): entrances: The Colorful (Brown): @@ -2828,6 +3110,12 @@ door_group: Colorful Doors panels: - OBSTACLE + panel_doors: + OBSTACLE: + item_name: The Colorful - OBSTACLE (Panel) + panel_group: Colorful Panels + panels: + - OBSTACLE The Colorful: entrances: The Colorful (Gray): @@ -2866,26 +3154,48 @@ orientation: north progression: Progressive Colorful: - - room: The Colorful (White) - door: Progress Door - - room: The Colorful (Black) - door: Progress Door - - room: The Colorful (Red) - door: Progress Door - - room: The Colorful (Yellow) - door: Progress Door - - room: The Colorful (Blue) - door: Progress Door - - room: The Colorful (Purple) - door: Progress Door - - room: The Colorful (Orange) - door: Progress Door - - room: The Colorful (Green) - door: Progress Door - - room: The Colorful (Brown) - door: Progress Door - - room: The Colorful (Gray) - door: Progress Door + doors: + - room: The Colorful (White) + door: Progress Door + - room: The Colorful (Black) + door: Progress Door + - room: The Colorful (Red) + door: Progress Door + - room: The Colorful (Yellow) + door: Progress Door + - room: The Colorful (Blue) + door: Progress Door + - room: The Colorful (Purple) + door: Progress Door + - room: The Colorful (Orange) + door: Progress Door + - room: The Colorful (Green) + door: Progress Door + - room: The Colorful (Brown) + door: Progress Door + - room: The Colorful (Gray) + door: Progress Door + panel_doors: + - room: The Colorful (White) + panel_door: BEGIN + - room: The Colorful (Black) + panel_door: FOUND + - room: The Colorful (Red) + panel_door: LOAF + - room: The Colorful (Yellow) + panel_door: CREAM + - room: The Colorful (Blue) + panel_door: SUN + - room: The Colorful (Purple) + panel_door: SPOON + - room: The Colorful (Orange) + panel_door: LETTERS + - room: The Colorful (Green) + panel_door: WALLS + - room: The Colorful (Brown) + panel_door: IRON + - room: The Colorful (Gray) + panel_door: OBSTACLE Welcome Back Area: entrances: Starting Room: @@ -2958,6 +3268,10 @@ door_group: Hedge Maze Doors panels: - STRAYS + panel_doors: + STRAYS: + panels: + - STRAYS paintings: - id: arrows_painting_8 orientation: south @@ -3155,6 +3469,13 @@ panel: I - room: Elements Area panel: A + panel_doors: + UNCOVER: + panels: + - UNCOVER + OXEN: + panels: + - OXEN paintings: - id: clock_painting_5 orientation: east @@ -3524,6 +3845,13 @@ - RISE (Sunrise) - ZEN - SON + panel_doors: + UNOPEN: + panels: + - UNOPEN + BEGIN: + panels: + - BEGIN paintings: - id: pencil_painting2 orientation: west @@ -3819,6 +4147,34 @@ item_group: Achievement Room Entrances panels: - ZERO + panel_doors: + ZERO: + panels: + - ZERO + PEN: + panels: + - PEN + TWO: + item_name: Two Panels + panels: + - TWO (1) + - TWO (2) + THREE: + item_name: Three Panels + panels: + - THREE (1) + - THREE (2) + - THREE (3) + FOUR: + item_name: Four Panels + panels: + - FOUR + - room: Hub Room + panel: FOUR + - room: Dead End Area + panel: FOUR + - room: The Traveled + panel: FOUR paintings: - id: maze_painting_3 enter_only: True @@ -3994,6 +4350,10 @@ panel: FIVE (1) - room: Directional Gallery panel: FIVE (2) + First Six: + event: True + panels: + - SIX Sevens: id: - Count Up Room Area Doors/Door_seven_hider @@ -4102,12 +4462,109 @@ panel: NINE - room: Elements Area panel: NINE + panel_doors: + FIVE: + item_name: Five Panels + panels: + - FIVE + - room: Outside The Agreeable + panel: FIVE (1) + - room: Outside The Agreeable + panel: FIVE (2) + - room: Directional Gallery + panel: FIVE (1) + - room: Directional Gallery + panel: FIVE (2) + SIX: + item_name: Six Panels + panels: + - SIX + - room: Outside The Bold + panel: SIX + - room: Directional Gallery + panel: SIX (1) + - room: Directional Gallery + panel: SIX (2) + - room: The Bearer (East) + panel: SIX + - room: The Bearer (South) + panel: SIX + SEVEN: + item_name: Seven Panels + panels: + - SEVEN + - room: Directional Gallery + panel: SEVEN + - room: Knight Night Exit + panel: SEVEN (1) + - room: Knight Night Exit + panel: SEVEN (2) + - room: Knight Night Exit + panel: SEVEN (3) + - room: Outside The Initiated + panel: SEVEN (1) + - room: Outside The Initiated + panel: SEVEN (2) + EIGHT: + item_name: Eight Panels + panels: + - EIGHT + - room: Directional Gallery + panel: EIGHT + - room: The Eyes They See + panel: EIGHT + - room: Dead End Area + panel: EIGHT + - room: Crossroads + panel: EIGHT + - room: Hot Crusts Area + panel: EIGHT + - room: Art Gallery + panel: EIGHT + - room: Outside The Initiated + panel: EIGHT + NINE: + item_name: Nine Panels + panels: + - NINE + - room: Directional Gallery + panel: NINE + - room: Amen Name Area + panel: NINE + - room: Yellow Backside Area + panel: NINE + - room: Outside The Initiated + panel: NINE + - room: Outside The Bold + panel: NINE + - room: Rhyme Room (Cross) + panel: NINE + - room: Orange Tower Fifth Floor + panel: NINE + - room: Elements Area + panel: NINE paintings: - id: smile_painting_5 enter_only: True orientation: east required_door: door: Eights + progression: + Progressive Number Hunt: + panel_doors: + - room: Outside The Undeterred + panel_door: TWO + - room: Outside The Undeterred + panel_door: THREE + - room: Outside The Undeterred + panel_door: FOUR + - FIVE + - SIX + - SEVEN + - EIGHT + - NINE + - room: Outside The Undeterred + panel_door: ZERO Directional Gallery: entrances: Outside The Agreeable: @@ -4195,7 +4652,7 @@ tag: midorange required_door: room: Number Hunt - door: Sixes + door: First Six PARANOID: id: Backside Room/Panel_paranoid_paranoid tag: midwhite @@ -4203,7 +4660,7 @@ exclude_reduce: True required_door: room: Number Hunt - door: Sixes + door: First Six YELLOW: id: Color Arrow Room/Panel_yellow_afar tag: midwhite @@ -4266,6 +4723,11 @@ panels: - room: Color Hunt panel: YELLOW + panel_doors: + TURN LEARN: + panels: + - TURN + - LEARN paintings: - id: smile_painting_7 orientation: south @@ -4277,7 +4739,7 @@ move: True required_door: room: Number Hunt - door: Sixes + door: First Six - id: boxes_painting orientation: south - id: cherry_painting @@ -4344,6 +4806,34 @@ id: Rock Room Doors/Door_hint panels: - EXIT + panel_doors: + EXIT: + panels: + - EXIT + RED: + panel_group: Color Hunt Panels + panels: + - RED + BLUE: + panel_group: Color Hunt Panels + panels: + - BLUE + YELLOW: + panel_group: Color Hunt Panels + panels: + - YELLOW + ORANGE: + panel_group: Color Hunt Panels + panels: + - ORANGE + PURPLE: + panel_group: Color Hunt Panels + panels: + - PURPLE + GREEN: + panel_group: Color Hunt Panels + panels: + - GREEN paintings: - id: arrows_painting_7 orientation: east @@ -4481,6 +4971,14 @@ event: True panels: - HEART + panel_doors: + FARTHER: + panel_group: Backside Entrance Panels + panels: + - FARTHER + MIDDLE: + panels: + - MIDDLE The Bearer (East): entrances: Cross Tower (East): True @@ -5333,6 +5831,11 @@ item_name: Knight Night Room - Exit panels: - TRUSTED + panel_doors: + TRUSTED: + item_name: Knight Night Room - TRUSTED (Panel) + panels: + - TRUSTED Knight Night Exit: entrances: Knight Night (Outer Ring): @@ -6017,6 +6520,10 @@ item_group: Achievement Room Entrances panels: - SHRINK + panel_doors: + SHRINK: + panels: + - SHRINK The Wondrous (Doorknob): entrances: Outside The Wondrous: @@ -6228,18 +6735,36 @@ - KEEP - BAILEY - TOWER + panel_doors: + CASTLE: + item_name: Hallway Room - First Room Panels + panel_group: Hallway Room Panels + panels: + - WALL + - KEEP + - BAILEY + - TOWER paintings: - id: panda_painting orientation: south progression: Progressive Hallway Room: - - Exit - - room: Hallway Room (2) - door: Exit - - room: Hallway Room (3) - door: Exit - - room: Hallway Room (4) - door: Exit + doors: + - Exit + - room: Hallway Room (2) + door: Exit + - room: Hallway Room (3) + door: Exit + - room: Hallway Room (4) + door: Exit + panel_doors: + - CASTLE + - room: Hallway Room (2) + panel_door: COUNTERCLOCKWISE + - room: Hallway Room (3) + panel_door: TRANSFORMATION + - room: Hallway Room (4) + panel_door: WHEELBARROW Hallway Room (2): entrances: Hallway Room (1): @@ -6278,6 +6803,15 @@ - CLOCK - ER - COUNT + panel_doors: + COUNTERCLOCKWISE: + item_name: Hallway Room - Second Room Panels + panel_group: Hallway Room Panels + panels: + - WISE + - CLOCK + - ER + - COUNT Hallway Room (3): entrances: Hallway Room (2): @@ -6316,6 +6850,15 @@ - FORM - A - SHUN + panel_doors: + TRANSFORMATION: + item_name: Hallway Room - Third Room Panels + panel_group: Hallway Room Panels + panels: + - TRANCE + - FORM + - A + - SHUN Hallway Room (4): entrances: Hallway Room (3): @@ -6338,6 +6881,12 @@ panels: - WHEEL include_reduce: True + panel_doors: + WHEELBARROW: + item_name: Hallway Room - WHEEL + panel_group: Hallway Room Panels + panels: + - WHEEL Elements Area: entrances: Roof: True @@ -6412,6 +6961,10 @@ panels: - room: The Wanderer panel: Achievement + panel_doors: + WANDERLUST: + panels: + - WANDERLUST The Wanderer: entrances: Outside The Wanderer: @@ -6553,6 +7106,10 @@ item_group: Achievement Room Entrances panels: - ORDER + panel_doors: + ORDER: + panels: + - ORDER paintings: - id: smile_painting_3 orientation: west @@ -6566,10 +7123,11 @@ orientation: south progression: Progressive Art Gallery: - - Second Floor - - Third Floor - - Fourth Floor - - Fifth Floor + doors: + - Second Floor + - Third Floor + - Fourth Floor + - Fifth Floor Art Gallery (Second Floor): entrances: Art Gallery: @@ -7281,8 +7839,8 @@ id: Panel Room/Panel_broomed_bedroom colors: yellow tag: midyellow - required_door: - door: Excavation + required_panel: + panel: WALL (1) LAYS: id: Panel Room/Panel_lays_maze colors: purple @@ -7309,13 +7867,24 @@ Excavation: event: True panels: - - WALL (1) + - STAIRS Cellar Exit: id: - Tower Room Area Doors/Door_panel_basement - Tower Room Area Doors/Door_panel_basement2 panels: - BASE + panel_doors: + STAIRS: + panel_group: Room Room Panels + panels: + - STAIRS + Colors: + panel_group: Room Room Panels + panels: + - BROOMED + - LAYS + - BASE Cellar: entrances: Room Room: @@ -7354,6 +7923,11 @@ panels: - KITTEN - CAT + panel_doors: + KITTEN CAT: + panels: + - KITTEN + - CAT paintings: - id: arrows_painting_2 orientation: east @@ -7608,6 +8182,10 @@ item_group: Achievement Room Entrances panels: - OPEN + panel_doors: + OPEN: + panels: + - OPEN The Scientific: entrances: Outside The Scientific: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 4a751b25ec5f143b6b055fd2043a6543754ef5b1..d221b8168d9164576b3f1c5b2fc57c604da5f100 100644 GIT binary patch delta 33771 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 WLAMjq6B6>Y{A>5)@ndWwlKvm@gojE1 From ab0903679c6de5407de01a4ba139b495be013ba5 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Jul 2024 11:57:10 -0700 Subject: [PATCH 78/78] Factorio: Fix ap-get-technology nil value crashes (#3517) --- worlds/factorio/data/mod_template/control.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 8ce0b45a5f67..ace231e12b4b 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})