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 001/222] 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 ecab25db3d..455c87d8e0 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -11,11 +11,12 @@ from Options import OptionError, PerGameCommonOptions, Toggle 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 @@ class WitnessWorld(World): ) 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 @@ class WitnessWorld(World): 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 @@ class WitnessWorld(World): 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 8eb889f820..b0d8fc3c4f 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 .item_definition_classes import DoorItemDefinition, ItemCategory, ItemData 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 e11544235f..de321d20c0 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 @@ OBELISK_SIDES = { "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 @@ for key, item in ALL_LOCATIONS_TO_IDS.items(): 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 ecd95ea6c0..a9175c0c30 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 @@ from .utils import ( 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 @@ class StaticWitnessLogicObj: 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 @@ class StaticWitnessLogicObj: 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 @@ class StaticWitnessLogicObj: 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 2934308df3..f89aaf7d3e 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 @@ from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Set, Tuple 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 535a36e13b..a1ca1b081d 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 hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Ite 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 df8214ac92..1796f051b8 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -61,9 +61,7 @@ class WitnessPlayerLocations: 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 @@ class WitnessPlayerLocations: 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 627e5acccb..718fd7d172 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 @@ class WitnessPlayerItems: 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 @@ class WitnessPlayerItems: 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 @@ class WitnessPlayerItems: """ 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 05b3cf3a98..b62c59b00a 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast 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 @@ if TYPE_CHECKING: 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 @@ class WitnessPlayerLogic: # 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 @@ class WitnessPlayerLogic: 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 + or entity_hex == "0x1C349" + ) - else: - return frozenset(all_options) - - 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 @@ class WitnessPlayerLogic: 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 @@ class WitnessPlayerLogic: # 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 @@ class WitnessPlayerLogic: 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 @@ class WitnessPlayerLogic: # 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 @@ class WitnessPlayerLogic: # 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 @@ class WitnessPlayerLogic: 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 @@ class WitnessPlayerLogic: 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 35f4e95442..2528c8abe2 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -9,9 +9,11 @@ from BaseClasses import Entrance, Region 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 @@ if TYPE_CHECKING: 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 @@ class WitnessPlayerRegions: 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 @@ class WitnessPlayerRegions: """ 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 @@ class WitnessPlayerRegions: 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 d42361a4aa..a35711cce6 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 b4982d1830..12a9a1ed4b 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 002/222] 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 b62c59b00a..e8d11f43f5 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -197,7 +197,7 @@ class WitnessPlayerLogic: # 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 003/222] 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 205cc9ad6b..cf61d93ca4 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -145,10 +145,10 @@ class ZillionWorld(World): 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 d75dd1a1c2..5de0b65c82 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 ae7d9b1733..b4f554902f 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 004/222] Core: Fix !remaining (#3611) --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index dc5e3d21ac..f59855fca6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1352,7 +1352,7 @@ class ClientMessageProcessor(CommonCommandProcessor): 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 @@ class ClientMessageProcessor(CommonCommandProcessor): 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 005/222] 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 6e65459851..22622cd0e9 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 006/222] 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 d9348628ce..2652a5b148 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 97270b5a2a..b9dbc1e226 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 007/222] 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 a77fb920f6..3a37330445 100644 --- a/worlds/sc2/ItemGroups.py +++ b/worlds/sc2/ItemGroups.py @@ -51,7 +51,7 @@ for item, data in Items.get_full_item_list().items(): 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 008/222] 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 33d165db41..2eb9d1e9a2 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 4c795d3508..8c0142debf 100644 --- a/worlds/aquaria/Options.py +++ b/worlds/aquaria/Options.py @@ -5,7 +5,7 @@ Description: Manage options in the Aquaria game multiworld randomizer """ 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 2812025925..93c02d4e67 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -300,7 +300,7 @@ class AquariaRegions: 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 @@ class AquariaRegions: 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 @@ class AquariaRegions: 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 @@ class AquariaRegions: 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 @@ class AquariaRegions: 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 ce46aeea75..1fb04036d8 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -204,7 +204,8 @@ class AquariaWorld(World): 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 5c63c9bb29..029db691b6 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -60,7 +60,7 @@ after_home_water_locations = [ "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 @@ after_home_water_locations = [ "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 @@ after_home_water_locations = [ "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 @@ after_home_water_locations = [ "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 bf0ace478e..82d8e89a00 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -39,8 +39,8 @@ class EnergyFormAccessTest(AquariaTestBase): "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 85fc2bd45a..f615fb10c6 100644 --- a/worlds/aquaria/test/test_li_song_access.py +++ b/worlds/aquaria/test/test_li_song_access.py @@ -24,7 +24,7 @@ class LiAccessTest(AquariaTestBase): "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 71432539d0..1d3b8f4150 100644 --- a/worlds/aquaria/test/test_nature_form_access.py +++ b/worlds/aquaria/test/test_nature_form_access.py @@ -38,7 +38,7 @@ class NatureFormAccessTest(AquariaTestBase): "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 40cb533648..f015b26de1 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 026a175206..a1493c5d0f 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 40a087a7fb..3bcbd7d72e 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -16,7 +16,7 @@ class SpiritFormAccessTest(AquariaTestBase): "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 009/222] 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 | 192 ++++++++-------- 4 files changed, 316 insertions(+), 337 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 92834d96b0..f63193e6ae 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -200,7 +200,7 @@ class TunicWorld(World): # 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 @@ class TunicWorld(World): 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 @@ class TunicWorld(World): 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 @@ class TunicWorld(World): 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 2652a5b148..81e9d48b4a 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -2,7 +2,6 @@ from typing import Dict, Set, List, Tuple, TYPE_CHECKING 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 @@ blue_hexagon = "Blue Questagon" 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 set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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 set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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 set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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 set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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 set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re 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 9d25137ba4..8689a51b76 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 b9dbc1e226..73eb811890 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): +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, world: "TunicWorld") -> bool: + if world.options.maskless: return True else: - return False + return state.has(mask, world.player) -def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.maskless: +def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: + if world.options.lanternless: return True else: - return state.has(mask, player) + return state.has(lantern, world.player) -def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool: - if options.lanternless: - return True - else: - return state.has(lantern, 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 010/222] 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 zcmaI7WmH_z(kiW;p*LY z3;>4L-~W%@JpXw)5a@Yo?}XNK{EoBbWb+KQzL69pyjQniOvOQ#YS zb64JJtJ73gWuv5Oo|BO)LIf%jwU-}xEs()WaN^(qx#b7>>dNZ#03MY@Imxma<~zBl z=<`HvRk6x4OXrFl7wP5angY#K7=)!%hD;o|{MDH;Dk>@@3f~ftT@i+(SgMlaP(e)+ zP6xKP2Q3r<03Z+mh6MnS{pYIyy4r8J6;awNJYo@!fvW!!_Amfk(SIrk;Q#plm!U!t za~$}|<%%YgkU_9;SO{EB4wks|@&sl<66{|$GQhvCFaVjjTnGT`-`mKE8VFzz7V7_Q z@vrz_Lk{`h!T*3l3L>t$N`-Y+6c~oA5(>crz{p6kkaMs=0MLK-fuKr;u<#G<|LyMI zLJmO=JEMb-Fi*2ov61Z8ZUHWO&t7Ry_<@q@fgFX?8un*n`QlU5PD39xW$Ikja_a-z z`Eptr@+xHeei7m_aBtd#RV9n#GD%~m8brCbV0st|nOoOz3>HZvu>c~3R8G`iw2npn zHbce{_P81+dHz&|KL%v2WZdi?3eNch#|Y~0E& zRe)QVh5##>=(u8%_NN2s*NicQaRJ6sc5j^eVJNQMV@Mq6xpX*1p975XYS6xOd`<#XsRx|g_q!NjvPte-csgeuo8m9 zeI3$8_&4h0gj3^)$}2Z0r&xB6(?zn(r=lUMY-Nrd%?#s;e{xwqwB|#Gy;b6n_B)5yb{Sb*pbwUBwb+ls4wP@_I+EXvk{ka1rrjMhc&5$7Jve z&5o~ulG_RW48E*BgkSLo0jVWj5`&xdSwIgm|KJK%DG0J&h>n>z2*mEw2vy1b*l2e0 z!?K>-)-R+JMq{%cmtTP~EtQJ5T@%6fG>wAf^0B9)7qwEMa6*t;3hp%`Y-1G@H)PC* zJAx;H!0~48(2bWjBdzKy91RUMpD`RF0WCb4g>u6ZEUa6eZrbX&X`%`DL{UGA*pS{t zN61dns@$VOgY{39n<-@ZN_5-~b0tQk_Cd0xqco#Gr=JLC5H`*qF@tOR!dv z)C!OhnLE}+s~n0XX1JJXWFGAlxo9aMcUYme;N|9)wlnbU{1$402jl=D7Gb=CvjcaT zmbWMw1D9U6Z7dqvW9L8Hnp!v{Y*qj;C$FLRrZW_P40@68W zWJD%sL+jhM`E}XTU?yN0Kp%l5{HM}s_S=gKQ(b^qhn2zIBbL5v;G6_TmxbW;IYvg) zt{Jv7;HC)8VZsuzu}24ufid1y&N+YAamBztgT#ULyJc1g`y=@h*U?|2s~)(c zZ&P3j$yx!|rU+>H{MHa1GHNL6Io!i@%J6#Nk8{z?-}Kd1kc=>JU{2!CmzA#i$n2isNIwFvMtW_9Wy1GorNI< zDA!_d%Av@mB5Oe-p3=7@U8DpZ4KZp1GwqsD*yTS_yfJ+?iQHfdx2{ zLau=Lp*0HmMOLjM-5z2Ii3X0<>M4fsv~{&cAAa5`d93=q7lg6q;AjM`Ft#g^lF8}7 z>u;sD#Q{0HzD|AE5WCYb)mNf6uTw%L^iH&7%JE)|It8r>UV+hl!_Iv=MHq69IGvHC zi!_l$DE%WSno_h`w#X6Ju~MQwK$?e|=#Q4^3b zd}n7D!3W@$Y3+d85t#;liX#P2#%cx}w7G>>OiHNC(oSz#oAGebwdF4J@qyHmFHrY- zHi$V+MrWC4^k!nl_`_c@Q|ox2$b|2IyBGCJ^=rgVBL)v1vXhmnW5E3COz8n%+Ho4E z(I&Xf5BlCPU4x`r5P>QSd=>Odd5b+LixD% z<-DUt`C?hGGQY(p*5|;KPZhW1^oPOss#XZp!MDPWtYIcc_zwlqcW)mjPKx#A%}?94 z@U$4;#OiPE2+A>U4ZT!nc%g}fD6DvuY) z{5{&+NU&&3B1=*n;F$epUh6K16~3@0q(QZ4I&wDZRlfGmvQ~TEc3S5}NB1{+0zlxQ zHe`@esPs7WLj;jAUlM&lU;ZfjR ziJ2=qkfV-Yfb&>BAstmdGg1vZE)zCIUNh8P0shfPxv0H z&55;*A>OZiuFBJ{Ee5Xc!gfNE*Y^w0F&rE(V%p8IDK!Ds^hBue6tTQfw>8brfLBUZVKp6>8PfeFFwswVQISX=a=wn_| zoJy1QiCcMH2h#-(ycm@WPqK~$71(lC4YLBSr#b+}l9r`tcu8`>VbrdO2T6=WBP<|X zW@l9_7ks50nry{}XVNN1tMbmS(a+kUPX4ipk;LvFmvF89Ii}cbm}#aP;#;q&#vZ02 zn39@K&Fg7{&lm;oOK!Bok?p)zzOf{-n5rpo2$|GHiYkLQ4ji^m%2(eul=$akx?N`t zxqc-yZfqX%d&`l-sCndc&|ca?2%pVeh2D%2PZ^AkbsTQPNu**kC{4Xy#bkKn`i^K+ z@RO4zxoPo)y_fAZkeU6Fd5ka}C7Unr?`z6J;eVD8Q2p^f(E2_>k+&D9L(7@W($ar* z3#=v6YYn8tQLKAv^_pqXD=+*VVNpMKWYo|Jd2YjK*T= ziMajA0d5PJfqXPZTh_%WggclqgPs1W98AC)l#PIj_kZPxoBM`*D0Yz+Z(iy8e zeLvQVfCo?tY)$Cig2SdV*Zo6GH&b^d{6B?#;H+_C2mruC!%2z$8zKhCn5J#rYS{{^EcMR87V1(kP;>#^B|3(ks z)qa>%pW-!Fp>yB`J;Ya7FNkS7l6@+{w9nOE-egpl35=4K%LYxxKoC^Ga7D;b1sVWk z=)VCOgj^8{QXx|%Wod&a;}<{@s7zHnFH8azR39&ts%tqMRFhD(DNceC9v~vY#vkV&>RCl7f~N1&S6*C5!&`C_<16tq1`C{&mFyV1`Q;{W}Jb zA%~D*C`&xF-;<&;n=H#10KY+p%^(Bg)!H#D2Gb@e+M6t&Rcd`t$_w_EE4r6)8L(^e zbs3`+Hh6^nHXN^?qV~M#R<VBdl0JtKSMk1~5r+VpP*OX!%0ED`mJvKq&kiMKetBrw&LLy#K-))Zd57PW(NKjV ztsm?PhqV`JK{ie+cFfjx>99Dqr~{5olagmnNfu#zg5`%|M=byxUw}0A6Ho-Mrh?tDan=IKg2xyDx zlho+@{h%rC52@$`vKDcEqE27XRo<4R03pFI!UOgWODd89gj{!o7|bDLDu{Bcv~(7@ zvW}7(zK|-6&{)BBI(-6-`9WOtGjxc-kmQIYxGDC11RUJZsD}7DY6uBQk#mw}3nV1^ z6&J7|gVYWwis~nTZ(&ByXwnHb0H6&F>KKpU@+Bc)0m0g`NML`|s;C_TF!FCWT%b!y z7Pk;4llzN=E{m8XQ%n1JtCcJgV2H?ClEPu4F*9T| z0XU?t2xQUM1+x{kcRV0!003jk+dx6U(`tT=+U-&g*dB&O4uDAs;zA1EfJ6*BtCi? zg#$bYL|V)y#k9f?;HKpGGVEyz6AEQf52!arL>YyvT%s}1O+(q%*7Jd_G_evl>;oO<-Wl+)|cq{$I20PB(e$>l5M;p3^FsP3Ci_< zxYH`oNtm}mRc#{;OA$=hB4I<*sGZ5Yz*|ELsD`qg!5>)G)YNo4BxSjVq5=4w8QammgJ&s{u{>uoSuUCQ5I89i0Jc()TT=3G-Iw{n zgF}Y5ykcO;N z;uM5>+WBS^gr&8nVZogy#(#l{PzXnbrACX?LT2r#M5h$mUW-(J0aQV@Ae+(sJOv?% z#FJ4kTRVIQrIr)?tUtbeMKJ?tNlhJmMtnRs9#J6%K(m-5L_|{`%CKU)-`2h+ia;L_ zli1~KySNXO*%L%x0cIVj0F?C&GH|;@KO&8 zNDY4=SCT&7rtbBNufjlnwnLD$Q5GXz%|F=5&X-;4t4$yHvLW8E?Udg2j&yBI+~h1a zWwAnMndO=tbr7t>kT!!F%vhQ1(?WpR-z_+j|i43a+Dz zRegi`F(gPyc^Ba(1Rfs;@s1+F#8viDUozx_a;?yjbx-6IgDov0dxBXz<#;$#qcbIy zx$xYDW?!Y~0!6i2ad8p|C?ysRA16>CdvTWB&*U}WVj~@M*UE>IQ%%_W7XbuOT8fIv_`mFM-mj zZp1R=Xk5q3nn>H4FN+k;D85K&5Z<3G>nyA!VnGDvg`u?y|FQqxf+I&AC{{yVF|}DF zX$yl;hfUC9$h|vRLAEp)cMv0xwi;*~@{;(J^|QxlyX~ECls86+AX;S?e$bD3v175S z9k?$Mwn?rKTi+t1e5B~%Hcu(1K#Hz20*eMZ66}ZK9?_8>a}l0NO~wo9<%Nzy=;dH9k8At-3!bB}Nj4r6XmbB}RVksd*vUn&b2}qC4Ij@kJ|3br zIeLmzUhf_&Kc*mBw-;Jm*rrguN0GN!Q7EFkEU`f0wjDlXG*m zYFmM%R4V*j`FZC^3r-qQ0Fl;?)G-=#c1z1?OtkrOG9d8JbnS5y_MG|HD|(D~FnXjc z>3?(RSD$lNs&Y_#5@MTg2IflLRs&O#Q$)}M?G7_y%j;`NRBJ>T-=v7LGIpZVY4YZZ z4HNNvI2&%?l?vYNoUK_>3VB1TLkm47v-yNErMDM_+vx)y=JwXc_gl{#s=*IxCn3%$~l zRZR%&-m)uCYl7**>B$#uJkWS?Zq5zo4{cl;8klm|#b9ctl*?7oBZ6?z4p+dg?Z4zI0UwzPa-4`#%(4yyalDs}9l3ZerV<$b#R(xTPwUBsX_ z5l0%!;dcW?aj9aePNvEmM|py*Y*PbPoO@0yz4go@EH^zckMZB$e?*$EG=RHdi*R3w^2l+xX*>opNsH$;xJo60dRhlf~zJi$%>JvGo z9XinfIG=hWRsWHfD=BR}5>2zB**K!3P$EwQT{g{Pt@TH)JtC2!4f%;Pp{mt~g_CdX z35$PGc(=q*O(1oOiwJ_)OvdVgPh_q;xu}QgnW|(c14_qw_cnJMpK`(Yq5|a3c1y7W zgoWQ~;9(Aaf)3nY4Tr+p`!+hPxXWkjg$QN?vKm8mdVY&h0-ny|X5@`)6n@>W-{>8X zH4lAuF!@4pFv!)9!z@aji{dVj7eAT#J+zQ0)xQ!atgEp_n{p)K+j#(GqUa@Nf*(4nV^A|$g<0LQj-1Kj=n0`J8 zk}VKd^r<-4`*un?appEP;Jj9{8~S=GT)Ho+VYB{HqK%Rq9?n~kJV}*V-HfQO_Qxm` zu|?6+0xsoF3CwOvp3@bLmZN27-Ur-E;n|C!Rvwane|oW(uU~|WbCccHXWaZg3mIO?LSuW7 zQAk3M$X0f4^8&p7A&Vtt6;jZfu#f)DDEc|P+IAOHGK-kpgoXeKAndwUVGXxL0?37* z>0PjfTTe(~+`g3l7|#TsM@3(_T^}?c9H^E$D_KXC0|$?`q<}Wvd~4NqRB=N*7kow$ z#a=aNK`8#-2tha^o^=$(b>Eu)zIH!J{u7j8YOEC9BV|frWH@(&KfA8_{nP$~&57L4 zwg5U~!>hBi)FN|2K}>O*%@IV@x;F`~roXZCrLz*WouXFJ2DS4#`rD$&nVP0A46xs~p zvbrbU;;th7>RA@iG98S%F|=GX>d-0Aj|Fzfp+`X!%See)gb1P0X#fwhAxoF)cVw{N zuex+TiX1t57V{rL>N}=+9NA|x*zZv|wg+LSN+A8N&1Lt(vdTVp?zX@SV!Al&gkm{7 z5bpZE_6BTpg?Y(vUk36VTE1G18~Mkz(~b9RqJGJSS7A%50lVGYpro-P@kP+lW<=w* z&FAcVFTc>xT_tDnM?sHm%muwRW+&=?F`C~9zl=owF7NKAr)66U)&6D)mN`lsk2JMt z#7^aIbgS}%qo<~;<`XxtRxJ(yz-}IC#~cyH4YfO~e&_0CU&F_IDrJi=zwCuHoV~c6 zEbCwr94hIjDb6IdcPE`1Ft_x$VC^~!2k1f|@V}cEl(3EcwZiZEeSIf~`2~$axvvRN zL0d`qI6kkwl4+r-Vw|~xr`QaS-kHq~6StMI(wcW~HkyJMYj*T&!!Eksk8Zv8_O2!??Va^W&|-VNlg?^;%Rext@E9e~ zlqEPl1+A|%cxW`#rZXVEm)z95R)aF+2Qld~Io-Wf;NS%J$laJT2W2y1 zpM5Anr)wA=ptj3Z%6CTkv&*HB@e8+|k7Tn=R1rh`;8Vf8kiPM5BLl`GrXrg;L5t)B zecYtltlMM&$!p%GBk}Qa+`Nh&Z(r$#&u7c27o#@#A^WTS*Wc@ojJ_8Sc=}bTMqcaf z9P14yUPx|QY5WM4jZa-I)a%4m6i-W5WBf&femlssJ3WlHYd^`CtV0_uOJ4b zgR6Z@1fs&9S>hGJMJBy61By`(LPCa|W z;S-EmPO#X)LDLD=CuEgMthO5T`=NEgQ*i)#AO@HCkbTQagPwLW+GAOkF!*rxbp-Z1 zp=AE@3h0G?t~4tt+NFks=RiuLGZ8gp`-WsB<)N_vc$$!qk`U@zO#&bty2+27#lk~6 z6pQeMH2={X6N`m`Fx_M|M!U8#L!a)#y$r7caOE3K-k=-axZ2)r!Suv?q-fS27Al8n zNJ|&Ov$SMP!TdB6$+B=W!;@)T(y(zByzotsEB7at~@e1;x#9v}hSs{(2{}77* zhhB^~)2syGHvS(P@qhTl-~XbUo11^I_b>Rn|87_O>i&oTp?`Gn0r0xo%9@ts2&8gJ zRX%Io-C7K|*5q#$@fIE1xw?P<>{nZ_E2AE?wfl75h<0)9_`R`LzW=Fvr&nbOe|Pr> zh#wUC@Ae%xRVq-W`X?!pSeBzhDr2@fK9c$b2vm@TR{&BetST&VEUbpITvnfrip!Th z;;8bd&@DeCDxjp@Rwk!892P`X7UfuT=OD<1Ys&!b$sCFylYC$-ROA$YC_gvj@}oeh z;-O_h#JvNbV%w4<7B3tYg4_ZKv?LAx@0y;>-W~w6x8?-|h5+pOd8s#5@!|RT#nSF7 zQUX9w5b{)Z(xbF0Pi0lelC&cZbD-*ZVM(|;-n)t%dz7lN3h+PefHeSN8GwZaLID4} z-wy{%;sfJT;q9#}W7@RGV(m%_a!5hT_fZ9vF;%dKf~19W=TueD1HZVcK#`ly2AEv? zpLPNCAI$>7@;`wA)<2B{=%Gxdx*{hqt6~yI1~6icg;E6@0fFEut5fB2ki%h6GAvTK z3dVxuA~Jgba)^CkIQXB`qX>W?mtze`ro{*U(;rL#Ey=JzSO8UZ?ZpBMvP86s>Sk35 zo% zf?DsTnV81Xp)*Son>55q>`bGTIv`0P)3`6{7K?Xct2 z7?IsLd&*?x6;2&(8dhD-KdsaetQA;}BuTA`I^l@ZA<#@)c6wNH;+Qi`h>UFh6ZO(k zYc!9NzR18uNg*JI;4a11PDVpv1Y04h>8Q!d3r}Y@7Gd3_I7h)Am$g2|Rzb~6pcz%` zSP~9`R>bGUncM3U&qpRA2@D)(NeXfMTteDC{4r5ET=re=^~iAh`K_q?0nFfI6fsq= zY{1wqsB;b6e>o;RwJRA@VK_q z(kM!1_Xq((=dhF+`&5PM{e;mehOS1ccWqp4<{cTrp9bJ6uwL!oSiD@>V|nfKFRR{c z)zb%4h7W)8qz-U8%w#bBR{DNQ=vrw_zAU~(KMstIC_ z9a^$HBL_#Eq7~ux%3QNM#~%}2m=BA8??vFdxi`G1w9<}XkR$)c0du=zI5}{{`JSSJ zs_jVd=#^>o?N`51pX$t#962B;Dasu4P~Vpyp5Z!s(ki~%OeF74C8mvVg!YHdP96Q; zgYSP3t=IGG4um!a#Gi5V^U!x4UA^F?XiPSx5|fs}{pKVCmn}*;0}^xvgVcXAkCe?W z$z*fHD9A+z61cXi7VHP1F1t)DqX>35BGu8a7t`hx=Cc~;2*pCJxELS(AETtZ7Noy7 zWxL{HHmsC5T9K@evpr8FpV_W9L;@%JGr80DLa52!>k<7jp$`6wsDl{GJ*jW#n=@!2 z)V7y__(oBm;cvTAGUEjEtnL+-cDBFlqL{;{1~W?83+ zR7gel+0|DEX7(5ys1j{X*0-*LbT+fJ_*R}l8M3U~5FNKXA!8+X>}j(Jsf~bj@H$V7v>9$++R;d`nHLDIc;9VbJN9z z5)mTvm(~^r7Bsb@kW&9UM`htX^F@mEJ6ZVpSLQ3NWD**DXzlpsepMh4#tMrpEhXcP1mETq zI4&%O{j_NH66XA?qSkd6d#P1Y=mHq}kgE1e@A{@JR^8HX0;l*ktIW#D z>I_BUm)-m$);@G6J>C=3Ue`nCv*+({TWv(5;R znsUqvPTr~gG_!w1&R|Hyu2lw!JXT=iI`=~Vrq!(PVXMk&zxAX3AYE@CTVT0^GuI^E zsWW})eTsFGAJGgh+3)FLX6L37R?PEy=iIN@1~gnS*V76?8GiwKq_!$jRD`ZM9bs&* zKxW#|B4_8`baQcbxw$!hzZy|mLU=Ng)z4Tbr#Cj49p4txyNm2}(aP_lxrf>a^Bji&_k}>UK1{*rMH&(T40<(rsPgY zWLv2wi`1Of9ZM3mLZ4=q`OZmpXDop+4AI7#Tl5II9lpe30*$1aqFP?30x?#B?F)k_ zu2xw{P9#!>vI8GeJ2)NZ@fQ&4xS#DF3X@0SZ`5~LTH~1wFOejoX;N(BHaU0+ z%3rMp9_b+qf6!#{^48q;nMdy&?~Q)zef#uRB*A`TvKz&n49egT;Uy0+yzSQHW>#A< zGEypFfG*%NOy`ZaRm8Cde8L+;<`3C~!@QwPJKj=O7BEV1mJgsMb7-EzS20*EgPT=F z-@slNry`^hw#$o^obi4SKN$3+rlT;7SE~6;LdcI|(F+=4anTANs4a9Hmr!c)J{u9t z7pOpnl5x@{H`>p_=50SkfaQOxWhwO8%>@vi#W26;J|oAE^{JqNC%GA6%LKNY`O!ldZ{+abPI}@>X zm98q~f?tR)KW4SXne}zYjn1bW&LDam8s3_|2h=fZlHqsY0(-1x*)=y_D=XfB6W6?# z5tc8;czuv%J^?8Lr?#W7Q4f4=bP_sYTGr}fEsietE$-PQ@g${`ajLb(46?1+P7)Wl z`sy0WX@f3J4OgAhlarGTDbO54TxSc>c|Pu$uV>)_$P9gB@2|GXj&yzLVezX;1lGQM zx4IaSjWdlwm6};`1QSWCr_xl&6e8LQJs(Z)EQ`-Tbj0!-XSy= zWJ^2_2PP)W(-na5_kZyMONz5G81CKi_@EW8ZTB|OoCQ(Bkv1#-G2W58rU&tq-mjua z^fEmj2W6eQ*VpK}9#VaCmi-)60+fDAN+%gXJ~TZwvH25=HmWA1=lq9@)H>f|O?Ih? z5&iGtNAKk-tCu7`q2)#%jkI){U6@m4O=62dHoZt34!>KZJo!70rTT4+m~~OW0xafG z4M*L}U_x@KOK@TdO;#Q>wj)Y6nePj*`3K!B*d@DF`bPE+d%}jyxa&ouP{e3H zbJBX%OUM}F8TZOAh={STD$`h~@u3BS5U ze!Z~;{prB2{aCnDblH};D)mH1l&bZD5<0YdOv?Ez@*G?nTi>BpiLYr&MuG@+E}-bg z#+WzlE``cnB~7QAy(7ZO+do zji!u-&bc+moAm-}!s3;bp5c5%4X!^I$fE#Nvv*6BGe{xnaPXz%D@WWP{*3aPN?lxr2XenwPiQH`@Jkj4BU zztDpuA{?B3{`>?pVmI{z1&ZeiJiT2)xtL2rBjMD+H8t`oM6a&icvfCv#FtG!UaKD- z8#&>1_w9p;%{fgY_4@dit#;{Ya@r&LnC~+y3Wyr(ZiNE}2CQVK1}apWNr%iOSF*@R z;yu%7d3kl1Qhk@2WQx<8Xro2wi6RZnT4g(?HIh+c50=o$nAp_R8sBZ7TyEAT%P6rC zo|>(-9V$_%%4$`8zc55=zu>U57>=@->PPy%e~_hs*mC|&eYY*8EOlF?gYWXulwjFz z3M;9dii%Pg9DCTBUXxupueE=Go#9TF1R$CsV>yJ{F+njhF)usl39BM#WDqIUFezAb zIpMz|d*G30-iJ5C_1>1i1FXMI@_JbvzwLJDCY5sKEw_@U4_!k!tSe@}edU`MKl4py zWo~F~nM^!Tc;xoc6J)XD&>3c%eAh8i|EXRij6zzOo3>nG9J?*c+u6rf^|OFZx7M>^ zJYJST%J6pTG>=>^O@wXA@o4-L=@#v+n8o4(rQRsKjlPv($z9l*oHue;`?19k&57>i ziwNGlbO5OLY z%LgBdVH2JBSf3R?7jsDx$f9*ZQVFO@SUft@*~ORs*~Gz zs;?Z~ZjosS`{0rB4QwvNr;!SR>1N2sNA7Qn2mjU=_MFsw9fC%tt1N1XUIreY#mq5Unq@(o8*GjZ!37;kacz`OJ@(B)t!3LKQ<1#IcIehv*PLlql1tXD%L9zvEoIdKhj}7CyFZOMJV|v#iJ(XR0a?FPC)K?S4;=kBUEmc z%H-tqz&-opnA>E6HDxgw7X626C^cYk#5q5kSl5eMobC5S4yo93Odd*Gs~ z!KCDFdQ<239$_qn#`mfT_o|7g!@b3JxD=)7%7&ZwT;1OUmOJyhTXB$OCvarEQ!(+G z(I?`oL#~M%U@y}qc;J2m$7jL>VoCRqYw-NJtk|C`L;+xKGp#SiR}>JP=Vlou;DhMG z?DWFy$5Y&JlOCLw2?quH6&CGotq}_X=ETm+rwaZ?@G|lrDV_9bI|}q4ger>&wRx6 zGm9v--y1Ye_E*uFJ3FnLF@f`9{ z?YGzGn;-3t(3%IiMAB>UU}hHO>JGo@v>pb3Y>O8fVkLE22}7@%xQLaymnIkaw(-wC zi7KAX*W8YJe15%i+&%u1Ep!ryj}o_?1}tbM&|@?v)C+NOls~l6222p0cRt0ynqpsl z#*V{E(zdBFw_3O{}(dtBypsy8V>Q9Gi{?PGW`5 z$pyBX$q6#a?mO!g;=r@#Rq>nCm(Lfcdv~`6KdENxLj=athAL2yqV&px(sIjz!Xf$l zx+dZDIzCemy-+UCL)l3XvX#Hzv!PyYn|~M{Oa4ggaj3u|Fx7RG_L$=O6z!s-QP;jF}{ra?nM|zxX4)dFRdza6`6S27MQ76mV zZ&ge7HCm-x& z_4g~=eKz->#_EUmcL{X$apHW&6(%22R*{I~fX@`|`!EcD9s zsB9=K^5WRlSVyV{?%hCyp)#ij5UGgKg8W|>h3QN}yc~fl^#T3g@H5y;xg8rP zlw=m}dcW)j8SA+#Z=Ng4i)%-&luZTo+tNEOwcMB3?~M*eV^avyaB1||R?SQ$27DH- zt|~Cry7F#+s%T&9yZ)7UZnW0f2s75PmdQ|Fsryn7^RR1gE+_tEHcJpir19v<7stfJ z#8`VVs@(|m1I!-8l@*Z9^mf#aLUCaGo^h-c4zEzP&ZhAEQ%K@F#NM6vGFO=gWc?OU ziQ5%X8KbX6ehZGE*RI=YL86>kEa}o-Wpz4b6LJ9;9Gc}iF?mia#-EN_pWRBnG-;!e zSm!!r8;&_`yeqn5)yi&G*!|=28?L%wOYQWIqOE41CB!B}l#WR{N-ce)(~GcWHc*LSIl)8S;*a^4k72~Ss-pGzb(L`ARZ(pD#Cq0r~>e{4s;16xH z9|v8A@chER<%6y50&<-^WBO_`id!Z=zD0FzUn4xew|t&Aety43lfW}nxyQe%Auts^ zD$t0yYBZ0lL_0vA+|;FJ#1U%GGWiwGOA{-ILqY9hyvy*BQMo0279ZRlTTF++x%FzR z(`aAO@hge2qI>>qK5^u+s)Md*Pqk0Sh@aQxJrw>+d;AYQY!yW8_7tCcqjuY!fek}7 zrptoc!KzVmaR#!F8b7}tK4bBu9~^!NIv}fL{~Je!Br8K&j(or_&-3K`H%1~MG3#u% zRg^qYWOc82z3$NcwJ$7CpxK?H0yHsZg<@9@u_$VR9VRfxq1n5XpIs85jR-P}?pwxX zwf)g$WY93uE&{>B3lBl~8AxJVX>m6{@+g_Q7b}?zzEZAYWTTjywe%A!Ckl z+N>9d$7Ij+oG9WOzay4ETJ^R(o|x%dLJ}PQh?c6z zM}pXT6B3@ZGCJ!b(p^7+R=s#@X{0ubOx~O7L+dw=A^l)KJfBu=yUJ6%D`BQD!-Z?v zfA%??Wpn9>BGGukBP8zn{%V0O7CMGla!_7I&WTfHd!Xzbo_T+zS(uQImae!1g=1Gn zW&eh=?@&@ACJwK;rKQ=$g1>cW7*E7GTwc|!bJy{H!hEXlm1tN0Hc>R>qY33w?;`#W zbEE4p*4B8`ctI@W2-SB)>GQ*PIHA!?lb9bh94Jj(x<|kHE#^8>sa2JKag#E3IO`r?s=FdDHHCmFFxjX_=- zc{>-PRA9t1VgvjX1xqLi4br|Mf}r<0d5&LGn-@_{b1k1>8F>wYpOhDqoJLe1;?s## zDr+8ZCtjGUbiBRBF#D$%yYXb5o)cS@K5Pa#>#N)LxpvW_j84Gntd%zbPC}rz(h}ZL zo{MO2?`W+IhiT>Sr{=1h+Zx=(ptcUMYYgz*HOMLP9 zSoB`$@UylI(q+6lV_mrtp9X?#?i#GtWjoB4#(1hGHnMdL`@Xv*Rw9)tM#VZ*d`hj_ zyi$zR5Y!??XXB0zvs?*B*WzUzqfjn(;2x~c8L3au5BAYj$~X&~ANipUb)xzp=+vo* zMH7r|QGD6CHCs9Q$iBABc_#EE+_sHf9x1P$i^vI#BnYM?kR`wK=^~mWF$D&RHoM~5 zIu76rJx3@f*v%1nH+*%WTvFcP-^!Z6ilR+zMiz_rJK4BXutB#u3Ce|gwRVob6<@Zr_;%S?xWNz{JJ*B8lE2f)J+-2w+Pd^QnZ1efy`H=p3}T-xuL8I0>Jq_D;;q0K>>CXd6`>08?fkYv3Kc=%n zI~n>i-AXX&`<>2zZbXOaxW%FF)JkUVe7h9(qLzH2l6)(q`fQneELKW0{dYH|H<;}M z*HdbP?2^hn#eu$qIIuZPVj6H*GVazr*vRH=DW7W{xQ7q%@nur8x9gzXRN;K$}JKWG7fPQVG_z1(I})6Ku+vWdDU4IscV$j zlx2sbe=d&*e&Yy&`CWbQ$KUmr(5^k|TvC$8h98-%-?-#hu$`8znM?}p%8vqVUML^A z2Dl!*KE@!nC^rd-5vP$qTT+WJxXc<-W7>}v3SNlS1X~~}pg69#Qt>MqGMP!k%!k5} z(bti=kPWDmB)-Bk0=|eN`x~&a3I1;QfIz2!%+s9f7TW5fjw{JLvrY_sS7Pq)7Ho3$ z`!`#==XRG_pKPs=L&?GLec;bd1R&!Me6%e){2i1{QyM8(%lP@tR3q5-PGqau zF`yCrw=p-p&EWC9%HgXk$_MFJN(`)y2q1z8AiBlN zlL8x$OV@Kyw0j+!O*GR@Mro#+X{MUS>R4}(mT>CnQge}Kd=^c%^Bi%<5^c8IZMNH; zDkDQ&CR-fB2#;Tr2|;p%>j^6lKNPe3&SqkQM-X?oE?fG!i7qlO{}_YY_kw^PI9}E-WeL@?B))n%3z@`7M7*Re_R(UP4WPW?9d6V~_b zQf%c!KYQsco$fH@;&P*1!^0KR(#`Cbz2M{0&gg2c`>iaoz#K7lcrn=B^bE&$UpFJi zyC%u$Gr1<=YQ$KlMi#i-)KLLFu1X)YMf*KX3Q=?@M%wg;&f(;AMweCC0}{79Iq*zf zn$Px0k1Djh$-X{p$o8@In`@nqjnDOm{JXhZxmg8kBcVh{Goy^|yBm(JG{g@2$O=e2 z#0|_DfeKiYh&2iP>83-AcA1`plIRyYROscXJ}rgzs}{3cKiEIPP4FARmPW~?MYc3fjHt$wC&KYVn!1`CYgfEkn( ea4!x;XHbpaqFR)5!N<@4#oUoj6eJ@Tlxe`P-o&v0 literal 17615 zcmZ^KWo#T#v*nl}X7(6j95XY|m}$(+%*-4!GsVoz%uF#e+i}bkLmWd)NjBfx)qC2N zw$!?HZnbXrtsgC&I(1uIT~bb3nwx#<1K_{R1n+4D_o7JfN?c7DN|rON7y1?m!tVW5HvWtA0+0`*7A3Om2Ta$YF4%y~{h@kOkN zvTEt7b&Z(D;?HbMxocSx`bUxK^rpI~-d0Maf5)R7dP~Gzn=%>39lmI4HM`84w6q zgo6TYi~enmNE$o_K!>>m0671x1EPf_w_ptw6M=~GX2ECjhsSRmbI}_oxiTrN^uzaT1*tx(ng{{XVRoLgr6lLVK&Mh z(y{vk3zZ_1b3~)rHKW4X=9S)-tbeMOEgF}W6R#N~N_+a66g@=B0|r^V7hTduO*AZD zJemd6D7jH~e(in%p+b&0Ec+8IpGvv3MUPNhpjlv7Q;J%p$?E}LT&3B-H?zZ0K%oJY z5h)?B+L$UwWFQ6zKs6+>fZ3D~Y+>v=SXoVt@fm>?1TZIOC?j~ zg=r(2sm=xWYS?Bt%|(o^>S9p6noNi&__Kku#pF1ne_=#gt6yJA#rB^i6ioTCb&w3Z zp`eOlA9p(oHN4$Q6)|eAd|okX78e)br#hl|btno^&Gc4*xhYKdR;~{v5X|13OwL~N zi64-9QJF~?1WZG9$snct!meuafaRsex^cvCg zLg;RP0H@!u!s=)ZxuZL;lQn8+M6TvsL2VcViw5yDI)^|;2?WRLNvXV3zkwBr&XIUb zc4)C^dKL!e!#FaY#maB_Ol`q7ySKQ`h>@=!1jlsE_Pg5{h%&{a#d~kCTr+OgJ!+d2 zJ8N3@(t~Q&^XZaE$&XFNHa}O|yYoeInArUMEDy2}ets624 zrwL`-s?NO3cg@(IsZ3l%rr5+>5j!HO);sLgVU=?TQ>7*;~1inr{Ie!S*a{ zlvKp`pxl}=1d6Je&6D-)8D^X6m??Mmiv2=MN?wfG76Bl8aE{Q^z6n0T)silR&KQk0A>C`l0jeel2 z%A}2k?XV6r*ZxxFCL*EN*{y^6O9*j`zZM0Si5nKp8aJm;@6evASaT~IH*4kSZk4vO zQnCH76QyAL;<83ocrGaS&r#CNfX?H68X>XGRCeTbWWDA*=?~0jRzY?UrFReckx>_~ zMy=K8E9~v}X#YI1=8+mZ&{s_zmu1JO2%46m9Vkah2oW$rik7ngWeOJ3AwiSH?#n1I zxoJ)RjbTMHL0vMLcXP*7P#hvFfg|fkf$hCaIMenZ2-G;FjHM!#09HalfQjW9(nxLf zxE|xdk5dsVoz2YBD@+$=5Gv`~7(?A5AlxDeN!w{7bB;|B^Cc}7yQW1bf-3cPOnEkU zad(32Ohq077sQ%#qWgHs@~S2RE>9JKX`&aV zG5bcfs+`JL5%5b~6wROV`$RX zWo57=6*C1~=<_pY4-v-?NBnD|Ta3OYt2Do!o`i&}6Ou%2+K73{v;DutY&qM+8j*U9 z{i-xmAL->E$$Me~XOlcYvZ4GZDVhcEN$%7MrV%~uNofVy$u==Q#H>aXkfRYYmg2C8 zpx>5zBkC-r%eo`$`SY0VZQri zar_dXS`+Vy_UzY)1j+AttW|iRx@5rP@uw}zQH#9FT%WEg5@Mqg%F*x^6Ii6{0|E9n z5r2O+OLp}``4&*(`D3<%N^zIpNK4JOeBf*MS4J<+9~FO(ZLCp^Ee=#NPe4kW{Jy^v zpa`@r`m$w*Lef1_R47B-rt!h10QGQSrOQ&Ebo#R#BxgHl)Qx^vrX2+W^g|<~b+-jGUD+^+xD{ivQQTFb~->d{N+gv818z>u!8X?g@s7K>karcrsDWw)}KH&vOy1 ztjn-HS=9_|Wr;ek=~16$zM3DRd?e%*0!mThXoEkJ2Ra~|oA7e)I5zX5#7me#%iw?# zs>tD!W{$APAziqU;9MlE^d@)>8ulgvBfTa+Y^AR(FOAGLbI^S6zodX{4cVy8;d02Z zjhxBW4-_(?5&R{P&l}Pwhyb`VI#jVVr2+8GZgrLT9gPU?dU-WfPMJq{R+fNRcr`S; z+-Y2V;DPVY==u5C)*lgGTzu|9P|wWT&e#i2B*DG{n}Md>%f%40QrEYvcT-qfo<1A$ zOE>Ws%9dFd51yEJ!%S=!+`5KRUGAv$MPp93rcXDh($}o6e|5gZNjN?KS>{WI1VC3w zdvL=E#lgobxRzSfd$fEi{=%Lr{SvmX@=D&u-<6k}hK(}WXgq6F22Sp$TTeT3zpZKb z>NvaWm!fhgrL+>2SHNR9bg!}RJ(19$;Sw))9A?XAS526K+xQ-4^uK2-x245!T4tbP ziwbr7S6{A1#AJJ3l6T*wAUAuLCE%m{teI*12)pqSC$W2vc>j+N2HQ)nmQnU*FzXJi zFcY9D6g<>if`|jtQyE)LXs=+lDE;JC6d&g+bJ8(={1F3*@8^1cCHI)5PM--lgrc*G zgPt*UvPpE`E{D@06Pxr~51^x#3I>3c<9OU3fK7E^^L?{P}o8Gk~P&|03=GHTm^l>(bctiJja zAi{9Ty#i&=;q--6m*7fAPsLPsRAi-1r50AG1x2qYi<^=_WGbX3q+?nv(S?g_+p?=e zM5tF+sF!Uka%_e<&1{QHR~K!#ZGhb9RW>lC6o3^9Ae=fl7#$R!zKxjb%UO^`v$9ky zocvRL^{J6Z%<bv>Z-uo0V0Ysp8@y z?iC3rTJ}Gp548Bl`WIn}OQC=j@&D%g{~rJ^pNdj_jF>=e=TIs^@d&gkT1s1;TtS|b zhLnOWxE}$F+yL%H;{Oyd|H>G&1jHN>@V`=q8y&!HJKFv;YeozSn=mzL21ePC8`u`< zB|QD(9Dm@h=XR=W%{-lHXMoXJ;Z#%a9R)XXB14MyDx`(V?T%A(k7Nh)3w8U>ovV+l zzLjS(B{o)k(UxwlOB)jY2dC!bsB&PU-MeAf`*a@lD4I#AfOvbukM*54;p^8cjGb5d zw3zMwR*TExZRmZj`bSFFU7D#Vf6djC38T>E0yh3&iUW_gG>bB;-;QzpAFX6z4m7=D z+?t-^nN<+^&p032_PB`KspJGTt|E0$NbQiWq~wUT=0h3dpnAhDj#}Z*DiD}ah4Ou~ z;d-J`REIv~P8oAv^)!f#RFDr$y2O|lP6jH>hF1+CY9t)R2v4-6AHDV^(F$!;S!FOj zJzk;|Ev+EvX|W&#BXS8~n~aMMn@zMt!VQy@3IA2L4e|zx1oE>uydK^p|E{n4IE|PC z;zY18xDvm>8W=)*_rMk}f%7g2hn)c6cs1$iMIdjCIUd@(fV-E(%OUV^VgPw6uSE%3 zEQ}5m?_gVsh;)E_Rzh8SsLoLRgN=f0`4jco80I?F_Ly1&YY>mD# zO|X*+Koa#lx`ySIp1#@<9~+BfQAz{5EgG3Wo;HxhXcc}+`~uf9v;vmi;wWo!k)d}O zm|e}10W&=2#-T9-@u^BUVv%r6qt~Q2=_DeSEj5LQW5WKYO9%*9O<{#oiAyzv<^;$` zt730Z6icee!OOd`nIh1~tjb4ILh({;49IU&#D9Z@D%!LHTiMl@^MS zjpTvTiTV|@Bp`eUHwPxQ!}ri=+FDxrU%c%i(;HZ`LT8+Ry+#_j>={(#vl8#({>FA`blqB8noGA4o>Ct%R)QR)SpY=G3Vv3BWC6t-F#E#~n;G z09BU>Cf6bkfQ>Z%5f3eP1$cWMc73f11eYe=`N8zQ1QA;@g^(uaSz=a}%5zi1tFWOs z6Jc9%s<>{Ag;_5e+ZTpZuSP;3+1>+V`sZHQUF`w<0k7fa4u7t~BQ=+;b1o{vj|_5DWHH5s9~` zw8+pyVecOv16Rwp6k{4oW~2bk<>`aff7>an_%2h7vWt)ElAuZqxkT0PaptqId~t@o z>4>%9suCLQS7dty9#H2yIMG+dI|WY@!1=50D!D0ls4uj zVC7raE&~&646Wp!o$~&0U2a7M&zLtjjyc$q_S<4K><1zlZ!wcEQUG-Z_8K=QVkojq zp$iAGW&(^4PI&>xcAj1iuc|&IDe~Uj5NwO4>V#iGshll1m#l8M?1_B(Gu22dep5HR zDBjNZt4#()Y?C|l1F2XCKU^SVTl;UjB!^=C%eI{M2wLB2txPJ~_QaG3n%CZ5c%;J4 z@er_$Lezp%oYV1<_4%Crk8wB&Ua?!kKr6QM!U8AVv(N>2w&fCzyNu^YRu zY%UJ(MVawQ@7Rl5vz<{yNxDqx(MKhKfO_$CR*+i1Mylw94m%|)Y|LADmR3q%Fyz;c z>q{~}g~adV`)G@UrtX}HzuV%Qc_$Zyo782%bhKhdS_(P}Dpzj?UdLTYNYqZCUkb~4 zrw#tKPI1NjPJql3G!sECmCXlJY#jmz*;k|{u1IWO=T7E2GIKX4kTGEA;vd$@#&XSu zwV_sq6$Dh#YG5p5m^$bMs}N2@Q+G!Hz^uum`U_bgGiPDLV`-wmF13Eur-G#~DoGIGfKj7KdCqsxBXtNVH z7wA>3R*kHRHxU+({Be`k(1l$R(KLV%+|^qbxL&BUbU4>>f4A*&M&W9Pg54w4+wNlZvORP5LO4a~Ssun7BAgNOBk@U=Cl;Y$Mh)J5SUiW-@9sH)ACLb)0DG>pY5 z-=LP@scJr*lSL>sWQh7)o|xa?>;@6Zt}Q%K7Lqu?1|H1+2F5Uxh}7@0+=vdL80SF2 zC8jRvH~Prj=EL}TTB|8vo#VP4Sx<@Q-FI2>#uIf`c75K{sXYU!;>>4emS-~xgOu8A zxpPa)q@kIh34q@Yq2vA@c!LkY5(`;7n@7 z5{AB*6&>zRXwPr_`}b~96=L?$gPXht6eI z()!a#dLYAywx>UPt%%-d|AJwbfwiv|0+_U`6T3f1joK-B`P`db&tj_V%K|ywBTu3` zE9Q<#ehF^46HmBZdvVles-4_Rn2f%iWr4UJV#;xyyQlT!Qle8O2i8M%Djl9MpBr*J z_Id)S=S2z|s!S8gD%=@95r>eqR8z|0-Z~{`j>5iu`k*cfLguwtcS;SqBS7>1kxBfz z_UrqjH?b$U*iZ}j6qM$$A37yC@xs`iZm`i+SHGfj&F1_pt!q_mN7YP+j5a2)*O9rj zOS31y3p1A2Ph-+M#VX}>&vveN`Tjr0$b;{kzYq#i=7{hWLW)lMT$Lvj~FUW?U;?}nd1N6{Q&c^*3fC=G7 zIXn+gM+u%QZKaMaLyJjg!p@!{9c`jR;^tm%tNgOQkcN)#&oW`Eb9&+r`Q9sP{n23t z0^sJ=zQ6ARnQ9M$Mcfl`I`|D+0lxd}ZJbpN8KA zw0%(S3LRtT2^3MWiJ~aVX2`EFJUsq3e|9krIkSx3C!SPUB8uLF7~qWK&J?G&F^McE zD+FYq*PM5K{9ZXz8w{Y(gMOMlA%i&adO!(%ONi+oZawf4nE-}NoUH68~eLz#Uyic3G8@Q zO!-+zGi_Y9ar|}fz8}snLw`x_Qms-KF`8+z z#=s69(m#y5j?+%PtF;ofbgt~gX6SKX`4Gn*XQ`Oi`s(g;s`XnCW(s zk7b=%AL9Hl2v$W<-Mo(9EBRf7GKxI7X?qMOwReBt^yv?_4kTt-I6EM44ub(OVQ^W2 zQp}L@xh4BMgQ}IM=3IDz2N*Ge(?=aH(myV_hRX&e9Gy=A$*d<&+~)eUiXB^bZ} zB#x7xfKY2I?kz^JCnNgtf^_HjB`WRvDIht#4Sxr&T@0%8xT5}n>W?EZ|aNs3Fw;q91OiEl33j5IapY0^?6_j@?CK7lC2D@sy0FB z5o_MpV}f6aTX=QTS^gdsq4+s@JC2IS8(zhFmOtz@n8_3p1no&Twy3#n|A>0c)h<|G zt~g2aD2mhay2VygciS^-z~9~WrVo6Uk}sZT5jxd6Z4Jt2z}4Vx(m$xkA1l3{dY&!u zD}uz{JS{s#?eof!$kf#W9d#2<&0KsND@6*rHP7u$C0>8(v-kvrH`u~d%^ zg8|Vd6!b;NH>p^Z6L?#SmfH<=pR8ln&t4aabcFNJ)3N9bDWgQB^7ukNP5$)ZF&K&5 zKBIbu3(ge{D>skl#GJ>>-0Of~jPCvRbU^}NTl+Im^Ol+W1=#3c`O7p%ex^VOsK5G) zd{iNE6B(KzI4E87H?Es1k6zZpY(ppbfAmFS<@!tjcJqJBi2nnA|AV);xBuL?f8g&v z`8VNkDzHCO7fcZwcI}EBl6oC-$FxzLuTt7x!xB$#9=*}y-r2yL=GHHswvDX7mSEra^%`!YJw6=x7xtj6N$+ zV)E#M>MMEQ>NHy(UiDQ+Wn6~CB=u$9B8x|1h7)qti}Y=q0_m9KhgI9?MRX_y7!KK# zlY80BT&^Od$}Bq>l&h}(kX6~HD4{Hi4zv`OmX5i!n3I5nFRoBlAfv;9Z7*SpxH*Z@ z0YCsu(drVSjk+Sk`ReL+g;fNvVtkr(uDBmHRATi}MCKAooNlv?7HsDuI{~J~`dDL{ z4lt2`vS$+${ma43Hl_j>nLLg9pU5YUU4jedv~!@Q#N7^dl&g#oQC7?W(Y14fABlms z=e8oslD6$4AY63CWx(SQ&k{swC^s7QzaB*RQU#Pf3k8RY8`fq88CROZW<|{(3V^CVEL%J* z)dVT#6jV2*t^Ujrk%nAaJSfZhg{{s>FJ^zN`gdkSI)R%L4vG#_3buhMLPyL2pwSK8 zeXOkfQdhDncttr)iP49(W)o7exSeJani=u@b=Cj0BzC%zch&C|)-8({$D}xUC$fw) zROO{T#m$6(k*Jn{1p(|Piwa@YX~xi~caTIJ+qnNqi0mkQ?bq)S#rj5Au;J)F?r5{t z080H~#(6Gqd-yi(Z5ML=PehO+-OIag#UqZ;KH~IEDiTe;J(UJ+4SP@6VT5FEN19sc zQ0wNF2zpkf8LXf}Fu7|K|N9^%V5akF$umV=nC&7H@++jk(&KYOdsxl*i`&nK= z)|j0bEC}AyrjBpY^}jV~ab+Y4&vuI4w%e{5zje-sd*y%F9Vim}^V}=tXYA)9J&uKMefgA&dXHObg~4jl-=z{EbycSTp(&WNY|Pn^}snVHBGh) z+flK9PldOVXEGh{0+04`v&s+@x$V)=%rIk`?vb?vbmT~jrg?3Yi_gF?=KaBuab8q3 zD!-7zzVEnBYf)E|w=<`TKJ^Mjgcj+g(g@?x>vzPAwe9BC5d6umO8}RxUnCzxu{KAk zY5#=6VY+Z04wSB*Q0x~x&~>>6M<=_ysg@iMWaF- zd)3~x0oQIngqdc)?zUq0Vu|xLis^D#AD*?~r702ud~Q6cMe(f?Di*5M**BVy4%qoB z%6hL9$&#MDoWp*}_e(tQmgr*SqPlgP-rHZ$WjYe9b`wn8{X2^EVWF`&40Z2YY9mdD zOeBa-hL0PnvxS9K-1mDFf+q*c5VV-zP@axv*=Xa%`I|s>7a}B0BeQBf*ZU;;?R$k z=Gc*DV~&#sfg?pm;W;s-fasQ`bLcO0`}~>R6XL;qdoWzS0lIjrhPueRQTMjTh0IMC zvO{i;zWRL+w&!N!*zoDW@_!1OvFh9u?^`4QVtuk%KRDRCotW_p2SJ!U!1mlkwx!_XWbbwY?uk7gyr3%yG+f^UlLv$JJlAxezYy{@Ww(Q7KWAY=%c>!TDsh!m0^p= zf0E}5(M>zMb-t_nspK6&>bTcFH@)hYvnU{<6FI9LZv{)9{IB=!dW85#94#av%FmUV zpULyHY5i_*r}C6_NzE0RiOW!`9M-?%1Ryxm>TPEH-9^mx@7uUphvX5$LM+u3=DJ(I zd^wTGQzC^bxfaNdF)Ttj+53Q2KoHl^y5hi}@;_au;n6d+ua$WfW|0DF3hg}{4sL~g z30>Z;QhYVu=g3&WGz?kW-C7c5eV0Fe3*B2$ZcSdvu5#ICt>I9ljOAwt!eb4qdN7$` zjp|fkDOT(mq}WwOh?r|3P?F$h$~I#hDs=}}nVvWzK)K6aVmC|V zdHmyYr1QrCZhTV3p1>2sZeIg#29O+*tIa5O>u#T0P zgTR8&sL}S5>>Dx}joDB{r05(+1kRUbWZ%O3(N2F^@_euVgfmFKZrjIOl+vEJG5htA zSFzOLJXy-e0mlK2_j#cAQ{w;?sdJHeCIZf9F*&j?26uJ=xmkZ?LNfNPmlj9_y!LP4 zbj#|R5`FeRkh5`u5Ky-2tBb2@U28U|Xu4aoesM2ZDr3er!U4yef+M}>N8B9C-39gL z9b^KjfVR!kxQf~ilW+yuA!S%69lNlw%quDK0*MhS)FHPU0{qf=-%9D6eI@whSwPqV zPHJI;^^2{>#XVK|f*j~)(CUy<>A2_cukGeY!+c0dB|o%+>ssZy^nWdtkMn*a=r%< zgcKDNzWu-|=ezB^d_!i|jw29!vP@*l=m%NOVBNPKugx<#KvjG~7K}BE3gHjLATvM|fmOI73%@`6ml*a?T#5Q)Ef7Za*Dd>6d6yc7Z7|55$<_&0w8 zQ#)>1J2cJZ(c+e0rtt%a+xA=!Q0Y%8RQaj68HN_Egs9UO%?@}#!nn%r-ob2_y zmem5iT3otCqb@TaK1WSy`wgi`AgGp4w3P+damlh%-!I2XOZ ze4w{yJ{OPX=>}lZs@j);iL{7=3m54GfyR;#izH0B77>nOWo~xq3tAj$&}UL(Yv-Op zDN8Zpw>zp~UKSBpdb8KWgdv9IxgTf$8Uhq8+FeeU0u711IsFqDGImrjuz<{CB{xKQ zRtIlXqEJJP^w)3+xvBhTV(%wfW1_24Q+c>4n&Ro_ef8A#-+D zzWBfZ1>dyZK0b@kBr3R1)rf0v1ayWq<_Z}6b4=7q5COYNohDlG2L7Th*=AMPBxa2% zRe2=)55zwjgmSJ5!y?MI9S^9^+P-((h8FmpCmUp zW?b6ij17<51+H^B{CJf+x0*x88qG+EPhXUBltX-BbDM@5;tC0fKz5R0nZFvl~?FRWpk6RX2A zdd#|fXF!r3jPHBqJgwz}-Imcw8)_RbI3ZKV%f(TtXHZn0znd!1i%`C4ixHT<2V5oP zmYsw#Qw@I@;lQ-pOIwav#XTOZY>l^6ELJuqi-?1gDM&LjeKRu|7K(lgxypqx@}qZU32$ zhr@-pM2)JIi`E>2LOdZ?Jkz&OF1=JUoz28tRiNd~-%h))6fR#JR{mKAx3{pwZa*7% z&0}Itx_S9=`w5$4$z;PAGC9S;nSrwv!S9DCS^Y2}|4~UeT{=1{o~!B0CL=W&b0v*L zU2EZ<#Nn*R>Ud6yqPR?`ZZ`eS<`j)Jyqy63H*fVQ(%;@B+L&KcEc$+i8m7J+z(tH^ zOg%Phteo7c=m+#-9vph3uap_}1d5K1x9aK_ZGn1iifJUE+C^nBeK>5^ulfTEKhL?< zTR%0%lH*9_!-Ht)nTPnCYOcCeVS46dY~4$w&<8P{_Llm$O(@7VNk#K?Th9`4FKp}S zV}qFirxngPZr~vlLV><{qgKr9t`kPFkxN{Y8ZA9cbvyQmtRQ<>c1*>Gp^q<5+&Ma! zpGNHXHpCasjWn^-*j4g3r}~)aW7az|IR~t|_LugtFb0e++wGn5Pgr8_1%jSXD@!Tf zMS@;xPx9f>Bqcb)k#7y7E4L^8jUJ`)))#bzNhm4=i9hK3@#5(URO1ZLGtnt^A1)|( zfpTmsP~m8TVM22@A3TCQ%-D5SO;6?fc)o9_d=xvUMQWprZJF*&`?9wuiQi7K^I&N5 z&K*aH@bo=76qx8&RBMsT>!qQR<<6t_>Zg!qY&t51$CwD z85_c#|2dsBUs_J3q%5>rT|p2!Q>hw;Yn{-${V3AV{9cg%5{O!n<0%F?T7+aXvPT~! z@^KOSO#<~wx%iy)Hh3EC^)?k}=PImRIpX!vX6OD%y%U_Y^-T83w%PUe24@1xJyz}Y z_KM$$^CJDbW|IlpyY1b#7{cMRv;T#y@)taDzU_1dx~mos&u0y-#C>LKFn|H0Ckf7S zh6zV0uvSopYkt;i!*5e+Q4XlL*Y6e~W*pawvHIMmm2a6TRh1D0mUZDL|J{PDHmYh{1nN%531=j=Qb{0TZ9<%( zZxG=>#j4a|+V{oQ{mCsqxc|#{mX@I-4xXB*p!GBddNa!Pbkl|6qNf*)lMmFed&K3b z_8$}%lj$%OtLAX91=v;Y8hZzu)(_1SE@Vc&Rn)#=quHo%-S`yuf0h!*Vle(a;LQxn z9Sk;<8k1d$c1v^>gjSywiX`z}7>g;-zSh2bi8;8u!?wK}&W;v;{ezZlv3gw#Ijcbs zAmFzCeDpj&;+dXY3eBNN+j4Gbtw~ktJT(7-Q5hik_Gs5|QsD`hoDvWa5N3s_1)AsO zRmbg%1xosgekVg1C6AQ;`Sc|=%|hfI-<*1rXP<0^_WNQKLT)&io~(J`8h^(>GStxM zZ%kIPF%FlPa$vdnD;xzF4EVr~OawqSCx*vaLZ@&DPOuZqJH>jb~=jYx6gdaO&!qIF|;~fFX zHAPrpO+x?@{;Egr-8j((#I8Y3ce37uEs=A$A+Oc3!lJ7hGwTV%iWN2YFmr}PBJ`d{Di2` zp!A=?snvbMyUv+6BB2>f9c5b2qn)v!_kd@6OYNx?0x0J_fB$Kt`^TcQ&guIHM`gVr z^RyZr;hrpMLn!L~@P~%d6&a>>_BmIJ+0R~h&Kqt ziuUjG@5J#`>j?y&2GM4e4dg^xCVCz*d=TMb8jLw9u)kVc>8ebxD;~&5R`{i+r#9_k z<*D9WHz;OJm_ldqm*gXR3nwQh6GTujAOw@0xDNXPXE=PcS8!L3Balq=s>N)ov~&CM zcl=yn2=*Z)HLzqLtNPtswVI=%&qEm--}Okw8Y4=oBj%$>h^8T*!PPC!lF8X)v8Lvy z7?F+FOri;Baj#rInbw<0rBd8+POr72VhZzX+T4tM$2fJ+{PFuzMfR4W#fhN3RIk$N zv<;i$D3^=UT6euCdQZ++_Q~#d9^z{HM8|g>>1`zQSjA6-Ia%?((Q64i|BHlDef%zmCD_h#M!%^hu5T2tx>!EPB7o`>gU!G#ab)cL-< zc5)nr4UuL0UC;~BpiJl8%*^M)cm#_1#g9*Y->Eq+uWIGBU8N|qs|P*-B5U_=!X8CS zo);7@1LqkxtPyj9cGP*l;mCwKO1G^E777TAw9=$hHJVal6qVDXOv9v^Qrq1Isf%iU z{eG-lZULLc0e7T{>(ID&vf5fRHC%M^sv{6X5A&I(ZP7?=UJOm*0J*DF{ST8Yv%mxC}|$o-tjyH66w>g)Egg{g*SI z&s8LEc@qgucDpkwzS~*hpu}Y7k4d5&T))0n*u^;0Ks`{3_>r@mO)s`NLHnTfSeLwjFwu z_AkSpV>O9~XbL%1J|B6gy_&!~;|nAW4oTK(_4~bgjOB&H_>bA;vYBzN2C#XXdWyMWLuB2eeLO*Q89K)ai2&%VODRGc9Zt1*wS#%R^9@x4+%__A8m=(RmuUXxwjA>tH97JptbcFx=|W#IWJ9kkudbIkQ00NL2sD--K2)3> zs&c@Hf-=j;=DInQZc<}Y%8Ej0im#`kSlw=PLYk?my3kATopctp25sedX5&Uwq_5_@ zZDzYuq0M-Oi@w=+dFP|r2l_O0nia+0e2(+KyG9*6YvolXk2S2cb!|l3-O>=Ld7b^0^~VF}jgj;DI^c=%R(yuR!@& z@k%$3-ez1;amq4PN4XG-40>+*}7>zOL_ z4yv?fM2+~$D`vmsGI9O^8Ju7?>)4UMp9n7#*1qP_(fM1p{9qZ~Hh_<-k&i=TCm{QR zm#$mEK>Hq=PA?gll^b`G#*yyhflVK~3bdaZBr^RxT*>n~$Nrl{|R9_RN`3%K7{zIi=0 zt1jO)r*4VBQ7oY&pLFj0B!ulfUB(7WwXP`|)&H>Exqw4q-Fgg&mxqImQM``iKeS#gcmO<*i+Yu`?#oaQa&z%tqjWMM2I-d_B!+Q47BcUhYb~vUZq&v9DaO%H{|iqXGn60@FI5#6;)m_XoJo> zP!VCPVdn_WLbE`V1Gu=}0X3#Fv8|nx94{|>8~0|-Yqg?W%A@$Mmr|_iu@ckOjGdCU zdok*n{|RFFO2TG+|K#fRXjGk@=1sfscKZ^3or;dWeY=b+GhIWW#*06auV+Ye6Qg?C zr|CT_;Aj}2Eya3hP{r`6quTVx*5~tI|0~i1E&PgX%bd?UQb;82J|d`dm(-&aTkou4jCHvq&y2L&58aHKNhfOc^a&*< z6+(Qx280aAaNV}MTz(`U+4eEDaq=<3meS#mX*snnI{iXDBf$U^*ijmB5 z`6O5w11t^`PW*sxrdI5@<*kJ{fQ%K==J`mQd8EQREXW)`YNdYn6^~)to*He%7bR_dgQ#6ajMy6W^}2g?`<# zH;PqXxWo(qTUfGB*o-zAtUPuOFQ#j48zkM*XnSwsuJZ!oN>`6M?l%xQZ(P5qh0Y9} z1g4F{m+YRDfZB{e?W$64nCucpP2RsO`q`7LO;qQ%GTCvXNGcLW9~YjEAt$wH#rB9| zS>ZWOKC#I=E|~e89Db0>)^-rI-OmP$E6!sI4$&da!dFp0;bpHY4;xdnx7qmnjyeA( z>Tc?8Ibq}UXhw%JP|WNv@>I%!&&LtPIR}Bxz32rxgq?AXG7?M4FCo*T z%TRo_)_QxYO;@9-%1$&Uqr2tj9qxgldVWw7(-q=cIWR>5ZRl%hV9N;}_r5OVig2MIGM`&4 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 011/222] 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 455c87d8e0..254064098d 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -185,21 +185,22 @@ class WitnessWorld(World): 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 f51d86ba22..4855fc7159 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -2,7 +2,7 @@ from dataclasses import dataclass 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 0000000000..0a24467fea --- /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 0000000000..16b1b5a56d --- /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 0000000000..e7cb1597b2 --- /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 0000000000..0e38c32d69 --- /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 0000000000..3423909166 --- /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 0000000000..f09897ce40 --- /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 0000000000..71743c3260 --- /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 0000000000..8012480075 --- /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 012/222] 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 | 130 ++- 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 | 161 ++-- worlds/stardew_valley/data/crops.csv | 41 - worlds/stardew_valley/data/crops_data.py | 50 -- worlds/stardew_valley/data/fish_data.py | 147 ++-- 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 | 69 +- 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 | 27 +- 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 | 189 +++-- .../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 | 314 +++++-- .../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 | 445 +--------- 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 | 196 +++-- .../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 | 440 ++++++---- .../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, 10298 insertions(+), 4540 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 61c8666316..07235ad298 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.bundle_logic import BundleLogic 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): 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 @@ class StardewValleyWorld(World): "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 199826b96b..43afc750b8 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 @@ class BundleTemplate: @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 @@ class BundleTemplate: 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 @@ class CurrencyBundleTemplate(BundleTemplate): 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 @@ class CurrencyBundleTemplate(BundleTemplate): 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 @@ class MoneyBundleTemplate(CurrencyBundleTemplate): 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 @@ class FestivalBundleTemplate(BundleTemplate): 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 8aaa67c5f2..7dc9c0e1a3 100644 --- a/worlds/stardew_valley/bundles/bundle_item.py +++ b/worlds/stardew_valley/bundles/bundle_item.py @@ -3,7 +3,8 @@ from __future__ import annotations 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 @@ class FestivalItemSource(BundleItemSource): 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 @@ class 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 a5cdb89144..8068ff17ac 100644 --- a/worlds/stardew_valley/bundles/bundle_room.py +++ b/worlds/stardew_valley/bundles/bundle_room.py @@ -3,6 +3,7 @@ from random import Random 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 260ee17cbe..99619e09aa 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_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, 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_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, 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_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_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)] +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) - rooms = [room for room in get_remixed_bundles(random, options) if room.name != "Vault"] + 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 0000000000..9130873fa4 --- /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 0000000000..fb8df8c70c --- /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 0000000000..74249c8082 --- /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 0000000000..5eade59325 --- /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 0000000000..18ef370815 --- /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 0000000000..02f9a632a8 --- /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 0000000000..3e1581b4e2 --- /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 0000000000..8dcf933145 --- /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 0000000000..c598fcbad2 --- /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 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py new file mode 100644 index 0000000000..97d38085d3 --- /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 0000000000..27b4ea1f81 --- /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 0000000000..f3ad138fa7 --- /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 0000000000..a78629da57 --- /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 0000000000..19380d4ff5 --- /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 0000000000..146b291d80 --- /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 0000000000..aae3617cb0 --- /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 0000000000..3172a55dbf --- /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 0000000000..7f88b2ebf2 --- /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 0000000000..ff8c089608 --- /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 0000000000..f74b80948c --- /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 0000000000..8f14300179 --- /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 0000000000..adfc64c95b --- /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 0000000000..f069866d56 --- /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 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/content/vanilla/base.py b/worlds/stardew_valley/content/vanilla/base.py new file mode 100644 index 0000000000..2c910df5d0 --- /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 0000000000..d824deff39 --- /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 0000000000..2c687eacbd --- /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 0000000000..d859d3b16f --- /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 0000000000..a207e169ca --- /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 0000000000..68d0bf10f6 --- /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 0000000000..729b195f7b --- /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 d14d9cfb8e..e69de29bb2 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 0000000000..593ab6a3dd --- /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 7e7a08c16b..8b2e189c79 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.material_names import Material 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 @@ holly = BundleItem(Forageable.holly) 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 @@ clam = BundleItem(Fish.clam) 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 @@ beet = BundleItem(Vegetable.beet) 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 @@ orange = BundleItem(Fruit.orange) 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 @@ chocolate_cake = BundleItem(Meal.chocolate_cake) 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 @@ legend = BundleItem(Fish.legend) 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 @@ barbed_hook = BundleItem(Fishing.barbed_hook) 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 @@ island_foraging_bundle = IslandBundleTemplate(CCRoom.crafts_room, BundleName.isl 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 @@ artisan_bundle_thematic = BundleTemplate.extend_from(artisan_bundle_vanilla, art 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 @@ slime_farmer_items = [slime.as_amount(99), petrified_slime.as_amount(10), blue_s 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 @@ winter_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.winter_fish, wi 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 @@ legendary_fish_bundle = BundleTemplate(CCRoom.fish_tank, BundleName.legendary_fi 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_vanilla = BundleTemplate(CCRoom.bulletin_board, BundleName.chef, che 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 @@ home_cook_items = [egg.as_amount(10), milk.as_amount(10), wheat_flour.as_amount( 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_vanilla = BundleRoomTemplate(CCRoom.abandoned_joja_mart, aba 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_vanilla = BundleRoomTemplate(CCRoom.vault, vault_bundles_vanilla, 4) 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 bfb2d25ec6..d83478a620 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 @@ cask = cellar_recipe(Machine.cask, {Material.wood: 40, Material.hardwood: 1}) 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 @@ fiber_seeds = special_order_recipe(WildSeeds.fiber, SpecialOrder.community_clean 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 @@ crystal_path = shop_recipe(Floor.crystal_path, Region.carpenter, 200, {MetalBar. 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 @@ dressed_spinner = skill_recipe(Fishing.dressed_spinner, Skill.fishing, 8, {Metal 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 @@ ring_of_yoba = skill_recipe(Ring.ring_of_yoba, Skill.combat, 7, {MetalBar.gold: 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 @@ fairy_dust = quest_recipe(Consumable.fairy_dust, Quest.the_pirates_wife, {Minera 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 @@ skull_brazier = shop_recipe(Lighting.skull_brazier, Region.carpenter, 3000, {Fos 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 @@ seed_maker = skill_recipe(Machine.seed_maker, Skill.farming, 9, {Material.wood: 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 @@ transmute_fe = skill_recipe(Craftable.transmute_fe, Skill.mining, 4, {MetalBar.c 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) + 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, +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 0bf43a7676..0000000000 --- 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 7144ccfbcf..0000000000 --- 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 aeb4167339..c6f0c30d41 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 @@ town_river = (Region.town,) 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 @@ sprite_spring = (SVERegion.sprite_spring,) 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 @@ undeadfish = create_fish(SVEFish.undeadfish, crimson_badlands, season.all_season 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(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) -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 +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 0000000000..2107ca30d3 --- /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 0000000000..087d7c3fa8 --- /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 9ecb2ba364..2604ad2c46 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 68667ac5c4..bb2ed2e2ce 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 544bb92e6e..b81c518a37 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 62dcd8709c..b482468762 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -7,15 +7,16 @@ 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.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 @@ fish_taco = friendship_recipe(Meal.fish_taco, NPC.linus, 7, {Fish.tuna: 1, Meal. 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 @@ maki_roll = queen_of_sauce_recipe(Meal.maki_roll, 1, Season.summer, 21, {Fish.an 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 @@ shrimp_cocktail = queen_of_sauce_recipe(Meal.shrimp_cocktail, 2, Season.winter, 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 @@ tropical_curry = shop_recipe(Meal.tropical_curry, Region.island_resort, 2000, {F 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 @@ frog_legs = shop_recipe(SVEMeal.frog_legs, Region.adventurer_guild, 2000, {SVEFi 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 @@ void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, 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 @@ pemmican = friendship_recipe(DistantLandsMeal.pemmican, ModNPC.goblin, 8, {Loot. 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 8dd622e926..24b03bf77b 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,16 @@ class SkillSource(RecipeSource): 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 0000000000..7e9466630f --- /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 0000000000..ca54d35e14 --- /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 0000000000..d0674f34c0 --- /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 718bce743b..70fb110ffb 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 @@ carpenter = (Region.carpenter,) 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 @@ scarlett_loves = goat_cheese + duck_feather + goat_milk + cherry + maple_syrup + 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 @@ harvey = villager(NPC.harvey, True, town + hospital, Season.winter, universal_lo 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 @@ linus = villager(NPC.linus, False, mountain, Season.winter, universal_loves + li 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 @@ sophia = villager(ModNPC.sophia, True, bluemoon, Season.winter, universal_loves_ 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 c29ae859e0..0ed693031b 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 74caf9b7da..c672152543 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 78170f29fe..e1ad8cebfd 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 == ExcludeGingerIsland.option_false: - early_candidates.append("Island Obelisk") + 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.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 d0cb09bd99..cb61020169 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -8,18 +8,20 @@ from typing import Dict, List, Protocol, Union, Set, Optional 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,37 +693,31 @@ 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: + +def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): + if ModNames.sve not in options.mods: return 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]) + + if options.quest_locations < 0: + return + items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items]) if exclude_ginger_island: return items.extend([item_factory(item) for item in SVEQuestItem.sve_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: - 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: - return - items.append(item_factory("Special Pumpkin Soup Recipe")) - - def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random, available_item_slots: int) -> List[Item]: items = [] @@ -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 103b3bd960..43246a94a3 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -6,17 +6,17 @@ from typing import Optional, Dict, Protocol, List, FrozenSet, Iterable 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 @@ events_locations = [ 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 ae12ffee47..add99a2c2e 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 820ae4ead4..dc5deda427 100644 --- a/worlds/stardew_valley/logic/action_logic.py +++ b/worlds/stardew_valley/logic/action_logic.py @@ -5,10 +5,13 @@ from .base_logic import BaseLogic, BaseLogicMixin 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 @@ class ActionLogicMixin(BaseLogicMixin): 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 @@ class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLo 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 cdc2186d80..23f0ae03b7 100644 --- a/worlds/stardew_valley/logic/artisan_logic.py +++ b/worlds/stardew_valley/logic/artisan_logic.py @@ -3,8 +3,13 @@ from typing import Union 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 @@ class ArtisanLogicMixin(BaseLogicMixin): 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 @@ class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMi 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 9cfd089ea4..7b377fce1f 100644 --- a/worlds/stardew_valley/logic/base_logic.py +++ b/worlds/stardew_valley/logic/base_logic.py @@ -2,6 +2,7 @@ from __future__ import annotations 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 0000000000..464056ee06 --- /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 fee9c9fc4d..0000000000 --- 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 7be3d19ec3..4611eba37d 100644 --- a/worlds/stardew_valley/logic/building_logic.py +++ b/worlds/stardew_valley/logic/building_logic.py @@ -15,6 +15,8 @@ from ..strings.fish_names import WaterItem 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 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL 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 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL 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 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL 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 1ae07cf2ed..4ca5fd81fc 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -2,17 +2,22 @@ from functools import cached_property 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 @@ class BundleLogicMixin(BaseLogicMixin): 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 @@ class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMix 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 @@ class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMix 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 @@ class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMix 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 ba825192a9..849bf14b22 100644 --- a/worlds/stardew_valley/logic/combat_logic.py +++ b/worlds/stardew_valley/logic/combat_logic.py @@ -3,10 +3,11 @@ 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 ..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 @@ class CombatLogicMixin(BaseLogicMixin): 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 @@ class CombatLogic(BaseLogic[Union[CombatLogicMixin, RegionLogicMixin, ReceivedLo @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 51cc74d051..46f3bdc93f 100644 --- a/worlds/stardew_valley/logic/cooking_logic.py +++ b/worlds/stardew_valley/logic/cooking_logic.py @@ -19,8 +19,8 @@ from ..data.recipe_source import CutsceneSource, ShopTradeSource 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 @@ BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]] # 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 @@ BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]] 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 @@ BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]] 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 8c267b7d10..e346e4ba23 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -13,12 +13,11 @@ from .skill_logic import SkillLogicMixin 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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 8c107ba6a5..0000000000 --- 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 b255aa27f7..88523bb85d 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 @@ class FarmingLogicMixin(BaseLogicMixin): 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 @@ class FarmingLogic(BaseLogic[Union[HasLogicMixin, SkillLogicMixin, FarmingLogicM 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 a7399a65d9..539385232f 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 @@ class FishingLogicMixin(BaseLogicMixin): 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 @@ class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, Region 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 @@ class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, Region 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 0000000000..ccd8c5dacc --- /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 0000000000..3b4d41953c --- /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 d92d4224d7..4331780dc0 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 @@ class HasLogicMixin(BaseLogic[None]): 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 @@ class HasLogicMixin(BaseLogic[None]): 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 @@ class HasLogicMixin(BaseLogic[None]): 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 a7fcec9228..74cdaf2374 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 .animal_logic import AnimalLogicMixin 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.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, 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.tool_names import Tool, ToolMaterial 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi # 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi 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 14716e1af0..87631175b3 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 0000000000..9af1d62257 --- /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 2c2eaabfd8..61eba41ffe 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -3,13 +3,15 @@ from typing import Union 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 @@ class MineLogicMixin(BaseLogicMixin): 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 @@ class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicM 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 @@ class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicM 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 92945a3636..73c5291af0 100644 --- a/worlds/stardew_valley/logic/money_logic.py +++ b/worlds/stardew_valley/logic/money_logic.py @@ -2,16 +2,18 @@ from typing import Union 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 @@ class MoneyLogicMixin(BaseLogicMixin): 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 @@ class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMix 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 @@ class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMix 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 @@ class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMix 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 @@ class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMix 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 790f492347..7e6d786972 100644 --- a/worlds/stardew_valley/logic/monster_logic.py +++ b/worlds/stardew_valley/logic/monster_logic.py @@ -4,11 +4,13 @@ from typing import Iterable, Union, Hashable 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 @@ class MonsterLogicMixin(BaseLogicMixin): 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 @@ class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLo 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 isinstance(monster, str): - 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) + + 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) + return region_rule & combat_rule & time_rule @cache_self1 @@ -48,13 +55,11 @@ class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLo # 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 @@ class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLo 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 59ef0f6499..4ba5364f55 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -6,10 +6,14 @@ from .base_logic import BaseLogic, BaseLogicMixin 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 @@ class MuseumLogicMixin(BaseLogicMixin): 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 @@ class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogic 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 @@ class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogic 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 5d7d79a358..0438940a66 100644 --- a/worlds/stardew_valley/logic/pet_logic.py +++ b/worlds/stardew_valley/logic/pet_logic.py @@ -6,11 +6,9 @@ from .received_logic import ReceivedLogicMixin 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 @@ class PetLogicMixin(BaseLogicMixin): 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 @@ class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMi 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 0000000000..54e2d24265 --- /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 bc1f731429..42f401b960 100644 --- a/worlds/stardew_valley/logic/quest_logic.py +++ b/worlds/stardew_valley/logic/quest_logic.py @@ -17,6 +17,7 @@ from .time_logic import TimeLogicMixin 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 @@ class QuestLogicMixin(BaseLogicMixin): 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 @@ FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillL 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 @@ FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillL 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 @@ FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillL 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 @@ FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillL 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 66dc078ad4..f5c5c9f7a2 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 @@ class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin): 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 81dabf45aa..69afa624f2 100644 --- a/worlds/stardew_valley/logic/region_logic.py +++ b/worlds/stardew_valley/logic/region_logic.py @@ -4,7 +4,7 @@ from Utils import cache_self1 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_accessible_regions_without_er = {*main_outside_area, Region.community_cen 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 @@ class RegionLogic(BaseLogic[Union[RegionLogicMixin, HasLogicMixin]]): @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 fb0267bddb..61e63a90c8 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 .received_logic import ReceivedLogicMixin 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 @@ class RelationshipLogicMixin(BaseLogicMixin): 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 @@ class RelationshipLogic(BaseLogic[Union[ 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) + + 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_() + + 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_() + + 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: - 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) + assert hearts >= 0, f"Can't have a negative hearts with {npc}." - if not self.npc_is_in_current_slot(npc): - return True_() - villager = all_villagers_by_name[npc] - if self.options.friendsanity == Friendsanity.option_bachelors and not villager.bachelor: + 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) - if self.options.friendsanity == Friendsanity.option_starting_npcs and not villager.available: - 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 0000000000..87d9ee0215 --- /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 1953502099..6df315c0db 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 @@ class SeasonLogicMixin(BaseLogicMixin): 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 @@ class SeasonLogic(BaseLogic[Union[SeasonLogicMixin, TimeLogicMixin, ReceivedLogi 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 52c97561b3..8d545e2196 100644 --- a/worlds/stardew_valley/logic/shipping_logic.py +++ b/worlds/stardew_valley/logic/shipping_logic.py @@ -10,7 +10,7 @@ from .region_logic import RegionLogicMixin 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 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil 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 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil 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 35946a0a4d..4d5567302a 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -4,7 +4,7 @@ from typing import Union, Tuple 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 .season_logic import SeasonLogicMixin 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.quality_names import ForageQuality 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 @@ class SkillLogicMixin(BaseLogicMixin): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): @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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): @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 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]): 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 0000000000..0e9b8e976f --- /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 e0b1a7e2fb..65497df477 100644 --- a/worlds/stardew_valley/logic/special_order_logic.py +++ b/worlds/stardew_valley/logic/special_order_logic.py @@ -4,7 +4,6 @@ from .ability_logic import AbilityLogicMixin 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 .shipping_logic import ShippingLogicMixin 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.monster_names import Monster 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 @@ class SpecialOrderLogicMixin(BaseLogicMixin): 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 @@ AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]): 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 9dcebfe82a..94e0e277c8 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 1b1dc2a521..ba593c085a 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 @@ class ToolLogicMixin(BaseLogicMixin): 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 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi 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 7699521542..26704eb7d1 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.skill_logic import SkillLogicMixin 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 @@ CookingLogicMixin]]): 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 @@ CookingLogicMixin]]): 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 8f5e676d8c..cfafc88e83 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.base_logic import BaseLogicMixin, BaseLogic 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 ...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, 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 @@ class ModItemLogicMixin(BaseLogicMixin): 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 @@ RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, To 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 @@ RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, To 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 @@ RegionLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, To 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 1840228385..32b3368a8c 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 typing import Tuple 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 40b5545ee3..1aa71404ae 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.food_names import Meal, Beverage 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 @@ class ModQuestLogic(BaseLogic[Union[HasLogicMixin, QuestLogicMixin, ReceivedLogi 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 ce8bebbffe..cb12274dc6 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.relationship_logic import RelationshipLogicMixin 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 @@ class ModSkillLogicMixin(BaseLogicMixin): 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 @@ ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): 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 @@ ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): 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 @@ ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, MagicLogicMixin]]): 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 e51a23d502..1a0934282e 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 @@ class ModSpecialOrderLogicMixin(BaseLogicMixin): 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 @@ ReceivedLogicMixin, RelationshipLogicMixin, SeasonLogicMixin, WalletLogicMixin]] 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 1254338fe2..fc093554d8 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.season_logic import SeasonLogicMixin 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 @@ class SVELogicMixin(BaseLogicMixin): 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 @@ class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixi 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 a4d3b9828a..54408fb2c5 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 df0a12f6ef..c075bd4d10 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 @@ stardew_valley_expanded_regions = [ 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 @@ mandatory_sve_connections = [ 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 @@ mandatory_sve_connections = [ 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 @@ boarding_house_entrances = [ 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 @@ boarding_house_entrances = [ 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 50709c10fd..d0f052348a 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 ba1ebfb9c1..5369e88a2d 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 e75eb5c5fc..e663241ac6 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -3,9 +3,12 @@ from typing import Any, Dict 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 @@ all_random_settings = { 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 @@ easy_settings = { 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 @@ easy_settings = { 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 @@ medium_settings = { 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 @@ medium_settings = { 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 @@ hard_settings = { 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 @@ hard_settings = { 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 @@ nightmare_settings = { 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 @@ nightmare_settings = { 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 @@ short_settings = { 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 @@ short_settings = { 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 @@ minsanity_settings = { 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 @@ minsanity_settings = { 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 @@ allsanity_settings = { 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 @@ allsanity_settings = { 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 @@ allsanity_settings = { 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 eaabcfa5fd..bd64518ea1 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 @@ class RegionData: 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 4284b438f8..2aca2d3f4d 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -2,11 +2,11 @@ from random import Random 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 @@ class RegionFactory(Protocol): 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 @@ vanilla_regions = [ 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 @@ vanilla_regions = [ 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 @@ vanilla_regions = [ 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 @@ vanilla_regions = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 @@ vanilla_connections = [ 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 b0922176e4..65e922a644 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 8002031ac7..c30d04c8a6 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.logic import StardewLogic 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.artisan_good_names import ArtisanGood 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.tv_channel_names import Channel 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) + 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): - 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) +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 - 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)) + 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 + + 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)) + 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) + 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 1dc60f79b1..c181faec7b 100644 --- a/worlds/stardew_valley/scripts/export_locations.py +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -16,11 +16,17 @@ if not os.path.isdir("output"): 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 007d2b64dc..576cd36851 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 @@ class AggregatingStardewRule(BaseStardewRule, ABC): 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 @@ class Or(AggregatingStardewRule): 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 @@ class And(AggregatingStardewRule): 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) - def evaluate_while_simplifying(self, state: CollectionState) -> Tuple[StardewRule, bool]: + 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 __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 @@ class Count(BaseStardewRule): 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 @@ class Has(BaseStardewRule): 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 2bbddb1681..17433f7df4 100644 --- a/worlds/stardew_valley/stardew_rule/indirect_connection.py +++ b/worlds/stardew_valley/stardew_rule/indirect_connection.py @@ -6,34 +6,38 @@ from . import StardewRule, Reach, Count, AggregatingStardewRule, Has 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 58f7bae047..93a8503e87 100644 --- a/worlds/stardew_valley/stardew_rule/literal.py +++ b/worlds/stardew_valley/stardew_rule/literal.py @@ -33,9 +33,6 @@ class True_(LiteralStardewRule): # noqa def __and__(self, other) -> StardewRule: return other - def get_difficulty(self): - return 0 - class False_(LiteralStardewRule): # noqa value = False @@ -52,9 +49,6 @@ class False_(LiteralStardewRule): # noqa 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 c20394d5b8..f69a3663c6 100644 --- a/worlds/stardew_valley/stardew_rule/protocol.py +++ b/worlds/stardew_valley/stardew_rule/protocol.py @@ -24,7 +24,3 @@ class StardewRule(Protocol): @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 0000000000..61a88ceb69 --- /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 a0fce7c7c1..cf0996a63b 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 @@ class TotalReceived(BaseStardewRule): 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 @@ class TotalReceived(BaseStardewRule): 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 @@ class Received(CombinableStardewRule): 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 @@ class Reach(BaseStardewRule): 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 @@ class HasProgressionPercent(CombinableStardewRule): 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 0000000000..a5cc10f7d7 --- /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 4ddd6fb503..0f311869aa 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 68dad8e752..6826b9234a 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 08b9d8f813..88f9715abc 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 ccc2765544..58371aebe7 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 a017cff1f9..366189568c 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 0000000000..3c32cd81b3 --- /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 de8d8af387..5f560a5454 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 74a77a8e94..83445c702c 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 295e40005f..fa7a77c834 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 5192466c9c..21ccb5b55c 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 00823d62ea..9b651f4276 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 73a9d3978e..b59b3cd03f 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 cd59d749ee..d94f9e2fd4 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 6e2f98fd58..5555316f83 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 24127beb98..c7dae8af3c 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 f9be78c41a..d9e249a335 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 16511a5bcb..797a42b737 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 bf15b9d01c..7798c06def 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 c42e7ad5ed..df2cacf0c6 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 2c02381609..6370b8b568 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 0fdab64fef..9cedb6b8ef 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 f3659bc87f..1c4971c3f8 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 398b370f27..f2799d4e44 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 bae4c26fd7..7f3a61f2df 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 ea8c00b9bf..761f50e0a9 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 28f09b0558..32655efe88 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 0000000000..3ca52f5728 --- /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 cd6828cd79..091f39b256 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 @@ class TestBundles(unittest.TestCase): 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 a77dc17319..86550705b9 100644 --- a/worlds/stardew_valley/test/TestData.py +++ b/worlds/stardew_valley/test/TestData.py @@ -2,19 +2,52 @@ import unittest 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 0000000000..f78edc3eec --- /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 0000000000..0bfacb6ef6 --- /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 0000000000..c5d87c0f8d --- /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 0000000000..842c0edd09 --- /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 1b4d1476b9..8431e6857e 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 @@ class TestBaseItemGeneration(SVTestBase): 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 @@ class TestNoGingerIslandItemGeneration(SVTestBase): 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 @@ class TestSkullCavernLogic(SVTestBase): 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): - options = { - Friendsanity.internal_name: Friendsanity.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 run_default_tests(self) -> bool: + # None is default + return False + def test_no_shipsanity_locations(self): for location in self.get_real_locations(): with self.subTest(location.name): @@ -779,6 +398,7 @@ class TestShipsanityNone(SVTestBase): 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 @@ class TestShipsanityCropsExcludeIsland(SVTestBase): 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 @@ class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase): 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 @@ class TestShipsanityFishExcludeIsland(SVTestBase): 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 @@ class TestShipsanityFishExcludeQiOrders(SVTestBase): 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 @@ class TestShipsanityFullShipmentExcludeIsland(SVTestBase): 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 @@ class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase): 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 @@ class TestShipsanityFullShipmentWithFishExcludeIsland(SVTestBase): 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 48bc1b1521..671fe63872 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 @@ class TestItems(SVTestCase): 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 @@ class TestItems(SVTestCase): 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 84d38ffeb4..65f7352a5e 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 39be7d6f7a..2f2092fdf7 100644 --- a/worlds/stardew_valley/test/TestMultiplePlayers.py +++ b/worlds/stardew_valley/test/TestMultiplePlayers.py @@ -19,7 +19,7 @@ class TestDifferentSettings(SVTestCase): 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 0000000000..ef552c10e8 --- /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 d13f9b8a05..2824a10c38 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 @@ class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase): 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 @@ class TestGenerateDynamicOptions(WorldAssertMixin, SVTestCase): 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 @@ class TestGoal(SVTestCase): 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) + 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) - 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_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) + 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 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase 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 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase 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 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase 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) + 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()] - 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 9109c39562..d953696e88 100644 --- a/worlds/stardew_valley/test/TestOptionsPairs.py +++ b/worlds/stardew_valley/test/TestOptionsPairs.py @@ -47,7 +47,7 @@ class TestCraftMasterNoQuests(WorldAssertMixin, SVTestBase): 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 0137bab914..a25feea220 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -4,7 +4,7 @@ from typing import Set 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 @@ class TestEntranceRando(SVTestCase): 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 @@ class TestEntranceRando(SVTestCase): 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 3ee921bd2b..0000000000 --- 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 89317d90e4..93b32b0d8a 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifying(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase): 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 @@ class TestEvaluateWhileSimplifyingDoubleCalls(unittest.TestCase): 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 826f49b1ac..dc44a1bb45 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 0000000000..e1ab348def --- /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 1a463d9fc2..bee02f3c3d 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.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, - options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.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.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + 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.NumberOfMovementBuffs.internal_name: 0, - options.NumberOfLuckBuffs.internal_name: 0, - options.ExcludeGingerIsland.internal_name: options.ExcludeGingerIsland.option_true, - options.TrapItems.internal_name: options.TrapItems.option_no_traps, + 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.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.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.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, - options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, - options.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.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, + options.Booksanity.internal_name: options.Booksanity.option_none, + options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + 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 @@ class SVTestCase(unittest.TestCase): *, seed=DEFAULT_TEST_SEED, world_caching=True, - dirty_state=False, **kwargs) -> Tuple[MultiWorld, StardewValleyWorld]: if msg is not None: msg += " " @@ -228,17 +209,8 @@ class SVTestCase(unittest.TestCase): 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 eec7f805d2..baba9bbaf8 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 @@ from ...mods.mod_data import ModNames 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 b384858f34..a07831f73e 100644 --- a/worlds/stardew_valley/test/assertion/option_assert.py +++ b/worlds/stardew_valley/test/assertion/option_assert.py @@ -63,8 +63,12 @@ class OptionAssertMixin(TestCase): 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 f9b1239431..5a1dad2925 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 f9bf976034..0000000000 --- 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 1e5512682f..c1c24bdf75 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -53,7 +53,7 @@ class WorldAssertMixin(RuleAssertMixin, TestCase): 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 0000000000..32821511c4 --- /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 0000000000..7e7f866dfc --- /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 0000000000..fa70916c9d --- /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 0000000000..b9d940d2c8 --- /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 0000000000..4130dae90d --- /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 0000000000..804ac0978b --- /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 0000000000..e69de29bb2 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 0000000000..381502da13 --- /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 0000000000..40927e67c2 --- /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 0000000000..4065498d6b --- /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 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index 9f76c10a9d..395c48ee69 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -2,22 +2,25 @@ import unittest 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 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): 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 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): 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 ca9fc01b29..0c8cfcb1e1 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 66bc5aeba8..f233fc36dc 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 @@ from ... import options 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 f3702c05f4..6d4931280a 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 81ac6ac0fb..0000000000 --- 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 3be437c3f7..0000000000 --- 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 57bca5f256..5e7e9d4143 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 @@ class TestBaseItemGeneration(SVTestBase): 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 @@ class TestModTraps(SVTestCase): 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 0d453942c3..b5ad0cae66 100644 --- a/worlds/stardew_valley/test/performance/TestPerformance.py +++ b/worlds/stardew_valley/test/performance/TestPerformance.py @@ -8,13 +8,10 @@ from typing import List 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 @@ class SVPerformanceTestCase(SVTestCase): 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 @@ class TestMinLocationMaxItems(SVPerformanceTestCase): 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 @@ class TestAllsanityWithoutMods(SVPerformanceTestCase): 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 0000000000..fb62a45637 --- /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 0000000000..b00e4138a1 --- /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 0000000000..25d4c70b2a --- /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 0000000000..81a91d1e74 --- /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 0000000000..59d41f6a63 --- /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 0000000000..84ceac50ff --- /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 0000000000..43c5e55c7f --- /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 0000000000..35dad8f43e --- /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 0000000000..378933b7d7 --- /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 0000000000..1c6874f315 --- /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 0000000000..4f53b9a7f5 --- /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 0000000000..a1fb152812 --- /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 0000000000..77887f8eca --- /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 0000000000..e69de29bb2 diff --git a/worlds/stardew_valley/test/script/__init__.py b/worlds/stardew_valley/test/script/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..04553e3996 --- /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 baf17dde84..4b31011d9f 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 @@ if __name__ == "__main__": 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 48cd663cb3..aaa8b33184 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -2,10 +2,14 @@ import json 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 013/222] 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 e69de29bb2..2eb340722a 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 bd78edd9c7..591c61d748 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 @@ class TestDocs(unittest.TestCase): 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 0000000000..e9dae41dd0 --- /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 014/222] 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 171c82f9b2..67684a6f3c 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 015/222] 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 be32028463..09d0565e1c 100644 --- a/worlds/zillion/client.py +++ b/worlds/zillion/client.py @@ -347,6 +347,11 @@ class ZillionContext(CommonContext): "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 aa24ff8961..13cbee9ced 100644 --- a/worlds/zillion/gen_data.py +++ b/worlds/zillion/gen_data.py @@ -28,6 +28,13 @@ class GenData: 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 b4f554902f..d6b01ac107 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 016/222] 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 8689a51b76..0bd8c5e806 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -67,7 +67,7 @@ tunic_events: Dict[str, str] = { "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 017/222] 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 9f70165b61..50c316f3b7 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -325,6 +325,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, 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 018/222] 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 50c316f3b7..ccffc40b38 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -330,6 +330,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict, 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 019/222] 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 f93441baea..5b2aaa1ffc 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -558,6 +558,10 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: 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 set_rules(world: "PokemonEmeraldWorld") -> None: 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 020/222] 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 415d7e7f21..dfacee148a 100644 --- a/UndertaleClient.py +++ b/UndertaleClient.py @@ -29,7 +29,7 @@ class UndertaleCommandProcessor(ClientCommandProcessor): 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 @@ class UndertaleCommandProcessor(ClientCommandProcessor): 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 @@ class UndertaleCommandProcessor(ClientCommandProcessor): 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 @@ class UndertaleContext(CommonContext): 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 021/222] 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 0ba0f5b9a5..c6aeaa3577 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -292,6 +292,9 @@ blacklisted_combos = { # 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 71f74b17d7..b0513c4332 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 022/222] 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 696d4dbab4..efe460b503 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 e84068a6f5..b6bd49c187 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -5,10 +5,10 @@ from ..AutoWorld import LogicMixin 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 e96cd44712..71d5d1c7e4 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -6,7 +6,7 @@ from .Items import (createResourcePackName, item_table, progressive_table, progr 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 @@ class RaftWorld(World): 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 023/222] 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 856117469e..58d8fa543a 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 024/222] 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 85c0f9f7ff..cb4d1a7511 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ from pathlib import Path # 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 025/222] 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 500203a881..1409e2dc0d 100644 --- a/kvui.py +++ b/kvui.py @@ -836,6 +836,10 @@ class KivyJSONtoTextParser(JSONtoTextParser): 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 026/222] 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 4ca5fd81fc..98fda1c73c 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -27,8 +27,8 @@ class BundleLogicMixin(BaseLogicMixin): 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 @@ QuestLogicMixin]]): 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 4ba5364f55..36ba62b31f 100644 --- a/worlds/stardew_valley/logic/museum_logic.py +++ b/worlds/stardew_valley/logic/museum_logic.py @@ -41,7 +41,7 @@ class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, TimeLogicMi 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 94e0e277c8..2ba76579ff 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 576cd36851..3e6eb327ea 100644 --- a/worlds/stardew_valley/stardew_rule/base.py +++ b/worlds/stardew_valley/stardew_rule/base.py @@ -431,7 +431,7 @@ class Count(BaseStardewRule): 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 61a88ceb69..a9767c7b72 100644 --- a/worlds/stardew_valley/stardew_rule/rule_explain.py +++ b/worlds/stardew_valley/stardew_rule/rule_explain.py @@ -34,7 +34,7 @@ class RuleExplanation: 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 @@ class RuleExplanation: 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 @@ class 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 cf0996a63b..5f5e61b3d4 100644 --- a/worlds/stardew_valley/stardew_rule/state.py +++ b/worlds/stardew_valley/stardew_rule/state.py @@ -122,4 +122,4 @@ class HasProgressionPercent(CombinableStardewRule): 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 027/222] 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 1409e2dc0d..a63d636960 100644 --- a/kvui.py +++ b/kvui.py @@ -595,8 +595,8 @@ class GameManager(App): "!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 028/222] 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 f6f4ec7732..645977d28d 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 @@ booster_contents: Dict[str, Set[str]] = { "Solemn Judgment", "Dream Clown", "Heavy Storm" - }, - "PHARAOH'S SERVANT": { + ], + "PHARAOH'S SERVANT": [ "Beast of Talwar", "Jinzo", "Gearfried the Iron Knight", @@ -43,8 +43,8 @@ booster_contents: Dict[str, Set[str]] = { "The Shallow Grave", "Nobleman of Crossout", "Magic Drain" - }, - "PHARAONIC GUARDIAN": { + ], + "PHARAONIC GUARDIAN": [ "Don Zaloog", "Reasoning", "Dark Snake Syndrome", @@ -71,8 +71,8 @@ booster_contents: Dict[str, Set[str]] = { "Book of Taiyou", "Dust Tornado", "Raigeki Break" - }, - "SPELL RULER": { + ], + "SPELL RULER": [ "Ritual", "Messenger of Peace", "Megamorph", @@ -94,8 +94,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "United We Stand", "Earthbound Spirit", "The Masked Beast" - }, - "LEGACY OF DARKNESS": { + ], + "LEGACY OF DARKNESS": [ "Last Turn", "Yata-Garasu", "Opticlops", @@ -143,8 +143,8 @@ booster_contents: Dict[str, Set[str]] = { "Maharaghi", "Susa Soldier", "Emergency Provisions", - }, - "MAGICIAN'S FORCE": { + ], + "MAGICIAN'S FORCE": [ "Huge Revolution", "Oppressed People", "United Resistance", @@ -185,8 +185,8 @@ booster_contents: Dict[str, Set[str]] = { "Royal Magical Library", "Spell Shield Type-8", "Tribute Doll", - }, - "DARK CRISIS": { + ], + "DARK CRISIS": [ "Final Countdown", "Ojama Green", "Dark Scorpion Combination", @@ -213,8 +213,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Ultimate Insect Lv3", "Divine Wrath", "Serial Spell" - }, - "FLAMING ETERNITY": { + ], + "FLAMING ETERNITY": [ "Insect Knight", "Chiron the Mage", "Granmarg the Rock Monarch", @@ -297,8 +297,8 @@ booster_contents: Dict[str, Set[str]] = { "Golem Sentry", "Rescue Cat", "Blade Rabbit" - }, - "THE LOST MILLENIUM": { + ], + "THE LOST MILLENIUM": [ "Ritual", "Megarock Dragon", "D.D. Survivor", @@ -311,8 +311,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Card Destruction", "Dark Magic Ritual", "Calamity of the Wicked" - }, - "Special Gift Collection": { + ], + "Special Gift Collection": [ "Gate Guardian", "Scapegoat", "Gil Garth", @@ -398,8 +398,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Blade Knight", "Marauding Captain", "Toon Goblin Attack Force" - }, - "Fiend Collection A": { + ], + "Fiend Collection A": [ "Sangan", "Castle of Dark Illusions", "Barox", @@ -480,8 +480,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Red Gadget", "Yellow Gadget", "B.E.S. Tetran" - }, - "Spellcaster Collection A": { + ], + "Spellcaster Collection A": [ "Exodia", "Dark Sage", "Dark Magician", @@ -544,8 +544,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Royal Magical Library", "Aussa the Earth Charmer", - }, - "Zombie Collection": { + ], + "Zombie Collection": [ "Skull Servant", "Regenerating Mummy", "Ryu Kokki", @@ -590,8 +590,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "Fushi No Tori", "Maharaghi", "Susa Soldier" - }, - "Special Monsters B": { + ], + "Special Monsters B": [ "Polymerization", "Mystic Swordsman LV2", "Mystic Swordsman LV4", @@ -656,8 +656,8 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 @@ booster_contents: Dict[str, Set[str]] = { "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 d58223f2e2..3559e7c515 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 @@ structure_contents: Dict[str, Set] = { "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 @@ structure_contents: Dict[str, Set] = { "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 @@ structure_contents: Dict[str, Set] = { "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 @@ structure_contents: Dict[str, Set] = { "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 @@ structure_contents: Dict[str, Set] = { "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 @@ structure_contents: Dict[str, Set] = { "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 029/222] 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 319ad8f20e..657035a030 100644 --- a/worlds/shorthike/Locations.py +++ b/worlds/shorthike/Locations.py @@ -328,7 +328,7 @@ location_table: List[LocationInfo] = [ {"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 030/222] 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 bb2ed2e2ce..0c5a12fb57 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 031/222] 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 c30d04c8a6..62a5cc5218 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 032/222] 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 863e2ad020..7ea9590190 100644 --- a/worlds/bomb_rush_cyberfunk/Locations.py +++ b/worlds/bomb_rush_cyberfunk/Locations.py @@ -762,7 +762,7 @@ location_table: List[LocationDict] = [ '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 033/222] 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 07235ad298..1aba9af7ab 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -255,7 +255,7 @@ class StardewValleyWorld(World): 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 d824deff39..2fbcb03279 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -3,6 +3,7 @@ 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.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.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.region_names import Region, LogicRegion from ...strings.season_names import Season from ...strings.seed_names import Seed @@ -62,6 +63,9 @@ ginger_island_content_pack = GingerIslandContentPack( 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 2c687eacbd..917e8cca22 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -290,9 +290,6 @@ pelican_town = ContentPack( 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 0c5a12fb57..6e30d2b8c8 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 7e9466630f..4744f9dffd 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 74cdaf2374..fb0d938fbb 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 .time_logic import TimeLogicMixin 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 ..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 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin 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 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin 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 87d9ee0215..9356440ac6 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -9,8 +9,9 @@ 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 +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -20,7 +21,7 @@ class RequirementLogicMixin(BaseLogicMixin): 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 @@ SeasonLogicMixin, TimeLogicMixin]]): @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 0000000000..14fe1c3390 --- /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 62a5cc5218..7c1fdbda3c 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 034/222] 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 2d87140fe5..0991622816 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -208,15 +208,15 @@ location_table: Dict[str, TunicLocationData] = { "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 035/222] 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 4d6771a735..970063d585 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 036/222] 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 970063d585..e12ca02297 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 zcmbV#2Yl2;7C)1+Nj9DIzNv(sK|M~03UuNFCc{B6o&6_uG-mItZ z41eYB@Q_PFZV1~El5bnSa^n<5@&y}qyiFuLkw&h)&D{PHjU2Au(PIdN0 z)VH>G+A6!cHtf1`*OpzL=Viv`%rs|TELU!9U$uH;Kxy5@!-Z2`az(Pdq$E^4FQZeM zLY^0c2FW!kCCKhgDZ}@4N)5j0sSEHuH+3eyCsOC*+nSjs$E6KMswRz~dsA}dZ_*Nw z+Oj51zMGaUp5K{13E9c%NXmKXlksh>$dHH99Z1zA50g0=^+>JFn1Jt=F-iDNt1gsh zGA==?HIwS@$sB?2f$<6Qjm#BD&B&_4_o}S9;zoHUYdF$T+4Jzt8J!{D851p!Plyu- z<^JqUz#h-`(Y5B(0YV*Hmoo&Zn{sC3`(aKoz6rKjRL1QuaiqvTTV*V9;VNRGLc;T< ze8-kYI9xwY4$nYuse@y*T~g>QWx@v5UBM?Rew zjnpf7i}CeVotqzv?CShQ2D5e+(5Uz2X3Mz+#LA9>bMWo$u9BY?(3IE;zcATKU0qQ` z)HG{Dziq!BR6^k5$+p7P1V~oWX{NN88h-RmGb5)FshrWT2JH=! zF^x7E(UK@fln_H&Hy6n@CFw{Vs7#l)GVIJq8_(50N?sru22bBAP3p(>-%2rNBrryg zDHcy7ogjfjHF&1fm<}_2G<|F`dT_L~tc5yaQhZ<>&FfF&h=WtckHNQ-zX!&XT)Z}Z z9KNv=mgCz!p#Wch8Coe%PaxTF_GHMUi93 z6nT6S$<_yxrr@jdR_U1Bgxv1QG!2UzQ{+dJiJJ?jW~$y7M7ZpjLTsA7JVhR!YSo~F zQ)&VFb_z}6lC~5%^PD9{jp^m6@!~oDH5e2(l}5U7DveZ=YDMbqiJ7u%T9_O+jVhS^ z>6$hNxeq2~$-hsdb!o!$r&B-ApKiAL>~zSa{4_a3R?MLK3un;$>ye+HLG)WQQ;yEG zdx+(`>uvIta$@55C6|~T4$=fc9mG;UEl8=(oq4KKAD^Z}OZn}fSbctgl zzJGQQN4H;^B1c!1BNdgMCAU}6%6p)y9TS{c{T{U@YA515yS_j!uBCQtg<0~WTIy2$ z%#GBBZS)ZN;mofLq~JP2@9mx;FRELQ)Y-aaTtZ7?cYPzipVZG5d*yT|%?xRv6Hc1z zgR?SaQAeyC?Bcc0Qa8)xLWwCUPr0(Gxuv#50Kat+L3;zS@x}&HH>G7W@ZCN;T~3=F zj?|jj>1t`a2xEC!GdrIPeY8){fR>wG3aD`B%aXIM3~C4Rkk*#%In=0`SBcI2Wc1!TIQiv>Ed*G{c@{(t%Ydkwq?lJxtU0< zpPw#&Y>k%gxuoJWel*HLW#3#AgyI<7_O>P#1ee!@KBM(N^wqtbozb3h_e z9-dbTcxH8uOq`#^7}M}wKcA%dzWL`fBwbElFd8Y{P?X%ZfTWB%5x11qxUZ3gLy>J= zc!9Ur>|@HJ^8lmqOkWToA6PUPIavjSrPKFOEZgNn4Jbu`FDk zUQ7ysY7bdLYov5>fn2wQnx4HpN4~YpDu1_REONdsOA9Rsn*%oQ|j-50k(yX$pLn`|SvS5^pJ=+7ZL^ot z!uWP+k^I+k8e1TBJ#%4l;ff5Daa${zk(vbj$Tn&rw{0rEOWIoSbuF6C5Sr~U`FLxR z9JP|>xqPJu-}_gt!}oNBjTeD^6|I)jDZ}LPRnH-Hc2%BS@4zHhFhsh(Y*C(l${c_qFqAgAnJFUO3)WBv+qL ztF(QCjW^Z9=WjtySF73d992uaunpe}E?k8#SwUZ4NXs$4w?JNf5m`o`oS(~#eElMt zz`BbU;d{@;#Nv$2B$BP0N%U^nOj91;OX>Pv>TT=hbUEtcaQW-rG~}M@J%R5Pm+YVt zsOv?RsH4-iG*Ab&UrJiy_@z|!g|aM}b{TCN6ECC9_m0a*3P??+Ts{`5S(i@}Y4V!O zuMW$I9yF-fGhC)$VHY;J@QMskEblp(W5CDBeOHVEJom~ZnR6(CQ;D*7Yci)QfZKMZ ziCZhLy|NJSBUerY{KG4A4H`{y@Kqxjo>Sz!+Ea`Mof~1W^~Ji|+SasfusJ$6ZfNaX z-nPr5JF{3`an;xi8{zeZxEdTzHj!JezIyATT~`wYS-Q7SetT82(LlRQ+B$+eG*Bmp z1bL`Wsq)pWnT6iQdpz+$g9cr^t1rTFe!F368_jQ#saH=Ks+$k0snzzm&IZ@HS~%Zk zbY?3$bJ5j>Mn|^GyRV*{sZm2+jg^kZU-bN^tFsgJC~wC2`m$e?&=|1tU1wPOGu z(l?fA6=d~zHt~Rq`yxth?w-!`S~sjS8Gcgk?JFEwgbvLLx?)%3E=OY=6AJGTzw!cxCt7@DmCXRD3vovY08eJv+%n+|SVux+d;k_WftM(HUCj=^}+)#-0U zf;_uzI55(;XU6%-$mK*;9!O1(Fk~YpMz(LS0OI}I)44N|Rq~rwJ;#empb0 zQ1)GK%4ebcHJ~Cjfi%&P<~lGv$GuCU$5GM9^)Bul)DU5t-hR^@Te&wb@!) zJ334j&6U&TI5giQN9Fm@VrsfNI=arY)d2In)=ehza`^{IEBzxmD#wQ$iNozmn|=B6 zw(f2dbFG}XD?bRG&E1k9*X_zgrR#Q$WI$z>d~H_(C21g0H;_s>d`~H@)=~G3vU%zD zcz(xo+ZRGB)g;^}Z@*z|vSv$ooy*;1D=r;sj`fopvV8TsJ%1XY{%$$!#;Fqww%Y3r z9Rs$q(s2S3u+II|R=4MKx$nkogW-3}r*5nhHf6U7tX;LhCNe^qh+KatNiI=^#omFy zL9n`gV;iKiqrG#*26zg*Y^m^T z@>#k5ri#I4=N)#fcfiR%-c;jZI#W@!UMjzjE0Cl|PWb zx!GYhT<5S?nNtwCdng2R+-`Gy{97*F-Gae9vfET{se7Wsjb#`sNA8)4!t?jg;yJV@ z+bD>Mm9OlXVJg%LOtYSqz^!1CcL}cFTLUV;-Ag*}%*mw8hMo=UI>;ZhVMBY{22*qj zW3l9lWa*w%IpG%C(q`Rq4k~QD#oVw;<>6Zzr)X}5I$e!THO8n|I4mu-&L%^%8}brf zS!=I%Iem+5>b|si@4R<=u4K-sb>E1jaAU?C^74HZX#3fjFb_OXaHv>@#%Z;Wf3D4!`xqY%qXh%thx99BTsQ z;ooJ-jlW9tErK4;yUdKfD8nOG)j>WmgDXlNxGh1w@p};^U;5P~w;|B(2D?}5@$zhP z*z5hLB&5mV2mnOX);HLjY8}odf3#qSy-9Z5o-@R6YCh1tiELWkzF~zepfiWinR&fN zADk|ymOW$2gWb&mErwP#JDv0VTMMnPt*!}ZKg4aX3LpZ2p=V6RQ(*!O1EWuFx_$64 zzyAKj{Vnc`CO=_!`-(R0B{cZ=f_(7yI#VlFxtbgOW=*EtF*4mtsn6ox*xJ6qtAXB- zi|-))zv~WKx5w@wCcRMd(4bE2GoXWmdR@ovw! z5K>ab1JCk*c_y4Nrw`C>C-M_Z_?||<1^Ea*zl%gr}sbBv; zlG8ij6!8D!drSkbSw`JEAHWpsmk5QA*>O14sP}}t_V6OK@vp-TmT)9jQLHgkua^*y20eI=pM^43Qk}!Wm~(o3f;eM7i;SA?W|!2eN5q zM=wZ8K#NS;PlKql&xus|(*v=1Jf=YErKa6Vst{hMK^X?u{) zTeb&na_xiBoXVAZAB;3$Cm*Ce=*-DP8UB#DpG=WC4^i(c9-89Q6Fr_Cp=`_ytj((B zT@N`Y>H&sU)i%0Kk>tI`;&xPE&kazyO*OTRCPa>W*iVAGJ$vNm51SVGLb>H(lG4W> z9_ACWZqMC*D64^zDDxg6JGbVM0JKN_&^F3FkC5Ix{m4*%O3(VCTq5%yodT34j|NbA z!w;oTKJe%SpnUjfu|JiM{ZMX_Q+`WS&iicul^^|3ZkNw8isi8pzSZCDiLuDuN6bC{ zFb}KiF&fqlj}7z3%kjs1obi5mtT00JKcu!%Uh>;qfBX`E{F8Fy<1~W%9v|s%`8X{! zzqkg=G!2zM%a|wT0de&c;~*sao)}_?)*1Qy6P19+9~%ky^kYQ^{A1a1tR>r!@@8j+ zYt8_t>UWQsy5XChPm;f4^pj-LK-Cz>EHRjEzV*ja<=sz`&G-0|q|d&4(lj-r2Fsk| zbVgiq+&rZ1J0553B`JgDL&vL3-B@1NZ0_P-RpozX8rbSAl07FJ<~h!f1dc{H34M5C zXu#>LpGRbz9R0h7Qq2U5tI_VPHhX}j&=*nB)w5yarq&HBcJ+l;H#)qB8JJ&Vvb_qg z&+719D;fqv_0FewOFS?2APGGwI8NPn&+AJLT(7 zlQ%8%WNN@TkI0^rr1)<+S&`5AxQ>oA5H+k(FbYu&RpW1YN9>!d)&Ka(aOd=>{< z`O>pVruElU1JSY-`?i(;Je%z&U7=NwLhqjUIT6ZcLJiN+#_+!gZGA2~pTx8;%-OSc zSsSSEb{bmMey;C~3?I!i&lQ-9kc?YH^~;~1quEM-J_p~)&kq3$mpwnkSh}eRa>w&E zrZaKAqtV5+Oa^LaVu4J3!JLZ%S^2_P5IX;bvNYe)358|n>&htkhZhRHJ*k~zE=E4? zq`pXdD-J$!;Z-Q|V;E_R4B7Q!45ZUYTIBv03kMpzg$->lJ7>y&zF1_MO77aZ=FqcW zf?=Sw(EOJSy6JIY!0>A~T8^5XEPwqHO~au#)8vaUB@Eaks|RnB?K0%l5YsQEb`A&dmPLZi_=2XGhUkcs;Q|@f<^zpe*t~gy}cERCy zoZ>FTg!+S`WyywQ`NrvJwDzylHhirwXW?7?G97Vey_}8jnNyka%9oQMAbVdn9XLnj ziI>aFxpdZgPkG!#Y=|EOy_I%bg7o~!3R=!T6={l5{x~xGrIY}GI%0wK{5n@Td{0h5 z84R#u^kveY#~G{dodm3b5@~-WSziBVuRq7n=_lX+Gwqrgub3wEC-R(EXun(Y3TM+}YgI%;lqAXZuO%gT$JOo0&@~6kEw3j;8=GfMBBpDI-0^0TeDSr3M&mN)^$h=t z!~H6nHc*pX{rVvcBJ~Z(kh-K*(4X36g3pP)?i6|Z8)^P_;1v1H8>W))kRQE43>f}q zmOtNH+6LhJxw8FDY=v0#BmIr;od%1es=7Ad8eoOZ-?4hB41KH8)R%6T;Y#$|N9F#v zO*6VrZh5Pk#(l~16m_gi#LCa!vX1g{t;citf3eA)u>IQVR zYye8#o=-HT2#&(?C-fK*%9C3bCd-j$MuVq|&d}a|?io{${88>XGgA+uYe};F`3$X@ zjCakm>C1BZyOqX*46S$68TP|~otPHEhoWD0;w|~qyX6&zNz&wMFt@V)tEk)4WssRg>w~mA)aYx>rnH3yNxVu`t zmn6mVhWF`^^TPYK;eI?C&d9h}!+Hwz$Bm^y7RuazX3DaElo&4MkSbTBwipJO{l0K# z+j#+Y=~SXoz%(BF$EdN!>^mL$B61)SmZmP~%@w9owOVHWb917heH;CD(P-JQD)q+o zB2ND0pNYOn@9|91qlb%j)!Mesz}wL|)SXg!Vs)OJ^ub2MHVkiUrW+WqX=qZ;9Oh>r zo9ngPvrN+uxX@{ndHITZY~9{}nOAq~G-u<#NRlW1HDQ$=T}YL0 z|EAXZLd)yym8NyZ5I;YYfr0JFbQx%j+KPZ0A&oAV>3ERqKTD9dkHX~zAI3or%-o$H z(vi~4z49R$fJScTM`R}Hj1MV7Hhg6D!o%gcANiB})kn0qo%|@@FbnRbu@=erm_!-* z@nq8+cg%xr+0?bp>)l%TajN014#%kH)z!`l7zgflOy8C1ZH5hPUiR?NLn`g_0ss+C z5|^ghKreMDLk$xY75kE#R<|MGbz{5C{3JfI!R0nDOl0gQ#TaJnCt0y!m5$m9+#{Hl zD`3&+l6jiPT@@hm-cO<#KRdGAUT!+WP?Ej#Eb)s_iq95-JFc^)Bz&y76W5t6cb=VS z`s@8Vp@&v)udDNQ+1b~xKa3R6ABI>O9S!!{0Qm^<>zO89=PEac6^&z$v(HlQYH`$= zoW$&BM!6fX^31WX|13l9`&3IwhJ5BzGRz6~nVE_Tfje-PKl-%K45X-#CZCX_)(n3` z!KU5`5-En*j7-MV6iV?~qWtPJlQE(G9pUbWH#%@}=XRLg)nlq}t}rK21FCAhvua@Y zdZ+<4wSgS=?`ZU(cv_C|&pAylM9UYW(_{Jq)+~YKPoGDVYDY4<&wsgcq4NvBLcf*D zh5NqHXDP$7EfBm31gM#nV2Tu9Cd(hbAa|tg%i*zpn!GA<&>-au3Q9IDwGHz8FJ}Pn z(JyH`$2Fb&{!7#3+$^)c8m@PIXLqVx@l`EQ4t{0&O8ewrzaj(qh4<6s@UJsbR`a!4 zcC*~@_0;i((rW1^nIW?r4!yM->(o+iudQ=nB|2AD-2j$prrcE=6)f#uC zM%_@{npT*S#Pbvscy}3E;c9j^$ANamyz^a- zRo_mBqzoD)M|@v^|B5GTE(bdd^UWAqB=1)Uw`i&{XUSMJ@R*pZc-!}>{#QiKj*R#ZMOfiZ%NRty?D$Us2C?Hmt$_QktVW|^K1-q!ABZg3{6lS) zSE>UiaSBF*?YqH}D*yDuSktfe%UC7aZQ!2!*@2XmP>zhkJJ1J zt#LI6id_hdV;YA5u_8KOfAr_{Xn6OSac?m?EE>|kgQOaB>#QNQZjx_ z*|dqj6iwO1SAdzRRzE{+bL84)nG0Ek=n&uN*Kpr$GJ$9t2&nyCVAf?k{ELDw&@GxyNU4BlO@G}AW zV#vX7+t_7mg2%tj8$4E?sxAo;L)HCZqO3vp)bfAn1@|kgsnOM3Ugw=Z>V8CJb3IrdT#5+_O4FzKzNY~w~FzoGuKUuIPdn7-Rxxd)(WhCpkoJJ6oM7<4&{Dp53|LHsp0j#= zCb*Zk<22d6vVHk4+2?}0<`<+?ag;C}D)*_nD1o~gwJ%Df&NF)DnmeyL zuvbwv^C}x%0Q#R!dEVU3)pq~*7-npQ0rj3_Rcy3KGLKaiHFd5=pY_c(EN)k2ZD1^r zg%0?Em(`&S&^M;2>M8UyUa3D}FtFXVfp&$Zs+mD4`UZ~ch13Jj6t&+*i!65K^M;1U zgY8`%U8^>EHz~tWc}Ce{MY4*E5fg@KPDPa0x~d!P4T1KGrNUm()a<3cK1L*)=cz_B z_RiQ`!kYpwgSc;@RgKMttJ&Ox>h1M7c^UoH1L&_gaRM-o76a^W$`UIQjJuqmG?g1G z>WnUhq5pFnbpiWUSb3vkPHj^F45oGG0HQ4118L@4f)bQ7G9rmim%sS%Y9w*wPNM3Y6T6Bp5NRM(MOsLV;1ak_IQAr{fRU49^-@(rjA~(ui$AobPBhvLE2Mfs#L&T)j2qjAC zT#=-HOyYS!%c)^}--&|J$)XChxcu5=k%TnZL9L*lY3j{n5rH&yR{fAHVvt6S>8h(z zgd?4Mo=8`tGemldFJeBT#?@r2Acq?@d^$tqspBb3<;Yx-sNPQDMjEDw%E3Jz4v0t8 zR0vM;ED81-A$Xot5ee$Ew~1V(Qbnxp0j>m6c>rJxjh~BC)3_MTYo-clrTBPPMj z9a}~{xGYUf0XCrUW~7NYq<371j@T{`36VG|+YsTv++`XS=^`E&rT1d9x;UK&jC6{+ zEnPFEUSx-dII$3kbmVmp@zwd0;&yoxQ>=Lv2N8^CvJ!N#}{y#H^YJb?tP$= zlL-? zhPfB_`NG*zg&K?0@$Gs6VbnHEgpO&)L~!gn5V@xux9-i*PKC_+Y*B$$>{vwVg=`)? zEhzO#Hp>LHj`)qwRcww(0Uk5HI)`VD(--9MByjp(okpD$^(T$P=^r&5Go7Xm-!GzK zrlsLo(V|`SZ?R|K@c2Z8`u`%-J(Ym(+63cnsZ1 zf$kU}^(-v7N@uV#a^rQJdvTu;&kmf~Lqu-48*7c1Dj~(`cy`d?xOL#19uA>goX6S{ z7&di99&-W9)D~7}ha1Hpq|W3q8>vEAoui6Z4)doZU#|qTrWWNh65zQJ4b6=Wup3c- zdp?UdR%MYorvU7F5@?}%ZcU&K)4-kCT)>^d%CV_C3Pfx_w$g30dTh0b0+%THj+snV zd4*yasMHsVarkptp_q(6Kd(ZgA50QS5Fj0s-WOWg=&)B($1uvQkZwewn>yWIB&MSh z`ae|v$yLu6vB>ba&3dHPRQ3=aH}N2hA4zFW6{8}M^DH%5tx-!Zk31m=@1?! zV>ZK=ipqqkrWKoWksC)5?-Z{YQt4`OdNIxxgL!(x;2NCQ%rx_)nK+$3l+^&*DdQAnMx$AWSt*4+by^@1$ zlIkiFk?PZtOj2)su6hSuCGfsMwZkgOCuFn{#t|mSFs+}wv}Tk-W?vp9l8Fm6IT53M z41R7or|qLz%BeUiT!+2;&)8^TF=Rx~;AmFeln#gOIfuD{9I*|pjZv4B@Mw`PRgaWl zbuqj&Djb%LerJSkQfYLA!)=G7qy+~$4Vq}+s(VUV*qFwLe(7OjxW~FS$Lg7q7XZ>C z3{Z*s+ZYyvZ^wux5I?X;Uxt)s%>>8)FDjJWC)FFvg z8@f6-5)07*n>sa4i|t&IlYrQDz3xqy6p(nwv5gmG8eTsU>!xA6Dd)E6)jgyo_}&1h zmyc&{dwjgeOT68ngv(0bG-7`(h@huwu75QyqYzEdkOlvs&@jn#T);j3CtaA z!^!H@1d)n9n5IO!%9_Y>+B#9>#8`C%dU^XQ<~3lWbbm=HVox5cKAy;=T4^(mouoHo zN|zXE+GHChvAwi(65AH|GtN*(oYZ=VlUfBKIV_xRELj&Im!euyMf~6%BiNmS*vV%C z*1#%FR?kjmF@0?^TTB0$EXor`g=&U-Bf|-I3Xa;^P)Y3ZjAIO6$htn}wu#;Ps{~ zG&t1t9zB3i9ky!15od0|Q9;iIbY4z;x93p}52z{v)T z>-M~%;n3l{A!2c51N&nG+uh)THv!mWJz#Er)Nt4s5xjZvw?Q->U^KUk<=cknvM z-7kj5SgV?GWYLkQz-8lSdKK?GoL*DK^$1t(uj0jU07q(dyo!%~e3F>sf}avtC^hcY|T76IZoR#h7YF5(v(`%`BDA8$UdsmsDvAiLX{Jjd zgGyL%xi&;mX2T)u6w5UZVI&)FidO)l_gIr3c&uSRj}Oxj&BmF6u9lsD%82_#8Nz;t zLL8r)DMkgo>g8(OEI|iLnglBZ6R`{2EQ@6$Ub!GZ1-&>x&J}VrSJQ0`0IY&);I(J3 z21%`YeHKqMr+?CErdkOXGOm887P~BJaBYS(w@_VMU9A_e?m$g*#Vlh%v&wVUHX#-O z*i1EYC%3>gajilx5e!1!J&gPnjV^tvAkFIUyuglK&&NsVwjzv~dZW(NvyTSypMIm0 zcOqjcm}e`DGzZ8amZENOvi3g!kou!ef8Hl-77EcvPdH8C`A8E^bOcFgwH=|~1Zr>z zveC38%tPD*^8}sA4hwG4%O83)C)hJJ%#?W)CaA1iVa<+g;B`vaR*Tc&s5IEdPKGMO zjYFk4|JPRQE+D6#Zs5hK>p|uFPF&e4V>a^>WjW#QX18`L5vJ;x&C8V1QHbbqRw5>Z zSHpqXe70kx4~^=xMNSkik9s?T%P`Q~M&>Ue!OGXiUdn`r)PNLo2fK?p+{m+msm%vl z$l3`DreE9u1+pd8NH=$aO`rKlNBgY+oYdF5MM*!Ezd%~Mq!LTB9t$=S#Gw$BwsxL7qS8=$X3<@5!p`^?w!wPSY!34M|xi`fV2t3 zo?)I3|F2u9`f&lD*+^YQO&nV`X5!eAab?sqgIcU=!2?(@#G5~`P>>h4d?XJUU)Lx`S0r0QR)>D_TBL&_lrvuzc7A;?jew!j4?bj ziRlY%aF8Z6HbV+-ry>f=f3j5!HqwZaw_0)JGL1bWYCkF%7b1*1Wf|LW6cmsCcP*nU zV8pTKGXg^7lio> zy$g?Yug|eCI!TY+sf zf#c0_OY262m-kfa^PNJ)VkqTmRhx+Jw`>5P>DPGXFN>KQRyBO3u*K^43pf}D_A5he zU&*?`>{OyUwo-(p1P+$&C-EB#&L*`Ve6{s>^rHrxj@ zaJjDP6G?OtuGdt5agI`F+C@sNetZEB9zCr9nsM~gFC}37H*4FDj(Dlv9^++0WUaFT zAw>8|3&XI%){O(8@mj(E0EiRY&&D`7<^?SfDfOX8-xblGpS4Dxp8*HDer*8Bpa%s& zr=3Ohu3@!>bAKg-dXA38_nu6s&uHFwWB}`eE7JK>E$9#tu*~U+EDa7IjKX+C5D5#^ zQBh;BFZW)5WhEfiP*b;HF1`5%+C@mG!*L@Fe`cF8(kTx>4z zGX%5X`E?@3|JY8Q)~^Gm4-76%k8n8o+K_iAp8_C09$t^DB%3<9UQ8UgcAQchM0SSN zUP;D~;eGSb@U_l}MFSIwSM}Yn%75MfMa{a?0(b% znxZah7x77SC&jl)0lmsSSBzFa_K0MEb`ZN!K%Q~#^@ejr!C>Qo1>%)3K;QW`rQ36! z(QA0J@S@Z7JqLOy?ATVw%8Cl7lZxptY$ey^9`9K7NTD? zpdFx&)HR0SQbGG=%qoM`rx&n2Xx2;Dn1!n0Le_%?7l~pxI}!W_N%+2Jgtu9dnNt2A?m){%baH* z0^9V}66ah_w?0pU9uAd8U;`DY`!C^fvZD!x7x#`bH*R6=WDui}I<$Zs-9Iui`<<_jN2xkTy#V^9PzVkP8V-#+_hg_ zCg>uIb@8j0iI~EEdW$2|$N*`;u4Wq1(ejh-G?jn34!hj9F*CX^qyjHCOdEU2V(w-i4^rE0^<8umN|TI6~Ff8+oVZk*&OfM@CUTYTFgeCEgbJ^#S;fOpbc< z3h$XVBD4vjt!)-~Na8(542cUdy`)@Y(UmMEl(x`Q1A`L0aP-cA#f|TE^a}&1PdHL3 z5?ABd;48u_0%VBeVf=;wnbNDYvxwU@-VB7c)Hrll1);(mS;Z4lY}Vny@bhgDz)5ec zirZ?An4@x-ClDy@vfr_l&r>*Xk&Yn_Ax1{OIq0*tz-8iStTWE-I3H@ad2qL9kUIP; zwyXe5Q_ibdTbT%vs`qMUtQQa#JZW4h4bKZvrvg#n>Y-@y0G#lU%6?5psM{uyEXbOG%{8go_bwS3|vX~az^y;VR7olV1n>E{)Yp=pGhfq%*r z*ve-URx_*~d40UrG4;su;2yh<&%7q16F|*OM_SFO zdLWxXD-IfVqidnKi9ig$rcAyRYC!GZ#uPce=-C}&aCivC3=x(WWHm$D0gT_yUS0gj zQ%kmc{n0?t!S$Lt1@q_tcDkGQh3wExK$Sxa0+a8(UL=OWm&xogAqi^n^-S3W(B@{Q z6L9D>4*@)Yvl^Gu)KLz1*S$dA=gdX9O?B>IMaHyfu*7Z-oAetlMLjWL>rUQbX{c5+ z=-nVpTb$QTZ^P|jufbz8INiXDvC>siV{?ViUddIT?PO&_ZSbvu-lKK@jeu>Vy6TdQ z%3UzSw9T^DW^wmuT;O)E))RfuTi)XaB%UI*%?kCqywgR&TfTdRTXzW(Bp&oDy9DWS zPS+3T)ktashg&oDI=s^5(jmqmY`;Nor4yl6uf2ik!%qV-#+>4}t*C9F*wouM@X-r_ zbwgC#jUoh1P}=ZpB1u;T4AHm0jCkA+aXjBQf9{8yO&h_5xjivDA_0_pAuZj8cMn7qATwh;R`^p39Q&^q1OdY;ibHH195|0jp zw%xd!wTh86!+^Y5qgNWy4B??tb+}(eqdz95(H%3Z<9E2?(mjDK2K2>Z8B^llH{^{- zR+{nHy*#Vwd&Sg#R{x5Nr-NIf0@Fz!Y zKfoI@kaE)88oM`9IO`O zK?d=P%t_CkdO9e5^PMaur|%R+hHgWWViuX@>33PDeFXgA}b{Ctr4hN?qV$sra6g3|#7uY0Iby)CyiVvK%9aLJenc!r7M zqlY*y1A9FlM8Ig_2_(T8dRR*|nbs|bd1yEu=S5ZGLfuy9s^)8Y8hC7^eNL_2Ns;XO zs+E8U+ki_3ZwHJsE^{dH2#?1n*|Uyt8@K|_iNcL@liQ43V@~Zk;$1b-5k|4zeQEi5 zL3+dE6`B2jcM4Sf1L8TcNu?cy>AhM_KPr;vcaK9f)oOfZ1<_SapZ{oRo+^e6c6EQDu9xc2|uU`A`FZU{o_mZOJdtMuQ9_&Q$+cksXd@-X9h zycGKVz_2b=xsQl6i;&V##`*v}8ui2T*uCq25V2t% zE?T2TJSfUE+8QrY)%8EX0cB$Bdr*vv)Yya(qQt`@NzRyRQQti%65@O%q09+2x*SLT zvWG;1?)Vy2|4;ydFje{>2vA(quYu)9I-E+9)msmlgI=S4cu0&b^fh7SCOpfvr^9$( z%f%h4>0vQMH`$@K_@xi}rQdj1;d{5pmAc=+)PFE<+zY zv+!BHe)V5hx3503dXZ-|!E6M3!b89H;K`&cIP=PJH?B+4seywAdSR@w-O!&&>2^EQ7kGn4h3# z{48F1Fgs|5CBA+#hevu`_&amZNy{MmpYnyz6iD@W8q{@`ph*#pRAw>C#?K7nGB;QB zG*ekPAl;rh3@P@^B~S!Fo_UnAQfj`+2n))NT0nq!uD?(@!h$-NEapm{rBsl}WLh<3 zIYE*bvVtMSo|ObjCUUDNl|rf2luD&kJEhVo<)KstrPipy;Xz}2I{>(Nm#359Ho$S$ zqao`FlFN_{8nTffc?>yML(V5i6hk&?$b|&SXUIhwvY8+S4C!S^v1bc`3IXz5nn=ka zN?t~(A(XmY{WUzuR(SLmKi3L1r`LQ4M*FAZ~^{t|7+=(!`J_HRJ?Ani=vt4f#Dm z<}lG6DE;Cx7+c8&9XR8WTfBLZ{~{f{ZN zmQtTksuL*$5cAKX&g>%aSpu)4)TfkMk5rH6Gi8qs8g|YX1ldSsUs9@vQeRQ(TuOaS zsq-lH4W-UUs>kzhBoU43`A*%1pAhK%p0l1G2;o8?Am-OV_?bW#QSCufkh+*_3namY zV1jIB^bifP5TutOVHy%akV_b1)sQHHT*{DW4T-HH&}9sY)1biwxtt*h8j?hiD;Scj zA*lqpk|AjfQSsm5#r0JT$vl=W~6#NHH_w&Ns!%Kah8VE o6J!rVoEp+VkiCH9cY9`QkefiaFjA9-%+r4sJiIh6=<}fe14$nIeE*iOHZf?N$Bm8TnZ$2;qC$i1QH;SoRC0@0@Cb&k+y-gfuPSW zO;ND7XP0KLh(7C6QQlKuQ~b}&e)oHKk@x;T-sgR@J3BkOJ3Bi&J3D*6eUItXU8aEb z0Xr_*7LaW&g?}SU7PT%aDH%O_#Hf~$MJ;8;3yVgLDO#{-{^-&34^_w_WaQkh-_-|N$7vNinkbnHZAMiYFO6&3JbRz;ZH zJNz!Xs4daP&y0u%@G}{n7Q)SS0J&jgA-~$30I6He)8Kp7TmavM%&G8|J(-rDz_(^r zMxS68U36Xl75#1j3HJv;m!BA#0cc2KcvcdmZY$5>OS6z!6LZt~Gg*TmH7!4epU=uR zsp|ZEc5=9_FQBrsv#bAFqz5?J$i!v28T_^EFi5?hJr}-6?c$sWNG-{k17FF+bp`1> zD;JqIF}E1L%W}uVcXIt?ex}gCcXh;aV;-_RC+~0Ym35}&_d;4S-ml`RF??5m6O>n217A7pYYGLV_pXb1sAy0>gX9Z?FM)6IkRtdtSETUum6?3^kP67% zR-Vj@Qgfq&t?i53dd%J=S-h`SIN@GY^{?%ZrK=q2S55WWH`)DDNyzhFnO+AilcLgz(~j!xyKa zvm`D;FkULII76iQ1<3Ig^Az?_{LkqcSb@GT`vRQjRx(s*oKjwOtTJCgXvmzf?~ zQx<{X8%FlP*KZW^deW#7@a>}Soug2oJ|0yD-_x>`WMW?(!uO4x2;c9h( zeCq@gUp5|Dvuk1| z3iv)SCRJ=b&y0M`B*fe|HdAI-O|nDwyRm6J(qiD>>xE~I0Pe8K*umA4RinozqqzPM zpTesxNTu0=(=P}6wxtEq=H!HjY~^m`>~5Qxzh8koJ72K~zEZSYp7pZMoy30|j?BTL z^y%UJ-l<`cC1z)D$7L;D%j6ominmw~0t$#{rjG#hg}d5X&GuzI?MvIcd)gQD+i<XGVpcH$v(&F9$Bu-ZaCYzi*Nt;2G} zPd1j!S5Ik#>>E?o!gt!#9+cSSMad}3{Or`9Bqlf3kA~Fb`a|&jTm5{3=JCsI*uZX^ z1HR{M(-;mQjW^pTL4J=N_2}W9of|Dj!LK*<-|#q zkf)p({0b*-2r7@dH_DmKpK+n~Q}r@L`eJ6_Yh0*U1{P1tj^PVGh~lx0IL3L6jz9E) zztN~vO!{Q%1jC{()IG(zh z8GO$o1HbX&36MK8C!hazG3s+qqjL%&+d5|z6=OeB=3WM0Mdt8aoODzLXXfIR%kFs^ zj^MlIMFM>PJe0PmW>hH>Cz$s%V;6QcqYCjfJ)s4eNdvn$gdb_aflX*h=1(*SbMt&+ z9`;o>X#!pt`R(%&si-NRhcA%5&*8QOxOJ;;Ub6tXvTtkxe{2CXhx|X*62!+YL=Nw2 z&JbIcv0%P-AqHI9gKQBYh z+NMP)L60wj#iYnQH9vtr--Oebo|MSvF2;3tXz?;{HG`uV*Pbt)`GiIA%iGc+`&)Z8 z|FmsCD1oi*!KeffF!_=={;#$aiTAv_l&>iZ=JPI@1I0>1eR&D?M=iqQC9wP+axLjV z3a2`A`PWNvOFDg7Dj(622`N_xDu<3``Fv<6D$J_RP4GR{iMv49vPa;1ZfXwydKs=V zzvZjpyKZ?ie9tULvDi8+i?8ZJO?0vg*ZJSOR>F5__qD;E<;_n;rtribHxwvS;gz1N zAXndu+sbwHeYW>;NUymRS0*i^hfZIbj(x^O8@b|jNPWB_AHG{VQhDY|6gcdcdnJxx zX}4MK?uS=i0a;nUZq;j8|Ip$XK6o{QvjkzK!L7-Y|_%p~=5 zdd+gkHLOK`?q7=wJ7b*{zH;}+ruMGumU&ry)OzI0wDmZUC*!jCKFYte{#nRxykZ-C zn=1xAWWBN;yMk)|zN@gppRYppo+?Y@X;c2`I?PJ zzp#rgDsV^h^lL4!-!)&G0ur+SK!!=eQ^WYdy|MhrwMkHT>e>SO#`5s%;wcr!4{VI) zb=RdrrS|JGG-5@3%XPU>{_u690e<$nObtGc$8Q))@Qe`KMt20XZ0qU2g102anB1~G zE8n=G1W=ybkOWnq-IK%rvmrsN-oTSK4xwf*)QuP@d46MQuA;5NLvupbMf~+gr zHPcONrH!X<8dqq<_WS&5YUMsFv;KAV8I*6`xN*ar{*AT`?g+kqQ?AyBOZfwv#%4>@ zK)a*T>LBD64cTi@C$i$zpw{#J>nA|>TCX3g4eHO=Cu;=z_!rkt2-OI>9F__z&wM%) z&>H%ZG!(&?^<_bI-Z$AZy1pn^SIecXOIjD2tsOmGEgcJ5`?b;C&i~mr0`LdlP$HX4 z>vcDSO~0nUpf9LcH!d{n1ANa7xrJfCi>ZFs^gH@-Mu2A{2I3tB3;^?@%^7JAYcmIU z9%DtL_cR~0IhQ@m8#c#j{eO`!*_`WLr)(hYV|>3a#>jyfZ}A^D=Z5O-_PQ7I%q{U+ z10V5;TPDGP*KR2pL<8~1f%FA6w6u5hw0A7-?x!&Ypd2VB{WXk#xTPE_rEg7*@RFHJ zfKejipDZOv8_&;SeCgH-Ks>Y+Me?<+Ia;e1@n5#$td?#Ysm(cFGfY!n7Y9(dyceYnVYjj+V4x1*b{M-&RpUOw( z=soX?oYC2}u-ojs4E~tuoT3e&nBU1OVl)Yx0W#}UME=8vqAcKb1DKb!tW?NT_$&Rm=6~oP1|vw{p*Z2<<9B2Oym?0v ze7Ehu^?&lY41Rh?G^Eb$zy_Lk#PWs_!8~nWEaYbG9A?&=>vi8tlH2DqdENY;oh9*d zNTxcw(`7CwE>xpBw=>OCzuW!T1?peHhlvS_t(JPL&ek;|DNJv>+x?y&U%D?%o1-oK zB~cehjq=Qcam*;@WwI3L@RS=T!jL!Ks1Wz?eK*_2gf(r78ShHG2KU&DFv zty7?I^{pfGb!PTOTNZba9X|JKP+BY9Ucw1-;X{G!qx7xCU*HBk1{ zF4SgGx5skB?kH_aLV5ab6f67g@le*cTWvIn{P1qaI8Eejc89A*8xd)I!;D&+OH}5lwN;!Po8lLO+o=@gY-S3iihqU86vR*D(y3D+BV?J3ualPT;7lXr=`k=2mtDR0Tl|MnGk(^&7IMMyn6nHoc_MB z@|Fdcbhj^T_0iXd`J;E%DdkpWZ*+Lg7f-osXtGYJ&*1E7Y47S*On;8gzYF#0jdvAh z>qxzA?O+3W`>(VqI?g}8Yh;3CO;D}5z}b3fYloU{UVL{VP?~c0$WWb9w>wo>9%gFS z{fysxH){Om9mDzgyMsMSrtd;i@?8WkxF?6uBm-jlwNs`f=aZN8Hp${)J-!nNg2Q_6SW=R?EUzG5JaEdJ7c|BHt2ZWR;K zxFn=PwFv@32;CD~EbAN0FMX3?~)Zr9vKWu_}GY@C`8N+zgs5E}_ z;fOdntUscowe*=py<$`hPkuN80D~UJGmrD(ETmvdPvlz&eA~mrfng^e&ej+i%>VW9 zAV8e=U{sZ4(CkwH)!x75%hks5_8o_z#+P<>>a_iU}; zn~$i4KaAgh#5P)X-=`5$q6Vk60(Mewof+t=sdXp_FFxue2i@)~`Ju-ZPMyCh z9Izb?#D2~TyPH4q*eGcI>|+Jq1mE;RS;r?mJ`PYWf83{opL?NP&yN#I@DoG4sr>AP zBKW)~aF{ne;X}p1PduTRbqD{BQ1YHE^rn*Fg>rzecoMsN@X4W`Gfua=%m8N`^Y1KO znJSO-=%<UB~LEnm{xYA$c->S}M*4TO)QU{&VuABV*8^k>lQFL?${7f>bIZt!&!-}=mOD0t=> z)L_3oldcu~J&I>PJ4qd`>@zj%-RlUB7GzRiST)$29Ru4vAH^Sgwn}Ns^14Q~cPkwg zNcx1c-HOj)eC2bxb?jZ?RAa& zqrWe(+F{ktAZTcvd_LZ@fnR9v9u^HGvLyHZ9D_RH#Naqy^XI5?Y|opx`yH@_=q`>* z!mLYLm$r8Fbek<*tu6iT542M6jSRSF=yg8@Lb4;mFICCNWd6;ct6(~cUMPj>cy>=N z-}HiFQ9Zx=h0#!U<^}W~`I@XV`S=$Lp={BMsH#xe-1nk7_AcPhzbMyxLI661sOtEa zRB;Dye94M~8@?!>KloCd_bC)CpRUC%t6IB!Y^Rm{o0r;CCC|ehwNBVDYC5~xS9N*} zn~s;02T26~Svu26bNM^%(l`aXZR4E&4b-CtUskT48~Ev$(M4u{CGih~ypgYb1viMj zuas+p{O2pm{{9n>e6<+L%3mEK%fbU(mv!p9%mKdbRjang`6~le2{SsMetb1W+nOK4 zSg!qcFN=*u5-78i|By2EDf zvDb5k%E1Mo{p;CE`b;&w^UaRV&ZYf*-bFn9SOyGX;<0?-@SOX3 ztVk0Ud(8u>lUq;Vl)rL9>BnOJ?TI{P9yx1g>gLf~2_gR8c`)4Oo(z|yA!Xnr1 zTzVw({U?KQO&o(el2a#RNl)|{ZM{q9x5A{5qsu^-)%qwJxam~B@(nnx({-tbKBn+F zr$Uh26>af+{i!HVGALM|e^nY3q#p-))EhZL9-m9p8)dXO2XrLR*52XaW;btpBTx0i z>UFS!e&k`p9ZTc+CvSwou)lx94BzlK)8ISgO+4A!-$c_Z>Wx&s>CHG`>Yg{#HA%iX z1_UsRB>BvnNouxiwfb2Pry?}K8&Ld~8Qa@7Hi<8J3#}ob>x;4XEofByZe(y({#FQJ zXg;d2J|9A{Zx=vcCcIsy_2otEOA$Zxc0BKYTleL7JMO-Dy9{b%yrV4UxA^3Dh5`M~ zcW{?`;GKMJD?P(Mcn4h~Deorb>EhY#e%UDfxTb+l-*031tankKH@~ZN@bPz}w1x0X z4FBp~RJ57zp+MT+QzsS1JKh^zAlF)84Y-`7y$I?@DL~XGC{(@fuZ{fpd&;dB%D;OL zU9Lmk&n?uu*B9x;!z}JBX3K&Ft=+l^Cd87qIrm;H-~2u(L%|lXoG}+{=ASgezHnz} zFPzQ>4l^4GW%Ir7$Mcd8U=i|pANW|4etgP@YPAjLM?Y}LwvxmT^H~rd`eBrp=>nG? z9^`R+$%h9}C^kD2M8kXri%(oVpXI@y`UMQngj+u<_O6`8kA0+Ca`GQOLT(KII1Sia z|FL549KQ5p+Q4A3ih~PS$mk0Wd8J8CYG_qw7u@@UN8ig)-HszR^WxL7{OT`K`NY$B z9I%~E@-`406rAE+s~c;9R)$);PosPtJ6-IVs9v}DUrny#`JdE6o#metd#U~4Ken=o zKl6zVIJBpO4dWBe1bZa4*ZnZH(074aJNeQxc4YOZP08Y>Wh|of0$k~Kza-!~clomwbWef`eZS@fiKx?jQbJd;Xk#S%u?0wIG=<_%haG z)pfg#!NhQb1AI{}`b9(pzv)Xn#+>@nJlKmJ(S!_lTCi+@(R~FY_@abNKGB(2IZet3+>)o2wIa(n95N8^+CFuT@)`!+T3R^z{U9 zU#VFR41A@F(fsylX#)1Pm|yWtEZ)0^^9kSdDEFVE5$`tijhG+$x{&|)O}xj~*6ps9 zlzr|;>UiE?C#B0_8(m05bH%{O^-ImG{;J&NGx+^~owitU$+K~iEg4u|XQ@<%kF=9L zjvWmHHV$>Xml(AbJ~aXycDwG0i25#tul+U>Sc|FqzC|Y#rr!S+H5;VD0%urUx_=%Z zpoHO@8d7O#(yukJ#;WfWmAk&ng+>1QciGxLyd7DZ$D_hydBpc)m0fFX0t?O6xlDH- zHGiL|`G!p}$fmm5sXpEI=?4ug0L)R<1qtx#69Iy3C40~+j3bw$A=en{g*XVK=sRMT15i7fzhqb{{>ofBK z<(6{g`jW)F`Xeiq+ha9)*d-BM8ZdV)bp?YJ|4*A{N!U=4dt_cQdVmdU;PtWr%(PQjmt!)vC5e0vO6IwRKh{Zv2~Hye^Kr-8Y8NUg9&f9emV3CYETLZU*=Vyk{b?qRw97 zGdF&o^>)uce2Cw50pj=ZU;Z&cyXi7OD2Hu;60EX2ysw*d-wSN?i~l*)dsYrzfcjJX z@IUd&^>6Ol9?7?tK%UKUU6!Q1@Df1#hv z{BOLuxBpuyi!&6Xm4B6-vD%5|$yQTBk(pE$HHp1wSThy98g5bl_?H66^C{hEV?!!J+ghkq^q|J*6X^SIy0X@A4&4Bqrx zZJKBJK5~_nKwG$FL?VCpH#~)foY!u$GkV>|P!dFAL+`3pOFYNnVOS?e><;it=2x9B zEz`7YjlI!VECU)WzW3Jva40e5-E+oEVBmjwme~Lm*#K8A3~_58rb6(NjwP1-J9QP^C`CvYGZL%0K`?+p<^`;{(m||y@mscy|KJb&A7-2Vi-)dJ_y!{J&5{>=@9>^x@n1p;9i$F&jrz% z`8tTH^NUSH2D4c?st>hJmt9Q-wdh~1$CQZ8!3@0J;{ISZ)_WW67w3XmthPxk5TPM# zWQL??LNi2n?0Ov?MN0@%?y#j|T?i{tcT(kGx6%}NZMx!Eh~D27;^z=HQFCz{rq;qD z@rjD@udv7kqFncK)6O%zO|s3+SLcfZ04TG>oNc1L2|}j7`%T3ax3X zbl9t{HhnNk%h_i(0ayvm@i3-NMM2KSYKwRL39(Z_^<$|IRCf^*)KpPZXLooTv|!H~ zoc79E-_RQa7)Mt9wmPf{BXa{AKq2{@F8l|aD~(`XJ8ON-3`14pKu|zEpsC?K70$p( zCq4>i%4~W`oDXO9+MX3uUTd#*SQ>n76hnoj!quo~(8z+}Fy^wBJK(6QktQ%4b$z#Q zlgsxqg|Gsv9F4U;O3*N?-ck<-FCSV16{_c~eL=T*fT8gXjxS!! ziDZ#tN+iqIMrwp%PPf*nos9ZrEO%I^*SdUQXm0p+wcavNt()MmR^f8gR(N;cWO3Q+ z2MU|fW}V###<9;pHB4WDGBh|KaP~rYz9QxohV3?rsZlJ&W3%+Szmpqb=#17S3p$tj zx{2b%<|qbNdg5dhCEV+WQoC$Re5aE2)CU?RLfZPjKR-k z*9n^A7??3WgC+*dm2hIyR2w*VWDgR>t#QN|LV6-jjUhf#MdKGxz`fRABSqV5nc@%& z5UQ~{%r1zYapxw%yM#IY@_%q2v2lZxa$_CbVAYvNV2ck-WQk%)JdK$ZuuO4FJhj*| zjim>H!3fdxY9vN~SgDw?WV1N2x{?J4^#y=@*${@q5Oy04H6oEsGJzKbl##;Tl0XxN zY!pu>FxY=2Lry2?2{D#xP$JkXH1L!g;qX{`M3d47WSXWQaDszow6*uN>PjIh%xQ-qm6l4Su}wCS;Q2R3MlpDCWcR5fi5HPu{R94=xpp(Z_A9q3NW zV`b3Tb$M*8Q6tH>m8FQk#~ClExBYCC-Q0aLRqF!Q38F7>k;vVt$?^l^JJ;RI0;!ZQ`{y?5Gxd?5H}&} z2+@Mmh(n<@@2`n=>*;>Fn{Imc>#w#Rob0O(NNn8(N*GPnFVeMcnXb_P; zh;$?9SZhRx7N$wJD}VsN1{luyK`cK|E_Fx_R*+I6LWO0X;@PJsGLhXuc@2U6y2k zDf0nI5fFjGIPvu`Y5L$koH|_Y!?`Rw5PVgN7{#7=>VKi?9~{ocOIBx!@FLoBfY+H~ ztW1BnkPQcW1>$9(E)@WhAyCdN5yQZ>H=tr@sMTo!x6cf@Ty$JhfVHu%96PLo#QqT^e;&Zu zfdFU#tv-d3;FXTi#8Q<7i!Vz^h<+_$F2DmTqEPrc8_71I}%!^u?hZ?*S@TGt;lV{85dnuFj4g({ev8EqKL35%OS}GcTsR3q_IMbAICy8soXu5 zm1%MU$uZDAL8SVkq*Py!9Av^%scN5bJWB266Jy`#I`hct8wS4VV^Kyzfb`#A}RXH9aCTnf-PHC$(d9sDHN z5imetPCl1#I8EVMUK7Nu$e%TIbb67DgL=iD7$6!Lo)HDqIwTe3Rb00_L4r-_Cbd-1 zQbdVQ7gNhwY?QBs1I8s^J}yygC}*JoXln9|@rmM~o;x)@K0=OE4_D^vCnSmUfB|Rp zkP47a@SRq$0(AYZpOhTcr}ti~!CNLJiQN?}jOdG#M_59n)@Pljn0o@2>vt6_JrU#J zQLsb7cUxhvx0=f}LlI;MCl;Oga_SNU{2&IZRNCp_^q353I%6HJ#50y!#KGe0N`{sv zoB)%>q!p+SSAdJ$14t3;XQ^Wm?s>S@#rIYc%b%@m6o7_Sk#r(?Qvk)c%V|S%3q@9( zt|E2vT@@P%Am|d4KU7yWhyYlD5g(kbQ=hPqoRK*9&0eR zR6IQuWWXp+Ol8CTp3=D&TSs;waU7%zLOdj^>RDot1GI)b^HP&H*U`35=_l)yo=*@T z*C_>?0DEaA*m|`U;5maKNtX_Iy5Tm8hDa`*8aR02c(IA<+UjbrIj^W`te7hEP;5qq zK^5^zy=oy*oUK$?r9!6NAx8Q>(DDEe){0XW!@vqe{ENLDx2_O^m_r$ZPO;E|+H zkS8i?`{KE(y##TSos>6h`WfO?PufT^`Yryo&GrH;^F12!Kn53+6GCv}3uytFd-OM6|cXaMLz z7b_hAS~;7g$HCa}d93FEj8ExAUgIAKENEoIa5652i>3P;siPpT=p@RS&hj*4UE1aV z7ylH4eyK&^d=sQEaF+!W=>y8&vQnMukM zDFju)O}&&yB2v^u$7UoTnwn^EXoZ;U6;`R3keb-vqzvZth~Bo($n8gO4WM7>LeLje z!7O6L%vr1m`oB@;9-XB&flTE(BTMJ}CZ#H~67;7D1EvmUwQ#^hADB&Cqjei{F$piE z%S6L0Nlx3=F1U-0DblrASkUN_(h;Leipolny+EQ+%$`GPhthY>p$+D-Ic%b~c}C2o zZHAQ0#JR+g0ZJlKtdR`=P7 znOq5RV`u;sHRcX_1En(39YrBP+w~Aw`3Jt%Vc~q~k$ry)1=(gxA8= ze06<)xSR0`;suU*J!ovfixuDlKEx3Ac9xLlkl2Cp_c9bRqJgyG%<41xfAUqD zhcruqaA^r~*NR@>fES2f{m>t2n8X@4i;uZP)*tBYOhlPdvKc0h?qbn!K#^}Kz;|ZX zU5a>ak?$P(0?HfdPKi!RiQVdPLP)Kx0s@l2jV28X*l^R}9HqTH@IFUHe5nnFqGmul z;E_w5y@a6~539kkEK>rrP!++EpR`?vY@RUV)rq| zU7vDIgyPagat%OADYA0I+2|0F+fb9I6bj3PSscW3ml+5kUt=`4%Laa9(dt5}nIfI4Uvi5enM!$4+alh%Kk_ zjJS-YM5#vxS_C&tAMNy>a|u{w=rtkZ%qDM9X;Z?02Apj);>5}2EZkSeXrcU}!raBO z|9erKxVejk=6R_QrTS$!rM1%Qf?d4c#YX$m5>egE^+!;(K;zpNjy)0$JrEP+-^1bu zoPbmzKF&Mzb*b3i!_v(1Q3G6=$!7|55ACD8PV}%TzKsX~P0Z|dmuhb}43`Mril)L3oi=&hW_f+bVFBR zrFapI63?%azRJs4-pjMZCXIIk@RlO*YzJ_$A z0we;_0aeznp*|{%D6vmg@j(j_A44yoGOz`&%m}+0EW7Iy3M?p01fQuB)X>@%7 zRUk*9`d)Z(MvB$W zLoD0_!20_sSe;n`HC~*#1CBg%*3)nSkQr_ZR zh6)V9LngxJB+gt#+cfmhEaI;wp(I~#wVuuttFESHNjO zE&=Ja10WNYeCYr=lsE%EbI?k}f$K=Lur~PgAwND)khJUpL`e)#_Br(?8~f{d(*hCPVsQA>xY*je@x2deSwZ_{-p8R@#@iMk0LP zK|udO%*=4%PR!i~XH+A-e}Dp)vsF3 zV%ZHO&p8&n0gJSVeaUgqjVeH2i}#k)vO=6wA)e4Bd4}Y}gJG?~ zkx9uQ#312$2>uj`kgcRgEn8Vuu!_wGI*`ZPdpziiEnB4o!8Qxyk)iQ;EskNSfJ%37#^Y%Nnje6fG~i zK=@~FAtmIgD?8%J%iehMvP+cTdaF-egJFbkrvM-WUjaKK1Ok`gGF#s8r0-%A1{?|6 zcCq3Cpu2XFHx;&+(dbZ!+>NeCdB4`Vo0K73ofU}PyBXS>kWLqG??zF&f*8l=S#Mavs%|h|aZ6uro|E-tONJOOVA(caE+a9?C zl4i$CFpZLa33yy00}-?mUkX)LB53>=k-3*{D9M7EwU;PiIxGm^Gr>xfCwD5|zn4}o zrQhF6CK#oSx6@$hZer~1I*F*TN~?_3_x75kM}<{5ELAQi+_=I02iY!2B`6R>hUmr$ zvKhuYkE11Bgx^8h#&QSC)s`S6F@^)5SC-S9IS~DLo9c-Wwd=K8)KhZ>BSy13~*c$XA{qQ8qoUSH$PlQ>c zKiGlTMQm6+btg?Sb;6MsU89??xpV1j(PlEk_zl;xc3_iAOx@sBfYPH40at%DeZ@I z<^c|RfLieY01K5X0oFf2Yu2aGsLhiO4ANdZKwG3f%=?toW{J9eL>Cv;y!}84pb;rTf`D_>*~n9y*}Vh?NKEEJHK>zyW=xp?etK3vNq? z_F4i*X9}oxG*S#5kSIjm1y35VK^pp;gH#>WKOBM`wfNEy%g|~*aFCYIk%JUaffq(b z_#FZo8VTIcL)2N~!R$lSSxR5;1%K2F{>>qe5=nfJhCsuyKj>L!t7Mv(v0J98tyewi zFt{PCa6u@x+?%mDBBag&uUuei@NqlzzxW~cEo&BOhgoi#y$pVppP?Vq=Lh*`0oZL9 z^A5B4bux{3{t~a$^V9jBvH)4;^~3v~vOrnp^^^OaG6R;W$ke_8zx0e~e(nbG%V9Pq zn(8!QKm4$c`mJCC!KaBi53@v-zWia9bcuv}`tAFRzMfLq^AMlk>h+Y$zK79I7khro z*HcdY-+3Hf+9p53B4r00qUaG;s?zR9*oX+K;LtlGp8bstHo7HLeEbM2QP}ZESeZ&s z^Gf#}VWlA+Dkib*2>9pVzaW*YVedT3N^(62!IDP{G;f9c1xFtgt>g%CcnI<;7^1Kt|_KZI@nqz)vU3QZfHgHh#i#cOvx1jXw$D zj)hO#du?;tPPJ`vUxkpV2&ryscc)^awe67GjNsvzd$KJ|eE2B4$$-(C?(2`NdW^jo z&E7d;ImUj8gTJHy!j|~655%2cvO4y`k-vS(#v~c0B)?*~s9Qwkv6vn8es2fuD1jeO z!+%GznBQbWQ0Z8@;&YD_ml*wW*(kBi=r@Lq5wAgR_*g1*kB2`~OfLf+_XG`n5`uyN z_vQZVHZ@j-qi>zn{s)9-FXxEbJZuOlHd5t2!eCJC91kSv1C zk&t-^2_;Cggv>`sHbE9hNGn2e2(pME1@1Nk%fta&TeY#@|f5^@_trV(V1gxro0CqeFzkUJ6LBFJ45at}fp339K5+>emy1bIM0 z_9J8lK@Lc+xep;|CV?K5NQV*9M39Fix*0{8O>nhT)5&~lL&=2wvMXN0r>#Qg%M=40wbOfA6FOJZJ_U#j_K0D!c=ittu| zce`IBNP+tpf)A}y1WG$h8Dj5ppd- zE*i(#&x!6S*HO-I9L-Sx{F$gh%?B;htxBq+jE@1Vq>l2)*t^StUjONtb^z=}+IlpXr#=Pc(BRQXc%7vV$P` z5>kkeodg*~kOKD*1PK7ShhpkRObu(n Date: Tue, 23 Jul 2024 03:04:24 -0400 Subject: [PATCH 037/222] 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 f63193e6ae..9b28d1d451 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 .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_opt 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 @@ class TunicWorld(World): "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 038/222] 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 272ed176e3..b7814626ad 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 63e7e36c24..1d1d010fde 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 1aa9655361..851031ab72 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 039/222] 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 a9dfcc5044..2fc0da075d 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,11 +8,13 @@ from .Locations import DLCQuestLocation, location_table 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 0000000000..9510c061e1 --- /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 040/222] 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 7de486314c..6cb35dd3b4 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ class FFMQClient(SNIClient): 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 d0898d7e81..f1c102d34e 100644 --- a/worlds/ffmq/Items.py +++ b/worlds/ffmq/Items.py @@ -222,10 +222,10 @@ for item, data in item_table.items(): 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 create_items(self) -> None: 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 af3625f28a..41c397315f 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 1b17aaa98f..1e436a90c5 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 generate_output(self, output_directory): 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 generate_output(self, output_directory): 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 8b83c88e72..f7b9b9eed4 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 set_rules(self) -> None: 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 set_rules(self) -> None: 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 set_rules(self) -> None: 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 set_rules(self) -> None: 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 set_rules(self) -> None: 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 ac3e913709..c464203dc6 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -10,7 +10,7 @@ from .Regions import create_regions, location_table, set_rules, stage_set_rules, 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 @@ class FFMQWorld(World): 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 @@ class FFMQWorld(World): 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 @@ class FFMQWorld(World): 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 @@ class FFMQWorld(World): 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 1dfef2655c..0000000000 --- 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 0000000000..38634f1076 --- /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 e0c2e8d7f9..0000000000 --- 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 041/222] 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 513d85257b..e2d2338b76 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -116,12 +116,19 @@ class KH2Context(CommonContext): # 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 @@ class KH2Context(CommonContext): 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 @@ class KH2Context(CommonContext): 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 @@ class KH2Context(CommonContext): # 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 @@ class KH2Context(CommonContext): 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 @@ class KH2Context(CommonContext): 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 @@ class KH2Context(CommonContext): 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 042/222] 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 2fc0da075d..b8f2aad6ff 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -8,12 +8,14 @@ from .Locations import DLCQuestLocation, location_table 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 0000000000..ccfd793995 --- /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 043/222] [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 5b90e99722..4acda4ef41 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 5b30357c94..9715cc6842 100644 --- a/worlds/tloz/Locations.py +++ b/worlds/tloz/Locations.py @@ -99,6 +99,14 @@ shop_locations = [ "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 a1f9081418..8ea5f3e18c 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -12,7 +12,8 @@ from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassi 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 @@ class TLoZWorld(World): 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 044/222] 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 21876ed671..c958ef212f 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 045/222] Project: Add .code-workspace wildcard to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5686f43de3..791f7b1bb7 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 046/222] 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 8d6a7fc4eb..3b67617873 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -3,7 +3,7 @@ Archipelago init file for Lingo """ 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 @@ class LingoWorld(World): 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 e12ca02297..3035446ef7 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 zcma)l34GMW^*EDbcazP1k&9##LK5zTB%Fe7lFdPq-LSinLlFW5!-@$IqJkpc0;K-HneTo#VE@1NQ!?K-Z{EC_dGp?zH*dCY z-D~^L{kEtpqBciviW=-*Ft20W{IT=qbyRkYAJbkrZ^5{-g;fj3mz9qnH>P|{<(R&p zZP9|BQ7hUnT-+CQbj)jC*j`atUQyXzHg;ZRWyONA^T$&lT z+Fe}>7ImHPa4zd;@0-mFT|;n)dRH?tzX{@S(H;`jjrN&{ng-gQnkoX@>3hgI<1d-81( zepWo+yCaS7zb*?Zk6c$u|EBST>ocg7&iAZM;mz0QLZe03=PP15BzV5vy*5w*J-e6pUCGN2}U$D_FdO^WnVykMmBq<(|5<(wQFYg zt#z*nRPZa-4N)y`xgbPZt1MYcNZp{DChC7D96#JZp%ao4#POe_8uQ9g8~_I9y%J zm$i4z@90x=^&C-&mo#yZXiqMe9wz{l+0f%VFeA5!d#4`*5E7f-z$pzTU2j z+m|iTG(H3j0Q0i-`Gek5>j8Kb{qg?FhpivN-s7$7(^U`u;EUG}F}JDe-{vIW9gZ=! zKgM_bE25gQp+L#ofCPT#hAA+p%QsX=_9Kz~U`%WKqORqzA$s~~BvGzQ7A@CNRnLN$-I={`O5ZDTck7A1Q80qM&3Yy-;b&|f4Dh*| z%i!Nln{nqJelDLM-JAlYlbf-Fxtr5?>zG(RXjdA1&Dv7bMBA&M60-UmTn?AJmbmX& zBllZq3-$E|o{JbTU{zl*Y*laL_im}kkfXOXdwhOJY57n+0VlT%0%j9Mrdo#k_$V=1 zPq(whWlZ-ffom_sP=&akza*NYi7GGHo6d%dIRz{Sn4fy{6hK^iv(_Ci^IbQ40RHpM zp!&ofiBxe*p~INa-atmAWZS%G@A2MSjD>f*oKkTFhVxf%DGjyl2@F0>+b?*^t&PTl zJH38m!Mko9W^P=0n#SMq1Gl>L6g0b>b$YH6ZyO3Dn{ZoEoYb+FMA-AgIEzf@m*0l_ z;ECI`zRI~hLCsSNFT1@7>Mpx|Y_T!oV2bnnWgQ*u^A>lUqKyXecW*}vlD#z>dPB|a zcv0($Qr^3@0qUOGikd3<&NOb>maICek8JZ!Qo`@{c>N7( zJfzJn(;MA>rIeLMx79W}TRiU2CUM?@OMlKC#i}PiUvmeliP!It+rF3&-R@9z?Ywon zD^~UvSv^xL=V!OqDugZ`w_~hbB1F}Crn{B;4j<@BzGnySlaF@DfoiF{mOJl6)xO|P zJ=4$MsV86)|KLvNS(37?p|RFwl5-lX0*=wbZw6Lq;7x~e`6YLy!GLeP%b^x}Coc3* zapVP-&ELFhLZ#x~*Xq>UgGfdBUCtKsfhz7KAw_& zjQWw~@E3kJVYTYR?Qu)ZsTL>F*QO~_pV4(~Zud-cr_n8q^$mVAB+BQk3nK!6mT*9g z1}G~YP&034pdD^_D+2uJqM`nkF+u~c^zGzvG){Fn(^LC<+ zyLso(!3I)q=OWNkv93kucPH?HoH$jy|T*QR?fssH00S4llna6DUo+2L+TDqU$$uas0OFLF3iFOK-TV`KDdx0)^wdoB%J|ovXxkeG+Vm z;o>hT5xIQ%?rNbrdVI&9viYaGQ5og$8KjzR!)C>NUvws)y9YhZ<$KWFgP*Jr z?n8u83L%Pry$9F(fBVZ_&uHK93I5XF`eZ3PkzSXylVCBBlM&AihmJb-4Oa5}I^VEw zHqidpKHP`#4{3At4ljS`{A@`h&eu2#w21FQF!Y89KJ-wp*07=I4ffo4B zaSX|(bBALjX!!+w!SryLw&rjGk^P0)cW{x{&hA@d^6&e4I`3Q6R~obywzT&w?^tGd z9w)Iws|LD}9?yI@zrkLStb2Ju@TNlaIx zIt3lQp-(01mE%%*_M?da81^WhMtqMJA_aF|Cf`8dJ02Yk>vQN)y>65F_m2((#A%N? z;-l*ty*@+Uragum4dDEa$Kt3|zz;q)3|gb1jN6~2R=$l7-j5vA@1GQB%usLOcD{bU z_WIlT1N+_MCHrBWkj}OETs2@H%_=t9-_YpQ5MF*@c!+XOU?<=A2Tl1Re(iw?(D~B` zhM7?wHlbX_Qyxc@=EuWO{%As3&+mA=N*mE;kF$=KJ(+K&_m&CyR$lV^N&3W8;{_Yl zANz9?_D=rT@5e#!$A4dHCitxhc5nSi(WsjB!8Ugwjm_?e~I8tDgq_$fT+q(42<)L-YTyn9!UI#*dz_|m7(2Ar3l z&NUq_Qebx#@vlat^So!!udjFpy_S%^%HbQHL6_~BXHZN2 z{7jyb&|+Tr>{K!z-R|Eti|b;_*7&^|~wACBc}Q?%rj~SAfds3r5#_ zUB*cQ9jwF8XM|jV(|G~yICX_`>i(FD8sX6Jbl&jC$w3;jZcp{EQceLfyg_QBk3)x&K9dsNKS!e^KuCfl;W)sp%On>FOQ4?Ijlu zZuG(o{?JS5<})VvP|MD5U)ixN%sYCJfBDj)Tsh|n-bNo-r-trji&l1rT%@j-vxiB9 z$XUjulirH-d8EGzPM$U`{Gpe1sOA}d^kqDFI9|y-WstA&6|bOq*zroW8svAcXuta% zp7d%t)K$MaLe?cj`7iD^Y|N*8)2l9JP$RWBt7RF3MPI#|qm2K*aMkm9`F$CDNh6Uk<;a@@Ps?A^KKqYy^C7LrUdsjc=Dwb;-AaE0$fKjwI0*1xU(3rg zv_det4n)`B-<}Fg+@nAlRVvHzQ7dTi)~&L4PPJ1IH*_1A}tlw*rRM>=Fj zg0_apEC@S_Lue1=k)`-Wc3DZJqg zjQ?HsMrD?1WZ-Fb7$a-oue>otYl^zYv-S49>CkXC{H8;Lx#tiXb?}~&Y*=kt1yUxr zAHoHHE9 zxFZhw2J$0^(@0AMt?u3n=XJo6P{_(chX!L5S99Bu5`EV9xz09Z9(c^*vya%3+?Aaf z{E8#Vp<+y|vHH3;CdN1f^5j1i#e{++$$zS%t=W%}X!oM7&}{DHoqsCUe7H4d#}B@%kAG+KWA7qwBi4H}VTVBI^Zv zRqUP5?Bh>6L-^ekalRc#?BL}`)A%)?1}FAD5eSe(b5eJjtK=*bOtZy^D?F zlRt?Kh3|R;@5yyMO{-VU&wpH|l>AHwfxB|Uyx5-Syn=8lES!$uL24x=dGd*`O}_P;hEw;LB` zfOctaYmwx@dLH@fsL-PI1RjvIPAeuU>AduBuxMMBu#(ZI<>bTP;v|p1j4j3CE>=9| zv`v5f9~}?lfzMp92zx&pVb)Tqv2yB88TVMuKCi>!9+{ub=YO6S^5}X3!(}6j7b05i z#uY;~zvXj0z#RG9G2GBKJ%O==TpEmlUDFZf0=D4P4&kdV$l_IhuNZ4M5?*P(Pcy`z z&E0Wv7>((C+ut!JfBf&6=D3}|o8lDeTCANrzPMZ)G_Tp9?fYW#NQ2kjK&>2DoUh}2 zbiKoRT7uk`^DDnh6VH6i68PjVm+K&+w+*i%3^UC4eKC}O^<{=RJb$629Cn?tniv1$ z%se@4s~?G|Ko5-ExPZL+A3FTKjz92^Y3J)%3YksvBcrREoweHSkv?)LY@{&2Z0Li% z#AvJuYZ2x3c#IfC^06F#`9G6jYO!?xKQY9GrT6}ciVaHf(bJuNBi;@W3^OJl%eB`! zXBw9j*ka`|O=ah?A+X0^KQ>rd;TLd9i+OTF8c+P!MD5eMW`cj_?_OfWspkGGQw5Q2 zFvywBjZI-}hw(uJivnL%vmgN`o(K@*Mk(+&h6j(M32O3UGyI)sCzmhcxhIn1qFOyZ zedWLtPYmK^$I;`!(#+%N1VRaC`fs{J5xQPqiKdRMo(=_)Zk4 zX|ZI`v^bAv@GU3Ct1D-7=WJ|r;&rt;8^cyUs@hqtgIr}*pU*X*7c5zX4jiI^IP@Z!# z9I2r(G$aFQQ5X{6coK!^v6DKSU(Y{2IZeq&w67JmcW43K!Tt=YscWmz3n>96?i7a8 z*JP#>WnxWY_{}>o3DOBLa^}{Z^Q8ae@(=!v!-GwmQ`bTcpbdN|az ze7yjsGx^R`{=(NfuDFwb_%$B(k9?lRM}LEM8SCeKqw62$7kx8X>+9(uF_aV2Ll!O*b841Ll>8u4(qvgsn|PXusY2Z5Z)a zPec48{>`_O)y0=whck-H=Jr47#7js5QTius~LUI%e1g@{`}Ahv@hL?=?I>Xl;=? z5VS=$-}plUx!k*;-TQ%}y=`tI3xSYgeUQlk5v6_; z5>0|+mKaOf*fEt=69^P8G$SY*sVC4(43`EIFn_|ayxZZ2*kOl})^nF!i)j(8XcP)N zgsvo;zM2MCxGlHVIo*DzPtTNaGM1XAv0ZR^pgkQ?X2EkK{>c_gEDXXLvV54)=@Q^M z;s*ve6~Yn0bXe*|(Hz0fFVz(PPyGYs6A>&Yq(gcG?Q(^}r1X6;ERxkvkfftqO(zXG z*lJH(Kc!{y)p(uLwKpK{k7Of(+jk?GHj*a*B z){{iAsFpUrp#gaJ>deGoG+fz)BnDyPgD94SI~U8*(JV!m%cEIInMpR+^coY)=BDm-eo!Q?a3CPxUyH!#R0&a|;&^KIzq_sO|olV=z4j9JA28yh)M&EWrs zA>ta2B%ZeFX1(GfKZap))Dq67ER zY!_R;C)wtXYm7wD-;4X>ST$H2oyJ-$M#i&THINrYYdo8#I*MzUS?l%GyWGZLw3hSC zmw%8^g17EZ`ow zjGOBCnV3f#-3kgR?0gZ~?__BO|Jv6W?q^u)+WLc{S^!NC@2La^Q9AKq0@IGu58{^u z)}l;ROm(BD-s@})cTy}h&KiH4ra>c%h2xmtRqchtsv=EbIP8YoZ<{~-DuuA3>%47^ zVM@?)PK&bzj$UE3`YY7XX(!ywk(CQ3wsx-QSv0@L(a+P!%@V&Ruyipyi6x1tNvuQ- z)e7T0+tsX188K$9_PWk)^oPOF;Rxro#o1r4+u*!bSaqb0WyZp%{>Cn?aM4Lb)rZUYutfMh20=$UvnV$7fNAjO zNjObWlN(|?l7mcfdpgYuAw7|<$B-e8rn5w2JeUrzST46ap?QPL>+nOeOkhS9JP?@O zC;tmKuxr;y!L~NTrB$vRW1!6O00haw80x7alhaFfo&zX@k%S=pT)}VIUYKIeFsAfh@5x~WP=;yEw^DLCG^Q?-a^)JI z%c_Jsm%2oFOtefJS7;1 z5;Qqz$*6p}gPK!$k1W$t0#0nq^v*@gJB*#19Pjf0dydj@u=wB_mXKoF6zW(KmQ5}$ zEnrz-|INwc;x&Sa&k9&Rc`?D*nq}QRJFo2*B& zyO1UVxX%}G6^bhD$VgUT18>+#+m~ua4<_M(a*1r1FV+kO)kksz+Yy4t5K@83rM3`Y z0n-RyfL|bSUx_;fmSkF{|Pw-O}3TZ3TZZq#rsn-D2-t@E1Rx&Fms! z2(v0Zc&La?g=X*vHZ*29V|VRCh&|Xm4gqPWi3#CTins6L=|v0!kWh?|o(YjhZSSbk z8es9_kusJV7s~twy3>kT6?AreF`H;rB>ASQ9PwB1s^t_65+h2$jOfZ=BQnMO653ZV z1&&yVp!z-Fs>&&Gh(jf`op5@rkcCFO2Xq{65X2YmQkpWFOlLEuBgmEo;tpAZSaG;U zsP3sb(E_L6t5L1~Hi!_b9v?Tb)u4w1pv< zKzaf8SBB^wL3{q%5wz$aj2Ytj5qe++R?0w^To;JEku;%bN8%b=Akfl?M^iN>Yb0c$ ztB-tf^GHp@0oD_oSHOaWCkP&w^I#1>DKGn?9L&~vevTfywMD9VY0_8Jr?o-C&k zXnHCf7r-jz?*OY{n=4~2umf8lJP+S6 zz)X?o*7iBe23Iw3T_S4+iZdoLyAs%K6Iqp#1Sn2`sU(P0Kva|p2#UjOcxg$S{PIa` zXt7RWH}cOzwg|n|HR!8CFc;Iriy&gD^VTsNOQod8B1|eqFX2zD6vcLTV7D zN^G4%3kn|jAPeT0BFED~N1T{K`r_LuY$D9@mD`ykCfZr!frp8Ru^lDtOwx7agXeol z=+Wg$g&cG0w-hoADRCVdKv!8dnNBXh27;SUx#aRQGY0bjAw(`eGg5j3J0NL5FWhOnWpnwih1;9q3Pwol)QNqFhhlgwc2A6kGN>AV|g--e42nR`aB=rP7mvFEF zMA~^FK)+SEzE%$eBajjBHDFC_kUsgV)Pub7-AegO{KM03nni-Q1Zji<%ss8&9qT3C#j#Hy2v`EosudhV?bRSebl1}k2Be{I;%R~VNE{0t zxiQ-tQzhV=PC#GPlbnO<92(UdIBXGRC8W-QgACzmAbp7303C4Gxm!nF(`}m z&RNpb`dis}Ed;w;iLP@g%ePFITNvFA5bXx57HwcURTNJnU5^8ZwYXibS~)-PJp^dM zpM0yYv7Wj_9P!#T+IJEkG_^hldC99nFzk>vp9~*RizQy#gv<-Wx>c4D9v=gylpIWX zkizN{p=y!oqg9a+MV{Xi?uVR`8=HhivX_Ah`yaoZ^b=C`p?xESZc?ac#i#3;xs z#%zktX2r^_mtH&o#sK`;vstNFeKw6+mu>1O0NKtZ15;R0(Hn&vX(m z064@~(^<4~sleXj$)CK>MtE(38$;t@%nZsPQ5dy{if6Bb#%IkSxzR|`s(*>7Vu?g< zD&{UgEBp|j%pm276k_V&K3vKp5hzo=0Yel+2X#Xh!-eiA=FdDh$HeCUIyzAPI@~kz6TEhk`!oR0I#?Yo}ul zAOOffOqoM#OtuA%cd8-U1ep7uY3^M!;Q9lZo26_h0Km0qZi<1F66@( zh>I66JpGdOssmR-?UsEDNP|K|U~E5m1g-~*x=xt3xgE4}hZnMvWXSkeS>if=08)kC z6xJ5?24tpwX)x9YZjT%MBD#tQh43Y(#NjY;!9o__uU8lbRibJFlKmAVR97b!pUOd{ z4SqC0+GA){nimE4BWSJ79fRNN4icJhcCx{cgfmtVGMqFsy0Hkj<#BZ2y40x#HeZ9s z>#u3k(wb>)^+?wV5wm&&orDT_!Wc&Vs%}kUjEt~P;z*d$5 z2Nd~IAsFTJ7y(5*x5(E7!Kmssy2YWBQf7~S0$^`+*Fdrl1lOcv0S0#voTF5Jzxf=M z7%LOaWfDAS2Rw3#;}=l;0h__GEJ>y;s?>dj13buF{nB70rsD&cY{TnY$u___x)#K_ zT~XK3Dm?!IBPvBd(kFw6c%VzXsXrp}+{-&Eo~93E;K;l7##X%px|s8WgbHn<^l=ukPGRaN?Ybc3@|PAPI#r1InK&iS2- zAVnTi$&DpfB1A7G;TZv~kUWmz^Ni7;X&}EJA1LnqtS}O_)~U}8mVR6kO(_99VNOQ8 z>2U;z!iH{!FT#)fwTK!+@_t;9W=!@0&kpFoC|_Kp5YFp!vqbk2mVio3J@r6e{@_v7 zi`Z}!&xlJ{PO{GBSBcWFd}QfE=MwPBiePV&bLN(}m1LAKYXDapoj7rLDN6{~F)Cv| zPB@ma!M|OVE^b}M;)+cwM5}(8wCJieU1E#Zm$C8Tv_x_b^PdW;7wCM4!;vG=x*QUZ zBA2s_ekUMZi7(*xeNicHTFwSJf_2IoHK3@pNO8ok1-oKoRCe z7qg-i^?t#4rT}Y+dt+cv;3~y@482%1uE&4t$d7Jl==gv8CE82M6wh2jHVw&&FE3%4 zHnq;B{MmnIa_`B3V*CnrAvD)f%@lD6Qqlpi^iqh+uU-jxL6OQeF(7caMH$w>uhGnK zbTw9viDkr67g#R)+(>=Qz_R^G9EJdupjQl#VPHAG!3dqmedl~f`28a{KFQXyqcF7`d z73oS1z$Ar!O)g(WJZg+&u}d}yL$iyIfD1Wryd5L#uFF}vwFQ&oVLTe5W(Pv8a89F3 z4wy;!qKtSGUb>Z1;!Th=iU7(WFUC_KZWvEF(-1cVZ%5v?kV@FKng)e-#7ghrfr@ls zA?pg6K^p+xU#4B3SphXed~z2Yd1hZh!v#P=tOfjAB?9srKGa1hev1#UpxMN7d`z0Ky(x{4%^9NeO zB|>oGSoLh62J_r-62Ul$Pp;P21P+mL4GAU1dh3mHfmnGBZA;?h-fQS|Ld%5FpdqJG zzcJ|5w1QFEJ3){P)G^y~mWz#`!+(%I4vk637yK$h|# z;4B{-z>$(36#z0}$p;1Sg%W4M*959Y?75yq3tK}>U%n;)H4S&`n1Bmd+PLZLkvuEC z7tl4n1{r)--Kz%!(bF4TGADu-5twJqQsc%R>Y5ftqEaJbWV0 zu-_?59zIDGJiQ*zurx*gTCe90rbs2k%NYmtsPo@dUbUoFX!kWi+MZv|8In(kh3y81 zD=mhQ34})?_%l@4H*jrl7-k%r|%9myI;f zlw;)C1o6SLO<{ms+225cm2eA(uXLar13yM3t^@4ICJMSy*LgQl*A#Mq%y#^x6*)-c z5E|L+Bt}Jf zX-q{)f{I4#;%Lb$L1pF&6(`ULfE|Y@yA2W|%EG=ee<4(7%(B8YhN8YD3xxm4ZKQ-k zZ6zb2Z^=#QTe3vo8*dM5Yq5+8X9@r^#}u&h?T{Y~_u}%BCvPj8-0xJ-xs{do1MS>O z0aY+D<1wa?v<;(?@`|l{8!1D$Ju4O4woy6@l=H+}+el7P+rrIpZ5#Yz9tfbkWP|)< znHYt&n2+W5_?++)F@S&&vGEQ~bEtUc4iZj+|J+MuBq9cFCzV5G_jYLkNwecUn4)Cd z1HK`Vfe1Q^|I{`P%}J^#*g=<+CxH;h@WuI-> zDGL0K!l_hN?gN%%EtKjW!aM#uS_rI*i9Q=LpN%Tx{3Ppq{+aTs2tTcn;0hv(*s*x( zZdznw!mGk@fK@1xcG3i*gP9~BOMuqbDZl91z(naRL`>Tf>vxhU;4wfvx|2kZ&U2sb zWM{#c$powC5zsl>bJ2AVEgqG(-V<6>DBHy0dq~0H%nAf3)=YMpM#RMAaC*kgZhlMsK*Psu{N|o$zj14+6qy` zJwS6nUW9&X0Cm#9(mMnAf`e@_lB7ol>N_s}2L zl`8?Rc#!sNSfy31lL3rTj|!kKQXh_8T51bL^Dd%}VrjQx(vx>4?H&Xq!7pO^9lnN{ zjt2ZiZ)lEC@zySq8I)1Gx6SWph6GJrnP`U_S4Ry#zH6i%jMAGbpDjQ>kV>0CY;w)l z@0MNwUM%Ce38i#5n*)Ce_E1(c3XNE?hmJF}(hu%2RvOsD1YU?-dex%@9Gxwy-rGhA zcR*q&>MnRafF07%XYZxvsQ%#`>}|vseOQO7K701k_SwIeaw_o7$O+jgvTVXH291>Fk^rD!{9*;x~2+#x1M1TGwdhpHvl+rjhOQY%a|q0h!-jGDnmam zAF7Lzb*7)D57kA>I@6EREBc2XVFeZ{Un{nla_S3ncTFFy9{djQbr*%X17UX%$8}LRo`!SXz8Mt1QJ;o|^Iq(=8 zlSmD&H<%I6{>+A30}?7edW=6Pxx!(Cw4`EL&l3IlwB0 zgb-qpub#ks6@rjIdPLQ0#Eb*1M7CNZu7z@vgd5!T@Z=S#;?MyWtFb;lpeK5*X`S4B}7OPksGm%YYs+Z7mxV33sRa-+zrQ zWbBvy&V%fmboe{|uWWH-L`0I<`8jKXzx)65IZMrq*<6=Cfc~@J@+)KqSf*ybf@G45 zS?Z%L5$V>L3no%xR^UQ$fi+?X>k^x+5fj)F@fv)MUP_gL9{4lWhW!VYEA)#H6a%2Z z#aN2P(j_9_7LgaX0s+YY=nbqCjkbucIhRqRz~xwxMr2k?$dw35C&*OZk3SR5mH2utpq6z+<~AW019kR!(uTOcVMXmOLvM7Ve9fo zSYLx}pTm-i+CDEj?GaTiFCe6WATJVn;AMnNC2Fro$ZH5`BFO6m3H|i))D{9CB5)~h zk4O{0*duaRACWEJ#Fnju@@ENo8zIvO@{WYOhY%k@-j|RM5#lGvM-uWeLfQ!OiG+NL zkh2N$R|)wHA=3%+xsu8+5Hy28UrMBZB4j2(j!DRIgv=ty2?;rgkaGy~ZwdJ?Le3?~ z*AntALS_@>JA#x3en8N901C!ci_G|l()=G0(hiWoPgt6VrJu1hA4|W8Iq?y>j$Z)) z(#|FUyaV7pfe0vqVhBVbXd%f<_W9T%8UdY9>J3;Fj%|`S7#}gNHU_H}BT_7uF2s@@ zOI=XXzwbG<8^Li1UV^1~EG>mnZy*7RJpYY?nyJgMCJAenV<{O+y;w@Y(nVNG#nQ!C zO2g76Q0fh&LlLAtS31bTxrSNl+O=t|3Udgj68pT7pzc$XJA2 zN04!oIQu#?F!g%+njq^ZVf`9{oFO4o5Hg$~XG(|@A!`YOOu*XKB4iywToO``kQ)fn zK#1PHwWk8I+Vf7;}q8T2RE|MxGZ_7Tl060#H_TM4pELN1nnR_tGp9C0$@{{bk|?uh^Z delta 27456 zcmbV!34GMW^*EDkvPW)mkc(s!NC@FdAmNl_lWdYD$!^GQh@gN$0$~M$gor12RX|rc zCNL@r;#F@ztB4okuUcva!P{CjwDqXTOvpAXpHyw7I2(z4CE#WKWu{^G@x%FD-{Kfb+ULdU|2vat)x$|sB~TQq6H_(=;F zO(^dT*_JNq9(_ss@@2gt=c4xVj)mhZCY4WETv1ukF>&I!2@@7|l#iRZxMOU2$JlZE zdTy-b!SN2BIlkqwN5)?iWmz0EU;sZpcI;zwCtcIXviJwT+Gx#57%-qDID)UZ&c&R3 z?{zt>gy-+gw@G+q0x!5Tja%1eL1n@E68bld&tIQGrF4FJ<3N7f`dnyqV12$K=HVZ% zFM|4*4HE%AZ9{>A2l(O*qX?cK=iL}Afu6mqd#~c{S*f<5>~B7Qcf&Y9N!XYLO;6rX z#2YtesOC%g(v2gj+cOO#6>3tiuYgwd*AG@LuHwtCpEBHu{e+_GJ-yfUUez0vpV5uJ zHu~<^xN*b0-i_W3!4iJ#`XbeG6aV4*$wMS+jIX7}-9pIi3fVOHR|_-r;BVn;ZkP(g zc>IRRH27(X*gd>5G&N2U3$(bZdD8Aez_=rnr7-sL=R<|i@-LwoMVc+Wp_IU?_KP}} zbu4naSFB#uzT*6jUd5)TdHs!J0spca$5Bs1`Rg}oHXY_CZ!8*K43ln%`c-dBFS58d zWFPDF_pDgczG|hWaU3uJ%&)vDfACSY6o5CbKi)h1(wmCdQGUlw>1wne@@H-;GPkMh z-{xcfQzXXt{up2I8Jm#m;!Oog)V@yOn>I~@NgdrZP7XgBhaa*xw=Z3>dg+QK-Musu zi~HiGtMw$JdFkeAXtQE7ir@{KOVtn)`7@hq0M53c9OY~os^D3?b_*`iwOhs$UOiBe3ADC& zmDq9Db|>G##}5rJaB^GMszu#S{~Gw?tm*1nrKdHMf6c2?mBh4x{P{JJhTf66VFEVy zB^}PT_GQa-!?QW>t%NcCzISwC7%j2BYuU1{HO_j#T+@DuMn0csZNq&!ecNbY#)@r& z)Wnwa(6%7}-@C00{ynj66#SdCy&V2^ZAaoOA4b`GVEfS0jtz42g!e;lPw<7P0Ryh+ z4MnU|h-Zm$6>>thMxQ_6EGZwZ=7h2rQoykis5d3hH&26h-`%YB#Q~moix1!{ zZUJ>CZYpGmboVWVPJ@*_!Hj4*JM(hA%s;-xSaPr1C6z_c$*0^}5^mca9CC)X@A7MI z^%zU-Y6%!i{o$>p=EfCgX#6Q3zQe6K(&%>8YB9NT$8eb013QMsOTGHmR^x`- zv!S=X-IEldEdKHB^#C8U6V=m}Z&LY1I|r(|BED%Siq-Qw&w{!icWUEM&Wm=nv?`JJ z`dR|@X7Pw^^LPVF2`de5tMRy+d|qQ~kQiBNJuPzInnmc9UB#+*H~)55HS|994!Qq_ z@|*7{RCO)<&>e25v+gd1k)6C@D4)ICsp=N+D|U}pD%?`zYxDBt-31YIzJNRL#Es&; zQ_p$Zom#V8#2>vA)%%-wBGZ%as#H|3=8bo`TxwU;durT2+Z7V1c7>j}1dh|foWRSS z+5FSH(t-9*cRAIHizHa_;k>Lgo7dbuNo_rgzuBdCjpSFr?P@YJ+7fWPHGuECJKZ5i z7wvWh_=$V+kui7Yt1k9X7b58imcz&2GijYf1=Z=5OQ21yzg1JDK4WTIz1}(IPGg!p zb@c%=#Nv0=Mi2o&QzW2T1Jp`~_?Pz#gz>R^^@1MZrS~>wDE8L+T3bx)<)M2=Wru}4 z*1x)a>8ft68Q>g){zvH5g z6~^kIm96gTS+)9-_En2|L(HS*$)Eo9_*h9?)IY&;l^Og#iRaum6#AZV-=ut_?@(Nz zb14{!+GQpy5yNl35B0a!(<6D#{SM$j*^gRdrgHcF#U>0C(4_5U{QLVIX4Y-sj8}k{g zPel9I9v-fwa&Rdv@!i^MLI9M{zfhGcbj-hV}JNyysN-o3hGmEjw_jnoXFcOLl7 z{NIn|4>Q_@5>*RlbNh;pWyaR&!-gY-YZ}YuZNI^VT=$zi_;=@ThFNe-BKauGOa{^} zKPYsq2KcGp6adogkLU2wkK3R$;qf6+&IH~*VKBes@uYM)oBoY%e|!j_9+IfXC#3Rk zA5Q{+{RuoEjD4a|c6jPPEQ7Cj0&CVjF$Q?{$P=2CL-;#SlmfhLzcV2QOo!iC`m+5f z@c`#*_s3JIfIqY!4Fj69&-QC4a0-ul5(iQI>d+DN%2z^n< zuYb}zQBEnMS747f6zg|agIP0c!5pkC1!%3n^11%dCyD%l)2AD4Dd%2%H8~7LiyV>Bh6G2dBL-q z8+(ZxEzcr1E`BzGQoaf889+8`3U7XXc9oL( zR&TX$_Aj14dqeS6%X&JTRqd-*E$uK45Z|Q0E*#1WUdY^fCtXc`aWWNCy^RxfY@MO)c_xxX7#DS;ZNw|^^{B84C#adsB%Uh@Wh0PRUcw_2X z+{TeX`b>}fc2L;7pUJY<4Fn>E#Ksqsfn6yt4x?o;6Apd_ZjcdK2qn0ebu8~#vAWyo zTGi3s8~mrLH5)tdEu(vaZ}Z(RYX5Q$Km1}XbddBCMljI#?0iWd#}@PJUYZDX2VcV2 zOxVl5kmvkvIKbV%L#2jlX~XYQ)jD3z@BAI^@SO(+SuoT>N7vlLDauBz$9Wa--7lr` z#>1mf(XLyZA@1m6as0N!4zrR6zi8Ex_DeffMQE_C;r4xpXXQ%vC$@O}U~B5TRxQ1> z%kY1A)9A3ytL zv@2J>T&3py*30^I@ht!9Wpot^{xDM3C50@3HC={vI?Px8!L4j#wDwcAcw-3Y)j#AY z7QW6;{h@5W+Ju2NpUHV7J#8gezXL0fK)JJedzLTk0DqDq7IE^+S>Ln52wa>%4i4of z9T|MvE9fOmKaxI3c4iOMgQPl346mPm@Je2uq47g8wct$}GdhKY@`zB`k;MN&=z=48 zz?SQeXqSWW2agnulHFR+Lk?S=kgdLFp|ho{YdIX{O;!BIBl*zF;G@HUlKZGuahbgP zs0ZNB9>t?L3h?(w^+FEi!(PRp-Z`&UWSOP`USo$bjnVwRSBum!7!Gl>ja^KKo9|vt zMj^#>PVq6^i0#J;>_z|qzG`V7_v3pkIpRhaGx^KMinV{`_nfW$D|Q@Rk5eyX^Qp&? zfmm96JP|u=Zp+}I<8crSxN;H1OZOd5J!3y~Bry6}%0D?iOrPle5Pi@mE?)2&I^!3< z<}{7{!q?DQL6~u%<0oGmYQi}AS|#bvezS}5E?p7kUx?SeUaUKHn~qb^X%64CT%rGkVNl7t<6 zBTH*|ug5s$A;;n^WY`Ea>H|(TZ+ds)FxhQPJw%I)ezg)r{eqgKC&>6i@9M)`5&y%x)1j-lKNbPK z@;~acOj#PKl4I|t@#TL6+WC^`?+%XmS(5;7_|qN~!w>v} z#Juwv>@~6FdhmJw zT<7O-$BEovCl}NOOa^oSiCx7LZ9M<3bwYV^BI3_z;XA1 zqcdYA!!zC9zm5(sR(J3YN#l$n@*lVZ`v@0n)<@-LMjrpAel6+PFW-G4ogeyW3^Y9T zQCW#;1y25#&XT$3V~}aS>En^%+o0~C%^}(JvpMGSzK?5hZUu|7dDSOrVQ;%T=#-6O zTOcOYZd?+p#9XYn;lj_|ylz9{DuW9C{0Mfj(NKLvMBb^rvgJA!{)kvdw>+I?5PqPjHf) zS-ihv3HsX+JtD(y+pQXXSoBuYPzxr8*IjX);Qi-^%=-@GbpWYy+ zXbrTqLZCt(zG0U7t}raUX^fU70WSoctGh59vIzWs8FybveB;So>HO}$PgtVa7Pg&8 zClpiF=&CV19&(Mt@gIc&c0xx02omk7j%Z0f5flzg!P7v?m28Y1QuF*usJ1^iS!6bUeoZTFYyT6(-@ z>ACQ$!SR-6pI=`qZ~*Y6uW*i7n)nsEeo#Vg=;JoTye|BzRAC&@xbc=oUzHvsAms7* zlSQKq$?Oj1_LB(LN;vRpiTGS{W%pMZyyxUZ9Wyr#OiroE)z}zz#=RkHjoVWVHw0!_ z#7L=GY>T_u<%y6JOO>liN7*E4)94f6@Bk5~DqowsQL_}5JcrLYg$fc%$+i|ZTrBzB zrfT%+ww%h1tM!Drx#iSg{=+G|s;T!FqbJNjSm23#*FR8Hw*5l~PDb(P{xM7OBF5hg z+ul6S{zFh4YOU65DFLS8C=8ds!A!?u8kWQ`jVob5GVVl(w0U*s{HuTF@~8fZA_Ap^ zh+PU0WP^m8_9mbHF9qjU|7)mO(+rhT5us@o^Oyd07LbYldPI^*fNJ9g3=k_VQATFL zDt^w_GXU?luhGlI)7788)|Phd)^A2Y)yQuaLDiOTbZ9fgpZ*3T0DC{l;)(yp+wm>m zfD!z6qH<1e;jRCks`YqVKWVcxxZTp)sym$6Dwn6xt>uUx{`Y9})b_!6```FJ!ZZFe zZJbhgZ4enSuS86Bqpvz*ZK90JJ-+@w5yTIkf%yONH~%wL-D$-_Jk8rr7lK_fUpW~; z7}(IY-;Od5{pB-If1C4f@s9NPx1-JO=;u7{JME7Cg^&8Kra;cl(dP0(f=DCS37Vs} z5<2Vx83XFX@nr@-_8q#3r{UicKKlCtr7FM809BdIyH01v_R3iMW#89^{ftm*EnH5m zmNzuc=AKJ0T^2t1Sq2Rlz>|I$3jZ&eEJqaWR_>Nsy*M&rkVg=x*Gs6j3-4bc+%zh{PQ2GVKU)Mry<<;<4|-*)?Jpz`Hxlqwh0;P5j9-*w_e>DFjon_j0=5IPg;$F!8gW@YWeNLw--t8BY>u?Z=xsY%`P} z{~?<%JY7*Kxf4_GYmF2~OS3ETx)}g1T0Hq5P7gMFZX?dz)j8PJ5PsR^S=+|4aUR)K zROD@IbdxKB|Ix0mt*D$tppK2k(16HDI;VCA zUBr}X&ppaG#lEAqEn*Piod$v9eJ;7t#x?=E`J1{to0s$z-L?* z$r-{mNsC*1JR-@8CrJF0Eqqp%pv!xr=$r}g9Pw@x;FO5d@JA`Zhs20zw&X0iSh4?K zgAqrgSx#7^^aN*1`H0Z-&kMVS)lE|J*WB3J)K9zmAj+?IqMBZHi>pn$0Aizsje?=? zw=iuo-xPfo)~q}yOI7%ONH%YD2f{ubfvmGVzDBoKbBth?rq+P&R_u;p*$FXl*JFqh zf`$Vpu||9p!{8=CmQ!L`vDsj)gE@dws1=l! zbjkC?msWPJV%-1ef;x?Dc6+^%0|xu$X{u6NgyL7?VH=BsPBE4hZ}A6=jgAluabShW zv$JAzbp6b`r7F|Ryg0Gh&MKM}^Zv(JXuxo0@wHYp>V+kky{5If(NhhMz8>`g2Q2Dn z2aOubanTKi=O_RGA$I(eEp|I-+x*VKbgs@4@s)$k9jb?fn^P|ebrxM`}#oy!D_^W)h#eNj~f{wYn% zWO~Iz@y2+!h&STdG!^HLZScU>&@ov#Uf_#TZbm*E8Wvl3@JrgweC4%ZY_5Q> z$=C&I4py(b6};VuIRIiEVnzx}Y;y}Fu=Ynpyh!me1N_|I*U~#&2zR!yW0%L*enfRh|{RSJ7T@AR!n`cylgrEmQ~AXxLBq0eI=6 zZx9=(da%hmKB)O>E-MxreZaEjOj=QoDZ$nxMxD18IPh%_bq^yg5XHGPtSuab)|bojp(&NG%O%z$pKUT@9GNf}`aGFS zVy@?$Ek+I|o?)d;CUi^G;XsS}7tORk^D+5svLiMCC!V@`^<4)INWM1ZvsBf4d^r7D z_URNS^J&i5Kgz&|nshz*a8Q$%$I*HxX-6tX`qKo=Jn$L^jYE;6TNjJxZekf=lv7G z!>QhLrLj&>XlPGr;R4dd zwHQ1E80r*bhOnu?23E=nVrwB&9Qc@W70B4*Lku?9V=zaE_B&N24<+db4i#8y-Ev`} z!YMk35*y_{hZr)1RDc|E(vA@pu#@t$z}o1pmD?V=jl&Nz7z$ZTFVY*`>4#Joq!)UfEE~Sqy&IG%s2`1pdGbVt_M~8WI$!Ovc zEdgeOs}|zeXhZPqDhXUx6~sXnUA=oOr&BWE?nAAigLgeNE-}JE;6=2OA?!`lOT~ z&Sm8ir<@fl&thLW2{g2Ff;kdoqL?tSg5t`)0kNdXAv2=ZXmSDtuh0vR=Q z@e*oWHGby^r%P(H6nKmX^CZPNcOs2x$wX3cP~#N;H<7qO+vB^5#J%&b1T%E-GL|An zPa@fJO=6>ggtWfGOlX0HoB0*N4)TngWx)cp*!f(4#WL$@zeDV$h`5AzKg9EH+T|FXTWB zsn2j6pPfZII({mvN*x^|Rfv(f4fUYvW&sz`DHvEu_2F!ASzVLX9X#t7YpQ>-X67%} z%sO+;!0yh6QNx*s2W-p~4^Ag1_P}&DI2Q9eVO$`M`F52=3ZIr4WUXf10kX4T2B|S9 z=Zp0-NK|Mso~zYfp!Abcbz*o4B=J$UIS@ijSVo!jt-ir)r;Kl-8WZp!L=E4_w&Wdh zIx$u5Iu8jT5Xl(W9lTG$0i701>~pb#qyrKP*Ou@=3`n49O@3LSQo1%mL57{l-NE-H zJlfw%gmDi7;m;Mezu5=T0Ki6T53BT}goA&9r&=w*8RFz7=7462u@shvi^@j0Uxa3$ z8S_)RgZUC}@w7;4$hq|HV3`ElFdXQrmfIF~NuD^nnx*w~E5w!>mMX@)%B+cSe@=Xm zNw`$O*PUe(&sMW|dIG=;re@%+t?@oid<9n)>N&z`mJtuBs4!jCergRX!Rw1-)3f27 zf}UBJI3D}4bH2c3i#qq{L= zjXIGzQhGX{MWIxg;)>e^;zc(ZQPNglyGhNRyqLLQD}lB_pVCk|;@MgfNbR8KS}CI% zI*F=d6998^9SJWUm24JD^Oj3mAP&}%>LaoKdmU{8wwnzXP_~C%nu54yc)p>17c#k* z*OM_Moiw>T zqzj>@P@LC5O{t6OdY6Y>cKWJs#rqn~xepr1Gk`YEjGo{zB?UHIeZTUTfgwdhOj=YQ zUBXC8kj`&p_^71(O}KwI2*^4}cFhq-8o|mq#h)6%9=)yESkOd%AaXs<3X%so1>`~@ z0G;J(ct?z*#qW3*1iI zh&W>FJau(ymS<9nOw?28&-yd5rEDS;i3sABXi z=?MZ-f*mEc4h}*j9PP8nyCWHy1Gy8#D;%9Qx(#luB!N6jPjISLlzYg%0hOF8M&Aie z=(H9RL_~+^klS4&nTOvnZ57IqUw%&=i64H6cUow(%66dr)d7g=M*3+}V;Rnttq?%L zK~U8de%iKJh9Cf3WP=@-TMCID^V8}O=J#PD33f`YX>vjKmnPI4klO^BL4-`6Ua=jV z7J(|F?Y^7kC3n6>Bl~qK7w$En8kq|L%_Rrd_oK3zR3CokSDVBz!5hv69W#F*9jTI|^Jlz3c zEJ)SSHVAl!+Gq~CY*U9e_#Oi&KLJYArN(-L1f_}WN)`ut3ZEeWd&rTd>?qk83E9JE z6IZcp15@Lnr6Cvl$FmI=+e%@3AVEzU=TJqMoH{(BFz=dU$gIsW6B8CxE7)K9#0+>2 zh)jS_N5V{@c!a%eaGO6T3I7R+>axbBUZKTvV$-P25957WYv|eJ@d9-4ZdVQ-5JK`76lQEIM zpGVB!OqY<8o*)SjD6HHYN*;ZdME;BTSb zxHO@h`2J&CCl^Jsk0Vv@QXyP7`2GBU12^Xn%LqHXLT}Xl6*%{0`7L?U~|PCowT?0NK?hpPG%cqk^*%F z2ER|>4T!x<*bvD2k#7v(Fy(K8>GbbguV0*ad8{q7rl%Qn%azr%!0+v7W z5=le;mW3wK|6yS}#=e0h%yLHRwX#L{Coy1r5LM08<3lly6EPRDbU1v;w-6B3^U3ft zoe||rg^;DHRl0=mX*GDtae-kVA@Th( za`aP{>-dQiFlE|52m@yJ114A+ybXcyKurpy(b(zeXwfGZ9A%kz4<6P%=E`x=5>s*y%P{L?SUp{t0K0ng z-zvne9yZu1Unam~F&yQYRP2XqV30rbu+qpu3JuRI=m|DCWaKPXj<>(q5bi)@GR9j- zN#m~5K6Iq#hu^ zBoJZ;tA=TG@CnGHWSe1DdT*dmwqHSRzr;570$>-x4T^jT05>R<-w!R0?txH8!&)}b zN(ugwMwk5rFYhnhXs=# zo&aL0Gy3&_E?WopC-c`46NxwguxG><&#%)A9c~ZF6nb+zE9KCeAZZl5lp$##Fd`)? zPD(PwG%?o`P*FL+Y2O9E=_JpZU?Z&bjvgveSy&+#jZ`mQ)I=r3>0>e&lMJ!oDvGdT zM9C2gUWf8Y@moB16>*5J*C@5KWPM4b@0uDtUm2!C|VJ#!rK~QKDfPl2Y`c8Hf9!QR5WWYboeJ z?gt)+kpC)WA~$ga@h}vvZj90Fq;?1KodD$B$NKBx4uCGXutGd@t-MMG8zlaIE%}VQ$kyD13%H#ogMGK7Ts*so+X4i-w-3Rz;2$rD4Ph3R8<{H?8Ku!P#h z2J_XEB`U6CDQHuk?8!|CS*mqv9_Wmf*Rio!<6WICp1Y1EU>RF{d>tDJvjxNN2H&#Y z)k4O-eLdYD!d+yhxMMxZrnDpUHbDuB_+mXRIM#sN!8;aOarn%G?I+(Q!1gm@tbis~ zY#`=hlURIqph&@$IYiBA)v$S5Pf)%*fIzHzVW1E(`UKAl+bkirdtkjS<_yY)$qhd>TEA&|j?sj<)ra05aT`nPicAen}bh>e(< zNok=lS)*0V638(wK^Us1CbkLa)AQ|-rl6l=C z&Kfm5Y@KK}U4@N^VFBC}|w>LhmFgJ9Fy&>BFdUIsk zLiFANhCsW`c_SS#QOAg;8;Kwo5{N6|&l>;|uDiJHMv{ShZe(Yt$hQMS_FMb3vg8 zqk;9Z!UWjGaHyh3G=Kz|O~Y*#%uzxF_$sxl#CSy^&PdT$0MWID7y{%A#cf-NvuL|v zk}??{CfQ%MF&IlZRGYWLNp{*++TR+GD!R7Pw$=c&Zn~Tb2U~r(z#g}A-C-dNl#=G< zxC-szcrp%0tmCBrfDi`l;LmXJ1t;qb)de~fjpaBaSzb-iCNt&bmlJdYX1r4*Q)RP5 z&AwiFx}h5k%3KE^1%YMOx@-)ilIi5SQl^s2$^!AgHqFWcp+fL9_UP?2b{rfUM}1@| z6brWN=|ctPU296K;uxGGRDA5??WE6Xo_T@<8`VMD!)h9~8lO+*Awvtin2uKv8RtZZ zxJ}TSU>U;r0njRBN~!KkA-y+D+}n9Gv+65oeXems;3B=fh*HmfUd$FudmD1 zw@h-Odhac|-+Zy)R^|xztGNw8K27d_mtP+VpLr zP2VQk_$_Aapu~P%%LfpQtCTz9;GL5pqn!!c!e4R^Fy5>s+DTx5>+(9Y&fE zbUREqZx16%HqN>|jA-;xEMC2x#mJ6|W5>#XG}}q~Xv9u7wckOaXD2H^Q>>;D-VATQ z2wvW7UQSXC#%dxS9tQ8CxF@PsYc%HGgV4dnVTpKf7diV-&J%y$MOU1NWRG#xw8GEw zNwnei7|67iw?R;g{E(DZ>t3>*$9beib8&J4GkRjnhv}`8_GaI<975e+WANZ^QNUj$1iY%Jax+T zCAx6o)2xqQ)a$alY1?2q&RXYban;IfD>P~B6FoyPr{rzN!N|blF({B9t0Tr_bG`SF zN}=+ad&pFgA8eeSsOH!`v?f&kUX}@KHryu2Iiu^1@7jTb(y5-KZolrOpgp$4n+i9j zF{m$}afT=mxxbe40&f?Lk9@lU#s^a2$=abi6qy-`q6I&}Lh6)~!hZW}Qj~N|{Kv1^ z3|L%r7#y+c4F`;j9Ei*Aqcx=R1NRy0y#0R2GM9P*nG2sf5nGJ7pT;JssRtUG+PeFR zGkAy;dw>M=O68UEV2gqXR#&~nz($zlR`vij!mAN0J;0FEobcKce?H1|i}cU~Gz`%G zg?9DO0&sDFiRM=p3MNKBC|8qY$9P?#Yu7$#h#lNT$Tt}Wjk&DbNoQ;^W~o?r|dq|l47s$3Cd7YeD^v4>U+%9zv- zXYvK`XY^ir2LY0SS7LHCAptGC(?#E2l1iFI@;+KcXf)gouPot(15(E-Jn6urA?l2b zd0Pui&~T3JF=b;Xz-v#y{_Q?W7r`Ncih;Bo1d_~8epE^}&Z+ZJJ*NV3vkCrE7@p{; zg&)F#Hw&QQkOp!`6g@`gRG3e(IQOyePJy!CM8pjGHr-;exTAFIlL zdK^-_sk~G~KfxyHa@7;8u$tgO90Ma-k?*X-wQ_{;TQu;)!NyPX%4#v|DVFICqsl?W zMf?bFxHg8mNuZxX>rV-X5sURb{Lqv^V%bj+mkLtT3&hm@tU`|b0`EE_OnV| z{%k)Rmn`v-6m``tX4JrmefDV+t@%%e+uZUbt1z{(VfWqK=Ncsb^(0HxloJlHaYbRu z4#EnaPp4-6ES##BiH-wom?XH&SlVu})r6LbgMcT=FB6|1&`YuWzb*yU z{_;{Rr=^fYmy52a*jZB(I0}ej1(FXO+ zVvj9q5^E8EgRe1ustmTmpP4r3sXI7Zp`VQ)JAi_7u;jqfTrt%il^1^w0#X3b6FgTe zu}7^~FrOL)&%=r|BGWD*=OZMYAd3i65?qX+3?#P%OM|e~iKR>|EyYqcmV#Ky!O{hy z!VxuY-7)}N(HmTWa3{dKgIyAGAwmiWvPwc$BczZZ7fHy)2#F`iB@%KOLWU6JatT?B zkf8)wN05@>RR}5qQ1I$BEEZ$&8Y~UN(zW6{N0hVXSA-p0kMO&w{RRoS9wDO%a)X52 zh>$S^xrrbp!OaLN6J_7S=D7%dtuL@uqV^)Zf>5_fh(O2$g4|4ylHjcfnh2oa4lGTE zQcv(UstoQ#$P_}~B|eLbTAR0DLIUC&+z-9(({H zGl|ZF67n!Y8VK?TLBc;6J+q0x`v_bj+T)_q#o&afoOO@MrjKLOX2N+wLY_p(EP@=6 zkf#yiC&)7r@*F|}1UV=nhY-?AkQXH6w+NX{kQXK7cL-@C$YHsn!Iu$qHi7;ik&Yl_ z4ndAe$T5V>CCG6Jc^x6=5abOBc?%)u669?Oc^4t`2=YgQlm!2Tp!ooTpXwFk6QfG< z-$zJ0K!P7&X(5(A#M1d#>JwKaM&&wB000F2F9`1dcz5ux1StuAjG)CNDtSw=#U}{p zgi=rNQ-$*xf|g2gvL;34)chR*%Mkl>EG@^<7g$;WCHkf9P%jF9UIGE72-Bjg5xloF&QI8uy+z&aZxno^=#*Oei{ zRzT_5VSRnY~1FszjO&e`anY$P5V~u52erH9<;(ZbT9Q3f5xj zW-QgM!{RMa> Date: Wed, 24 Jul 2024 08:37:18 -0400 Subject: [PATCH 047/222] 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 9b28d1d451..b3aa1e6a34 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -160,9 +160,9 @@ class TunicWorld(World): 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 @@ class TunicWorld(World): 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 @@ class TunicWorld(World): 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 048/222] 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 3b6c5cd4dd..ea9ec6a805 100644 --- a/worlds/subnautica/rules.py +++ b/worlds/subnautica/rules.py @@ -150,7 +150,7 @@ def has_ultra_glide_fins(state: "CollectionState", player: int) -> bool: def get_max_swim_depth(state: "CollectionState", player: int) -> int: - swim_rule: SwimRule = state.multiworld.swim_rule[player] + swim_rule: SwimRule = state.multiworld.worlds[player].options.swim_rule depth: int = swim_rule.base_depth if swim_rule.consider_items: if has_seaglide(state, player): @@ -296,7 +296,7 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(multiworld, player, loc) if subnautica_world.creatures_to_scan: - option = multiworld.creature_scan_logic[player] + option = multiworld.worlds[player].options.creature_scan_logic for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(multiworld, player, creature_name) From 2307694012f3d49dbacbcdb059473f52c87aedea Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 24 Jul 2024 20:08:58 -0500 Subject: [PATCH 049/222] 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 78287305df..fbc6461f6a 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -554,7 +554,8 @@ class HKWorld(World): for effect_name, effect_value in item_effects.get(item.name, {}).items(): if state.prog_items[item.player][effect_name] == effect_value: del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + else: + state.prog_items[item.player][effect_name] -= effect_value return change 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 050/222] 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 051/222] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Also=20fix=20Rt?= =?UTF-8?q?=204=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 251beb59cc..6aee25df26 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -427,7 +427,7 @@ location_data = [ LocationData("Seafoam Islands B3F", "Hidden Item Rock", "Max Elixir", rom_addresses['Hidden_Item_Seafoam_Islands_B3F'], Hidden(50), inclusion=hidden_items), LocationData("Vermilion City", "Hidden Item In Water Near Fan Club", "Max Ether", rom_addresses['Hidden_Item_Vermilion_City'], Hidden(51), inclusion=hidden_items), LocationData("Cerulean City-Badge House Backyard", "Hidden Item Gym Badge Guy's Backyard", "Rare Candy", rom_addresses['Hidden_Item_Cerulean_City'], Hidden(52), inclusion=hidden_items), - LocationData("Route 4-E", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), + LocationData("Route 4-C", "Hidden Item Plateau East Of Mt Moon", "Great Ball", rom_addresses['Hidden_Item_Route_4'], Hidden(53), inclusion=hidden_items), LocationData("Oak's Lab", "Oak's Parcel Reward", "Pokedex", rom_addresses["Event_Pokedex"], EventFlag(0x38)), From f34da74012ba0f69433b158a68e008dc36a258d7 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 25 Jul 2024 00:13:16 -0400 Subject: [PATCH 052/222] 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 2604ad2c46..e026090f86 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -307,7 +307,7 @@ id,name,classification,groups,mod_name 322,Phoenix Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 323,Immunity Band,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", 324,Glowstone Ring,useful,"RING,RESOURCE_PACK,MAXIMUM_ONE", -325,Fairy Dust Recipe,progression,, +325,Fairy Dust Recipe,progression,"GINGER_ISLAND", 326,Heavy Tapper Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 327,Hyper Speed-Gro Recipe,progression,"QI_CRAFTING_RECIPE,GINGER_ISLAND", 328,Deluxe Fertilizer Recipe,progression,QI_CRAFTING_RECIPE, diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 6e30d2b8c8..242d00b445 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 cb61020169..31c7da5e3a 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -409,8 +409,9 @@ def create_special_quest_rewards(item_factory: StardewItemFactory, options: Star else: items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE items.append(item_factory(Wallet.iridium_snake_milk)) - items.append(item_factory("Fairy Dust Recipe")) items.append(item_factory("Dark Talisman")) + if options.exclude_ginger_island == ExcludeGingerIsland.option_false: + items.append(item_factory("Fairy Dust Recipe")) def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): From 496f0e09afe7e863cf1bcce948d48bf591086274 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 01:21:51 -0500 Subject: [PATCH 053/222] 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 f8d1fcb7a2..09937e4b9a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -61,6 +61,7 @@ class ClientCommandProcessor(CommandProcessor): if address: self.ctx.server_address = None self.ctx.username = None + self.ctx.password = None elif not self.ctx.server_address: self.output("Please specify an address.") return False @@ -514,6 +515,7 @@ class CommonContext: async def shutdown(self): self.server_address = "" self.username = None + self.password = None self.cancel_autoreconnect() if self.server and not self.server.socket.closed: await self.server.socket.close() diff --git a/kvui.py b/kvui.py index a63d636960..f83590a819 100644 --- a/kvui.py +++ b/kvui.py @@ -596,6 +596,7 @@ class GameManager(App): def connect_button_action(self, button): self.ctx.username = None + self.ctx.password = None if self.ctx.server: async_start(self.ctx.disconnect()) else: From deae524e9ba9f1bab1c48642f11108799d7b3b3a Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 02:05:04 -0500 Subject: [PATCH 054/222] 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 3b40d7e77a..ab841e65ee 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,8 +1,8 @@ # Archipelago World Code Owners / Maintainers Document # -# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder. For any pull -# requests that modify these worlds, a code owner must approve the PR in addition to a core maintainer. This is not to -# be used for files/folders outside the /worlds folder, those will always need sign off from a core maintainer. +# This file is used to notate the current "owners" or "maintainers" of any currently merged world folder as well as +# certain documentation. For any pull requests that modify these worlds/docs, a code owner must approve the PR in +# addition to a core maintainer. All other files and folders are owned and maintained by core maintainers directly. # # All usernames must be GitHub usernames (and are case sensitive). @@ -226,3 +226,11 @@ # Ori and the Blind Forest # /worlds_disabled/oribf/ + +################### +## Documentation ## +################### + +# Apworld Dev Faq +/docs/apworld_dev_faq.md @qwint @ScipioWright + diff --git a/docs/apworld_dev_faq.md b/docs/apworld_dev_faq.md new file mode 100644 index 0000000000..059c33844f --- /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 055/222] 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 7ab618c344..7927705214 100644 --- a/settings.py +++ b/settings.py @@ -3,6 +3,7 @@ Application settings / host.yaml interface using type hints. This is different from player options. """ +import os import os.path import shutil import sys @@ -11,7 +12,6 @@ import warnings from enum import IntEnum from threading import Lock from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar -import os __all__ = [ "get_settings", "fmt_doc", "no_gui", @@ -798,6 +798,7 @@ class Settings(Group): atexit.register(autosave) def save(self, location: Optional[str] = None) -> None: # as above + from Utils import parse_yaml location = location or self._filename assert location, "No file specified" temp_location = location + ".tmp" # not using tempfile to test expected file access @@ -807,10 +808,18 @@ class Settings(Group): # can't use utf-8-sig because it breaks backward compat: pyyaml on Windows with bytes does not strip the BOM with open(temp_location, "w", encoding="utf-8") as f: self.dump(f) - # replace old with new - if os.path.exists(location): + f.flush() + if hasattr(os, "fsync"): + os.fsync(f.fileno()) + # validate new file is valid yaml + with open(temp_location, encoding="utf-8") as f: + parse_yaml(f.read()) + # replace old with new, try atomic operation first + try: + os.rename(temp_location, location) + except (OSError, FileExistsError): os.unlink(location) - os.rename(temp_location, location) + os.rename(temp_location, location) self._filename = location def dump(self, f: TextIO, level: int = 0) -> None: @@ -832,7 +841,6 @@ def get_settings() -> Settings: with _lock: # make sure we only have one instance res = getattr(get_settings, "_cache", None) if not res: - import os from Utils import user_path, local_path filenames = ("options.yaml", "host.yaml") locations: List[str] = [] diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 7174befca4..3edbd34a51 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -1,11 +1,12 @@ import os +import os.path import unittest from io import StringIO -from tempfile import TemporaryFile +from tempfile import TemporaryDirectory, TemporaryFile from typing import Any, Dict, List, cast import Utils -from settings import Settings, Group +from settings import Group, Settings, ServerOptions class TestIDs(unittest.TestCase): @@ -80,3 +81,27 @@ class TestSettingsDumper(unittest.TestCase): self.assertEqual(value_spaces[2], value_spaces[0]) # start of sub-list self.assertGreater(value_spaces[3], value_spaces[0], f"{value_lines[3]} should have more indentation than {value_lines[0]} in {lines}") + + +class TestSettingsSave(unittest.TestCase): + def test_save(self) -> None: + """Test that saving and updating works""" + with TemporaryDirectory() as d: + filename = os.path.join(d, "host.yaml") + new_release_mode = ServerOptions.ReleaseMode("enabled") + # create default host.yaml + settings = Settings(None) + settings.save(filename) + self.assertTrue(os.path.exists(filename), + "Default settings could not be saved") + self.assertNotEqual(settings.server_options.release_mode, new_release_mode, + "Unexpected default release mode") + # update host.yaml + settings.server_options.release_mode = new_release_mode + settings.save(filename) + self.assertFalse(os.path.exists(filename + ".tmp"), + "Temp file was not removed during save") + # read back host.yaml + settings = Settings(filename) + self.assertEqual(settings.server_options.release_mode, new_release_mode, + "Settings were not overwritten") 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 056/222] 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 0000000000..c05c936de3 --- /dev/null +++ b/worlds/stardew_valley/content/mods/alecto.py @@ -0,0 +1,33 @@ +from ..game_content import ContentPack, StardewContent +from ..mod_registry import register_mod_content_pack +from ...data import villagers_data +from ...data.harvest import ForagingSource +from ...data.requirement import QuestRequirement +from ...mods.mod_data import ModNames +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.seed_names import DistantLandsSeed + + +class AlectoContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + if ModNames.distant_lands in content.registered_packs: + content.game_items.pop(DistantLandsSeed.void_mint) + content.game_items.pop(DistantLandsSeed.vile_ancient_fruit) + content.source_item(DistantLandsSeed.void_mint, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)),), + content.source_item(DistantLandsSeed.vile_ancient_fruit, + ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.WitchOrder),)), ), + + +register_mod_content_pack(ContentPack( + ModNames.alecto, + weak_dependencies=( + ModNames.distant_lands, # For Witch's order + ), + villagers=( + villagers_data.alecto, + ) + +)) diff --git a/worlds/stardew_valley/content/mods/archeology.py b/worlds/stardew_valley/content/mods/archeology.py index 97d38085d3..5eb8af4cfc 100644 --- a/worlds/stardew_valley/content/mods/archeology.py +++ b/worlds/stardew_valley/content/mods/archeology.py @@ -1,20 +1,34 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack -from ...data.game_item import ItemTag, Tag -from ...data.shop import ShopSource +from ...data.artisan import MachineSource from ...data.skill import Skill from ...mods.mod_data import ModNames -from ...strings.book_names import ModBook -from ...strings.region_names import LogicRegion +from ...strings.craftable_names import ModMachine +from ...strings.fish_names import ModTrash +from ...strings.metal_names import all_artifacts, all_fossils from ...strings.skill_names import ModSkill -register_mod_content_pack(ContentPack( + +class ArchaeologyContentPack(ContentPack): + def artisan_good_hook(self, content: StardewContent): + # Done as honestly there are too many display items to put into the initial registration traditionally. + display_items = all_artifacts + all_fossils + for item in display_items: + self.source_display_items(item, content) + content.source_item(ModTrash.rusty_scrap, *(MachineSource(item=artifact, machine=ModMachine.grinder) for artifact in all_artifacts)) + + def source_display_items(self, item: str, content: StardewContent): + wood_display = f"Wooden Display: {item}" + hardwood_display = f"Hardwood Display: {item}" + if item == "Trilobite": + wood_display = f"Wooden Display: Trilobite Fossil" + hardwood_display = f"Hardwood Display: Trilobite Fossil" + content.source_item(wood_display, MachineSource(item=str(item), machine=ModMachine.preservation_chamber)) + content.source_item(hardwood_display, MachineSource(item=str(item), machine=ModMachine.hardwood_preservation_chamber)) + + +register_mod_content_pack(ArchaeologyContentPack( ModNames.archaeology, - shop_sources={ - ModBook.digging_like_worms: ( - Tag(ItemTag.BOOK, ItemTag.BOOK_SKILL), - ShopSource(money_price=500, shop_region=LogicRegion.bookseller_1),), - }, skills=(Skill(name=ModSkill.archaeology, has_mastery=False),), )) diff --git a/worlds/stardew_valley/content/mods/distant_lands.py b/worlds/stardew_valley/content/mods/distant_lands.py index 19380d4ff5..c5614d1302 100644 --- a/worlds/stardew_valley/content/mods/distant_lands.py +++ b/worlds/stardew_valley/content/mods/distant_lands.py @@ -1,9 +1,26 @@ -from ..game_content import ContentPack +from ..game_content import ContentPack, StardewContent from ..mod_registry import register_mod_content_pack from ...data import villagers_data, fish_data +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import QuestRequirement from ...mods.mod_data import ModNames +from ...strings.crop_names import DistantLandsCrop +from ...strings.forageable_names import DistantLandsForageable +from ...strings.quest_names import ModQuest +from ...strings.region_names import Region +from ...strings.season_names import Season +from ...strings.seed_names import DistantLandsSeed -register_mod_content_pack(ContentPack( + +class DistantLandsContentPack(ContentPack): + + def harvest_source_hook(self, content: StardewContent): + content.untag_item(DistantLandsSeed.void_mint, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(DistantLandsSeed.vile_ancient_fruit, tag=ItemTag.CROPSANITY_SEED) + + +register_mod_content_pack(DistantLandsContentPack( ModNames.distant_lands, fishes=( fish_data.void_minnow, @@ -13,5 +30,13 @@ register_mod_content_pack(ContentPack( ), villagers=( villagers_data.zic, - ) + ), + harvest_sources={ + DistantLandsForageable.swamp_herb: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsForageable.brown_amanita: (ForagingSource(regions=(Region.witch_swamp,)),), + DistantLandsSeed.void_mint: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.void_mint: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=DistantLandsSeed.void_mint, seasons=(Season.spring, Season.summer, Season.fall)),), + DistantLandsSeed.vile_ancient_fruit: (ForagingSource(regions=(Region.witch_swamp,), other_requirements=(QuestRequirement(ModQuest.CorruptedCropsTask),)),), + DistantLandsCrop.vile_ancient_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=DistantLandsSeed.vile_ancient_fruit, seasons=(Season.spring, Season.summer, Season.fall)),) + } )) diff --git a/worlds/stardew_valley/content/mods/npc_mods.py b/worlds/stardew_valley/content/mods/npc_mods.py index 3172a55dbf..52d97d5c52 100644 --- a/worlds/stardew_valley/content/mods/npc_mods.py +++ b/worlds/stardew_valley/content/mods/npc_mods.py @@ -73,13 +73,6 @@ register_mod_content_pack(ContentPack( ) )) -register_mod_content_pack(ContentPack( - ModNames.alecto, - villagers=( - villagers_data.alecto, - ) -)) - register_mod_content_pack(ContentPack( ModNames.lacey, villagers=( diff --git a/worlds/stardew_valley/content/mods/sve.py b/worlds/stardew_valley/content/mods/sve.py index f74b80948c..a68d4ae9c0 100644 --- a/worlds/stardew_valley/content/mods/sve.py +++ b/worlds/stardew_valley/content/mods/sve.py @@ -3,15 +3,27 @@ from ..mod_registry import register_mod_content_pack from ..override import override from ..vanilla.ginger_island import ginger_island_content_pack as ginger_island_content_pack from ...data import villagers_data, fish_data -from ...data.harvest import ForagingSource -from ...data.requirement import YearRequirement +from ...data.game_item import ItemTag, Tag +from ...data.harvest import ForagingSource, HarvestCropSource +from ...data.requirement import YearRequirement, CombatRequirement, RelationshipRequirement, ToolRequirement, SkillRequirement, FishingRequirement +from ...data.shop import ShopSource from ...mods.mod_data import ModNames -from ...strings.crop_names import Fruit -from ...strings.fish_names import WaterItem +from ...strings.craftable_names import ModEdible +from ...strings.crop_names import Fruit, SVEVegetable, SVEFruit +from ...strings.fish_names import WaterItem, SVEFish, SVEWaterItem from ...strings.flower_names import Flower -from ...strings.forageable_names import Mushroom, Forageable -from ...strings.region_names import Region, SVERegion +from ...strings.food_names import SVEMeal, SVEBeverage +from ...strings.forageable_names import Mushroom, Forageable, SVEForage +from ...strings.gift_names import SVEGift +from ...strings.metal_names import Ore +from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.performance_names import Performance +from ...strings.region_names import Region, SVERegion, LogicRegion from ...strings.season_names import Season +from ...strings.seed_names import SVESeed +from ...strings.skill_names import Skill +from ...strings.tool_names import Tool, ToolMaterial +from ...strings.villager_names import ModNPC class SVEContentPack(ContentPack): @@ -38,6 +50,24 @@ class SVEContentPack(ContentPack): # Remove Lance if Ginger Island is not in content since he is first encountered in Volcano Forge content.villagers.pop(villagers_data.lance.name) + def harvest_source_hook(self, content: StardewContent): + content.untag_item(SVESeed.shrub, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.fungus, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.slime, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.stalk, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.void, tag=ItemTag.CROPSANITY_SEED) + content.untag_item(SVESeed.ancient_fern, tag=ItemTag.CROPSANITY_SEED) + if ginger_island_content_pack.name not in content.registered_packs: + # Remove Highlands seeds as these are behind Lance existing. + content.game_items.pop(SVESeed.void) + content.game_items.pop(SVEVegetable.void_root) + content.game_items.pop(SVESeed.stalk) + content.game_items.pop(SVEFruit.monster_fruit) + content.game_items.pop(SVESeed.fungus) + content.game_items.pop(SVEVegetable.monster_mushroom) + content.game_items.pop(SVESeed.slime) + content.game_items.pop(SVEFruit.slime_berry) + register_mod_content_pack(SVEContentPack( ModNames.sve, @@ -45,12 +75,24 @@ register_mod_content_pack(SVEContentPack( ginger_island_content_pack.name, ModNames.jasper, # To override Marlon and Gunther ), + shop_sources={ + SVEGift.aged_blue_moon_wine: (ShopSource(money_price=28000, shop_region=SVERegion.blue_moon_vineyard),), + SVEGift.blue_moon_wine: (ShopSource(money_price=3000, shop_region=SVERegion.blue_moon_vineyard),), + ModEdible.lightning_elixir: (ShopSource(money_price=12000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.barbarian_elixir: (ShopSource(money_price=22000, shop_region=SVERegion.galmoran_outpost),), + ModEdible.gravity_elixir: (ShopSource(money_price=4000, shop_region=SVERegion.galmoran_outpost),), + SVEMeal.grampleton_orange_chicken: (ShopSource(money_price=650, shop_region=Region.saloon, other_requirements=(RelationshipRequirement(ModNPC.sophia, 6),)),), + ModEdible.hero_elixir: (ShopSource(money_price=8000, shop_region=SVERegion.isaac_shop),), + ModEdible.aegis_elixir: (ShopSource(money_price=28000, shop_region=SVERegion.galmoran_outpost),), + SVEBeverage.sports_drink: (ShopSource(money_price=750, shop_region=Region.hospital),), + SVEMeal.stamina_capsule: (ShopSource(money_price=4000, shop_region=Region.hospital),), + }, harvest_sources={ Mushroom.red: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.summer, Season.fall)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) ), Mushroom.purple: ( - ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) + ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave, SVERegion.junimo_woods), ) ), Mushroom.morel: ( ForagingSource(regions=(SVERegion.forest_west,), seasons=(Season.fall,)), ForagingSource(regions=(SVERegion.sprite_spring_cave,), ) @@ -64,17 +106,59 @@ register_mod_content_pack(SVEContentPack( Flower.sunflower: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.summer,)),), Flower.fairy_rose: (ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.fall,)),), Fruit.ancient_fruit: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ForagingSource(regions=(SVERegion.sprite_spring_cave,)), ), Fruit.sweet_gem_berry: ( - ForagingSource(regions=(SVERegion.sprite_spring,), seasons=(Season.spring, Season.summer, Season.fall), other_requirements=(YearRequirement(3),)), + ForagingSource(regions=(SVERegion.sprite_spring,), seasons=Season.not_winter, other_requirements=(YearRequirement(3),)), ), + # New items + + ModLoot.green_mushroom: (ForagingSource(regions=(SVERegion.highlands_pond,), seasons=Season.not_winter),), + ModLoot.ornate_treasure_chest: (ForagingSource(regions=(SVERegion.highlands_outside,), + other_requirements=(CombatRequirement(Performance.galaxy), ToolRequirement(Tool.axe, ToolMaterial.iron))),), + ModLoot.swirl_stone: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.galaxy),)),), + ModLoot.void_soul: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEForage.winter_star_rose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.winter,)),), + SVEForage.bearberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.winter,)),), + SVEForage.poison_mushroom: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.fall)),), + SVEForage.red_baneberry: (ForagingSource(regions=(Region.secret_woods,), seasons=(Season.summer, Season.summer)),), + SVEForage.ferngill_primrose: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.spring,)),), + SVEForage.goldenrod: (ForagingSource(regions=(SVERegion.summit,), seasons=(Season.summer, Season.fall)),), + SVEForage.conch: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,)),), + SVEForage.dewdrop_berry: (ForagingSource(regions=(SVERegion.enchanted_grove,)),), + SVEForage.sand_dollar: (ForagingSource(regions=(Region.beach, SVERegion.fable_reef,), seasons=(Season.spring, Season.summer)),), + SVEForage.golden_ocean_flower: (ForagingSource(regions=(SVERegion.fable_reef,)),), + SVEForage.four_leaf_clover: (ForagingSource(regions=(Region.secret_woods, SVERegion.forest_west,), seasons=(Season.summer, Season.fall)),), + SVEForage.mushroom_colony: (ForagingSource(regions=(Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west,), seasons=(Season.fall,)),), + SVEForage.rusty_blade: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + SVEForage.rafflesia: (ForagingSource(regions=(Region.secret_woods,), seasons=Season.not_winter),), + SVEForage.thistle: (ForagingSource(regions=(SVERegion.summit,)),), + ModLoot.void_pebble: (ForagingSource(regions=(SVERegion.crimson_badlands,), other_requirements=(CombatRequirement(Performance.great),)),), + ModLoot.void_shard: (ForagingSource(regions=(SVERegion.crimson_badlands,), + other_requirements=(CombatRequirement(Performance.galaxy), SkillRequirement(Skill.combat, 10), YearRequirement(3),)),), + SVEWaterItem.dulse_seaweed: (ForagingSource(regions=(Region.beach,), other_requirements=(FishingRequirement(Region.beach),)),), + # Fable Reef WaterItem.coral: (ForagingSource(regions=(SVERegion.fable_reef,)),), Forageable.rainbow_shell: (ForagingSource(regions=(SVERegion.fable_reef,)),), WaterItem.sea_urchin: (ForagingSource(regions=(SVERegion.fable_reef,)),), + + # Crops + SVESeed.shrub: (ForagingSource(regions=(Region.secret_woods,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.salal_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.shrub, seasons=(Season.spring,)),), + SVESeed.slime: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.slime_berry: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.slime, seasons=(Season.spring,)),), + SVESeed.ancient_fern: (ForagingSource(regions=(Region.secret_woods,)),), + SVEVegetable.ancient_fiber: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.ancient_fern, seasons=(Season.summer,)),), + SVESeed.stalk: (ForagingSource(regions=(SVERegion.highlands_outside,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEFruit.monster_fruit: (Tag(ItemTag.FRUIT), HarvestCropSource(seed=SVESeed.stalk, seasons=(Season.summer,)),), + SVESeed.fungus: (ForagingSource(regions=(SVERegion.highlands_pond,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.monster_mushroom: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.fungus, seasons=(Season.fall,)),), + SVESeed.void: (ForagingSource(regions=(SVERegion.highlands_cavern,), other_requirements=(CombatRequirement(Performance.good),)),), + SVEVegetable.void_root: (Tag(ItemTag.VEGETABLE), HarvestCropSource(seed=SVESeed.void, seasons=(Season.winter,)),), + }, fishes=( fish_data.baby_lunaloo, # Removed when no ginger island diff --git a/worlds/stardew_valley/content/vanilla/pelican_town.py b/worlds/stardew_valley/content/vanilla/pelican_town.py index 917e8cca22..220b46eae2 100644 --- a/worlds/stardew_valley/content/vanilla/pelican_town.py +++ b/worlds/stardew_valley/content/vanilla/pelican_town.py @@ -229,7 +229,7 @@ pelican_town = ContentPack( ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.mapping_cave_systems: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.adventurer_guild_bedroom), + GenericSource(regions=(Region.adventurer_guild_bedroom,)), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.monster_compendium: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), @@ -243,12 +243,12 @@ pelican_town = ContentPack( ShopSource(money_price=3000, shop_region=LogicRegion.bookseller_2),), Book.the_alleyway_buffet: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.town, + GenericSource(regions=(Region.town,), other_requirements=(ToolRequirement(Tool.axe, ToolMaterial.iron), ToolRequirement(Tool.pickaxe, ToolMaterial.iron))), ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),), Book.the_art_o_crabbing: ( Tag(ItemTag.BOOK, ItemTag.BOOK_POWER), - GenericSource(regions=Region.beach, + GenericSource(regions=(Region.beach,), other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium), SkillRequirement(Skill.fishing, 6), SeasonRequirement(Season.winter))), diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py index c6f0c30d41..26b1a0d58a 100644 --- a/worlds/stardew_valley/data/fish_data.py +++ b/worlds/stardew_valley/data/fish_data.py @@ -46,7 +46,8 @@ pirate_cove = (Region.pirate_cove,) crimson_badlands = (SVERegion.crimson_badlands,) shearwater = (SVERegion.shearwater,) -highlands = (SVERegion.highlands_outside,) +highlands_pond = (SVERegion.highlands_pond,) +highlands_cave = (SVERegion.highlands_cavern,) sprite_spring = (SVERegion.sprite_spring,) fable_reef = (SVERegion.fable_reef,) vineyard = (SVERegion.blue_moon_vineyard,) @@ -133,9 +134,9 @@ bonefish = create_fish(SVEFish.bonefish, crimson_badlands, season.all_seasons, 7 bull_trout = create_fish(SVEFish.bull_trout, forest_river, season.not_spring, 45, mod_name=ModNames.sve) butterfish = create_fish(SVEFish.butterfish, shearwater, season.not_winter, 75, mod_name=ModNames.sve) clownfish = create_fish(SVEFish.clownfish, ginger_island_ocean, season.all_seasons, 45, mod_name=ModNames.sve) -daggerfish = create_fish(SVEFish.daggerfish, highlands, season.all_seasons, 50, mod_name=ModNames.sve) +daggerfish = create_fish(SVEFish.daggerfish, highlands_pond, season.all_seasons, 50, mod_name=ModNames.sve) frog = create_fish(SVEFish.frog, mountain_lake, (season.spring, season.summer), 70, mod_name=ModNames.sve) -gemfish = create_fish(SVEFish.gemfish, highlands, season.all_seasons, 100, mod_name=ModNames.sve) +gemfish = create_fish(SVEFish.gemfish, highlands_cave, season.all_seasons, 100, mod_name=ModNames.sve) goldenfish = create_fish(SVEFish.goldenfish, sprite_spring, season.all_seasons, 60, mod_name=ModNames.sve) grass_carp = create_fish(SVEFish.grass_carp, secret_woods, (season.spring, season.summer), 85, mod_name=ModNames.sve) king_salmon = create_fish(SVEFish.king_salmon, forest_river, (season.spring, season.summer), 80, mod_name=ModNames.sve) diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index 242d00b445..0d7a10f954 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 b482468762..3123bb9243 100644 --- a/worlds/stardew_valley/data/recipe_data.py +++ b/worlds/stardew_valley/data/recipe_data.py @@ -5,7 +5,7 @@ from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood from ..strings.craftable_names import ModEdible, Edible from ..strings.crop_names import Fruit, Vegetable, SVEFruit, DistantLandsCrop -from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish +from ..strings.fish_names import Fish, SVEFish, WaterItem, DistantLandsFish, SVEWaterItem from ..strings.flower_names import Flower from ..strings.forageable_names import Forageable, SVEForage, DistantLandsForageable, Mushroom from ..strings.ingredient_names import Ingredient @@ -195,7 +195,7 @@ mixed_berry_pie = shop_recipe(SVEMeal.mixed_berry_pie, Region.saloon, 3500, {Fru ModNames.sve) mushroom_berry_rice = friendship_and_shop_recipe(SVEMeal.mushroom_berry_rice, ModNPC.marlon, 6, Region.adventurer_guild, 1500, {SVEForage.poison_mushroom: 3, SVEForage.red_baneberry: 10, Ingredient.rice: 1, Ingredient.sugar: 2}, ModNames.sve) -seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEFish.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) +seaweed_salad = shop_recipe(SVEMeal.seaweed_salad, Region.fish_shop, 1250, {SVEWaterItem.dulse_seaweed: 2, WaterItem.seaweed: 2, Ingredient.oil: 1}, ModNames.sve) void_delight = friendship_and_shop_recipe(SVEMeal.void_delight, NPC.krobus, 10, Region.sewer, 5000, {SVEFish.void_eel: 1, Loot.void_essence: 50, Loot.solar_essence: 20}, ModNames.sve) void_salmon_sushi = friendship_and_shop_recipe(SVEMeal.void_salmon_sushi, NPC.krobus, 10, Region.sewer, 5000, diff --git a/worlds/stardew_valley/data/requirement.py b/worlds/stardew_valley/data/requirement.py index 4744f9dffd..b2416d8d0b 100644 --- a/worlds/stardew_valley/data/requirement.py +++ b/worlds/stardew_valley/data/requirement.py @@ -31,6 +31,27 @@ class YearRequirement(Requirement): year: int +@dataclass(frozen=True) +class CombatRequirement(Requirement): + level: str + + +@dataclass(frozen=True) +class QuestRequirement(Requirement): + quest: str + + +@dataclass(frozen=True) +class RelationshipRequirement(Requirement): + npc: str + hearts: int + + +@dataclass(frozen=True) +class FishingRequirement(Requirement): + region: str + + @dataclass(frozen=True) class WalnutRequirement(Requirement): amount: int diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index ca54d35e14..f14dbac821 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -16,8 +16,8 @@ class ShopSource(ItemSource): other_requirements: Tuple[Requirement, ...] = () def __post_init__(self): - assert self.money_price or self.items_price, "At least money price or items price need to be defined." - assert self.items_price is None or all(type(p) == tuple for p in self.items_price), "Items price should be a tuple." + assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." + assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." @dataclass(frozen=True, **kw_only) diff --git a/worlds/stardew_valley/logic/requirement_logic.py b/worlds/stardew_valley/logic/requirement_logic.py index 9356440ac6..6a5adf4890 100644 --- a/worlds/stardew_valley/logic/requirement_logic.py +++ b/worlds/stardew_valley/logic/requirement_logic.py @@ -3,15 +3,20 @@ from typing import Union, Iterable from .base_logic import BaseLogicMixin, BaseLogic from .book_logic import BookLogicMixin +from .combat_logic import CombatLogicMixin +from .fishing_logic import FishingLogicMixin from .has_logic import HasLogicMixin +from .quest_logic import QuestLogicMixin from .received_logic import ReceivedLogicMixin +from .relationship_logic import RelationshipLogicMixin from .season_logic import SeasonLogicMixin from .skill_logic import SkillLogicMixin from .time_logic import TimeLogicMixin from .tool_logic import ToolLogicMixin from .walnut_logic import WalnutLogicMixin from ..data.game_item import Requirement -from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, WalnutRequirement +from ..data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement, YearRequirement, CombatRequirement, QuestRequirement, \ + RelationshipRequirement, FishingRequirement, WalnutRequirement class RequirementLogicMixin(BaseLogicMixin): @@ -21,7 +26,7 @@ class RequirementLogicMixin(BaseLogicMixin): class RequirementLogic(BaseLogic[Union[RequirementLogicMixin, HasLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, BookLogicMixin, -SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): +SeasonLogicMixin, TimeLogicMixin, CombatLogicMixin, QuestLogicMixin, RelationshipLogicMixin, FishingLogicMixin, WalnutLogicMixin]]): def meet_all_requirements(self, requirements: Iterable[Requirement]): if not requirements: @@ -55,3 +60,21 @@ SeasonLogicMixin, TimeLogicMixin, WalnutLogicMixin]]): @meet_requirement.register def _(self, requirement: WalnutRequirement): return self.logic.walnut.has_walnut(requirement.amount) + + @meet_requirement.register + def _(self, requirement: CombatRequirement): + return self.logic.combat.can_fight_at_level(requirement.level) + + @meet_requirement.register + def _(self, requirement: QuestRequirement): + return self.logic.quest.can_complete_quest(requirement.quest) + + @meet_requirement.register + def _(self, requirement: RelationshipRequirement): + return self.logic.relationship.has_hearts(requirement.npc, requirement.hearts) + + @meet_requirement.register + def _(self, requirement: FishingRequirement): + return self.logic.fishing.can_fish_at(requirement.region) + + diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index cfafc88e83..ef5eab0134 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -23,24 +23,15 @@ from ...logic.tool_logic import ToolLogicMixin from ...options import Cropsanity from ...stardew_rule import StardewRule, True_ from ...strings.artisan_good_names import ModArtisanGood -from ...strings.craftable_names import ModCraftable, ModEdible, ModMachine -from ...strings.crop_names import SVEVegetable, SVEFruit, DistantLandsCrop -from ...strings.fish_names import ModTrash, SVEFish -from ...strings.food_names import SVEMeal, SVEBeverage -from ...strings.forageable_names import SVEForage, DistantLandsForageable -from ...strings.gift_names import SVEGift +from ...strings.craftable_names import ModCraftable, ModMachine +from ...strings.fish_names import ModTrash from ...strings.ingredient_names import Ingredient from ...strings.material_names import Material from ...strings.metal_names import all_fossils, all_artifacts, Ore, ModFossil -from ...strings.monster_drop_names import ModLoot, Loot +from ...strings.monster_drop_names import Loot from ...strings.performance_names import Performance -from ...strings.quest_names import ModQuest -from ...strings.region_names import Region, SVERegion, DeepWoodsRegion, BoardingHouseRegion -from ...strings.season_names import Season -from ...strings.seed_names import SVESeed, DistantLandsSeed -from ...strings.skill_names import Skill +from ...strings.region_names import SVERegion, DeepWoodsRegion, BoardingHouseRegion from ...strings.tool_names import Tool, ToolMaterial -from ...strings.villager_names import ModNPC display_types = [ModCraftable.wooden_display, ModCraftable.hardwood_display] display_items = all_artifacts + all_fossils @@ -58,12 +49,6 @@ FarmingLogicMixin]]): def get_modded_item_rules(self) -> Dict[str, StardewRule]: items = dict() - if ModNames.sve in self.options.mods: - items.update(self.get_sve_item_rules()) - if ModNames.archaeology in self.options.mods: - items.update(self.get_archaeology_item_rules()) - if ModNames.distant_lands in self.options.mods: - items.update(self.get_distant_lands_item_rules()) if ModNames.boarding_house in self.options.mods: items.update(self.get_boarding_house_item_rules()) return items @@ -75,61 +60,6 @@ FarmingLogicMixin]]): item_rule.update(self.get_modified_item_rules_for_deep_woods(item_rule)) return item_rule - def get_sve_item_rules(self): - return {SVEGift.aged_blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 28000), - SVEGift.blue_moon_wine: self.logic.money.can_spend_at(SVERegion.sophias_house, 3000), - SVESeed.fungus: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.green_mushroom: self.logic.region.can_reach(SVERegion.highlands_outside) & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.logic.season.has_any_not_winter(), - SVEFruit.monster_fruit: self.logic.season.has(Season.summer) & self.logic.has(SVESeed.stalk), - SVEVegetable.monster_mushroom: self.logic.season.has(Season.fall) & self.logic.has(SVESeed.fungus), - ModLoot.ornate_treasure_chest: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_galaxy_weapon & - self.logic.tool.has_tool(Tool.axe, ToolMaterial.iron), - SVEFruit.slime_berry: self.logic.season.has(Season.spring) & self.logic.has(SVESeed.slime), - SVESeed.slime: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - SVESeed.stalk: self.logic.region.can_reach(SVERegion.highlands_outside) & self.logic.combat.has_good_weapon, - ModLoot.swirl_stone: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEVegetable.void_root: self.logic.season.has(Season.winter) & self.logic.has(SVESeed.void), - SVESeed.void: self.logic.region.can_reach(SVERegion.highlands_cavern) & self.logic.combat.has_good_weapon, - ModLoot.void_soul: self.logic.region.can_reach( - SVERegion.crimson_badlands) & self.logic.combat.has_good_weapon & self.logic.cooking.can_cook(), - SVEForage.winter_star_rose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.winter), - SVEForage.bearberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.winter), - SVEForage.poison_mushroom: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has_any([Season.summer, Season.fall]), - SVEForage.red_baneberry: self.logic.region.can_reach(Region.secret_woods) & self.logic.season.has(Season.summer), - SVEForage.ferngill_primrose: self.logic.region.can_reach(SVERegion.summit) & self.logic.season.has(Season.spring), - SVEForage.goldenrod: self.logic.region.can_reach(SVERegion.summit) & ( - self.logic.season.has(Season.summer) | self.logic.season.has(Season.fall)), - SVESeed.shrub: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEFruit.salal_berry: self.logic.farming.can_plant_and_grow_item((Season.spring, Season.summer)) & self.logic.has(SVESeed.shrub), - ModEdible.aegis_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 28000), - ModEdible.lightning_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 12000), - ModEdible.barbarian_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 22000), - ModEdible.gravity_elixir: self.logic.money.can_spend_at(SVERegion.galmoran_outpost, 4000), - SVESeed.ancient_fern: self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic), - SVEVegetable.ancient_fiber: self.logic.farming.can_plant_and_grow_item(Season.summer) & self.logic.has(SVESeed.ancient_fern), - SVEForage.conch: self.logic.region.can_reach_any((Region.beach, SVERegion.fable_reef)), - SVEForage.dewdrop_berry: self.logic.region.can_reach(SVERegion.enchanted_grove), - SVEForage.sand_dollar: self.logic.region.can_reach(SVERegion.fable_reef) | (self.logic.region.can_reach(Region.beach) & - self.logic.season.has_any([Season.summer, Season.fall])), - SVEForage.golden_ocean_flower: self.logic.region.can_reach(SVERegion.fable_reef), - SVEMeal.grampleton_orange_chicken: self.logic.money.can_spend_at(Region.saloon, 650) & self.logic.relationship.has_hearts(ModNPC.sophia, 6), - ModEdible.hero_elixir: self.logic.money.can_spend_at(SVERegion.isaac_shop, 8000), - SVEForage.four_leaf_clover: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.forest_west)) & - self.logic.season.has_any([Season.spring, Season.summer]), - SVEForage.mushroom_colony: self.logic.region.can_reach_any((Region.secret_woods, SVERegion.junimo_woods, SVERegion.forest_west)) & - self.logic.season.has(Season.fall), - SVEForage.rusty_blade: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - SVEForage.rafflesia: self.logic.region.can_reach(Region.secret_woods), - SVEBeverage.sports_drink: self.logic.money.can_spend_at(Region.hospital, 750), - "Stamina Capsule": self.logic.money.can_spend_at(Region.hospital, 4000), - SVEForage.thistle: self.logic.region.can_reach(SVERegion.summit), - ModLoot.void_pebble: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_great_weapon, - ModLoot.void_shard: self.logic.region.can_reach(SVERegion.crimson_badlands) & self.logic.combat.has_galaxy_weapon & - self.logic.skill.has_level(Skill.combat, 10) & self.logic.region.can_reach(Region.saloon) & self.logic.time.has_year_three - } - # @formatter:on - def get_modified_item_rules_for_sve(self, items: Dict[str, StardewRule]): return { Loot.void_essence: items[Loot.void_essence] | self.logic.region.can_reach(SVERegion.highlands_cavern) | self.logic.region.can_reach( @@ -141,7 +71,7 @@ FarmingLogicMixin]]): self.logic.combat.can_fight_at_level(Performance.great)), Ore.iridium: items[Ore.iridium] | (self.logic.tool.can_use_tool_at(Tool.pickaxe, ToolMaterial.basic, SVERegion.crimson_badlands) & self.logic.combat.can_fight_at_level(Performance.maximum)), - SVEFish.dulse_seaweed: self.logic.fishing.can_fish_at(Region.beach) & self.logic.season.has_any([Season.spring, Season.summer, Season.winter]) + } def get_modified_item_rules_for_deep_woods(self, items: Dict[str, StardewRule]): @@ -160,36 +90,6 @@ FarmingLogicMixin]]): return options_to_update - def get_archaeology_item_rules(self): - archaeology_item_rules = {} - preservation_chamber_rule = self.logic.has(ModMachine.preservation_chamber) - hardwood_preservation_chamber_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) - for item in display_items: - for display_type in display_types: - if item == "Trilobite": - location_name = f"{display_type}: Trilobite Fossil" - else: - location_name = f"{display_type}: {item}" - display_item_rule = self.logic.crafting.can_craft(all_crafting_recipes_by_name[display_type]) & self.logic.has(item) - if "Wooden" in display_type: - archaeology_item_rules[location_name] = display_item_rule & preservation_chamber_rule - else: - archaeology_item_rules[location_name] = display_item_rule & hardwood_preservation_chamber_rule - archaeology_item_rules[ModTrash.rusty_scrap] = self.logic.has(ModMachine.grinder) & self.logic.has_any(*all_artifacts) - return archaeology_item_rules - - def get_distant_lands_item_rules(self): - return { - DistantLandsForageable.swamp_herb: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsForageable.brown_amanita: self.logic.region.can_reach(Region.witch_swamp), - DistantLandsSeed.vile_ancient_fruit: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsSeed.void_mint: self.logic.quest.can_complete_quest(ModQuest.WitchOrder) | self.logic.quest.can_complete_quest( - ModQuest.CorruptedCropsTask), - DistantLandsCrop.void_mint: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.void_mint), - DistantLandsCrop.vile_ancient_fruit: self.logic.season.has_any_not_winter() & self.logic.has(DistantLandsSeed.vile_ancient_fruit), - } - def get_boarding_house_item_rules(self): return { # Mob Drops from lost valley enemies @@ -251,8 +151,3 @@ FarmingLogicMixin]]): BoardingHouseRegion.lost_valley_house_2,)) & self.logic.combat.can_fight_at_level( Performance.great), } - - def has_seed_unlocked(self, seed_name: str): - if self.options.cropsanity == Cropsanity.option_disabled: - return True_() - return self.logic.received(seed_name) diff --git a/worlds/stardew_valley/mods/mod_regions.py b/worlds/stardew_valley/mods/mod_regions.py index c075bd4d10..a402ba6068 100644 --- a/worlds/stardew_valley/mods/mod_regions.py +++ b/worlds/stardew_valley/mods/mod_regions.py @@ -183,7 +183,8 @@ stardew_valley_expanded_regions = [ RegionData(SVERegion.first_slash_guild, [SVEEntrance.first_slash_guild_to_hallway], is_ginger_island=True), RegionData(SVERegion.first_slash_hallway, [SVEEntrance.first_slash_hallway_to_room], is_ginger_island=True), RegionData(SVERegion.first_slash_spare_room, is_ginger_island=True), - RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave], is_ginger_island=True), + RegionData(SVERegion.highlands_outside, [SVEEntrance.highlands_to_lance, SVEEntrance.highlands_to_cave, SVEEntrance.highlands_to_pond], is_ginger_island=True), + RegionData(SVERegion.highlands_pond, is_ginger_island=True), RegionData(SVERegion.highlands_cavern, [SVEEntrance.to_dwarf_prison], is_ginger_island=True), RegionData(SVERegion.dwarf_prison, is_ginger_island=True), RegionData(SVERegion.lances_house, [SVEEntrance.lance_to_ladder], is_ginger_island=True), @@ -276,6 +277,7 @@ mandatory_sve_connections = [ ConnectionData(SVEEntrance.sprite_spring_to_cave, SVERegion.sprite_spring_cave, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.fish_shop_to_willy_bedroom, SVERegion.willy_bedroom, flag=RandomizationFlag.BUILDINGS), ConnectionData(SVEEntrance.museum_to_gunther_bedroom, SVERegion.gunther_bedroom, flag=RandomizationFlag.BUILDINGS), + ConnectionData(SVEEntrance.highlands_to_pond, SVERegion.highlands_pond), ] alecto_regions = [ diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 7c1fdbda3c..89b1cf87c3 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1031,6 +1031,7 @@ def set_sve_ginger_island_rules(logic: StardewLogic, multiworld: MultiWorld, pla set_entrance_rule(multiworld, player, SVEEntrance.wizard_to_fable_reef, logic.received(SVEQuestItem.fable_reef_portal)) set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_cave, logic.tool.has_tool(Tool.pickaxe, ToolMaterial.iron) & logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) + set_entrance_rule(multiworld, player, SVEEntrance.highlands_to_pond, logic.tool.has_tool(Tool.axe, ToolMaterial.iron)) def set_boarding_house_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions): diff --git a/worlds/stardew_valley/strings/book_names.py b/worlds/stardew_valley/strings/book_names.py index 3c32cd81b3..6c271f42ae 100644 --- a/worlds/stardew_valley/strings/book_names.py +++ b/worlds/stardew_valley/strings/book_names.py @@ -27,10 +27,6 @@ class Book: the_diamond_hunter = "The Diamond Hunter" -class ModBook: - digging_like_worms = "Digging Like Worms" - - ordered_lost_books = [] all_lost_books = set() diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index 9b651f4276..58a919f2a8 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -358,6 +358,7 @@ class SVEEntrance: sprite_spring_to_cave = "Sprite Spring to Sprite Spring Cave" fish_shop_to_willy_bedroom = "Willy's Fish Shop to Willy's Bedroom" museum_to_gunther_bedroom = "Museum to Gunther's Bedroom" + highlands_to_pond = "Highlands to Highlands Pond" class AlectoEntrance: diff --git a/worlds/stardew_valley/strings/fish_names.py b/worlds/stardew_valley/strings/fish_names.py index d94f9e2fd4..d4ee81430e 100644 --- a/worlds/stardew_valley/strings/fish_names.py +++ b/worlds/stardew_valley/strings/fish_names.py @@ -137,7 +137,6 @@ class SVEFish: void_eel = "Void Eel" water_grub = "Water Grub" sea_sponge = "Sea Sponge" - dulse_seaweed = "Dulse Seaweed" class DistantLandsFish: @@ -147,6 +146,10 @@ class DistantLandsFish: giant_horsehoe_crab = "Giant Horsehoe Crab" +class SVEWaterItem: + dulse_seaweed = "Dulse Seaweed" + + class ModTrash: rusty_scrap = "Scrap Rust" diff --git a/worlds/stardew_valley/strings/food_names.py b/worlds/stardew_valley/strings/food_names.py index 5555316f83..03784336d1 100644 --- a/worlds/stardew_valley/strings/food_names.py +++ b/worlds/stardew_valley/strings/food_names.py @@ -102,6 +102,7 @@ class SVEMeal: void_delight = "Void Delight" void_salmon_sushi = "Void Salmon Sushi" grampleton_orange_chicken = "Grampleton Orange Chicken" + stamina_capsule = "Stamina Capsule" class TrashyMeal: diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 9cedb6b8ef..58763b6fcb 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -296,6 +296,7 @@ class SVERegion: sprite_spring_cave = "Sprite Spring Cave" willy_bedroom = "Willy's Bedroom" gunther_bedroom = "Gunther's Bedroom" + highlands_pond = "Highlands Pond" class AlectoRegion: diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 5e7e9d4143..97184b1338 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -14,7 +14,8 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in options.Mods.valid_keys: - with self.solo_world_sub_test(f"Mod: {mod}", {options.Mods: mod}) as (multi_world, _): + world_options = {options.Mods: mod, options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false} + with self.solo_world_sub_test(f"Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) self.assert_stray_mod_items(mod, multi_world) @@ -22,8 +23,9 @@ class TestGenerateModsOptions(WorldAssertMixin, ModAssertMixin, SVTestCase): for option in options.EntranceRandomization.options: for mod in options.Mods.valid_keys: world_options = { - options.EntranceRandomization.internal_name: options.EntranceRandomization.options[option], - options.Mods: mod + options.EntranceRandomization: options.EntranceRandomization.options[option], + options.Mods: mod, + options.ExcludeGingerIsland: options.ExcludeGingerIsland.option_false } with self.solo_world_sub_test(f"entrance_randomization: {option}, Mod: {mod}", world_options) as (multi_world, _): self.assert_basic_checks(multi_world) 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 057/222] AHIT: Update Setup Guide (#3647) --- worlds/ahit/docs/setup_en.md | 61 +++++++----------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md index 509869fc25..23b3490707 100644 --- a/worlds/ahit/docs/setup_en.md +++ b/worlds/ahit/docs/setup_en.md @@ -12,41 +12,29 @@ ## Instructions -1. Have Steam running. Open the Steam console with this link: [steam://open/console](steam://open/console) -This may not work for some browsers. If that's the case, and you're on Windows, open the Run dialog using Win+R, -paste the link into the box, and hit Enter. +1. **BACK UP YOUR SAVE FILES IN YOUR MAIN INSTALL IF YOU CARE ABOUT THEM!!!** + Go to `steamapps/common/HatinTime/HatinTimeGame/SaveData/` and copy everything inside that folder over to a safe place. + **This is important! Changing the game version CAN and WILL break your existing save files!!!** -2. In the Steam console, enter the following command: -`download_depot 253230 253232 7770543545116491859`. ***Wait for the console to say the download is finished!*** -This can take a while to finish (30+ minutes) depending on your connection speed, so please be patient. Additionally, -**try to prevent your connection from being interrupted or slowed while Steam is downloading the depot,** -or else the download may potentially become corrupted (see first FAQ issue below). +2. In your Steam library, right-click on **A Hat in Time** in the list of games and click on **Properties**. -3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. +3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`. + While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601)) -4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. +4. Once the game finishes downloading, start it up. + In Game Settings, make sure **Enable Developer Console** is checked. -5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. -In this new text file, input the number **253230** on the first line. - - -6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. -You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. - - -7. Start up the game using your new shortcut. To confirm if you are on the correct version, -go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running -the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. +5. You should now be good to go. See below for more details on how to use the mod and connect to an Archipelago game. ## Connecting to the Archipelago server -To connect to the multiworld server, simply run the **ArchipelagoAHITClient** -(or run it from the Launcher if you have the apworld installed) and connect it to the Archipelago server. +To connect to the multiworld server, simply run the **Archipelago AHIT Client** from the Launcher +and connect it to the Archipelago server. The game will connect to the client automatically when you create a new save file. @@ -61,33 +49,8 @@ make sure ***Enable Developer Console*** is checked in Game Settings and press t ## FAQ/Common Issues -### I followed the setup, but I receive an odd error message upon starting the game or creating a save file! -If you receive an error message such as -**"Failed to find default engine .ini to retrieve My Documents subdirectory to use. Force quitting."** or -**"Failed to load map "hub_spaceship"** after booting up the game or creating a save file respectively, then the depot -download was likely corrupted. The only way to fix this is to start the entire download all over again. -Unfortunately, this appears to be an underlying issue with Steam's depot downloader. The only way to really prevent this -from happening is to ensure that your connection is not interrupted or slowed while downloading. -### The game keeps crashing on startup after the splash screen! -This issue is unfortunately very hard to fix, and the underlying cause is not known. If it does happen however, -try the following: - -- Close Steam **entirely**. -- Open the downpatched version of the game (with Steam closed) and allow it to load to the titlescreen. -- Close the game, and then open Steam again. -- After launching the game, the issue should hopefully disappear. If not, repeat the above steps until it does. - -### I followed the setup, but "Live Game Events" still shows up in the options menu! -The most common cause of this is the `steam_appid.txt` file. If you're on Windows 10, file extensions are hidden by -default (thanks Microsoft). You likely made the mistake of still naming the file `steam_appid.txt`, which, since file -extensions are hidden, would result in the file being named `steam_appid.txt.txt`, which is incorrect. -To show file extensions in Windows 10, open any folder, click the View tab at the top, and check -"File name extensions". Then you can correct the name of the file. If the name of the file is correct, -and you're still running into the issue, re-read the setup guide again in case you missed a step. -If you still can't get it to work, ask for help in the Discord thread. - -### The game is running on the older version, but it's not connecting when starting a new save! +### The game is not connecting when starting a new save! For unknown reasons, the mod will randomly disable itself in the mod menu. To fix this, go to the Mods menu (rocket icon) in-game, and re-enable the mod. 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 058/222] 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 aa4f6ccf75..abdee26f57 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -52,8 +52,17 @@ class PokemonEmeraldWebWorld(WebWorld): "setup/es", ["nachocua"] ) + + setup_sv = Tutorial( + "Multivärld Installations Guide", + "En guide för att kunna spela Pokémon Emerald med Archipelago.", + "Svenska", + "setup_sv.md", + "setup/sv", + ["Tsukino"] + ) - tutorials = [setup_en, setup_es] + tutorials = [setup_en, setup_es, setup_sv] class PokemonEmeraldSettings(settings.Group): diff --git a/worlds/pokemon_emerald/docs/setup_sv.md b/worlds/pokemon_emerald/docs/setup_sv.md new file mode 100644 index 0000000000..88b1d38409 --- /dev/null +++ b/worlds/pokemon_emerald/docs/setup_sv.md @@ -0,0 +1,78 @@ +# Pokémon Emerald Installationsguide + +## Programvara som behövs + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Ett engelskt Pokémon Emerald ROM, Archipelago kan inte hjälpa dig med detta. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 eller senare + +### Konfigurera BizHawk + +När du har installerat BizHawk, öppna `EmuHawk.exe` och ändra följande inställningar: + +- Om du använder BizHawk 2.7 eller 2.8, gå till `Config > Customize`. På "Advanced Tab", byt Lua core från +`NLua+KopiLua` till `Lua+LuaInterface`, starta om EmuHawk efteråt. (Använder du BizHawk 2.9, kan du skippa detta steg.) +- Gå till `Config > Customize`. Markera "Run in background" inställningen för att förhindra bortkoppling från +klienten om du alt-tabbar bort från EmuHawk. +- Öppna en `.gba` fil i EmuHawk och gå till `Config > Controllers…` för att konfigurera dina inputs. +Om du inte hittar `Controllers…`, starta ett valfritt `.gba` ROM först. +- Överväg att rensa keybinds i `Config > Hotkeys…` som du inte tänkt använda. Välj en keybind och tryck på ESC +för att rensa bort den. + +## Extra programvara + +- [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest), +används tillsammans med +[PopTracker](https://github.com/black-sliver/PopTracker/releases) + +## Generera och patcha ett spel + +1. Skapa din konfigurationsfil (YAML). Du kan göra en via att använda +[Pokémon Emerald options hemsida](../../../games/Pokemon%20Emerald/player-options). +2. Följ de allmänna Archipelago instruktionerna för att +[Generera ett spel](../../Archipelago/setup/en#generating-a-game). +Detta kommer generera en fil för dig. Din patchfil kommer ha `.apemerald` som sitt filnamnstillägg. +3. Öppna `ArchipelagoLauncher.exe` +4. Välj "Open Patch" på vänstra sidan, och välj din patchfil. +5. Om detta är första gången du patchar, så kommer du behöva välja var ditt ursprungliga ROM är. +6. En patchad `.gba` fil kommer skapas på samma plats som patchfilen. +7. Första gången du öppnar en patch med BizHawk-klienten, kommer du också behöva bekräfta var `EmuHawk.exe` filen är +installerad i din BizHawk-mapp. + +Om du bara tänkt spela själv och du inte bryr dig om automatisk spårning eller ledtrådar, så kan du stanna här, stänga +av klienten, och starta ditt patchade ROM med valfri emulator. Dock, för multvärldsfunktionen eller andra +Archipelago-funktioner, fortsätt nedanför med BizHawk. + +## Anslut till en server + +Om du vanligtsvis öppnar en patchad fil så görs steg 1-5 automatiskt åt dig. Även om det är så, kom ihåg dessa steg +ifall du till exempel behöver stänga ner och starta om något medans du spelar. + +1. Pokemon Emerald använder Archipelagos BizHawk-klient. Om klienten inte startat efter att du patchat ditt spel, +så kan du bara öppna den igen från launchern. +2. Dubbelkolla att EmuHawk faktiskt startat med den patchade ROM-filen. +3. I EmuHawk, gå till `Tools > Lua Console`. Luakonsolen måste vara igång medans du spelar. +4. I Luakonsolen, Tryck på `Script > Open Script…`. +5. Leta reda på din Archipelago-mapp och i den öppna `data/lua/connector_bizhawk_generic.lua`. +6. Emulatorn och klienten kommer så småningom ansluta till varandra. I BizHawk-klienten kommer du kunna see om allt är +anslutet och att Pokemon Emerald är igenkänt. +7. För att ansluta klienten till en server, skriv in din lobbyadress och port i textfältet t.ex. +`archipelago.gg:38281` +längst upp i din klient och tryck sen på "Connect". + +Du borde nu kunna ta emot och skicka föremål. Du behöver göra dom här stegen varje gång du vill ansluta igen. Det är +helt okej att göra saker offline utan att behöva oroa sig; allt kommer att synkronisera när du ansluter till servern +igen. + +## Automatisk Spårning + +Pokémon Emerald har en fullt fungerande spårare med stöd för automatisk spårning. + +1. Ladda ner [Pokémon Emerald AP Tracker](https://github.com/seto10987/Archipelago-Emerald-AP-Tracker/releases/latest) +och +[PopTracker](https://github.com/black-sliver/PopTracker/releases). +2. Placera tracker pack zip-filen i packs/ där du har PopTracker installerat. +3. Öppna PopTracker, och välj Pokemon Emerald. +4. För att automatiskt spåra, tryck på "AP" symbolen längst upp. +5. Skriv in Archipelago-serverns uppgifter (Samma som du använde för att ansluta med klienten), "Slot"-namn samt +lösenord. From 79843803cf3a2547390f6e139be9c229a77d370b Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 25 Jul 2024 16:01:22 -0500 Subject: [PATCH 059/222] 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 059c33844f..71e2e4152e 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 060/222] 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 71e2e4152e..8d9429afa3 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 061/222] 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 3b67617873..a1b8b7c1d4 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -9,7 +9,7 @@ from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP -from .options import LingoOptions, lingo_option_groups +from .options import LingoOptions, lingo_option_groups, SunwarpAccess, VictoryCondition from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -54,14 +54,17 @@ class LingoWorld(World): player_logic: LingoPlayerLogic def generate_early(self): - if not (self.options.shuffle_doors or self.options.shuffle_colors or self.options.shuffle_sunwarps): + if not (self.options.shuffle_doors or self.options.shuffle_colors or + (self.options.sunwarp_access >= SunwarpAccess.option_unlock and + self.options.victory_condition == VictoryCondition.option_pilgrimage)): if self.multiworld.players == 1: - warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression" - f" items. Please turn on Door Shuffle, Color Shuffle, or Sunwarp Shuffle if that doesn't seem" - f" right.") + warning(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on Door" + f" Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage victory condition" + f" if that doesn't seem right.") else: - raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any" - f" progression items. Please turn on Door Shuffle, Color Shuffle or Sunwarp Shuffle.") + raise OptionError(f"{self.player_name}'s Lingo world doesn't have any progression items. Please turn on" + f" Door Shuffle or Color Shuffle, or use item-blocked sunwarps with the Pilgrimage" + f" victory condition.") self.player_logic = LingoPlayerLogic(self) From cc2216164489f89d78ac1e53aaa71b9dce04ac28 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 26 Jul 2024 04:53:11 -0400 Subject: [PATCH 062/222] 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 a1b8b7c1d4..9853be73fa 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -170,7 +170,8 @@ class LingoWorld(World): slot_options = [ "death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels", "enable_pilgrimage", "sunwarp_access", "mastery_achievements", "level_2_requirement", "location_checks", - "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps" + "early_color_hallways", "pilgrimage_allows_roof_access", "pilgrimage_allows_paintings", "shuffle_sunwarps", + "group_doors" ] slot_data = { diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 3035446ef7..1c9f4e551d 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 zcmbt-3w)eKwRmTfY?3zVGfneqyM2;UpzoL1$|l()yV>l9-AzNqQqndw`Dh!+X3Lut zMWwjPV}l=xdO^A1^(qz%77$dts9d~P%JqfZi#!xX@d2W!Jpbo0XTI;-%?5wJzxQ|P zmvhdXnK^Uj%$YN1W?%hM#ho9kEInL$)6^SEm&EpLT)lbiy0vTiH*eXyabWe9^_w=V z*|P!uS-*Slh7B9{jAtu`_NGs}r2oMF@oZrI0Q_J7rVU&6u3Nol^Xl`~ui3r2fAyBt ztM{zmxM$PmEq7;*PMccUJ~WaZ4J3yyKDM&zx<^iT{vNlMS5KMJZ0(&hWlDBlEOTJ@ zz(}Aob8s|pYQPj4&(1QK+?#hS$gOy4)=YnL;LyOqv@ke$?6DI~N`3s;Z=Tqt)X`%d zKl*_3{|vb<&b>Uf_Sm<6e6#YG)WF}XbI<*>CU@aazoKU47XIv0YJTqipIxb%bDN*c zs&%>VKKW6#E!X|?D^xi5gP&jHPhpXzxf4IH$sKs=i2q#tb4l*-Q? zn7a5_*Z+M`sq1q;eEJIY>0I;|xA^Zsxk_xw(g5_^4W+3;+6)>Wtj8zqVC0ckyqo@b@9X zGW)rVs?XKDIH>w_SG;(YIzRW$i+}ZBinukoo}rrD^xwUvj^xT-y4pWZ1#&Zo=N`NL zrPV(FO54k@BDC!A?7VeWf}_sgK!znyTJVx7sgFQ}dV3P7Dtp2+)5{ z4J1>+a3VRrbKF`9+8@jM8IW7_RJ|RpQZ?#J_BmB5qwckTT&3<&kJv}5)xW9}$5%~P zUs1l&`s2Ttp_Z!B<@Hmh*ehnKCCA}Etx6^A1GCjP$6}-7SB+==o#D2&P;A`V4OGoL zp)&lxBly3;so9y>lHkF;J^cp<_O}fWkBsNW2dhn?j{pZ|Q<~b*6Pk!gOz@rIqz6By zYtaN{eTi^tQVz*r_ayktVCxm*&Dn~!P_Qi!inV#_+BtsBk!y||k+#Hppu?dFdTb4Y z5C&Vbr9|QCT{Il>wq6={PmU!PJ^^~^7i*$(xY-)GBB^-UXmKZ8mTVP;~3h7Sy+d|1uA~i{K z%7NltNJ^5EcBmv7;{J~1sj+T@FBPAp{*qLDGW5G@XGy3}lvDR#G(_uvS1dkR|NXIG zZ%5~36k2&)f-aDA#+v1uGGz?sfv+<>S#P70igH>I8rvC41X76|6L7gkOSC*1+8&Aq z*5;8kg32G!G?~E;Y)x4_(H2SooqhKT)v%nWS%-j~c~+T>k{FowxuB}+6IvI_p)X4% zf~_KV7KawQ6w?$=k&;9ZrHl=kYd5ukM~j^|MY&f+&&Ul zHPiLjN8?GS&VzNzucv5vdpyw>Oyt`tw>c7WG)~?CfWCpu%7)o^u z>+P4{ua;30XK)kCLSfcE``n$X!S1P7wM~S{ScgHSuyAE(JQZk7^m13Os#i_3xNLb( zINFg2cau~%sG4~={$ldVyMxJ8h}6jL->S}-r@3COxt6AQU9exdMK#P}hLx#scPNmE z$BE^`5!E!vh^l03I23C&xPlc+OJqi}GaTCi99p}CO)*kX^^U}jKu^4nbq;H*)wOkp zTNCj>G}IA}((RmyhgROo}n|kq;yMx`oQ%{8JJ6)&sonMsIW^CY zH>s+*OgOC_+La9S1XG>3TrvJ5O=>#hGlOxOjcuXUAo0Q(wutpVo-JdrYX-z%8Prua z8nRM?I#d&=jtHU;M~XBbMAx`PD)MW%i$@=bg<6b+%|bYhz=0(GGwz~h0L!9rfu^<& zH%^)25S6BS6K2#zQ(M3>2#{q&F*cqg6~QcHgj2o(3}7I%gVsp<%mr#$AnU(mVE_K% zi^lui&V;`;o`|vT0M$zAdS^V!HWtgS6hn~gqjzk!5tF;>megtwh|G67( zYOX)o69h*A2#WMSTh=+W|EVrkb;G5k5PsWq<#k z^8-Eo`-cwh9T`3_kT$)|J#l1?5Bh*bv>=hC^&U@GtCMk%dZ3y3peIN4?`6SHM9-^j zbKl_5=)lCDpM7+ZsynNwY^26XIA}8x+%q@?+vWoU2S*)Q=f-YM^1=^M(Rk$@nG6YZ|O_ zsU&QR62U$fyD|oNC5{EGyWA4bSnB~s>tHk#Y?lVGwIN)#5)py+5KKv@wWaj8=6(t{ z2RT`fc(@Plq*W5uyJWC65yC~?M*FJ0ot@cA2jws?Yl*_@Ep1s$+y{4NHP}Vh3d%w= z1BIfEG0-vCp(~As+Qrvsl;s0qfo7iFP`ptV{e&#~$WxnV;K^Wzr@yB+(G#W0N0zCT z!nHlTUA6}{xIFLi!nrnti}8bvrgWfDU~R$o6AYJKS)=-Cjf%w+DKddKFIOMYhPppn z9vvP&e>gMh8lH-_aIiZbn)azQ?q*hZ9uSc_*or0Ty&VjlVA(18)U~ z@ZRg zd!Gow?t@kV936qSftGVy5GoRrDiJ3BQd_vaJ(LK=%)m=sdI>^W8S`KihE406VVdeR zPO(uMm{hnWqyD)Y05J%eyiWPv7Q<8OZgp6SvG}6QzqWWEEgitOfrV@#E(@DdKscIL zfjT3L7&bz5jNy((d*SQEoHmMIq`gpKg`Q{uin$?5xS<~q^58|enoW|4HxPyftC#1*$USy5Zwn@5;V>0~!(v2hU$|P$hicZWRgJmP zJ65jOQN)sD_$-=Z13SmFW&1`31`eW2>k3ohHqoW`vIf76Q@fuxXl+ z6s}&nLSsi;LNHx~2Z-$y8QYdFxyxj#tOVMe|x=ZXwYj{c4}y7-{2^PqsJkZ zxpTZZx9PiE7J=TB?%tmn@TyIDFwh)7NUo`U_XgE4OOFeHQx^@haRRVZjo?90K7$bX z0iu04GcqB3=#WG2;twZAhFrkmiCY8UiCTl;30nh*30s5kiCP12cWb1#2*!gbX=Z%` z+sUD9RmV_2oMY)@Dx6ey*Cthu_5q|N`oIIxJ~Dg|J#s>ca>j-Z4hXE5WS+6!TgslU zKN$jF6pf|ZPE&x1Cm9mLBx5RPMo(>nZDwF)Ajodttj(%X&*=?RAh>7GK-$^yfIcLH zQDc+2)0x2xt|TSe7v8s7HA`>WLJ(YB8R!F_1zV2n^*#XETh|BJWZ2)_tQtTNxLp?Q zNc7_DT1#>~_b`M%zIB^<#IcA z(~WjyQxQ&d1v+8Zk4=XnwGFO#KxXjrE5NE2hSTgfwx~wv3&gqV^_Ji;NG%NhufuvO z{ta8gp?%apH?8#-Efj8f6?%tJ=qbp9+Kw{R`|1fgT3TdZ4MX4C-A!I*fr7-iCV#Lu z)!U7oElQE>NrYn^lTcLk1i>f*jkZC+$^OgfYOzGq95fH?E2l$v^0w2}Tof357t*ma zH8GAID#XbYkys+Y=GhnTR@L_5_o}+No*rS;@|sw7sU6$~5zz0wS1nmufS)L;S+!Ly zQ89bfR<#CNi|*q#7F;Er*0ly{+g3z&8S9KPoT=ITIH}#VP1R|B96?N5u_C1ZRY>}Q ziAi@;%hH2i!HMarvPkQ)(=Z4Y4YwqM1qT5vF5;INyc_sgZ_j|KOQxxQp*C(-zej1y zXdS|7Q+kFvS!Q$(gnU}Hhj!+zj^`D`$QlwOaI>!IUTnGBi73|eVj7>ZK0~sBLkZXr zQZT!K1)Bh*%hnT701PB-F#`~+PP7Hk+kw5QGi}j+0(zne;IOjB8Wy`fp)z9jE@iw$ z{JQ1|rlPAJPnnE$2dhF^O9%ql6Ha3;i*yioAR^PXv~^4uhpMTE6s6Kp5X6BP3F9jA z2p54MBS%><3ZhFO3ZsvJa25E8t^<}T-UXl`)`1_yL70Out|GtYA|=UQQW01x>vaLd zWC`vf|C_EM*%RkjJ_H;ZS8>M?mn~zAt4M{PCuTI1f{=jN*joGL>r`1CmqZc>NoWEr zYk#Xn)xxwzH@23E>XE3d4?_4!>a+cF3)wZ$>V~;Yax#*Xh7-^(@=%D)^UQwJg6)#5HCaU)}vc|gc^ ztZ-o!8(*-(2|}EAUx!*DJL7l)&V9(Ge@};6DK2AgtTn!!LoV1G9T9shTP_GAcR2hz zi+vX)T*}>kOhX;2iXJ%q62BYGSPd@?Q`=!pO_<1rx=k(3$M8ak`#obPiI)SHR`)=h zMsJ9b(ORyeEZGgm_Y;{HJ-p>80qvW?d}|VMZIA2or^B!YH-~8p37xF1^K22@T<5fa zZIbc9=n(mV`9Kmj0;Qf#P8hM>gu#{_Nl_+;WuM;LyGuhRg{ejcH5dgyC5$VEn-by$ zhNVz;#bj6Srfk{4J$J*pgM6@wFewdZcpbbSM5y$f(%RYE+Et8Xj8mzSv2dytgVx3BsRTEnH3XZg34H|_ zfu3kD8>eDy3S9Cn1@bS&gThH#BB=lvx#wgBvFlHeUGK$HURqjl^-i&Ydm`)CaL2@H zk70s7OPPG10v2bE_0SpL%pO%colRDu%chS$%h0yMugw<8DoNyq8aj5GbNf!s(!9EK=%(mI#dF;F-ZENzu(geKF;# zgar*FA-t@i6T^o`)&$$^5 z8{n~CwN?hL#hl$;`T?~}NPz`Muut0Vluy9ko1)5lx2w}cAfU@=o%x*ozU`{Mg{SM2 z*-}C|?JeQ0Suydr!>()kR6rkLLbo9v>o}@erep5~=|ntMa=Cj~RGI38Sf#j3KkQSB zb+F{gENqwnFoFcM{xl258hN{(BP0)1Osh9$p!QYZ0 z>IwDqXxX|>kR*!<#8R)1qko&7icm;QgJ7p?I+QU0WC)8BLGGgDvYyX1v>QD_Aj_WuUFkZv^sAr_PP~;B zxQo&WgZ#c$P--|*+N*bS82%PjGn)jVmC#4GF(_|WZ$GDTNGULZK-`h1YE5QT z-7N+;;Z#k?L5d<&8SD4rMjxNMM_ud-PprN!?WK`~Vh~ajC3j>}4!LQvnt@YUrxW;y z+Ta{=x4oeAY?%fy^~r-@c%BN&!bd(h*^HyA#(w8KHA8Q@tf~q=ugl*W%{7)U&ph^o zwBHYD!pzF9-lv+tRzQXG?dAbB+upHHmeMR7EJa}@mWY~{Mw$nC8SGjZo`RvgJ8U== zTsf##O2d~IYq&AQ-iDW^v_oL(5P`vU6>~Gz?mQOMBuR7%~O;6ft8BQmt8UGT3r^qop=2riP`hLrhX8S!#}Q zHruc4g{|f)9DoNbHOhiYTUT=l@Md@y(|W$@YtY=TV_beFTb+H*`Kr?6UOXQpS}ebrL;rlbUYKF9tY~&c8Db68=*mh}B_w1=fna;DJwD zk1+|P2q%J3v)tH^A5<8Q$ED{KEs!^uvI6df1#|+ZqXKgC$6htehXK$Ed)F||&f}L4 zs~dfG^@s^d$P(v-B(X)ZaKfRDG*Un@y5{6ra^V_@A(JEa6yDSbwTW+bW?G$DfMQn+ zQR1`1?Fuf3wce9fT@pSBw&5PZ#chNA#ZhIV3xYNr87=n1Y*wMJjrKViQ0SQ%RVip{ zE7o{YMqS4`m$BNWvPrDK21&&yGOArX2!?Zw2)FkiQgv0X`QB`=IiwcbUpu6dBITBl z$O>Ar%>Ko*YKGl;ky=Fs#>g!q}M`)r`YdYr0RGM!{!v51ATDR zKvxe$GwknRLq2q|T43LLF&%b*GBvK}h3Eiu6t>4&?q+F8TOZUCsMMz+YiRq5{;*#B zpSQgJun@9L{t3c$hGQGMH~AckU8i&}(|%YT^bH7{BK9kn&>nflrRq{R7e8{TS^<;C zq-w~RL*6r|17CERS}MA&k0TId=wO$_{<9=|2(jJ=Be9GQob%J7YihH9a2aJ?TzR=# z#6}`pMce9@;nC6I1KfSrMH^_;j%c`xW)rLni=-8O5T_<{fsCX!hzd|>%MEMOr(sxF zCKW}mFoh3Q+|AXTLLdi8l5HYW3j1CN)KdcV55KBv?XD|TmDJmt$nVhE&(xOJQ}p0MQ<186hV_986M-(+%qk zb9AAA0Y)gWwFV*bf{-SM=#Y(cp?VtIBnU-?csS8mOW!Grh5C-GADa|cR)pCNbMa zMxjoobok26A`UrJG@B48lTP2Bbkntg;Ug_UptK*nUsH>~Qc2DncNd0(8$NV%10nR( zYHF6!5Rf()5OSFOJz>!TPlu`mbIE^&;$X6O=1{50R?bPNfomC zDoY_l`x95IdGB*A2kwih)|pTm4#_(qZ>r1LOQx-j>_?UAtRY(E#evz%xM{5r%Z7{D z5c4zguorxoJPkZzw&TvX415b1#;!mtPO}_^W#9E-vWuv`;ud%_zK{Ry!>UB_>Xxx~ zX#u)&hR9#*({f981{3Jp70N-6z#h$@1OoPXbnf^EL=;RIuwG{zV$rN0B!VXlW_iq( zc7;OXPU>L0g{0O^r?~@O{+1wYeK@NcVrU%hnuxG0C3iqE)~2_8g$y&+gW3p`n$!rO zU(IVd;<7Va;Zz9L3nzcVX*C|v>ia%Wig$O!(fXFB**+Z#befE;^N7l*BAoizWSv3j zp|hX9hTILr>&(W-u!++lZ6lThUkGlS;x@r)AcVjZoEh)54~tm8b+^y*%{0kgI_t2I z2+|uno%9?n(L%C}^%LNaS(AO8c=JkkXAMgfad7bqF)d*KV2@|onzr__R=HcF_nkwQ zH}$MA02r2u?F|4{!EqQIX2HF3@SZ2p)cl#yFBSyD2YKb(-V$U2u`5wzs$@$zji3;1 zzj7_jSvZ2?EF|^d_!ILv?M)pXK z+DRR7V}AXHEfWrKRHOx2gPiJ&%@8RhyTES*??pyPS^`cj1e$`7ZGjlLCvWx$beUYk zr21s5_;bix-05}*73d2&ZZMv8bM_1AgyA8X_5`)R4R@Du-=ps_I|mb9a|4%4Imwl- z@?U?W<6^^PAYC*Fsi-{x_n&AbHz?5)I7F2G&a;u3NFGv3II4pk5k@VrOtF02&}1uS zmWw=v*5ss6?_{rXnVx*5Esz&0f|eqj7b#G2MPPYpkr#)EmmuJ{(4@u?LWW^`IF7MP zX_54`yVuFq;tc7Gb*?wv7!xMtk9Q+AQ#n~YB8LH3<#G2$1@s^ z!MdTK$M}d9%s7D7>$_c82PTOU%B)ve$d}}M0-D?;;J`gbZ(Br|8sRi153!E6_}HF2 z#QI{04aE=}iy<}@Lu@t(S!Y^9VNCsSOVz&ZQ)IWXjRj;*^9xW;2MSP57Ya}tO_Rie zqQ~uSj~DI;)(dwU=7l@0^1_`adEv#{)zIJty4z6{2&Q4Ue9QTLaZgSUeeI z{}JJ0$K#xshvjGTRsvs^>EZPp2?gqeu#r|)!yVcsMc6DZPftgM&!UAB83-vx9ks;F z%~x1_V{lH+ew3RSpP~_B`>oq3SOwlDXDilnMR2LA2u*M)K0huYLCOw1c7V>0eNI6ocO)P{=+?MB$|ZY$d|yaigl+DAW&2 zjQbo+kl^FAr;_0KLNX%-5wN6;8{XQcbryjlwW#YfO$bwA$e@>cr9H^eEeJ21jSDgs z0d<6-Eqt9E!S?;Pdn1BU9{1(K*B$SY%m_Cu)X`ylbNj-7RdYaQh?j=T9+ZHhSnY}4 z34)H`Eo|qgtee4h2%DN3M1^%g-c1LQL3V%;D0FZHib6w>NEh~4^dO4|rq5>)<`n`9 z5#do9?2Ppf?Xyh&Xd96gyxV57<|!g8A_~G`JxyLZZGGrY8We~H&$Iu0r#eM6zjH@h zBHrQLt;Ri+$TNP{4!~ef5n5P+dx1F7{bX;4c^{H=IS=6|Paa|&nR-+o5makuG!8S5 zb_4E4CF5;j{qljUsi0dS-jlnJCwe5g8G$l*dXUjZhz3ItKB;F13QPLm-s^_SNW-ir z#lc2NW8tL{Po|{wt|c7rNCbO$PwsYwTOm5rYi7K-_i(_G3c+1P-WRwhFT9*Zl5k6i zD|JLF15eXHCbjgXJJ=1UE&`Rl$gam-m7GW*Oaug-fQJ&v)L}0K4C}*oZ5k0QTqSb^ z4~3li@e&%=k5G{tY<*k7zP2Kq_Ja5noFId#xUm-oDOJZpy%6G}4jG)cAPdGIr6D~D zxEC;4xpkz9rkqF>u9R^5-Ah@#2kv$lGa~iVkn>xxWFw@_cvm~YkLTdjmkE^&rvk~& z9dIMtJXU9L@QffVYpFm_m@9Xil0You2jCCL6J3!!or0rhdI}*LLadp=Ni!)p)L#;we8Nyb0uZ&dCy}P4V zdkKmYT1Z!~g_pju{3Ca()vK@vnz9MJ>W0pty?X}^N|E>&MA`C7;ES);Sq)O8t~0aS(4!Ub(p!5BWs{52 zJOpc~v-PQggZ+DkhBN6v=-}u`|G_;2fiY?;_j7ov7xMW)d?#{ z9^D5G-8>f^PEAcXrsc#hYg#FIDp3onb!0f59vSZ6n}&cWy{u_?PuLT8M|K%D^&U2O zC(Cf5@i|~+3~i%_wvXQ4^itCkncTj`USfQH^Avt)Qx{3=o>`wY6*T3Jj@Rt*vXloW z>6J)keNuE79`O&34D>_K`C-X5D^Ag0Yggp)PwNLI=^;pFur&9e-MFuSj-O?uR}`7< zO@XP5)fo^tzz+l&N_|PL<5fHGQUPgD$^f{9V=R z1ygz%m45a!6%D2|sgw0`9?6xTEg+c&uj)zfp$%g&$dumY<&+tOXZ+~hJf@w~I(*=Q z{&dp7^cxVrY8cmd4(#1G5a{l|bO0w0yb0%Jp2j!ItPd@~hHOQ%G#@pgXJBN{ zzy+i5C=V3E+j@D0EPRd%Z4Ne#Y zbFbVnXUtnqW^f1^ho_HR$86X8KJZ*9yrZAiwv}#>0WCgvqoouFvj#m+4POMmZ{a28 zCw;6B8{!q3IQPZ)tlZ(RmYzfh5Z=b5EqurZ>iDrKcACBD2k;vS(XUl6%%dGV421a1 z3^!&u{9uAHuzINY?F=&FRaTAll;ceyBEdTj8X6`ZSiXu z)-{IDg1pA)S1^im(v0vq3{bOi!zjN&7`Fv?iw1bb_*k(q$2T*8&#i_J43=lC@Hq{` zrWhA|R|B|w*>G8y$7R2~x$$nS@I4Ic9mA(7e$oCwXaD~F7xiBPipF-q-#vDW15zr~jWY3}EqsGYk>5ek4&F9*_K70>Jx5 zXfv9vRV!q`$b_{F^pC)z4ZlZ)89=UW4lILXlll`&4I{E)h7n)DGd*JUvM^m7OwFq+ zrS>IV2kDi@n0c!a8llt4b>T+Y&Qanalx~7#F4oUwxizoVP-PHG7rttMN84!0Bm|@xW9pXwnA^ixqZHnXe{y?(5YB;^o98 z>jgvG7#tZ5bik4^0GkhRrV97UX*rv>9$F)fY@q(pf8+B1XCic_!ssR5H-fWkCU2Qghw0>PJ8Y8kUY=25ON>yMZ z(z$qWp8c2m)EvWn`u%F{a)y828w}FsrfIt3lzl>7{mMS_uz<)ujCm^X?DzZ(Nd zxELkUzsABt(q`J1hRcBx_8A9a{)G`2756t0v$=6ik`!bL*W+IPRM<~F&0+`&b_)u67;hHB(BBR#&Ta7*lS zA6GI|tagj0>$eSs476|Q4E;zU^9DlD!o{_G)6!8FQhnA%s!u;iD2zZMm)e07s?yPU zP|GEC`u#xR4O-z+&jTKM(9gw<&QpP|BCM*pTmP-zzW%4Gbc|kTVSKw0r#!Rbg0qV0 zw^692M9!+k&&9Z?D{1n2-tKV9uwmd^&TNW^6&nP3^T8n}Lm*FVxaug!I`l&tE-=X? zBXiNOuM98&O+cDT>Fi8e*zH62s~JlQll5Knth6*?>kD$`g$^h0!UdibbwR;6QIFI1 zB@d`Ir&~3UYchD@;I)HK(v_6Kor4`#9m0kNpBTJ*uzK(zT1~B0n3K{)i7AL3u$qp) z@PG<0ti0)PXbS!B_-Rk6*OuLTsxMhF^{%b#kRIJ?Ut8f@>brZZeYC>2*>`NK{X6)3 z^*w~Qz7GFHDjxMuNn6KtiTe>$2_WkM{F;hi-`HwztMtvUeh?wE0FtpD+G-D0`VO4? zEh=U`jF=i?^c{vgf{?ic`7S}4t?whK78yQ@U-R(mKk=&$zaGP{2K@Q~el_CPe{Hol zO!ciD`)`1N6ZJSE0)R+cCm8ZS2w6msA2H-72w6;!pE2a;2&p2-Qw;e(ge)P*(+qhA zAxjDJEJ2#B=Ml6FK-Mqk;^%Vw{1twkgkLXgwcnWPTiEt%BDY>d#21LcZyE9uLQW&d z?-}w(gsdXSp9s=yy@H_C_Uboa9Xd447wCPJN&kxQ^+fs_L;jACO$7M|L7J^M5VRRU z)|>eC9{9>wZxPaZ2O*~u`IK$&JEFcT7W%faPL#sGkyb2LhF>8n=HF(2w92=>y8=O- z1ga!@YZ^i##HorQ(-G1|kQoGlcZ2)Wqu`^CN4g0-o50O>|1{rRdw#XAajb@m)?(2f zqM65#dW4)wkOqb{AtXtV`3zZzkQ6}z3|WkjUVLDn*4JwnbV$OeXNLdXXQvY8?8LC8*moKBEtYb%1z0Z_K8 z#ojpG*Bsb}kbZzz@58U%_;m(;?ZL0$Hv7uyzQ#ZcKtS2s5HSFVv=t&qv(OsMcO(?Q=E&S%dvPreqBMucN5aui;ydce1IYQ5Hd!PL55fe zIZTlA8FBz2A0)^@hFpO1jeLke7cwY~kgEwY%8)|{`7l8)V#p;3`3ONS-IjayuXX1~ zK1zRG&iE@4e+@yjvR#Fcl?3@9(_D>^BLw*{Lq3X-YYB10#|ZK z=vMeAauY#rW5^c}@=1c+PLO8n4g}c%vhKvMoAK+5*uBW7;4@?W8_`++j*w4N$uBeH zE`;1dkgqc2ZiIY>AYWt1JqYd9z z>ujITH`8^zO&I&-2UWu<=68rAUqJ@hX|B_0azZJIB61fZ?N`33=8b(7KC`Z$5{(>1 zfc}xt$k*@>`Wev3G5q9gvdBH~oykshPE3I}JRiAc?<`48-m{hJuq?zenNvr#d(h>@&jyu+%bs!w|)1Us$sqN*ES+QBtBK1y*#wbuYcbn z@;Ksi4>vZ>La2T+J8}Y{_QD^i1#>*V5fOQU$Zy!tI0MM}#~mX7L&S^T1|M;f$K)eF zdepw^ZOE%#^|+et`!W47A9o%VzMnp7AAMUjR`8x9@-thVfG2-mej6Uq_%h<3eAKRf zM>UnxRwMFrM)$O-iYjP?e%Czm6oOay{#V!Xz7tT(|Ir^1oeKJG*h5TJPumxtP|Zt! zfi2EXbDdzIfW35gPrZYFhVOqQzJLBEKVAe%vqe z68*uC@Eu|H8kyhtB;nb7pcOytk_E$?ncUZubEGgy(o&|msj z@FIW3zx1nHk=Ni4unnlF9JeJcG!I2Z{zgUhQ)-dF;}p!~4)2HCSGm=h?MH-g(sSD)Y^^Kl-Ao9-H!?qy_F685GdJb99|KEOg-`G!EG z6#mL&XT~$5bf%87>KjTAV1L|M`jIl?S<7_xedWHIDs6%Nh{@d@tA*dT_Pl-=DMw5u zTf^y3o~0v6ZJH=+NQW;YcY`R3Bz#1^g}8!505}eo!vBQ?1qafRpCH(M_9FDJAY#J! zc;rn)%`~z!Rg{q6YFxn?`>q<_clwJ3KgS~vAg4*frjc(WfhP#+4v$9u2ToNz_f*}bB3&}#V2?+$as_N?R z`(`ww-0v>?Tip70b#--hb#--hfAf`3SO4y-RpmF9-(7ZB`SQrF9fRw4cJA0Q*gm+S zZJ>R}uJtXuJ9ckqY2C2Ct+lPaZ6aGWv}<(Dbp!kMPGmiUI|gpKPqHtg&iT(^72&c`#4)yRu?i<;x@wRik;y*n{@_2B-|33FxLv}w)euHze? zUns;a$5a3NN+BLT{^XA?7w&(XJ#AWcnLPLdG4FWGPwp1(KOxYP`}l>X+>ejd=hnn( za({l|n5fL%`Qp7|cJ8c`H;Bc#hfik3*|~Ku-7ea5-+JkgxF~ntsUu=@?z^YHGsSuRrSkzGD5}rA@h?ynd(qC-~>G+_QU{ayPuO zPn^!Z^TtEso!qgX-Qs$`A=mix8-y!2@$=Wj8M(1vJm_BB2-Hh+4-7Qs*1mbWSd}~R z<~^b_7x|?WzTBI?Jml_0f-`e>{c2I}@UM2c`|!^d@aMu@?OWT$KyKe#zjq%%uvI>L zNHpXg`;S?){oj9Cd@i^D4^N4Qb7#MQuQ;9)AI#3Z@c!4t zb5xRld_=rTr7eH@s(24d_qsl6%H8+DgQ6;T!H0+4v*!T+`MFm=nV-Ax@2Yd}d^l9T z7@*vtKd*rQ|D3xO>*wA5H8C&O{?QR}LGI~~zAQqy9Up($ox-ZtT+BT;x8jp5jP&tO zcHO;L?3|f_io5HkiD~#hnA7p==iU9FxL+KV7Ygx5_e0ojh5TWeXgU#hi7w&(9wKWA zkzX$p-xa6iHRa+3@t-Fm72+l#8czJsEj}&8h7*@piED(oT*BY0PJCP~t`OoTxvfTg zDDIK{wPI0YLwtC6pNIZ)uBXo%4tttg&z1v^fc(Gw#ToL6TG1#Tl+V?QjCe#|SSP+N zo|3<-6MtL&y}8q-$!BMXGf%*OZWHAn;NSb|#d7@5Ct~4=eY3=n5JCCk8R9$RV-q(` zWZl6~AYeo$%&qfap=_H_|92Gs7a@Z3WPi+SN- zm=nvB>d4U}mQ>N$ltPC?Mp3Ec$p*1#CUec!1c!F*8r&}rCdF)}jw+wwjib6t52>m* z6fyp28#yA{q}E@dB!B#SXc>S=63EdaF)^Y3M{I zpYIp*&LO-a8Tb09&}doQi|Ue>G>b-gMOe61v+}>NR=c7plt+5c7AxgzTSP;h@(kf< z!e;ltJmFSLw6ZH2@AJk31w(MmT&2%Q_8C+8hbo`f-!mnXOu7fPRnEeZbCg|Yi=`yt z45bevqzptJSSIGl?e~f%dEb1oKx~!IeqAiAC!)$&DBK+n_2LL;?Gz17I3w&^D|@|( zq(QSP>lcWN(*V5Uxt{uUzEEFr|5;(Ir8jX95Ua+}HDC~{tCig6&aUMJ&md+x=D%|C|;|3Zy z^*4v$8L@CG0_N%ofk{Si%H;Gc>!w7 z!-jS3G5X2X%YR!UI#`$fXgs2(rPm`?GZ>79(NRF}2y#z}KBZL@vX*pK)@_85=pQ{6 zyJRp5u1td~h-^B{aUv0qdTBtDj6_D4iur8FEGw=@PcykY8bTB4+x!u`xN_LYtwC#z ztIeIrH3YjSN&-t}t$c8qXh@W@B;h;X`^DAlbVVUQo%TOj` zsVkG-hO4L={8Pf~j~is)xRRKmFAV-u%fy%;eS^lBaoFh6E_{KuB7n3NVSn5@u3af& zZ1AyEJQgOF-)I)AxqVk?i;jD6$Jm$$E8|3%1%T+bP^N(QQi9N#E>SLHfLul-8c*UP zvDZSgyeI>Xy*?vS^72)p?u-a6ovJ{{+Z&B2PjGCNXr7_)@~%*vSii7JthW3}C;~1X z^|Exe=$u288S^krSGFP$H@uc|tC0JsDa+*R4Wglv1<4JpoGX8{S~T(8`}(aBv7X`v zgh{mK9m)&9l&gLbWOeA?5aJCc5bEkO;zq=pC+>z_4N}gaQCE1wp!$LdP6n+lUr%r% z8OMN;M&;-_k+f}=rAelI7C-LQZ?luHpKIAEPcqIXN8V)Kd9a#$>@g2sXDxNwAF?&P z=W0HH+Xlg)q{c+^(0tzJPB?mc_zUf#o^B4@cQEL1`D<#=Y%}M8T-LOb%LmySH$jwgM4te=)rxU8u#aLH^L6d05-~4r0FGHt8eWQA z@*u;4*akzrpvll=8#HWdV5j&i-B%>4wh?5^wv$L%eA!~VQjrJ+0Z7GIuhY@6AttzNTv-b~158pgfT5hjq`q*9ND;~>Ra_D0Hq1go)M`YWjPh}tepqcbL%7-0cz33UAVs-v zaQ_&Hrj}N7w$9i~(5S~dGB_}it@aORM#ioi7}+(Eoo)>6Nsm!lXkt*MX|m-z_GShr z1{}6f>Gd>6_fs^2$Bp?sJAfvy87B9DD2s?_dSuY1X8i)*1sz-D4tVHlo7(1`7`oOX zW;9e(4nT{@foL%~052v7!bRi&*eM5(5mC$qTk_3Q({K={fpzMGh!70~Rl{bWl-)Hl zO!GrEY^RZ-{ezlV-gUm1bA}^g(603P^XY8ztQ7O=^=Yp>;SF26EJ>G1XHbeV4zLmz zh-U6SV8EegwWkj@X6y_3xQ-r`-?^YLD37ObDJUZohs6p$H-V3jM-spx6;bIIi^ahU zm1M9V@U({FJ)R&OIIyGlmWrl%4(Fb}{?J6WqR2V-+>7X(izn6D6dp`Zgf4~6**CO@ zG>sMViHk6*rLJu)L>vbI&Ow6b?xk48xy3jR^lon|nd(IWOA#bu@ld3@G(m053qAu# zc%dpjS-IOQ7W2{8rcm0v(ksrKRHd9@(JHmE7p+p8J65p`=>t7LbL7c~MV&Z0 z5mIXTBty=a_tDrP$p$BZ1g`ZGZQTi!21Dc$1Qp3(XsQ&K8~0|%X=QU^-acbKtmdJ@ z2YH;*OJ~$P&UC<0WmqsWA;TXv|5??QhojyuR1+QnWlNCaa*Qa+TaB7;DzdUfij;|N zl;X8f^L=KXh(%Q{iBedu5SapawWfRS7jCAV=OzP$6Kpt4Arb}+EXXQVS00KhHaK78 z$Vbghs;VmL11tBd^CEg5Rh|AO*mS{ed*Y(a;>%(u8KK!v&6~5eeMZ<10h$NHqtX+D zGY99b(gf9T4u;%f$T|zFR49!jlYSbF%MC{)Z$AwY)xk~%&t~Yc<0-Nn%VGaIEat)r zqZH$kUf6EP`l=HcB5KcXRq_&;?6CC_)oHW^fmc~XoXzKkXdJSG6pfu9fuk(-Sec6W zqg&LG62<1$(XDEWLP#$*$ZG0<2q%g+h%RvAaqIlH{cB?JJco~;uB(;zMI+2(XTvN` z433$6WI$e>4qg?BUbr18Jx7%|I^J~AjCm+qqI6XyVIQ3%Z@5Fuke9)rK=&}VCi&Mf z;bw^f&h$8tu-5U2OB+1u!hw+@9V!c|8#QFB$-KV)gk=!AX&;||98KYR%IbmV${U8Q zF+{5q`0-HSJY}(Cx}JzW`8fIYt06m}Uav}4Bg9Q1f7`8sa|BsDe-QQ*-7RCmRIfS$ zA|0PAe4IT3WHlO6#(bHV+>}n1Q)2SAg!m^u0lnd-#0X5t?xa|(Vh2bPswFQ2bVLLy)G?hDPfj?6*8#L&Ic~V7-Ym0a33W&Ye>-2EJ|G&3 zxz#P?F3UOhSz>e*I^kKOenuR25KnV(cx32P!}j5Y!+TYk$m|~(8nw@3yx=ZXqrzQS zAMMqq={WhJtHQJnE#O~n6|1$+(LsP?q4z^7LpdGT{h4DQZGm(wl6UqiC)qD%=Vd^; zqEV|EJCcZD1k?`gxR&Wsmx>fzu=`4cx~-X!J1!Mxpc97PnGN0v*=bYllmQC!*#c?x zh*EGR21Y{ddFz1SDv`A1Us5(hhaA1p z6vb;Khn^Rem3QbO$hTYwok!j>eUhxzy8J0&FV(R1K=8mD5S|xj>T_oqn>kKD%N~Rn z19XK4Cg*Jw#BX7}Nvz){*0KsMEVE6t@f7&9{mRIoSTQp~HeUe@RKo3TyLB8+UX%$t z`wFp!=NAkQouvwDZn#3sTdNq9sSv%y7%Lr_rv+CE&n(qQ!74sLc(#J8lR?PL=s4tx zE5)*c3e}=Wg*xP6gu@R^xCqjznoeBEtP{=3vtS={o|1 zxQi}Ge=ApkAP=q!LJ9t zFLLx!QI1#c6iY2*(@D~kJH;{$TFO|s%bq?R^j^73ENvp|$fVz*Wfe1GF*PzrK33n} zI>e`Z3d)Lpkr$EkCJGmUz8zAynz@NVv3wTwn5~4pLtBHjf8^DvCRUvu5=Hvh_y1dz z-$7P4WvrzHtlqG$y=}v~mX3DrjJCL$OSA6bk%9etD9;E<2}N8L^THV+8lr0+E%MAp zJ9U(G_hU#z*N#nTv0D1lIwdxD+-Bpr057rX|35cUcP~6}D+p&A^PV7;^Ft6?RBW2? zih@RlCcqX54o@2^8$cfr>9&sj(sGxG@d}C?NURNrLf1_CBQkblYY10msnG{hQ#}OZ z)SJgt6RC=o}xkuCaAD-Des!%;(9gpZu?#q(B<;#tHfemWgi^Bag_+j z`n{AYfa7?>EQee!b+>}2bQQ(ds8hA4h`f$tT^&gzwMbD2V z($s0z4QtZAn_nrf8x!m0<6~s9|6@!%Lc<+3Z&lRR?f#LB=#!^2l$C+o|GDy^47{oU zSyd)y)V!Y?B%=0}H}m=ySNT%v)fZ+ToI=^Yk!!CN_BiK8K6(wL`tfEr}W z^~D_Skeu-Ql&UIAsK-ahiU;`Ff<`ouZ(VB-@V#qA6Grpk8##gZifRHoia*o^fw}gQ zUNzi%Aj@>NIl|46!YPswtv64ArQFwEjM2tEZa-s~i4CAQ0$4f1q@XsqH!&AEYJ z&y_#FRa_=7zD=ABdd8bO9wI!gq9;rD9Dzr)-G{|Y9ez+U#X>bT-1AXbfgG4p9T6-_ z)+Ssz(aJ(X(pJWtOWp#WdC@r~IeY+b?x5XLTFcp5f_=kdW5fICP7mCuey#~Z_J>0~ zWY`F^IDL@d!h;er;eO%dgeMyZ65!l7wqvE!g2Sf~K#6;y=BF1TwwB94BdDu8AQ>Au zB5JvVUOq)Yvj+K(N5l+vY5^nU=T{C^l_e(Uu`OO!m)QQW%0#9o=Foak=kRDN~0joKSURR^G5V)N()Lh}7#k!;WQ#!=rrbY|f zc{#_%*+aoDJ*(lY?%W!Q&)r~V@$xw@gA@#@{Tmr_FR;8EiF<3>@U1+mZjlTFsaVSp@L3%&luW2lz`-9< za4Priz~Jodw|d~F;B%~DXo%BGjy@Co&X)_mQ|Z5aaz;Eet0SiUAui%aHG zFBx-nE$=q)^@6%h#}>>MpWY%{^bHndKcnQG6dwG?Ltb^0jWUt68MBi)yX=%Ci`J_? z8o`bj@v7vt1K}%9M0ojmnO`}4_CcN>>sYPoC6?9z<YivbG3)O2-dG&h_w6^;B{L zD>~OWbBAwk+B?p3P8x63VXZuPx}jvk31GD(dLY7pEl*3u5`cGvgS%eq@LkOc)~x=* z1tIP@;kQBn;Hy)@a5KrzUa>bu@v=qTo{|x02KpTZfvm^@tw^T>(1v8Fr#ke#&&Piw zW-t!sOg3lX+(B0yUKG$5k%gH;O$lyHxICC^ijk7wd3_N~O9b&}{&U()7b5ZX=}y%CAnT)D9y@)Y#))?p3F zcr@kXyEo7%56^i$PF=AbW_R(H5Vr4@Dx; zE%4wRKP2H(5A`X3_>js|04ZI*Wg+)m{H=UM1xVjurB(&-J2!qzjA>0?XbM(md4$=h zFc+*cSq>2F7#@w`L^>GgM~NENRuV4gFCVu%%*WNC7TM?H>gnQST)kVIjH|B_CgbX} zgvq%2E&*}gF8oFauOFQnE3C1{P*`J+qOisuM`4XUlENB$EIelznmGl_gKequU|Xa- z*p?>`wuQ-qOGsiXHILfXX&!tYozxvuz{6=NfvXJ3WO{p`Y%0vh9%(*q4>cdR$C{7Z zgU!e7(dOg!aP#pJ<83Q3-Zp!@^awRuiKm9%ypS?IHv$jCuiZIt^}yKB@P0WHzJz4I z8s^l+V~>ayeK+h=&uXQ36uwa-zug=nFjCluT ziXiBM8l4h~rD8GK;;=otR&O8`Ca;ei8f*^P3ZK5fMQXN4jLps^Ed>(->uQ*!sHS zsmaM8*qygbYP|&#S{6rMZTxBi<>5XcYome?#Fz^51E@ZS)Npp8ch63U@01YI(+x*q zof=2DQw0Zq(!uQ1zkgu8V03=5>xG*^ig)nRMc)4;y|AKm-1AS0bJ!Y#{egJ2+y3~- zaq5U}?S~2+4T6*C)y!iR8+;EnQT68TS`8zb_0jy`-9;qtTZcGz_XW#214rV zJco>UA&?;7;K=yK^aC`+@@Eg-0vjsiJrleDP(j*8QcjQ;3aoFk2JD#U3qDZIdzZsw z@`a+^ac_)Iun+?>TR-F+Qp5uX$^kPZkduZFe(*vaZtaB$U)5VsUVpX@*#ya?^?K#L zdcD1n&(5@qd0U{!k=B>I&U&+t$eGF`-~VVNYv0Mtg5_bN)=Weix7*$LYbnKGB&FfYtc#_ zpm{K-XbhgwT0Vfgrnyx=Mnp@Tg$+iNp1As?8rQCT;@j4xD=!=PCII%Pq$d{Q^oYIk z+4+0nxTPp5eNZ$G$_kILFZmKZLP&4<0$@+xp-; zNjOQ9HXR#tcRWQWyTXnjEdbB*(PEI8=g1Ggu0XC8<4l%_ei({CT$xPR(zae@Gfq7t zR<=^^t{TFbu>OsV61sx#Q|N~x$l38~j)8CSfCgznIOYc2Vh9+5a_I#k)^LjeynFV9 zqumzHv+3Ou@<#B-Nt8py7VHGN-~}4|ERQ2W&pSOlX-&e%rT+B5kb1j|TIX)KP6eX4 zerdbNRx?g(qr^Bhjamr~_swlp5Iz}KK{(<(B={*7_^qG>%oW8NI3%@cWwNti8Nyh> zziu(EgyqtkfZv#D&NjtHhWCsNj*h~Im!8}-pBG$Jp0L9APRYsa){J>oDX*Q?t+@TB>gfsq3f z=4SvlZ|#uBUJ#9!@gG1~rU=_75kg(Ls_Q6LSE1@UC)M5aqVQA_|Jyp`SuctWwfqM# zbREup_?3mK_Hc*%>leiw>yJlY6mxeH>0umzlB=d`X!ls!)3tYac!Y;QwMw!Y_&T*% zt&**dzEYi3FV8wD)~Q}^?~nt|KfZ8MwAPRax049+^s8`Xy!};CBi}zM>a9jIUJ`8% zx(cRyNi48PUU?Os`8@oRqtUFN!ppAHfH+!K$!A}J9_mkt`Bux;Q=)C9EyQ%x^C;hf zWXp|fhsIbDM>^!UUxPP2zggvS*Q)6~f?~Vme}4^_{`)CWrk)H?yg4qmB48y7Oiw8s-|DHk$k+l zZ=?Lw8rKTf{*Cg%HLeY=0~_Tj_`BsG)tjU6&*rM<+|x$QF>Y}+Le&77*I;Qnmag3> z*Q|9ds=E%6S%75BPi>T2*Shv?zn+?zH(*TzF&bCMO^7rSax)>#=B)@dA;a6SGzUwE zu{0M;w_|BOmX2U)0hW$#lxMcM+Q#ny{8B@(3bp33-%|X7e8qYLTzCyXMN>tu9aMF-7_W;_XCwTp>>) zvYwFtMo6>y4TLrTWPTG%8=;gjpQ1|h+lXu;@~1b-ms?$jmOMi>&E~UM<;P~<#gakI z{%NDUrp?vf`!5Iu3HlzS~mA>s$-QpI1$PgiT{a^J9g)fXF3;yr__u5J?bnN+GWxk|g9+g}jDHijdO^c>|Fx zg#1h)zd)pqkT=;Ve}&Lig5FX@ZzIxA$U6%84I-Bk@}CNM50T3V`K?0!3z5qS`MpBk zM`RlzeL0btQ^DAEG%WPbOx5L#?ovo zU4x|tEL{twjM)f96nxGm`BaB%$@pCQ+nkRSe+3ky<^rZzgwXZad@+^|QS&8KX)Z97`AaW}qtqNI($ZdqQE2I;V z!-TBgl#Bdf?p2#_r@ziq^_#H%2qEVy`UyNS+BBl3A_ zIi!%Q5cvWjdlj-Dk^2Z4R>(m_?k8kKA=jvXuEUDI!HP_FHeOs@*$r1UF^U1N3^iT0bMk(3xx^BL@Q`49RP&isD z2~rhhF=vrbELhwDPo%M7uWsrNimnGVEmzm2viwr>)C{Fmws*|!IR6r7w}U%62e^1N zl@;M1efvpSimUYaYZ_e*k!Fm&%2gF?GCP=<#xiFpjo0XpG$tKEdL)YETugOilkyrX zEn}~D)x#ysx+u{WTKVJ%lr;!6aK22c&?_WS-`F zwm2t`q!+l3%%$q5WHr(LR+cyQhi1L=nFDb8=Ckqe7fi4~T$9gIq1>>X^%YO&!x4S1 zpl09XgY|W;DA)uqJm2kLJw$15maGBZ-n5>204OH@|?6Xet5XkIRlc@G4e4I z#vj3k*m!$0PK_9j?OBI#^GH0$1xetKy=TpfntO4 z^&P~A7L$xo->(#t#DKizieCaOG1;=;Agtk383nMvt(gPLudfHu+}6IhRr_nj!zB#D z6vX}#=$%3bL|iF!SRryt*+k1cyGluWSjLU%2gXfecd68cO3VCK%F0wTH9t%>Ft!L! zU#(S08(rO(*2O|^IO07g_V(A^^2KE#r#ZB>4SGybWE(ur&ilrP?F4JvCEKI6QH{&83N`gxrT`l z;SacYX%=2S6r@S7j9V_iHsEKf%zOYsl-i)sV=1peeiVuU!)VOF*FrFxl(9kmQ6_`# zkFpr#2X1GiZu?i1XvrY?gw`3NY&h$wMj>Vlrx6jIDd>OGbSOXvKajw2lfb^wtk(b? zl;!A_{XkZ?0-8p!m@tA3kt>z!kctz?tsOyv4P4JH(Lm551N&I_3>gmre1(C5T(N)+ z297|46MSI1Ffs2)QU(Al5UWPg$)?M8v1cTUmfaUP;8_9Uu4MZHkur+r4pSN`e4~iF zR0g<1tQkek05C=wnpl?<=MK2Q?bR$dB7z*z?j~;~5~aqwN3(cK0|DP?4nW-m`PWb} zaZI>hC_9v#1IiGwV+={-!7(iNe}!z?44ddRmX!9wXP7e~)&&oG7j=--1E4^x8B1FU zE5*)mIne=#)$JF;Q38WDn?kUf2@sRUN&5hs8fLj5(F0vz6rqWk=l~E#>zNR7Y+Sh8 z-0_+YwWSa=!kpF$hjE9PH=bmJhPZn?af-^v#?x|uq35hH##7os8D!H)kE2y-TMbuZ zyl_pRL&tR!Sg6YUHAaZ*CXk8+Q7%Z5sTK5s1hap00-Ya-KiI^F?u<1~iy(HX%m;{K zLT9bkU2C5Phr^PIWNln|E$i1KS$cP!70xDKKwdA9a&jbP@%2P9M9|K$lUcM`^qr)Q zolJ4LlO>4xlZY7+Mxb5%eG;fFFvb4Ha!~F5V-n34KjY<@y3F{|DJmwjUVw#N7e#h$ zos^0h113Nx!xMnJP4)HQ>jwatFW;T6O{Z+}$K-Iiw~JK&VHX>z1ec0ZK~qsGXef?_ zQNUjn4t(t*pfM|zje*npGB?L=Q3biA^XO@lUvDoHt+r~yXhJL#$JxN$IX&T zx}72rZr~#uM`mMC^fk-`w?S{R7ut=Mh}DUy0k??{nUYq`#sO2(9)>MFznY{C%1$Aw z!?xx`AMF=d$r1gG+h~knW4me%T}!!wV=uSA zJZ7*CIAi+RXi++i=VTMO)Iv%uE(~0@V`Yoy#HL!7k@~hG3ff;D@3?>a(f-9MFlMR3 zGK|k<3pmAT9dWHCK30SXY{C9n(GN8E!Mg`TSli0-1`>U&!TBce9gF=H0uJA=S%GSH)x6EI=EI=h`c&5 zEh~hpj+N01f^hxBG84nx1EU@4dh#O^9pZl3gua(ej_YA^2FEE$4=S}~iT_Bv1TQPa z_xbwDY*FWBeX^(19LgXK&F!u7gD(M{7dR1Ai1Fd$M=HpjUQ$40Qhn?tm5WOTr&?A$ zxay?>?ymdKtEX!a>GeD6=}M!^`Qk)9?QQ~S2Z%#V@{!#Q(Ba^9p&S$#($zk?zV7sq za!?&-_{k7bUjaWWP>LftzPbtS|E8~P{=@KyvCmHzcbGtd_>bST+C{$xIAFS>pIg)-$@xIAGUSCS0^KMe;P31-(PLZ#3OBpoF)UPo4q?^BvP@ zP5>%$KtK<;s$oh8EeL%YH9}#$-31SGJ`bWm0m;dv6a)M4C(GA&I`h^Ng@1V4HdQLrzm6n*(snRpx1 zfq1nGo|K^{_#j8*nXHfa;!N6#z!hg4oLO$kzJ-5>K~6l2WC5!B#Ki=f0`My|ILxz1 zcM!%7iSfx+=1c@Di7qI_eymLZac}UtAngEJA&QuAHq8eKijEfI49o_dhcvuempn6n z_&d~pbT%1#Qo`Wn1Wa4_6G@?US@>V9OG=LdylK+VZ<`@C)6=l|ADqGByFCg#JA)1B z24d&15#2yl=dixrKo^~JQo~2jVS`{Yq(zUPL)VO)nV^@IUF(}k{JeE0D8~+Q*-X;M z_$6I3?}0OC+ANl#?r%aLYHy6_3UY3cGWL^>(v2~bijcNKT?A$kCF?R=wiomEWHq_3h>*S z4rGmJkCC>yL{_gBRJrP0QsPuT+CN!EN$u&m3|~9+sH4 zR+3ERje;M*35(R+R&qMwVYie_=&y{0eXZaV^GcMy0k`FOhAZ8s(>= zLcXYzs6^A##J*}4CZc#{Uyg1HF(r zmkhPhbD0~ymdqvV5sc?_@zh*8t@hpn=g`r)`kf^^4fFHCAMBitHh}q#DaOy!C|O{T zeJeH1&{J3{(cPcHE!k9Pm!Bd@2($h$7*gg6L3$LWBc)tiK z6}by&70|p;H;-?{nG49B01m`a-A<3a3s@aUEj;R_3*TavnWB@%w4V{=oNSB?z5lvx z)CRXY%uNzSGBd0!1WA+_)uo(85t@VM33E#nGZ!&uFB9`*9TS0x{#@Npy4bpi#aW+} zaM=|962zB_SV4+RE-Qn(l60DylhHaNax&2nghNMrxQ|dLb97_brcO3EU#6RZhapf8 zlZ@CzQXC?G>~jI-^tmBFTVb7*c0I$vG~Bb1E#+Wj$9*1jiJQs~&z4gN|T$`k|LP zkn6tUsS8;~ip*hycK|>9a2Jq)JC!r7(~ucwaA|9j@B46dD#-5C$zcf|A4ItN;3tu! zwFyGoJJn18({2MeNc#Y(c$udZO)-kl1CkOiUPQ_l8;ajAB2|HKK$jU@u=bdw1?Y58 z&%Btd25^kkq_ik{`aX#xhR#*mR#d=6%~a<%c0)!m%tPgAB|yNayQ4J<-L*;5Y8eCN#h-M;bP*o}$%i;C7o!;=}JFAD8ZWV*~H@d&z zAWF=Pssz{UwsuIW!c?OOfy8G$_*H-+8Gk5K_0m9~AQ7xIhKvCY$z6$ZjnutO=ccI? zKn$<+UB*gCMa|3-q03kTmWRyDhExU1uR`dQuXvGQtyJ9k4U8?OhI?`(lo56uzT z8RY-GEdi1c-L#b8EUCy#4#W-U{55C{M@yOb&vrUyfI=y_&?s+B_Ku&FwI(t9CdJWK!$2b3O3)aulF)7x zcDfSWTKkq0t5R2j>Q7lgH)$-#qYo96)}WQf>w)jilpkgR_Kc+jF@Rt*=$_KUEC|p_ z<%dZebUJ7^TY93S5rPw40T~Jh9SxP^>MxG0Btuza;scN_XF&Kqglo$_`o~gMnEa@S zLytZrU*V^A<@M|`k|@Il&Ud)^NlyH5JvlJ&bcxlE7kH+`LWnXcaC#@sPS8FOaE@n+ ziQCZ!QtOcpJ7`-Zz6;S^5z7$JFa=`ivF~m0>2Sdh2^0UIvM?#mOA&Q$NHLTMC3gIk z6OHVL2R7;#kwJRYTRL{(Tt$nG6^>}|RuBRC6%5GZdD|nb#Eya2njE<9t4wl^XmG%o zBN4q(X+>>uZ60g*d#HmE$2eBel0EyAeFV*huV2v}KFt z8yV%}A@mg2sF@T9+Zy>fqE;>2ndoc}2`vPf`#b4&obM zS%*Veb*fnc^&@e_k@XZ65@yE9ZM$^3T}*Q={5q^2u4KmB;*1Tf*p5FPrO3;xMfb3$I3Zy>J_tUE?IfsFXzFWN$XY}YQ^9J_MZqtIk^89&Qi6WfAL{2g6;=*)Pv62C!M%`rD_E8?bq^H0%+G^bM0S8%fSzjUu^GQ) zKoGqUZMl^WJ8%m-lV#dnSqKyARNL4n%%)QxV$f}-+XgEL3-pvT&5&txu(c>PPUCO6 zO*?OjD8$0l*~!*qIKVcS3zkCW!&!l1=IwOD(O5}oIxSA6PXRJHi}rv7fHY9~*zM#r zfe7AypnM?V5odv4@b)3I<5Vl~g{ssz6QJ|rG~JIk>UrSlAXCopbiiy7z=3rh*xMBU z2=5#G_OkSDQY7(`JLo-8m($~Q!V(bCz>HY@DKj~LOaMo{*e8XL}r zQrGlYx87-vReX3S9T{@gDn(6pHBz%lJ^Uc)1T$p~aO86vrmIQ91rUv~Qe_Z<%u|w? z>N44yVJ2b4v6Nf}9|xzg(io-r@o8_zU-j^c95S^_QAq$(S0+;_fS7a_9c(bP0x|C{ znh6dQZ4-U(1a>sYq$t%0Lz%-bgnc%X7am)Qrpg_ER*eC}~nr(xjrK3B`dxlOU2N&12HNdLB*z=ES!);}FCVk&=pWKZQ!!MTI5B(07F=G0A!~HZvn5?Qd z`K7*1rqMU;Y!i1xB&`^ehpMf~NcAnp74Bk^Xyo7k5SD~2NI8N?r*1c9xLF>5 zfR)N4^9j-KL6+a`N*H{Qm5iW+3x&)$Hq>g|L|1~n51T7&2P*q4+UuGwwp|F}5TsHJ z*;X8QkW4}F@{C5mLg7Q?na2PQe27`{5Sa_`xL>52U5$29k$CeVdQO3g-Xe1g*#*$1 zKzO&1N}*_grLs)$`?k=cVHr*w7sW{+QoOy&-`F4(6I9}pKY|_6)77yAU&8^24IQ8v zYv^s6NFIHd_OYx9!qJ2(1-_i%%>Kqa$-!o&=T*Goz%yupcyKF?klsI0VZ%O`9wLBL z01_Fw{kPFeCY8_HCg0rPC=;u<(TPFuC%4gQOy#e_<+zwC$W4%u*Z6?z4g2fhNjB)I zk;Wdpw&G;ruTr&fV06y-8c(AOqOx%+cy`+U&zy+9BVm{X9_?`YsHYG4<*unQ&q49p zb~>fD;SEP!X924yjuPoK8qSt5mOWzLSdC=2Wkpxx{P0yTL16)LxiSsj7(>Qj`asFfHhI*aKW#)k#5F&qZs!Xsa(2( zwm$}0YtkMOuRca+068!J{TMAElnca?$3aQRZAVYq^evTw{zfGLOW_pvKTcCX8gK;4 zEKC>$(LC`u(MKPFL#AFr4Se+#vz{OZp(w(!uTn0d5<>3_<5apOeB!&LI3jV0;^8OF zk>N-a!`YL7jmHaY51BfN-{_mW{z+O`GUF(B5-MomN1ik#Gfw48$~Mk8l`9FK)Qs{Z z^#}bT3(_FrQ(2MN5IRpVQz8KX2OOSH=oA6#RHZ=z905u2dwn_y5gVX>_{L^J{WT@0fD+RG&Cn<9iOKjXp+ZR|}7^gBGp#h3t0wtcRd_t;7-=MKFB>_S@ocOaPRUX>O04&JRDU=Xi zszSbSo6-ej;x5ufX1ov3A-~S@U2Li@f3=IQFC;Jnc1zuX7R0RG(saS)U9p=6D0Ny* zV-rP+6D4S!F23EZCtm<+29k}W-vP`7F?kPFp;*Pm3?4da#26ghqCFJ%g=2)rRmj*! zAc~s!c8@&0kuAA&+a*tW`r%*G zI=|H^{{^Ckmieu7JJ3LBdwBX$k11dV?cKr$lNI75Q2 zeifb?WC*S?M}l5PEE!{KERdko*VyPJ8AxkfEmHeT4? zfiPU}J7Bl0eg^^;_PW6dTkf{?68Y7(T(RgKP$(b24G#Hd-(fXPQ%9e6S{=h(9*P?p z{%6;M05+)3ChIK!{9Cv#O4eEa;Wt?)X1>cZJz=~UiD&s|-NJQttkcPJp>F@=5TR9z zUGK6nJ!Bb&OG8rsu8p0*wPOE=Aa1IB^g~dWs+@Qz3}5zMxV-T_*lG%A*L!TZEc?V2 zAF*OFsKk~MYf!CEJp2(Hp=VnmAAba?KC9n{!$YB@eGJy|Q&yD89|OwQ07+D2aNKs$ z@jlDo4U-&V+y|^@f^3a7v7+H4c>J1FY6I_pBD(5>@Td|FLkHh{5blwgQ>+ut58*_h zMs=bd%93s!(uLHzBP^pQ+{__0K>gT}!o250I69ABzj_Ys^=RHijB z%fNj&+<~&;6TtTYx(qBg;lS(x7G_%5{M#g3qICtre2y2deasx18t}SU9RHZbY5*`? z4bvwk9cBZT(h$5jL%CVtF^s&dVYu8VD#|kqmU|@nOm`!MXNSRYB=)3t@%Ife5iwjD zP5VVg>PToC_AIxNjt2~*(-sQp9}5V>V~LFNH-^Cu>RP?GgzQz2N`+xNgh0~d6mM7B zdMH68nR1E;kFd0p1WSB$1S}QR@aH3J{Df?9AGB?2Yo)iawxw{3#7fwqgZLADb1h!{1N56-?*`fC z&wK_TSFAIh!-{mM=ro>}kQWh>L6DaSQf9n@piHFmDweXa^ct43vGh8YavcX?d#}RszSTxkOq<0Kelo_#D zQ-!S@SaMUVIFUTeHljWOA+-caBy=MgAybJ?iiD&gq>dmx2onBhrKZ*sID^3SFEWYE z!)$p=vt-L0Y}r64xe}6xkZA>6$>JKcql7JwL)SUML=Ralyhr72jNgZS2e^=9f{0n%l-p%O%` z260*e(rMHZq|BHq#*eTS=G0+L8{v9YjSoTXVs8PMN~=fMdS&<#Fc)x)1}x3P(loJq zge^0(5djMj5WvzxA`}$gjIi|^e>y@IV_h?ry0CNxmM+B7bSzzjr8BW~F_gNDv!IB^ z_Y5&@q|LeX9Qrh7A;M*V&}pVGNysG#xrQK@O3391xt1VTh#{kF&Nd}Q zS7OC-s<=v`{skcw1i3~+u0zNQf-IMil?Yi$kn1Jn283KskW~aJGu9v|Bnq6i46$#N z&Cb?}w@2BU3f3X~MnLT{))T66ll<#e`PW8KH`AGfgtw~ zq|CSvK>|R={aCsQOAlb_W+-(T4-%TO1tGUk!-pkg8$xa+$loR85ro_ZkfKiGQ3-k+ 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 072/222] 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 8ce0b45a5f..ace231e12b 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi end local tech local force = game.forces["player"] + if call.parameter == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + end chunks = split(call.parameter, "\t") local item_name = chunks[1] local index = chunks[2] local source = chunks[3] or "Archipelago" - if index == -1 then -- for coop sync and restoring from an older savegame + if index == nil then + game.print("ap-get-technology is only to be used by the Archipelago Factorio Client") + return + elseif index == -1 then -- for coop sync and restoring from an older savegame tech = force.technologies[item_name] if tech.researched ~= true then game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."}) From e764da3dc6c476be7bd69267eebb8ad511bc3fbb Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 28 Jul 2024 16:27:39 -0500 Subject: [PATCH 073/222] HK: Options API updates, et al. (#3428) * updates HK to consistently use world.random, use world.options, don't use world = self.multiworld, and remove some things from the logicMixin * Update HK to new options dataclass * Move completion condition helpers to Rules.py * updates from review --- worlds/hk/Options.py | 6 +- worlds/hk/Rules.py | 39 ++++++++++ worlds/hk/__init__.py | 162 +++++++++++++++++------------------------- 3 files changed, 108 insertions(+), 99 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 38be2cd794..e2602036a2 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,10 +1,12 @@ import typing import re +from dataclasses import dataclass, make_dataclass + from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from schema import And, Schema, Optional -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -538,3 +540,5 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { }, **cost_sanity_weights } + +HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,)) diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index a3c7e13cf0..e162e1dfa8 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -49,3 +49,42 @@ def set_rules(hk_world: World): if term == "GEO": # No geo logic! continue add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + + +def _hk_nail_combat(state, player) -> bool: + return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) + + +def _hk_can_beat_thk(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and _hk_nail_combat(state, player) + and ( + state.has_any({'LEFTDASH', 'RIGHTDASH'}, player) + or state._hk_option(player, 'ProficientCombat') + ) + and state.has('FOCUS', player) + ) + + +def _hk_siblings_ending(state, player) -> bool: + return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3) + + +def _hk_can_beat_radiance(state, player) -> bool: + return ( + state.has('Opened_Black_Egg_Temple', player) + and _hk_nail_combat(state, player) + and state.has('WHITEFRAGMENT', player, 3) + and state.has('DREAMNAIL', player) + and ( + (state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player)) + or state.has('WINGS', player) + ) + and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1 + and ( + (state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks + or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive + ) + ) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index fbc6461f6a..e5065876dd 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -10,9 +10,9 @@ logger = logging.getLogger("Hollow Knight") from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions -from .Rules import set_rules, cost_terms +from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option + shop_to_option, HKOptions from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names @@ -142,7 +142,8 @@ class HKWorld(World): As the enigmatic Knight, you’ll traverse the depths, unravel its mysteries and conquer its evils. """ # from https://www.hollowknight.com game: str = "Hollow Knight" - option_definitions = hollow_knight_options + options_dataclass = HKOptions + options: HKOptions web = HKWeb() @@ -155,8 +156,8 @@ class HKWorld(World): charm_costs: typing.List[int] cached_filler_items = {} - def __init__(self, world, player): - super(HKWorld, self).__init__(world, player) + def __init__(self, multiworld, player): + super(HKWorld, self).__init__(multiworld, player) self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { location: list() for location in multi_locations } @@ -165,29 +166,29 @@ class HKWorld(World): self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) def generate_early(self): - world = self.multiworld - charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) - self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) - # world.exclude_locations[self.player].value.update(white_palace_locations) + options = self.options + charm_costs = options.RandomCharmCosts.get_costs(self.random) + self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs) + # options.exclude_locations.value.update(white_palace_locations) for term, data in cost_terms.items(): - mini = getattr(world, f"Minimum{data.option}Price")[self.player] - maxi = getattr(world, f"Maximum{data.option}Price")[self.player] + mini = getattr(options, f"Minimum{data.option}Price") + maxi = getattr(options, f"Maximum{data.option}Price") # if minimum > maximum, set minimum to maximum mini.value = min(mini.value, maxi.value) self.ranges[term] = mini.value, maxi.value - world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], + self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key], True, None, "Event", self.player)) def white_palace_exclusions(self): exclusions = set() - wp = self.multiworld.WhitePalace[self.player] + wp = self.options.WhitePalace if wp <= WhitePalace.option_nopathofpain: exclusions.update(path_of_pain_locations) if wp <= WhitePalace.option_kingfragment: exclusions.update(white_palace_checks) if wp == WhitePalace.option_exclude: exclusions.add("King_Fragment") - if self.multiworld.RandomizeCharms[self.player]: + if self.options.RandomizeCharms: # If charms are randomized, this will be junk-filled -- so transitions and events are not progression exclusions.update(white_palace_transitions) exclusions.update(white_palace_events) @@ -200,7 +201,7 @@ class HKWorld(World): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -230,12 +231,12 @@ class HKWorld(World): pool: typing.List[HKItem] = [] wp_exclusions = self.white_palace_exclusions() junk_replace: typing.Set[str] = set() - if self.multiworld.RemoveSpellUpgrades[self.player]: + if self.options.RemoveSpellUpgrades: junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) randomized_starting_items = set() for attr, items in randomizable_starting_items.items(): - if getattr(self.multiworld, attr)[self.player]: + if getattr(self.options, attr): randomized_starting_items.update(items) # noinspection PyShadowingNames @@ -257,7 +258,7 @@ class HKWorld(World): if item_name in junk_replace: item_name = self.get_filler_item_name() - item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) + item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name) if location_name == "Start": if item_name in randomized_starting_items: @@ -281,55 +282,55 @@ class HKWorld(World): location.progress_type = LocationProgressType.EXCLUDED for option_key, option in hollow_knight_randomize_options.items(): - randomized = getattr(self.multiworld, option_key)[self.player] - if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): + randomized = getattr(self.options, option_key) + if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]): continue for item_name, location_name in zip(option.items, option.locations): if item_name in junk_replace: item_name = self.get_filler_item_name() - if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ - (item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): + if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \ + (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak): _add("Left_" + item_name, location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized) continue - if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: + if item_name == "Mantis_Claw" and self.options.SplitMantisClaw: _add("Left_" + item_name, "Left_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized) continue - if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: - if self.multiworld.random.randint(0, 1): + if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak: + if self.random.randint(0, 1): item_name = "Left_Mothwing_Cloak" else: item_name = "Right_Mothwing_Cloak" - if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: + if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms: _add("Grimmchild1", location_name, randomized) continue _add(item_name, location_name, randomized) - if self.multiworld.RandomizeElevatorPass[self.player]: + if self.options.RandomizeElevatorPass: randomized = True _add("Elevator_Pass", "Elevator_Pass", randomized) for shop, locations in self.created_multi_locations.items(): - for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): + for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value): loc = self.create_location(shop) unfilled_locations += 1 # Balance the pool item_count = len(pool) - additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) + additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value) # Add additional shop items, as needed. if additional_shop_items > 0: shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) - if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there + if not self.options.EggShopSlots: # No eggshop, so don't place items there shops.remove('Egg_Shop') if shops: for _ in range(additional_shop_items): - shop = self.multiworld.random.choice(shops) + shop = self.random.choice(shops) loc = self.create_location(shop) unfilled_locations += 1 if len(self.created_multi_locations[shop]) >= 16: @@ -355,7 +356,7 @@ class HKWorld(World): loc.costs = costs def apply_costsanity(self): - setting = self.multiworld.CostSanity[self.player].value + setting = self.options.CostSanity.value if not setting: return # noop @@ -369,10 +370,10 @@ class HKWorld(World): return {k: v for k, v in weights.items() if v} - random = self.multiworld.random - hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value + random = self.random + hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value weights = { - data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value + data.term: getattr(self.options, f"CostSanity{data.option}Weight").value for data in cost_terms.values() } weights_geoless = dict(weights) @@ -427,22 +428,22 @@ class HKWorld(World): location.sort_costs() def set_rules(self): - world = self.multiworld + multiworld = self.multiworld player = self.player - goal = world.Goal[player] + goal = self.options.Goal if goal == Goal.option_hollowknight: - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) elif goal == Goal.option_siblings: - world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) elif goal == Goal.option_radiance: - world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player) elif goal == Goal.option_godhome: - world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) + multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: - world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) else: # Any goal - world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) + multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) set_rules(self) @@ -450,8 +451,8 @@ class HKWorld(World): slot_data = {} options = slot_data["options"] = {} - for option_name in self.option_definitions: - option = getattr(self.multiworld, option_name)[self.player] + for option_name in hollow_knight_options: + option = getattr(self.options, option_name) try: optionvalue = int(option.value) except TypeError: @@ -460,10 +461,10 @@ class HKWorld(World): options[option_name] = optionvalue # 32 bit int - slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) + slot_data["seed"] = self.random.randint(-2147483647, 2147483646) # Backwards compatibility for shop cost data (HKAP < 0.1.0) - if not self.multiworld.CostSanity[self.player]: + if not self.options.CostSanity: for shop, terms in shop_cost_types.items(): unit = cost_terms[next(iter(terms))].option if unit == "Geo": @@ -498,7 +499,7 @@ class HKWorld(World): basename = name if name in shop_cost_types: costs = { - term: self.multiworld.random.randint(*self.ranges[term]) + term: self.random.randint(*self.ranges[term]) for term in shop_cost_types[name] } elif name in vanilla_location_costs: @@ -512,7 +513,7 @@ class HKWorld(World): region = self.multiworld.get_region("Menu", self.player) - if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: + if vanilla and not self.options.AddUnshuffledLocations: loc = HKLocation(self.player, name, None, region, costs=costs, vanilla=vanilla, basename=basename) @@ -560,26 +561,26 @@ class HKWorld(World): return change @classmethod - def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): - hk_players = world.get_game_players(cls.game) + def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): + hk_players = multiworld.get_game_players(cls.game) spoiler_handle.write('\n\nCharm Notches:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] for charm_number, cost in enumerate(hk_world.charm_costs): spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write('\n\nShop Prices:') for player in hk_players: - name = world.get_player_name(player) + name = multiworld.get_player_name(player) spoiler_handle.write(f'\n{name}\n') - hk_world: HKWorld = world.worlds[player] + hk_world: HKWorld = multiworld.worlds[player] - if world.CostSanity[player].value: + if hk_world.options.CostSanity: for loc in sorted( ( - loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) + loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player))) if loc.costs ), key=operator.attrgetter('name') ): @@ -603,15 +604,15 @@ class HKWorld(World): 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeRancidEggs' ): - if getattr(self.multiworld, group): + if getattr(self.options, group): fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in exclusions) self.cached_filler_items[self.player] = fillers - return self.multiworld.random.choice(self.cached_filler_items[self.player]) + return self.random.choice(self.cached_filler_items[self.player]) -def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: - ret = Region(name, player, world) +def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region: + ret = Region(name, player, multiworld) if location_names: for location in location_names: loc_id = HKWorld.location_name_to_id.get(location, None) @@ -684,42 +685,7 @@ class HKLogicMixin(LogicMixin): return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) def _hk_option(self, player: int, option_name: str) -> int: - return getattr(self.multiworld, option_name)[player].value + return getattr(self.multiworld.worlds[player].options, option_name).value def _hk_start(self, player, start_location: str) -> bool: - return self.multiworld.StartLocation[player] == start_location - - def _hk_nail_combat(self, player: int) -> bool: - return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player) - - def _hk_can_beat_thk(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and self._hk_nail_combat(player) - and ( - self.has_any({'LEFTDASH', 'RIGHTDASH'}, player) - or self._hk_option(player, 'ProficientCombat') - ) - and self.has('FOCUS', player) - ) - - def _hk_siblings_ending(self, player: int) -> bool: - return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3) - - def _hk_can_beat_radiance(self, player: int) -> bool: - return ( - self.has('Opened_Black_Egg_Temple', player) - and self._hk_nail_combat(player) - and self.has('WHITEFRAGMENT', player, 3) - and self.has('DREAMNAIL', player) - and ( - (self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player)) - or self.has('WINGS', player) - ) - and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1 - and ( - (self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks - or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive - ) - ) + return self.multiworld.worlds[player].options.StartLocation == start_location From fac72dbc207f698f8c61f37953189f1f8a55de66 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 29 Jul 2024 13:40:58 -0400 Subject: [PATCH 074/222] FFMQ: Fix reset protection (#3710) * Revert reset protection * Fix reset protection --------- Co-authored-by: alchav --- worlds/ffmq/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ffmq/Client.py b/worlds/ffmq/Client.py index 6cb35dd3b4..93688a6116 100644 --- a/worlds/ffmq/Client.py +++ b/worlds/ffmq/Client.py @@ -71,7 +71,7 @@ class FFMQClient(SNIClient): 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 != b'01' or check_2 != b'01': + if check_1 != b'\x01' or check_2 != b'\x01': return def get_range(data_range): From 80daa092a7782c941411cd1568f232e7781cc316 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 29 Jul 2024 13:42:16 -0400 Subject: [PATCH 075/222] - Take shipsanity moss out of shipsanity crops (#3709) --- 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 0d7a10f954..608b6a5f57 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -2212,7 +2212,7 @@ id,region,name,tags,mod_name 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", +3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT", 3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY", 3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY", 3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY", From 954d72800541c8bc6e515f9e046d50a203ac9ab5 Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:09:51 -0700 Subject: [PATCH 076/222] sc2: Removing unused dependency in requirements.txt (#3697) * sc2: Removing unused dependency in requirements.txt * sc2: Add missing newline in requirements.txt Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/sc2/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/sc2/requirements.txt b/worlds/sc2/requirements.txt index 9b84863c45..5bc808b639 100644 --- a/worlds/sc2/requirements.txt +++ b/worlds/sc2/requirements.txt @@ -1,2 +1 @@ nest-asyncio >= 1.5.5 -six >= 1.16.0 \ No newline at end of file From 77e3f9fbefa32a51c6c6ce1c6a0aee584c0a72f1 Mon Sep 17 00:00:00 2001 From: Remy Jette Date: Mon, 29 Jul 2024 20:13:44 -0400 Subject: [PATCH 077/222] WebHost: Fix NamedRange values clamping to the range (#3613) If a NamedRange has a `special_range_names` entry outside the `range_start` and `range_end`, the HTML5 range input will clamp the submitted value to the closest value in the range. These means that, for example, Pokemon RB's "HM Compatibility" option's "Vanilla (-1)" option would instead get posted as "0" rather than "-1". This change updates NamedRange to behave like TextChoice, where the select element has a `name` attribute matching the option, and there is an additional element to be able to provide an option other than the select element's choices. This uses a different suffix of `-range` rather than `-custom` that TextChoice uses. The reason is we need some way to decide whether to use the custom value or the select value, and that method needs to work without JavaScript. For TextChoice this is easy, if the custom field is empty use the select element. For NamedRange this is more difficult as the browser will always submit *something*. My choice was to only use the value from the range if the select box is set to "custom". Since this only happens with JS as "custom' is hidden, I made the range hidden under no-JS. If it's preferred, I could make the select box hidden instead. Let me know. This PR also makes the `js-required` class set `display: none` with `!important` as otherwise the class wouldn't work on any rule that had `display: flex` with more specificity than a single class. --- WebHostLib/options.py | 7 +++++++ WebHostLib/templates/playerOptions/macros.html | 8 ++++---- WebHostLib/templates/playerOptions/playerOptions.html | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 33339daa19..15b7bd61ce 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -231,6 +231,13 @@ def generate_yaml(game: str): del options[key] + # Detect keys which end with -range, indicating a NamedRange with a possible custom value + elif key_parts[-1].endswith("-range"): + if options[key_parts[-1][:-6]] == "custom": + options[key_parts[-1][:-6]] = val + + del options[key] + # Detect random-* keys and set their options accordingly for key, val in options.copy().items(): if key.startswith("random-"): diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index 415739b861..30a4fc78df 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -54,7 +54,7 @@ {% macro NamedRange(option_name, option) %} {{ OptionTitle(option_name, option) }}
- {% for key, val in option.special_range_names.items() %} {% if option.default == val %} @@ -64,17 +64,17 @@ {% endfor %} -
+
- + {{ option.default | default(option.range_start) if option.default != "random" else option.range_start }} {{ RandomizeButton(option_name, option) }} diff --git a/WebHostLib/templates/playerOptions/playerOptions.html b/WebHostLib/templates/playerOptions/playerOptions.html index aeb6e864a5..73de5d56eb 100644 --- a/WebHostLib/templates/playerOptions/playerOptions.html +++ b/WebHostLib/templates/playerOptions/playerOptions.html @@ -11,7 +11,7 @@ From 1d19da0c76cca65b1b5b6916345486726b96e188 Mon Sep 17 00:00:00 2001 From: Jarno Date: Wed, 31 Jul 2024 11:50:04 +0200 Subject: [PATCH 078/222] Timespinner: migrate to new options api and correct random (#2485) * Implemented new options system into Timespinner * Fixed typo * Fixed typo * Fixed slotdata maybe * Fixes * more fixes * Fixed failing unit tests * Implemented options backwards comnpatibility * Fixed option fallbacks * Implemented review results * Fixed logic bug * Fixed python 3.8/3.9 compatibility * Replaced one more multiworld option usage * Update worlds/timespinner/Options.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Updated logging of options replacement to include player name and also write it to spoiler Fixed generation bug Implemented review results --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/timespinner/Locations.py | 21 +- worlds/timespinner/LogicExtensions.py | 23 +- worlds/timespinner/Options.py | 329 +++++++++++++++------ worlds/timespinner/PreCalculatedWeights.py | 68 ++--- worlds/timespinner/Regions.py | 24 +- worlds/timespinner/__init__.py | 185 +++++++----- 6 files changed, 417 insertions(+), 233 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 7b378b4637..86839f0f21 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -1,6 +1,6 @@ from typing import List, Optional, Callable, NamedTuple -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic @@ -14,11 +14,10 @@ class LocationData(NamedTuple): rule: Optional[Callable[[CollectionState], bool]] = None -def get_location_datas(world: Optional[MultiWorld], player: Optional[int], - precalculated_weights: PreCalculatedWeights) -> List[LocationData]: - - flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) +def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]) -> List[LocationData]: + flooded: Optional[PreCalculatedWeights] = precalculated_weights + logic = TimespinnerLogic(player, options, precalculated_weights) # 1337000 - 1337155 Generic locations # 1337171 - 1337175 New Pickup checks @@ -203,7 +202,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ] # 1337156 - 1337170 Downloads - if not world or is_option_enabled(world, player, "DownloadableItems"): + if not options or options.downloadable_items: location_table += ( LocationData('Library', 'Library: Terminal 2 (Lachiem)', 1337156, lambda state: state.has('Tablet', player)), LocationData('Library', 'Library: Terminal 1 (Windaria)', 1337157, lambda state: state.has('Tablet', player)), @@ -223,13 +222,13 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], ) # 1337176 - 1337176 Cantoran - if not world or is_option_enabled(world, player, "Cantoran"): + if not options or options.cantoran: location_table += ( LocationData('Left Side forest Caves', 'Lake Serene: Cantoran', 1337176), ) # 1337177 - 1337198 Lore Checks - if not world or is_option_enabled(world, player, "LoreChecks"): + if not options or options.lore_checks: location_table += ( LocationData('Lower lake desolation', 'Lake Desolation: Memory - Coyote Jump (Time Messenger)', 1337177), LocationData('Library', 'Library: Memory - Waterway (A Message)', 1337178), @@ -258,7 +257,7 @@ def get_location_datas(world: Optional[MultiWorld], player: Optional[int], # 1337199 - 1337236 Reserved for future use # 1337237 - 1337245 GyreArchives - if not world or is_option_enabled(world, player, "GyreArchives"): + if not options or options.gyre_archives: location_table += ( LocationData('Ravenlord\'s Lair', 'Ravenlord: Post fight (pedestal)', 1337237), LocationData('Ifrit\'s Lair', 'Ifrit: Post fight (pedestal)', 1337238), diff --git a/worlds/timespinner/LogicExtensions.py b/worlds/timespinner/LogicExtensions.py index d316a936b0..6c9cb3f684 100644 --- a/worlds/timespinner/LogicExtensions.py +++ b/worlds/timespinner/LogicExtensions.py @@ -1,6 +1,6 @@ -from typing import Union -from BaseClasses import MultiWorld, CollectionState -from .Options import is_option_enabled +from typing import Union, Optional +from BaseClasses import CollectionState +from .Options import TimespinnerOptions from .PreCalculatedWeights import PreCalculatedWeights @@ -10,17 +10,18 @@ class TimespinnerLogic: flag_unchained_keys: bool flag_eye_spy: bool flag_specific_keycards: bool - pyramid_keys_unlock: Union[str, None] - present_keys_unlock: Union[str, None] - past_keys_unlock: Union[str, None] - time_keys_unlock: Union[str, None] + pyramid_keys_unlock: Optional[str] + present_keys_unlock: Optional[str] + past_keys_unlock: Optional[str] + time_keys_unlock: Optional[str] - def __init__(self, world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): + def __init__(self, player: int, options: Optional[TimespinnerOptions], + precalculated_weights: Optional[PreCalculatedWeights]): self.player = player - self.flag_specific_keycards = is_option_enabled(world, player, "SpecificKeycards") - self.flag_eye_spy = is_option_enabled(world, player, "EyeSpy") - self.flag_unchained_keys = is_option_enabled(world, player, "UnchainedKeys") + self.flag_specific_keycards = bool(options and options.specific_keycards) + self.flag_eye_spy = bool(options and options.eye_spy) + self.flag_unchained_keys = bool(options and options.unchained_keys) if precalculated_weights: if self.flag_unchained_keys: diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index f7921fcb81..20ad8132c4 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -1,59 +1,50 @@ -from typing import Dict, Union, List -from BaseClasses import MultiWorld -from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList +from dataclasses import dataclass +from typing import Type, Any +from typing import Dict +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, OptionDict, OptionList, Visibility, Option +from Options import PerGameCommonOptions, DeathLinkMixin, AssembleOptions from schema import Schema, And, Optional, Or - class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" display_name = "Start with Jewelry Box" - class DownloadableItems(DefaultOnToggle): "With the tablet you will be able to download items at terminals" display_name = "Downloadable items" - class EyeSpy(Toggle): "Requires Oculus Ring in inventory to be able to break hidden walls." display_name = "Eye Spy" - class StartWithMeyef(Toggle): "Start with Meyef, ideal for when you want to play multiplayer." display_name = "Start with Meyef" - class QuickSeed(Toggle): "Start with Talaria Attachment, Nyoom!" display_name = "Quick seed" - class SpecificKeycards(Toggle): "Keycards can only open corresponding doors" display_name = "Specific Keycards" - class Inverted(Toggle): "Start in the past" display_name = "Inverted" - class GyreArchives(Toggle): "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" display_name = "Gyre Archives" - class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" - class LoreChecks(Toggle): "Memories and journal entries contain items." display_name = "Lore Checks" - class BossRando(Choice): "Wheter all boss locations are shuffled, and if their damage/hp should be scaled." display_name = "Boss Randomization" @@ -62,7 +53,6 @@ class BossRando(Choice): option_unscaled = 2 alias_true = 1 - class EnemyRando(Choice): "Wheter enemies will be randomized, and if their damage/hp should be scaled." display_name = "Enemy Randomization" @@ -72,7 +62,6 @@ class EnemyRando(Choice): option_ryshia = 3 alias_true = 1 - class DamageRando(Choice): "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." display_name = "Damage Rando" @@ -85,7 +74,6 @@ class DamageRando(Choice): option_manual = 6 alias_true = 2 - class DamageRandoOverrides(OptionDict): """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds""" @@ -191,7 +179,6 @@ class DamageRandoOverrides(OptionDict): "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, } - class HpCap(Range): "Sets the number that Lunais's HP maxes out at." display_name = "HP Cap" @@ -199,7 +186,6 @@ class HpCap(Range): range_end = 999 default = 999 - class LevelCap(Range): """Sets the max level Lunais can achieve.""" display_name = "Level Cap" @@ -207,20 +193,17 @@ class LevelCap(Range): range_end = 99 default = 99 - class ExtraEarringsXP(Range): """Adds additional XP granted by Galaxy Earrings.""" display_name = "Extra Earrings XP" range_start = 0 range_end = 24 default = 0 - class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" - class ShopFill(Choice): """Sets the items for sale in Merchant Crow's shops. Default: No sunglasses or trendy jacket, but sand vials for sale. @@ -233,12 +216,10 @@ class ShopFill(Choice): option_vanilla = 2 option_empty = 3 - class ShopWarpShards(DefaultOnToggle): "Shops always sell warp shards (when keys possessed), ignoring inventory setting." display_name = "Always Sell Warp Shards" - class ShopMultiplier(Range): "Multiplier for the cost of items in the shop. Set to 0 for free shops." display_name = "Shop Price Multiplier" @@ -246,7 +227,6 @@ class ShopMultiplier(Range): range_end = 10 default = 1 - class LootPool(Choice): """Sets the items that drop from enemies (does not apply to boss reward checks) Vanilla: Drops are the same as the base game @@ -257,7 +237,6 @@ class LootPool(Choice): option_randomized = 1 option_empty = 2 - class DropRateCategory(Choice): """Sets the drop rate when 'Loot Pool' is set to 'Random' Tiered: Based on item rarity/value @@ -271,7 +250,6 @@ class DropRateCategory(Choice): option_randomized = 2 option_fixed = 3 - class FixedDropRate(Range): "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" display_name = "Fixed Drop Rate" @@ -279,7 +257,6 @@ class FixedDropRate(Range): range_end = 100 default = 5 - class LootTierDistro(Choice): """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items @@ -291,32 +268,26 @@ class LootTierDistro(Choice): option_full_random = 1 option_inverted_weight = 2 - class ShowBestiary(Toggle): "All entries in the bestiary are visible, without needing to kill one of a given enemy first" display_name = "Show Bestiary Entries" - class ShowDrops(Toggle): "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" display_name = "Show Bestiary Item Drops" - class EnterSandman(Toggle): "The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces" display_name = "Enter Sandman" - class DadPercent(Toggle): """The win condition is beating the boss of Emperor's Tower""" display_name = "Dad Percent" - class RisingTides(Toggle): """Random areas are flooded or drained, can be further specified with RisingTidesOverrides""" display_name = "Rising Tides" - def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]: if with_save_point_option: return { @@ -341,7 +312,6 @@ def rising_tide_option(location: str, with_save_point_option: bool = False) -> D "Flooded") } - class RisingTidesOverrides(OptionDict): """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" @@ -373,13 +343,11 @@ class RisingTidesOverrides(OptionDict): "Lab": { "Dry": 67, "Flooded": 33 }, } - class UnchainedKeys(Toggle): """Start with Twin Pyramid Key, which does not give free warp; warp items for Past, Present, (and ??? with Enter Sandman) can be found.""" display_name = "Unchained Keys" - class TrapChance(Range): """Chance of traps in the item pool. Traps will only replace filler items such as potions, vials and antidotes""" @@ -388,67 +356,256 @@ class TrapChance(Range): range_end = 100 default = 10 - class Traps(OptionList): """List of traps that may be in the item pool to find""" display_name = "Traps Types" valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" } default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ] - class PresentAccessWithWheelAndSpindle(Toggle): """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired.""" - display_name = "Past Wheel & Spindle Warp" + display_name = "Back to the future" +@dataclass +class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin): + start_with_jewelry_box: StartWithJewelryBox + downloadable_items: DownloadableItems + eye_spy: EyeSpy + start_with_meyef: StartWithMeyef + quick_seed: QuickSeed + specific_keycards: SpecificKeycards + inverted: Inverted + gyre_archives: GyreArchives + cantoran: Cantoran + lore_checks: LoreChecks + boss_rando: BossRando + damage_rando: DamageRando + damage_rando_overrides: DamageRandoOverrides + hp_cap: HpCap + level_cap: LevelCap + extra_earrings_xp: ExtraEarringsXP + boss_healing: BossHealing + shop_fill: ShopFill + shop_warp_shards: ShopWarpShards + shop_multiplier: ShopMultiplier + loot_pool: LootPool + drop_rate_category: DropRateCategory + fixed_drop_rate: FixedDropRate + loot_tier_distro: LootTierDistro + show_bestiary: ShowBestiary + show_drops: ShowDrops + enter_sandman: EnterSandman + dad_percent: DadPercent + rising_tides: RisingTides + rising_tides_overrides: RisingTidesOverrides + unchained_keys: UnchainedKeys + back_to_the_future: PresentAccessWithWheelAndSpindle + trap_chance: TrapChance + traps: Traps -# Some options that are available in the timespinner randomizer arent currently implemented -timespinner_options: Dict[str, Option] = { - "StartWithJewelryBox": StartWithJewelryBox, - "DownloadableItems": DownloadableItems, - "EyeSpy": EyeSpy, - "StartWithMeyef": StartWithMeyef, - "QuickSeed": QuickSeed, - "SpecificKeycards": SpecificKeycards, - "Inverted": Inverted, - "GyreArchives": GyreArchives, - "Cantoran": Cantoran, - "LoreChecks": LoreChecks, - "BossRando": BossRando, - "EnemyRando": EnemyRando, - "DamageRando": DamageRando, - "DamageRandoOverrides": DamageRandoOverrides, - "HpCap": HpCap, - "LevelCap": LevelCap, - "ExtraEarringsXP": ExtraEarringsXP, - "BossHealing": BossHealing, - "ShopFill": ShopFill, - "ShopWarpShards": ShopWarpShards, - "ShopMultiplier": ShopMultiplier, - "LootPool": LootPool, - "DropRateCategory": DropRateCategory, - "FixedDropRate": FixedDropRate, - "LootTierDistro": LootTierDistro, - "ShowBestiary": ShowBestiary, - "ShowDrops": ShowDrops, - "EnterSandman": EnterSandman, - "DadPercent": DadPercent, - "RisingTides": RisingTides, - "RisingTidesOverrides": RisingTidesOverrides, - "UnchainedKeys": UnchainedKeys, - "TrapChance": TrapChance, - "Traps": Traps, - "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle, - "DeathLink": DeathLink, -} +class HiddenDamageRandoOverrides(DamageRandoOverrides): + """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that + you don't specify will roll with 1/1/1 as odds""" + visibility = Visibility.none +class HiddenRisingTidesOverrides(RisingTidesOverrides): + """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on. + Areas that are not specified will roll with the default 33% chance of getting flooded or drained""" + visibility = Visibility.none -def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: - return get_option_value(world, player, name) > 0 +class HiddenTraps(Traps): + """List of traps that may be in the item pool to find""" + visibility = Visibility.none +class OptionsHider: + @classmethod + def hidden(cls, option: Type[Option[Any]]) -> Type[Option]: + new_option = AssembleOptions(f"{option}Hidden", option.__bases__, vars(option).copy()) + new_option.visibility = Visibility.none + new_option.__doc__ = option.__doc__ + return new_option + +class HasReplacedCamelCase(Toggle): + """For internal use will display a warning message if true""" + visibility = Visibility.none -def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]: - option = getattr(world, name, None) - if option == None: - return 0 +@dataclass +class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions): + StartWithJewelryBox: OptionsHider.hidden(StartWithJewelryBox) # type: ignore + DownloadableItems: OptionsHider.hidden(DownloadableItems) # type: ignore + EyeSpy: OptionsHider.hidden(EyeSpy) # type: ignore + StartWithMeyef: OptionsHider.hidden(StartWithMeyef) # type: ignore + QuickSeed: OptionsHider.hidden(QuickSeed) # type: ignore + SpecificKeycards: OptionsHider.hidden(SpecificKeycards) # type: ignore + Inverted: OptionsHider.hidden(Inverted) # type: ignore + GyreArchives: OptionsHider.hidden(GyreArchives) # type: ignore + Cantoran: OptionsHider.hidden(Cantoran) # type: ignore + LoreChecks: OptionsHider.hidden(LoreChecks) # type: ignore + BossRando: OptionsHider.hidden(BossRando) # type: ignore + DamageRando: OptionsHider.hidden(DamageRando) # type: ignore + DamageRandoOverrides: HiddenDamageRandoOverrides + HpCap: OptionsHider.hidden(HpCap) # type: ignore + LevelCap: OptionsHider.hidden(LevelCap) # type: ignore + ExtraEarringsXP: OptionsHider.hidden(ExtraEarringsXP) # type: ignore + BossHealing: OptionsHider.hidden(BossHealing) # type: ignore + ShopFill: OptionsHider.hidden(ShopFill) # type: ignore + ShopWarpShards: OptionsHider.hidden(ShopWarpShards) # type: ignore + ShopMultiplier: OptionsHider.hidden(ShopMultiplier) # type: ignore + LootPool: OptionsHider.hidden(LootPool) # type: ignore + DropRateCategory: OptionsHider.hidden(DropRateCategory) # type: ignore + FixedDropRate: OptionsHider.hidden(FixedDropRate) # type: ignore + LootTierDistro: OptionsHider.hidden(LootTierDistro) # type: ignore + ShowBestiary: OptionsHider.hidden(ShowBestiary) # type: ignore + ShowDrops: OptionsHider.hidden(ShowDrops) # type: ignore + EnterSandman: OptionsHider.hidden(EnterSandman) # type: ignore + DadPercent: OptionsHider.hidden(DadPercent) # type: ignore + RisingTides: OptionsHider.hidden(RisingTides) # type: ignore + RisingTidesOverrides: HiddenRisingTidesOverrides + UnchainedKeys: OptionsHider.hidden(UnchainedKeys) # type: ignore + PresentAccessWithWheelAndSpindle: OptionsHider.hidden(PresentAccessWithWheelAndSpindle) # type: ignore + TrapChance: OptionsHider.hidden(TrapChance) # type: ignore + Traps: HiddenTraps # type: ignore + DeathLink: OptionsHider.hidden(DeathLink) # type: ignore + has_replaced_options: HasReplacedCamelCase - return option[player].value + def handle_backward_compatibility(self) -> None: + if self.StartWithJewelryBox != StartWithJewelryBox.default and \ + self.start_with_jewelry_box == StartWithJewelryBox.default: + self.start_with_jewelry_box.value = self.StartWithJewelryBox.value + self.has_replaced_options.value = Toggle.option_true + if self.DownloadableItems != DownloadableItems.default and \ + self.downloadable_items == DownloadableItems.default: + self.downloadable_items.value = self.DownloadableItems.value + self.has_replaced_options.value = Toggle.option_true + if self.EyeSpy != EyeSpy.default and \ + self.eye_spy == EyeSpy.default: + self.eye_spy.value = self.EyeSpy.value + self.has_replaced_options.value = Toggle.option_true + if self.StartWithMeyef != StartWithMeyef.default and \ + self.start_with_meyef == StartWithMeyef.default: + self.start_with_meyef.value = self.StartWithMeyef.value + self.has_replaced_options.value = Toggle.option_true + if self.QuickSeed != QuickSeed.default and \ + self.quick_seed == QuickSeed.default: + self.quick_seed.value = self.QuickSeed.value + self.has_replaced_options.value = Toggle.option_true + if self.SpecificKeycards != SpecificKeycards.default and \ + self.specific_keycards == SpecificKeycards.default: + self.specific_keycards.value = self.SpecificKeycards.value + self.has_replaced_options.value = Toggle.option_true + if self.Inverted != Inverted.default and \ + self.inverted == Inverted.default: + self.inverted.value = self.Inverted.value + self.has_replaced_options.value = Toggle.option_true + if self.GyreArchives != GyreArchives.default and \ + self.gyre_archives == GyreArchives.default: + self.gyre_archives.value = self.GyreArchives.value + self.has_replaced_options.value = Toggle.option_true + if self.Cantoran != Cantoran.default and \ + self.cantoran == Cantoran.default: + self.cantoran.value = self.Cantoran.value + self.has_replaced_options.value = Toggle.option_true + if self.LoreChecks != LoreChecks.default and \ + self.lore_checks == LoreChecks.default: + self.lore_checks.value = self.LoreChecks.value + self.has_replaced_options.value = Toggle.option_true + if self.BossRando != BossRando.default and \ + self.boss_rando == BossRando.default: + self.boss_rando.value = self.BossRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRando != DamageRando.default and \ + self.damage_rando == DamageRando.default: + self.damage_rando.value = self.DamageRando.value + self.has_replaced_options.value = Toggle.option_true + if self.DamageRandoOverrides != DamageRandoOverrides.default and \ + self.damage_rando_overrides == DamageRandoOverrides.default: + self.damage_rando_overrides.value = self.DamageRandoOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.HpCap != HpCap.default and \ + self.hp_cap == HpCap.default: + self.hp_cap.value = self.HpCap.value + self.has_replaced_options.value = Toggle.option_true + if self.LevelCap != LevelCap.default and \ + self.level_cap == LevelCap.default: + self.level_cap.value = self.LevelCap.value + self.has_replaced_options.value = Toggle.option_true + if self.ExtraEarringsXP != ExtraEarringsXP.default and \ + self.extra_earrings_xp == ExtraEarringsXP.default: + self.extra_earrings_xp.value = self.ExtraEarringsXP.value + self.has_replaced_options.value = Toggle.option_true + if self.BossHealing != BossHealing.default and \ + self.boss_healing == BossHealing.default: + self.boss_healing.value = self.BossHealing.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopFill != ShopFill.default and \ + self.shop_fill == ShopFill.default: + self.shop_fill.value = self.ShopFill.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopWarpShards != ShopWarpShards.default and \ + self.shop_warp_shards == ShopWarpShards.default: + self.shop_warp_shards.value = self.ShopWarpShards.value + self.has_replaced_options.value = Toggle.option_true + if self.ShopMultiplier != ShopMultiplier.default and \ + self.shop_multiplier == ShopMultiplier.default: + self.shop_multiplier.value = self.ShopMultiplier.value + self.has_replaced_options.value = Toggle.option_true + if self.LootPool != LootPool.default and \ + self.loot_pool == LootPool.default: + self.loot_pool.value = self.LootPool.value + self.has_replaced_options.value = Toggle.option_true + if self.DropRateCategory != DropRateCategory.default and \ + self.drop_rate_category == DropRateCategory.default: + self.drop_rate_category.value = self.DropRateCategory.value + self.has_replaced_options.value = Toggle.option_true + if self.FixedDropRate != FixedDropRate.default and \ + self.fixed_drop_rate == FixedDropRate.default: + self.fixed_drop_rate.value = self.FixedDropRate.value + self.has_replaced_options.value = Toggle.option_true + if self.LootTierDistro != LootTierDistro.default and \ + self.loot_tier_distro == LootTierDistro.default: + self.loot_tier_distro.value = self.LootTierDistro.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowBestiary != ShowBestiary.default and \ + self.show_bestiary == ShowBestiary.default: + self.show_bestiary.value = self.ShowBestiary.value + self.has_replaced_options.value = Toggle.option_true + if self.ShowDrops != ShowDrops.default and \ + self.show_drops == ShowDrops.default: + self.show_drops.value = self.ShowDrops.value + self.has_replaced_options.value = Toggle.option_true + if self.EnterSandman != EnterSandman.default and \ + self.enter_sandman == EnterSandman.default: + self.enter_sandman.value = self.EnterSandman.value + self.has_replaced_options.value = Toggle.option_true + if self.DadPercent != DadPercent.default and \ + self.dad_percent == DadPercent.default: + self.dad_percent.value = self.DadPercent.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTides != RisingTides.default and \ + self.rising_tides == RisingTides.default: + self.rising_tides.value = self.RisingTides.value + self.has_replaced_options.value = Toggle.option_true + if self.RisingTidesOverrides != RisingTidesOverrides.default and \ + self.rising_tides_overrides == RisingTidesOverrides.default: + self.rising_tides_overrides.value = self.RisingTidesOverrides.value + self.has_replaced_options.value = Toggle.option_true + if self.UnchainedKeys != UnchainedKeys.default and \ + self.unchained_keys == UnchainedKeys.default: + self.unchained_keys.value = self.UnchainedKeys.value + self.has_replaced_options.value = Toggle.option_true + if self.PresentAccessWithWheelAndSpindle != PresentAccessWithWheelAndSpindle.default and \ + self.back_to_the_future == PresentAccessWithWheelAndSpindle.default: + self.back_to_the_future.value = self.PresentAccessWithWheelAndSpindle.value + self.has_replaced_options.value = Toggle.option_true + if self.TrapChance != TrapChance.default and \ + self.trap_chance == TrapChance.default: + self.trap_chance.value = self.TrapChance.value + self.has_replaced_options.value = Toggle.option_true + if self.Traps != Traps.default and \ + self.traps == Traps.default: + self.traps.value = self.Traps.value + self.has_replaced_options.value = Toggle.option_true + if self.DeathLink != DeathLink.default and \ + self.death_link == DeathLink.default: + self.death_link.value = self.DeathLink.value + self.has_replaced_options.value = Toggle.option_true diff --git a/worlds/timespinner/PreCalculatedWeights.py b/worlds/timespinner/PreCalculatedWeights.py index ff7f031d3b..c9d80d7a70 100644 --- a/worlds/timespinner/PreCalculatedWeights.py +++ b/worlds/timespinner/PreCalculatedWeights.py @@ -1,6 +1,6 @@ from typing import Tuple, Dict, Union, List -from BaseClasses import MultiWorld -from .Options import timespinner_options, is_option_enabled, get_option_value +from random import Random +from .Options import TimespinnerOptions class PreCalculatedWeights: pyramid_keys_unlock: str @@ -21,22 +21,22 @@ class PreCalculatedWeights: flood_lake_serene_bridge: bool flood_lab: bool - def __init__(self, world: MultiWorld, player: int): - if world and is_option_enabled(world, player, "RisingTides"): - weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(world, player) + def __init__(self, options: TimespinnerOptions, random: Random): + if options.rising_tides: + weights_overrrides: Dict[str, Union[str, Dict[str, int]]] = self.get_flood_weights_overrides(options) self.flood_basement, self.flood_basement_high = \ - self.roll_flood_setting(world, player, weights_overrrides, "CastleBasement") - self.flood_xarion, _ = self.roll_flood_setting(world, player, weights_overrrides, "Xarion") - self.flood_maw, _ = self.roll_flood_setting(world, player, weights_overrrides, "Maw") - self.flood_pyramid_shaft, _ = self.roll_flood_setting(world, player, weights_overrrides, "AncientPyramidShaft") - self.flood_pyramid_back, _ = self.roll_flood_setting(world, player, weights_overrrides, "Sandman") - self.flood_moat, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleMoat") - self.flood_courtyard, _ = self.roll_flood_setting(world, player, weights_overrrides, "CastleCourtyard") - self.flood_lake_desolation, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeDesolation") - self.flood_lake_serene, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSerene") - self.flood_lake_serene_bridge, _ = self.roll_flood_setting(world, player, weights_overrrides, "LakeSereneBridge") - self.flood_lab, _ = self.roll_flood_setting(world, player, weights_overrrides, "Lab") + self.roll_flood_setting(random, weights_overrrides, "CastleBasement") + self.flood_xarion, _ = self.roll_flood_setting(random, weights_overrrides, "Xarion") + self.flood_maw, _ = self.roll_flood_setting(random, weights_overrrides, "Maw") + self.flood_pyramid_shaft, _ = self.roll_flood_setting(random, weights_overrrides, "AncientPyramidShaft") + self.flood_pyramid_back, _ = self.roll_flood_setting(random, weights_overrrides, "Sandman") + self.flood_moat, _ = self.roll_flood_setting(random, weights_overrrides, "CastleMoat") + self.flood_courtyard, _ = self.roll_flood_setting(random, weights_overrrides, "CastleCourtyard") + self.flood_lake_desolation, _ = self.roll_flood_setting(random, weights_overrrides, "LakeDesolation") + self.flood_lake_serene, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSerene") + self.flood_lake_serene_bridge, _ = self.roll_flood_setting(random, weights_overrrides, "LakeSereneBridge") + self.flood_lab, _ = self.roll_flood_setting(random, weights_overrrides, "Lab") else: self.flood_basement = False self.flood_basement_high = False @@ -52,10 +52,12 @@ class PreCalculatedWeights: self.flood_lab = False self.pyramid_keys_unlock, self.present_key_unlock, self.past_key_unlock, self.time_key_unlock = \ - self.get_pyramid_keys_unlocks(world, player, self.flood_maw, self.flood_xarion) + self.get_pyramid_keys_unlocks(options, random, self.flood_maw, self.flood_xarion) @staticmethod - def get_pyramid_keys_unlocks(world: MultiWorld, player: int, is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + def get_pyramid_keys_unlocks(options: TimespinnerOptions, random: Random, + is_maw_flooded: bool, is_xarion_flooded: bool) -> Tuple[str, str, str, str]: + present_teleportation_gates: List[str] = [ "GateKittyBoss", "GateLeftLibrary", @@ -80,38 +82,30 @@ class PreCalculatedWeights: "GateRightPyramid" ) - if not world: - return ( - present_teleportation_gates[0], - present_teleportation_gates[0], - past_teleportation_gates[0], - ancient_pyramid_teleportation_gates[0] - ) - if not is_maw_flooded: past_teleportation_gates.append("GateMaw") if not is_xarion_flooded: present_teleportation_gates.append("GateXarion") - if is_option_enabled(world, player, "Inverted"): + if options.inverted: all_gates: Tuple[str, ...] = present_teleportation_gates else: all_gates: Tuple[str, ...] = past_teleportation_gates + present_teleportation_gates return ( - world.random.choice(all_gates), - world.random.choice(present_teleportation_gates), - world.random.choice(past_teleportation_gates), - world.random.choice(ancient_pyramid_teleportation_gates) + random.choice(all_gates), + random.choice(present_teleportation_gates), + random.choice(past_teleportation_gates), + random.choice(ancient_pyramid_teleportation_gates) ) @staticmethod - def get_flood_weights_overrides(world: MultiWorld, player: int) -> Dict[str, Union[str, Dict[str, int]]]: + def get_flood_weights_overrides(options: TimespinnerOptions) -> Dict[str, Union[str, Dict[str, int]]]: weights_overrides_option: Union[int, Dict[str, Union[str, Dict[str, int]]]] = \ - get_option_value(world, player, "RisingTidesOverrides") + options.rising_tides_overrides.value - default_weights: Dict[str, Dict[str, int]] = timespinner_options["RisingTidesOverrides"].default + default_weights: Dict[str, Dict[str, int]] = options.rising_tides_overrides.default if not weights_overrides_option: weights_overrides_option = default_weights @@ -123,13 +117,13 @@ class PreCalculatedWeights: return weights_overrides_option @staticmethod - def roll_flood_setting(world: MultiWorld, player: int, - all_weights: Dict[str, Union[Dict[str, int], str]], key: str) -> Tuple[bool, bool]: + def roll_flood_setting(random: Random, all_weights: Dict[str, Union[Dict[str, int], str]], + key: str) -> Tuple[bool, bool]: weights: Union[Dict[str, int], str] = all_weights[key] if isinstance(weights, dict): - result: str = world.random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] + result: str = random.choices(list(weights.keys()), weights=list(map(int, weights.values())))[0] else: result: str = weights diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 757a41c388..f737b461d0 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -1,14 +1,16 @@ from typing import List, Set, Dict, Optional, Callable from BaseClasses import CollectionState, MultiWorld, Region, Entrance, Location -from .Options import is_option_enabled +from .Options import TimespinnerOptions from .Locations import LocationData, get_location_datas from .PreCalculatedWeights import PreCalculatedWeights from .LogicExtensions import TimespinnerLogic -def create_regions_and_locations(world: MultiWorld, player: int, precalculated_weights: PreCalculatedWeights): +def create_regions_and_locations(world: MultiWorld, player: int, options: TimespinnerOptions, + precalculated_weights: PreCalculatedWeights): + locations_per_region: Dict[str, List[LocationData]] = split_location_datas_per_region( - get_location_datas(world, player, precalculated_weights)) + get_location_datas(player, options, precalculated_weights)) regions = [ create_region(world, player, locations_per_region, 'Menu'), @@ -53,7 +55,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w create_region(world, player, locations_per_region, 'Space time continuum') ] - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: regions.extend([ create_region(world, player, locations_per_region, 'Ravenlord\'s Lair'), create_region(world, player, locations_per_region, 'Ifrit\'s Lair'), @@ -64,10 +66,10 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w world.regions += regions - connectStartingRegion(world, player) + connectStartingRegion(world, player, options) flooded: PreCalculatedWeights = precalculated_weights - logic = TimespinnerLogic(world, player, precalculated_weights) + logic = TimespinnerLogic(player, options, precalculated_weights) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") @@ -123,7 +125,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Sealed Caves (Xarion)', 'Skeleton Shaft') connect(world, player, 'Sealed Caves (Xarion)', 'Space time continuum', logic.has_teleport) connect(world, player, 'Refugee Camp', 'Forest') - connect(world, player, 'Refugee Camp', 'Library', lambda state: is_option_enabled(world, player, "Inverted") and is_option_enabled(world, player, "PresentAccessWithWheelAndSpindle") and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) + connect(world, player, 'Refugee Camp', 'Library', lambda state: options.inverted and options.back_to_the_future and state.has_all({'Timespinner Wheel', 'Timespinner Spindle'}, player)) connect(world, player, 'Refugee Camp', 'Space time continuum', logic.has_teleport) connect(world, player, 'Forest', 'Refugee Camp') connect(world, player, 'Forest', 'Left Side forest Caves', lambda state: flooded.flood_lake_serene_bridge or state.has('Talaria Attachment', player) or logic.has_timestop(state)) @@ -178,11 +180,11 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Space time continuum', 'Royal towers (lower)', lambda state: logic.can_teleport_to(state, "Past", "GateRoyalTowers")) connect(world, player, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: logic.can_teleport_to(state, "Past", "GateMaw")) connect(world, player, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: logic.can_teleport_to(state, "Past", "GateCavesOfBanishment")) - connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not is_option_enabled(world, player, "UnchainedKeys") and is_option_enabled(world, player, "EnterSandman"))) + connect(world, player, 'Space time continuum', 'Ancient Pyramid (entrance)', lambda state: logic.can_teleport_to(state, "Time", "GateGyre") or (not options.unchained_keys and options.enter_sandman)) connect(world, player, 'Space time continuum', 'Ancient Pyramid (left)', lambda state: logic.can_teleport_to(state, "Time", "GateLeftPyramid")) connect(world, player, 'Space time continuum', 'Ancient Pyramid (right)', lambda state: logic.can_teleport_to(state, "Time", "GateRightPyramid")) - if is_option_enabled(world, player, "GyreArchives"): + if options.gyre_archives: connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp") @@ -220,12 +222,12 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str return region -def connectStartingRegion(world: MultiWorld, player: int): +def connectStartingRegion(world: MultiWorld, player: int, options: TimespinnerOptions): menu = world.get_region('Menu', player) tutorial = world.get_region('Tutorial', player) space_time_continuum = world.get_region('Space time continuum', player) - if is_option_enabled(world, player, "Inverted"): + if options.inverted: starting_region = world.get_region('Refugee Camp', player) else: starting_region = world.get_region('Lake desolation', player) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cab6fb648b..66744cffdf 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -1,12 +1,13 @@ -from typing import Dict, List, Set, Tuple, TextIO, Union -from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from typing import Dict, List, Set, Tuple, TextIO +from BaseClasses import Item, Tutorial, ItemClassification from .Items import get_item_names_per_category from .Items import item_table, starter_melee_weapons, starter_spells, filler_items, starter_progression_items from .Locations import get_location_datas, EventId -from .Options import is_option_enabled, get_option_value, timespinner_options +from .Options import BackwardsCompatiableTimespinnerOptions, Toggle from .PreCalculatedWeights import PreCalculatedWeights from .Regions import create_regions_and_locations from worlds.AutoWorld import World, WebWorld +import logging class TimespinnerWebWorld(WebWorld): theme = "ice" @@ -35,32 +36,34 @@ class TimespinnerWorld(World): Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers. Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family. """ - - option_definitions = timespinner_options + options_dataclass = BackwardsCompatiableTimespinnerOptions + options: BackwardsCompatiableTimespinnerOptions game = "Timespinner" topology_present = True web = TimespinnerWebWorld() required_client_version = (0, 4, 2) item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {location.name: location.code for location in get_location_datas(None, None, None)} + location_name_to_id = {location.name: location.code for location in get_location_datas(-1, None, None)} item_name_groups = get_item_names_per_category() precalculated_weights: PreCalculatedWeights def generate_early(self) -> None: - self.precalculated_weights = PreCalculatedWeights(self.multiworld, self.player) + self.options.handle_backward_compatibility() + + self.precalculated_weights = PreCalculatedWeights(self.options, self.random) # in generate_early the start_inventory isnt copied over to precollected_items yet, so we can still modify the options directly - if self.multiworld.start_inventory[self.player].value.pop('Meyef', 0) > 0: - self.multiworld.StartWithMeyef[self.player].value = self.multiworld.StartWithMeyef[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Talaria Attachment', 0) > 0: - self.multiworld.QuickSeed[self.player].value = self.multiworld.QuickSeed[self.player].option_true - if self.multiworld.start_inventory[self.player].value.pop('Jewelry Box', 0) > 0: - self.multiworld.StartWithJewelryBox[self.player].value = self.multiworld.StartWithJewelryBox[self.player].option_true + if self.options.start_inventory.value.pop('Meyef', 0) > 0: + self.options.start_with_meyef.value = Toggle.option_true + if self.options.start_inventory.value.pop('Talaria Attachment', 0) > 0: + self.options.quick_seed.value = Toggle.option_true + if self.options.start_inventory.value.pop('Jewelry Box', 0) > 0: + self.options.start_with_jewelry_box.value = Toggle.option_true def create_regions(self) -> None: - create_regions_and_locations(self.multiworld, self.player, self.precalculated_weights) + create_regions_and_locations(self.multiworld, self.player, self.options, self.precalculated_weights) def create_items(self) -> None: self.create_and_assign_event_items() @@ -74,7 +77,7 @@ class TimespinnerWorld(World): def set_rules(self) -> None: final_boss: str - if self.is_option_enabled("DadPercent"): + if self.options.dad_percent: final_boss = "Killed Emperor" else: final_boss = "Killed Nightmare" @@ -82,48 +85,74 @@ class TimespinnerWorld(World): self.multiworld.completion_condition[self.player] = lambda state: state.has(final_boss, self.player) def fill_slot_data(self) -> Dict[str, object]: - slot_data: Dict[str, object] = {} - - ap_specific_settings: Set[str] = {"RisingTidesOverrides", "TrapChance"} - - for option_name in timespinner_options: - if (option_name not in ap_specific_settings): - slot_data[option_name] = self.get_option_value(option_name) - - slot_data["StinkyMaw"] = True - slot_data["ProgressiveVerticalMovement"] = False - slot_data["ProgressiveKeycards"] = False - slot_data["PersonalItems"] = self.get_personal_items() - slot_data["PyramidKeysGate"] = self.precalculated_weights.pyramid_keys_unlock - slot_data["PresentGate"] = self.precalculated_weights.present_key_unlock - slot_data["PastGate"] = self.precalculated_weights.past_key_unlock - slot_data["TimeGate"] = self.precalculated_weights.time_key_unlock - slot_data["Basement"] = int(self.precalculated_weights.flood_basement) + \ - int(self.precalculated_weights.flood_basement_high) - slot_data["Xarion"] = self.precalculated_weights.flood_xarion - slot_data["Maw"] = self.precalculated_weights.flood_maw - slot_data["PyramidShaft"] = self.precalculated_weights.flood_pyramid_shaft - slot_data["BackPyramid"] = self.precalculated_weights.flood_pyramid_back - slot_data["CastleMoat"] = self.precalculated_weights.flood_moat - slot_data["CastleCourtyard"] = self.precalculated_weights.flood_courtyard - slot_data["LakeDesolation"] = self.precalculated_weights.flood_lake_desolation - slot_data["DryLakeSerene"] = not self.precalculated_weights.flood_lake_serene - slot_data["LakeSereneBridge"] = self.precalculated_weights.flood_lake_serene_bridge - slot_data["Lab"] = self.precalculated_weights.flood_lab - - return slot_data + return { + # options + "StartWithJewelryBox": self.options.start_with_jewelry_box.value, + "DownloadableItems": self.options.downloadable_items.value, + "EyeSpy": self.options.eye_spy.value, + "StartWithMeyef": self.options.start_with_meyef.value, + "QuickSeed": self.options.quick_seed.value, + "SpecificKeycards": self.options.specific_keycards.value, + "Inverted": self.options.inverted.value, + "GyreArchives": self.options.gyre_archives.value, + "Cantoran": self.options.cantoran.value, + "LoreChecks": self.options.lore_checks.value, + "BossRando": self.options.boss_rando.value, + "DamageRando": self.options.damage_rando.value, + "DamageRandoOverrides": self.options.damage_rando_overrides.value, + "HpCap": self.options.hp_cap.value, + "LevelCap": self.options.level_cap.value, + "ExtraEarringsXP": self.options.extra_earrings_xp.value, + "BossHealing": self.options.boss_healing.value, + "ShopFill": self.options.shop_fill.value, + "ShopWarpShards": self.options.shop_warp_shards.value, + "ShopMultiplier": self.options.shop_multiplier.value, + "LootPool": self.options.loot_pool.value, + "DropRateCategory": self.options.drop_rate_category.value, + "FixedDropRate": self.options.fixed_drop_rate.value, + "LootTierDistro": self.options.loot_tier_distro.value, + "ShowBestiary": self.options.show_bestiary.value, + "ShowDrops": self.options.show_drops.value, + "EnterSandman": self.options.enter_sandman.value, + "DadPercent": self.options.dad_percent.value, + "RisingTides": self.options.rising_tides.value, + "UnchainedKeys": self.options.unchained_keys.value, + "PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value, + "Traps": self.options.traps.value, + "DeathLink": self.options.death_link.value, + "StinkyMaw": True, + # data + "PersonalItems": self.get_personal_items(), + "PyramidKeysGate": self.precalculated_weights.pyramid_keys_unlock, + "PresentGate": self.precalculated_weights.present_key_unlock, + "PastGate": self.precalculated_weights.past_key_unlock, + "TimeGate": self.precalculated_weights.time_key_unlock, + # rising tides + "Basement": int(self.precalculated_weights.flood_basement) + \ + int(self.precalculated_weights.flood_basement_high), + "Xarion": self.precalculated_weights.flood_xarion, + "Maw": self.precalculated_weights.flood_maw, + "PyramidShaft": self.precalculated_weights.flood_pyramid_shaft, + "BackPyramid": self.precalculated_weights.flood_pyramid_back, + "CastleMoat": self.precalculated_weights.flood_moat, + "CastleCourtyard": self.precalculated_weights.flood_courtyard, + "LakeDesolation": self.precalculated_weights.flood_lake_desolation, + "DryLakeSerene": not self.precalculated_weights.flood_lake_serene, + "LakeSereneBridge": self.precalculated_weights.flood_lake_serene_bridge, + "Lab": self.precalculated_weights.flood_lab + } def write_spoiler_header(self, spoiler_handle: TextIO) -> None: - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: spoiler_handle.write(f'Modern Warp Beacon unlock: {self.precalculated_weights.present_key_unlock}\n') spoiler_handle.write(f'Timeworn Warp Beacon unlock: {self.precalculated_weights.past_key_unlock}\n') - if self.is_option_enabled("EnterSandman"): + if self.options.enter_sandman: spoiler_handle.write(f'Mysterious Warp Beacon unlock: {self.precalculated_weights.time_key_unlock}\n') else: spoiler_handle.write(f'Twin Pyramid Keys unlock: {self.precalculated_weights.pyramid_keys_unlock}\n') - if self.is_option_enabled("RisingTides"): + if self.options.rising_tides: flooded_areas: List[str] = [] if self.precalculated_weights.flood_basement: @@ -159,6 +188,15 @@ class TimespinnerWorld(World): spoiler_handle.write(f'Flooded Areas: {flooded_areas_string}\n') + if self.options.has_replaced_options: + warning = \ + f"NOTICE: Timespinner options for player '{self.player_name}' where renamed from PasCalCase to snake_case, " \ + "please update your yaml" + + spoiler_handle.write("\n") + spoiler_handle.write(warning) + logging.warning(warning) + def create_item(self, name: str) -> Item: data = item_table[name] @@ -176,41 +214,41 @@ class TimespinnerWorld(World): if not item.advancement: return item - if (name == 'Tablet' or name == 'Library Keycard V') and not self.is_option_enabled("DownloadableItems"): + if (name == 'Tablet' or name == 'Library Keycard V') and not self.options.downloadable_items: item.classification = ItemClassification.filler - elif name == 'Oculus Ring' and not self.is_option_enabled("EyeSpy"): + elif name == 'Oculus Ring' and not self.options.eye_spy: item.classification = ItemClassification.filler - elif (name == 'Kobo' or name == 'Merchant Crow') and not self.is_option_enabled("GyreArchives"): + elif (name == 'Kobo' or name == 'Merchant Crow') and not self.options.gyre_archives: item.classification = ItemClassification.filler elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \ - and not self.is_option_enabled("UnchainedKeys"): + and not self.options.unchained_keys: item.classification = ItemClassification.filler return item def get_filler_item_name(self) -> str: - trap_chance: int = self.get_option_value("TrapChance") - enabled_traps: List[str] = self.get_option_value("Traps") + trap_chance: int = self.options.trap_chance.value + enabled_traps: List[str] = self.options.traps.value - if self.multiworld.random.random() < (trap_chance / 100) and enabled_traps: - return self.multiworld.random.choice(enabled_traps) + if self.random.random() < (trap_chance / 100) and enabled_traps: + return self.random.choice(enabled_traps) else: - return self.multiworld.random.choice(filler_items) + return self.random.choice(filler_items) def get_excluded_items(self) -> Set[str]: excluded_items: Set[str] = set() - if self.is_option_enabled("StartWithJewelryBox"): + if self.options.start_with_jewelry_box: excluded_items.add('Jewelry Box') - if self.is_option_enabled("StartWithMeyef"): + if self.options.start_with_meyef: excluded_items.add('Meyef') - if self.is_option_enabled("QuickSeed"): + if self.options.quick_seed: excluded_items.add('Talaria Attachment') - if self.is_option_enabled("UnchainedKeys"): + if self.options.unchained_keys: excluded_items.add('Twin Pyramid Key') - if not self.is_option_enabled("EnterSandman"): + if not self.options.enter_sandman: excluded_items.add('Mysterious Warp Beacon') else: excluded_items.add('Timeworn Warp Beacon') @@ -224,8 +262,8 @@ class TimespinnerWorld(World): return excluded_items def assign_starter_items(self, excluded_items: Set[str]) -> None: - non_local_items: Set[str] = self.multiworld.non_local_items[self.player].value - local_items: Set[str] = self.multiworld.local_items[self.player].value + non_local_items: Set[str] = self.options.non_local_items.value + local_items: Set[str] = self.options.local_items.value local_starter_melee_weapons = tuple(item for item in starter_melee_weapons if item in local_items or not item in non_local_items) @@ -247,27 +285,26 @@ class TimespinnerWorld(World): self.assign_starter_item(excluded_items, 'Tutorial: Yo Momma 2', local_starter_spells) def assign_starter_item(self, excluded_items: Set[str], location: str, item_list: Tuple[str, ...]) -> None: - item_name = self.multiworld.random.choice(item_list) + item_name = self.random.choice(item_list) self.place_locked_item(excluded_items, location, item_name) def place_first_progression_item(self, excluded_items: Set[str]) -> None: - if self.is_option_enabled("QuickSeed") or self.is_option_enabled("Inverted") \ - or self.precalculated_weights.flood_lake_desolation: + if self.options.quick_seed or self.options.inverted or self.precalculated_weights.flood_lake_desolation: return - for item in self.multiworld.precollected_items[self.player]: - if item.name in starter_progression_items and not item.name in excluded_items: + for item_name in self.options.start_inventory.value.keys(): + if item_name in starter_progression_items: return local_starter_progression_items = tuple( item for item in starter_progression_items - if item not in excluded_items and item not in self.multiworld.non_local_items[self.player].value) + if item not in excluded_items and item not in self.options.non_local_items.value) if not local_starter_progression_items: return - progression_item = self.multiworld.random.choice(local_starter_progression_items) + progression_item = self.random.choice(local_starter_progression_items) self.multiworld.local_early_items[self.player][progression_item] = 1 @@ -307,9 +344,3 @@ class TimespinnerWorld(World): personal_items[location.address] = location.item.code return personal_items - - def is_option_enabled(self, option: str) -> bool: - return is_option_enabled(self.multiworld, self.player, option) - - def get_option_value(self, option: str) -> Union[int, Dict, List]: - return get_option_value(self.multiworld, self.player, option) From 83521e99d9c78f3240f4bba1447d13f19ca34681 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 05:04:21 -0500 Subject: [PATCH 079/222] Core: migrate item links out of main (#2914) * Core: move item linking out of main * add a test that item link option correctly validates * remove unused fluff --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 79 ++++++++++++++++++++++++++++++++++++ Main.py | 77 +---------------------------------- test/general/test_options.py | 14 ++++++- 3 files changed, 93 insertions(+), 77 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1c7dad7f3b..6456210e95 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,5 +1,6 @@ from __future__ import annotations +import collections import copy import itertools import functools @@ -288,6 +289,84 @@ class MultiWorld(): group["non_local_items"] = item_link["non_local_items"] group["link_replacement"] = replacement_prio[item_link["link_replacement"]] + def link_items(self) -> None: + """Called to link together items in the itempool related to the registered item link groups.""" + for group_id, group in self.groups.items(): + def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ + Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] + ]: + classifications: Dict[str, int] = collections.defaultdict(int) + counters = {player: {name: 0 for name in shared_pool} for player in players} + for item in self.itempool: + if item.player in counters and item.name in shared_pool: + counters[item.player][item.name] += 1 + classifications[item.name] |= item.classification + + for player in players.copy(): + if all([counters[player][item] == 0 for item in shared_pool]): + players.remove(player) + del (counters[player]) + + if not players: + return None, None + + for item in shared_pool: + count = min(counters[player][item] for player in players) + if count: + for player in players: + counters[player][item] = count + else: + for player in players: + del (counters[player][item]) + return counters, classifications + + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) + if not common_item_count: + continue + + new_itempool: List[Item] = [] + for item_name, item_count in next(iter(common_item_count.values())).items(): + for _ in range(item_count): + new_item = group["world"].create_item(item_name) + # mangle together all original classification bits + new_item.classification |= classifications[item_name] + new_itempool.append(new_item) + + region = Region("Menu", group_id, self, "ItemLink") + self.regions.append(region) + locations = region.locations + for item in self.itempool: + count = common_item_count.get(item.player, {}).get(item.name, 0) + if count: + loc = Location(group_id, f"Item Link: {item.name} -> {self.player_name[item.player]} {count}", + None, region) + loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ + state.has(item_name, group_id_, count_) + + locations.append(loc) + loc.place_locked_item(item) + common_item_count[item.player][item.name] -= 1 + else: + new_itempool.append(item) + + itemcount = len(self.itempool) + self.itempool = new_itempool + + while itemcount > len(self.itempool): + items_to_add = [] + for player in group["players"]: + if group["link_replacement"]: + item_player = group_id + else: + item_player = player + if group["replacement_items"][player]: + items_to_add.append(AutoWorld.call_single(self, "create_item", item_player, + group["replacement_items"][player])) + else: + items_to_add.append(AutoWorld.call_single(self, "create_filler", item_player)) + self.random.shuffle(items_to_add) + self.itempool.extend(items_to_add[:itemcount - len(self.itempool)]) + def secure(self): self.random = ThreadBarrierProxy(secrets.SystemRandom()) self.is_race = True diff --git a/Main.py b/Main.py index 56b3a6545d..ce054dcd39 100644 --- a/Main.py +++ b/Main.py @@ -184,82 +184,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." multiworld.itempool[:] = new_items - # temporary home for item links, should be moved out of Main - for group_id, group in multiworld.groups.items(): - def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ - Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] - ]: - classifications: Dict[str, int] = collections.defaultdict(int) - counters = {player: {name: 0 for name in shared_pool} for player in players} - for item in multiworld.itempool: - if item.player in counters and item.name in shared_pool: - counters[item.player][item.name] += 1 - classifications[item.name] |= item.classification - - for player in players.copy(): - if all([counters[player][item] == 0 for item in shared_pool]): - players.remove(player) - del (counters[player]) - - if not players: - return None, None - - for item in shared_pool: - count = min(counters[player][item] for player in players) - if count: - for player in players: - counters[player][item] = count - else: - for player in players: - del (counters[player][item]) - return counters, classifications - - common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) - if not common_item_count: - continue - - new_itempool: List[Item] = [] - for item_name, item_count in next(iter(common_item_count.values())).items(): - for _ in range(item_count): - new_item = group["world"].create_item(item_name) - # mangle together all original classification bits - new_item.classification |= classifications[item_name] - new_itempool.append(new_item) - - region = Region("Menu", group_id, multiworld, "ItemLink") - multiworld.regions.append(region) - locations = region.locations - for item in multiworld.itempool: - count = common_item_count.get(item.player, {}).get(item.name, 0) - if count: - loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}", - None, region) - loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ - state.has(item_name, group_id_, count_) - - locations.append(loc) - loc.place_locked_item(item) - common_item_count[item.player][item.name] -= 1 - else: - new_itempool.append(item) - - itemcount = len(multiworld.itempool) - multiworld.itempool = new_itempool - - while itemcount > len(multiworld.itempool): - items_to_add = [] - for player in group["players"]: - if group["link_replacement"]: - item_player = group_id - else: - item_player = player - if group["replacement_items"][player]: - items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player, - group["replacement_items"][player])) - else: - items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player)) - multiworld.random.shuffle(items_to_add) - multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)]) + multiworld.link_items() if any(multiworld.item_links.values()): multiworld._all_state = None diff --git a/test/general/test_options.py b/test/general/test_options.py index 6cf642029e..2229b7ea7e 100644 --- a/test/general/test_options.py +++ b/test/general/test_options.py @@ -1,6 +1,6 @@ import unittest -from BaseClasses import PlandoOptions +from BaseClasses import MultiWorld, PlandoOptions from Options import ItemLinks from worlds.AutoWorld import AutoWorldRegister @@ -47,3 +47,15 @@ class TestOptions(unittest.TestCase): self.assertIn("Bow", link.value[0]["item_pool"]) # TODO test that the group created using these options has the items + + def test_item_links_resolve(self): + """Test item link option resolves correctly.""" + item_link_group = [{ + "name": "ItemLinkTest", + "item_pool": ["Everything"], + "link_replacement": False, + "replacement_item": None, + }] + item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} + for link in item_links.values(): + self.assertEqual(link.value[0], item_link_group[0]) From a05dbac55ffdd1e2f2192421b3fba7fca5341c5c Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 05:13:14 -0500 Subject: [PATCH 080/222] Core: Rework accessibility (#1481) * rename locations accessibility to "full" and make old locations accessibility debug only * fix a bug in oot * reorder lttp tests to not override its overrides * changed the wrong word in the dict * :forehead: * update the manual lttp yaml * use __debug__ * update pokemon and messenger * fix conflicts from 993 * fix stardew presets * add that locations may be inaccessible to description * use reST format and make the items description one line so that it renders correctly on webhost * forgot i renamed that * add aliases for back compat * some cleanup * fix imports * fix test failure * only check "items" players when the item is progression * Revert "only check "items" players when the item is progression" This reverts commit ecbf986145e6194aa99a39c481d8ecd0736d5a4c. * remove some unnecessary diffs * CV64: Add ItemsAccessibility * put items description at the bottom of the docstring since that's it's visual order * : * rename accessibility reference in pokemon rb dexsanity * make the rendered tooltips look nicer --- BaseClasses.py | 23 ++++++--------- Options.py | 29 +++++++++++++++---- test/general/test_fill.py | 4 +-- test/multiworld/test_multiworlds.py | 2 +- worlds/alttp/Options.py | 5 ++-- worlds/alttp/Rules.py | 17 ++++++----- worlds/alttp/test/inverted/TestInverted.py | 4 +-- .../TestInvertedMinor.py | 2 +- .../test/inverted_owg/TestInvertedOWG.py | 2 +- worlds/cv64/options.py | 4 ++- worlds/ffmq/Regions.py | 2 +- worlds/messenger/options.py | 8 ++--- worlds/minecraft/docs/minecraft_es.md | 2 +- worlds/minecraft/docs/minecraft_sv.md | 2 +- worlds/pokemon_rb/__init__.py | 2 +- worlds/pokemon_rb/options.py | 5 ++-- worlds/pokemon_rb/rules.py | 2 +- worlds/smz3/Options.py | 3 +- worlds/smz3/__init__.py | 2 +- worlds/stardew_valley/presets.py | 12 ++++---- 20 files changed, 75 insertions(+), 57 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 6456210e95..a0c243c0fd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -64,7 +64,6 @@ class MultiWorld(): state: CollectionState plando_options: PlandoOptions - accessibility: Dict[int, Options.Accessibility] early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]] local_items: Dict[int, Options.LocalItems] @@ -602,26 +601,22 @@ class MultiWorld(): players: Dict[str, Set[int]] = { "minimal": set(), "items": set(), - "locations": set() + "full": set() } - for player, access in self.accessibility.items(): - players[access.current_key].add(player) + for player, world in self.worlds.items(): + players[world.options.accessibility.current_key].add(player) beatable_fulfilled = False - def location_condition(location: Location): + def location_condition(location: Location) -> bool: """Determine if this location has to be accessible, location is already filtered by location_relevant""" - if location.player in players["locations"] or (location.item and location.item.player not in - players["minimal"]): - return True - return False + return location.player in players["full"] or \ + (location.item and location.item.player not in players["minimal"]) - def location_relevant(location: Location): + def location_relevant(location: Location) -> bool: """Determine if this location is relevant to sweep.""" - if location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["locations"] or location.advancement): - return True - return False + return location.progress_type != LocationProgressType.EXCLUDED \ + and (location.player in players["full"] or location.advancement) def all_done() -> bool: """Check if all access rules are fulfilled""" diff --git a/Options.py b/Options.py index b5fb25ea34..d5e0ce1a55 100644 --- a/Options.py +++ b/Options.py @@ -1144,18 +1144,35 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect class Accessibility(Choice): - """Set rules for reachability of your items/locations. + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. - - **Locations:** ensure everything can be reached and acquired. - - **Items:** ensure all logically relevant items can be acquired. - - **Minimal:** ensure what is needed to reach your goal can be acquired. + **Minimal:** ensure what is needed to reach your goal can be acquired. """ display_name = "Accessibility" rich_text_doc = True - option_locations = 0 - option_items = 1 + option_full = 0 option_minimal = 2 alias_none = 2 + alias_locations = 0 + alias_items = 0 + default = 0 + + +class ItemsAccessibility(Accessibility): + """ + Set rules for reachability of your items/locations. + + **Full:** ensure everything can be reached and acquired. + + **Minimal:** ensure what is needed to reach your goal can be acquired. + + **Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and + some locations may be inaccessible. + """ + option_items = 1 default = 1 diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 485007ff0d..db24b70691 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -174,8 +174,8 @@ class TestFillRestrictive(unittest.TestCase): player1 = generate_player_data(multiworld, 1, 3, 3) player2 = generate_player_data(multiworld, 2, 3, 3) - multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal - multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations + multiworld.worlds[player1.id].options.accessibility.value = Accessibility.option_minimal + multiworld.worlds[player2.id].options.accessibility.value = Accessibility.option_full multiworld.completion_condition[player1.id] = lambda state: True multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 677f0de829..5289cac6c3 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -69,7 +69,7 @@ class TestTwoPlayerMulti(MultiworldTestBase): for world in AutoWorldRegister.world_types.values(): self.multiworld = setup_multiworld([world, world], ()) for world in self.multiworld.worlds.values(): - world.options.accessibility.value = Accessibility.option_locations + world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) with self.subTest("filling multiworld", seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index ee3ebc587c..20dd18038a 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,8 +1,8 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ - StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed +from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \ + PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle from .EntranceShuffle import default_connections, default_dungeon_connections, \ inverted_default_connections, inverted_default_dungeon_connections from .Text import TextTable @@ -743,6 +743,7 @@ class ALttPPlandoTexts(PlandoTexts): alttp_options: typing.Dict[str, type(Option)] = { + "accessibility": ItemsAccessibility, "plando_connections": ALttPPlandoConnections, "plando_texts": ALttPPlandoTexts, "start_inventory_from_pool": StartInventoryPool, diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 67684a6f3c..f596749ae6 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -2,6 +2,7 @@ import collections import logging from typing import Iterator, Set +from Options import ItemsAccessibility from BaseClasses import Entrance, MultiWorld from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) @@ -39,7 +40,7 @@ def set_rules(world): else: # Set access rules according to max glitches for multiworld progression. # Set accessibility to none, and shuffle assuming the no logic players can always win - world.accessibility[player] = world.accessibility[player].from_text("minimal") + world.accessibility[player].value = ItemsAccessibility.option_minimal world.progression_balancing[player].value = 0 else: @@ -377,7 +378,7 @@ def global_rules(multiworld: MultiWorld, player: int): or state.has("Cane of Somaria", player))) set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) @@ -393,7 +394,7 @@ def global_rules(multiworld: MultiWorld, player: int): if state.has('Hookshot', player) else state._lttp_has_key('Small Key (Swamp Palace)', player, 4)) set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)') set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5)) if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']: @@ -423,7 +424,7 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player)) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)') set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5)) @@ -522,12 +523,12 @@ def global_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or ( location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))) - if multiworld.accessibility[player] != 'locations': + if multiworld.accessibility[player] != 'full': set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5)) set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6)) @@ -1200,7 +1201,7 @@ def set_trock_key_rules(world, player): # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player) - if world.accessibility[player] == 'locations': + if world.accessibility[player] == 'full': if world.big_key_shuffle[player] and can_reach_big_chest: # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', @@ -1214,7 +1215,7 @@ def set_trock_key_rules(world, player): location.place_locked_item(item) toss_junk_item(world, player) - if world.accessibility[player] != 'locations': + if world.accessibility[player] != 'full': set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player))) diff --git a/worlds/alttp/test/inverted/TestInverted.py b/worlds/alttp/test/inverted/TestInverted.py index 0a2aa7a186..a0a654991b 100644 --- a/worlds/alttp/test/inverted/TestInverted.py +++ b/worlds/alttp/test/inverted/TestInverted.py @@ -1,11 +1,11 @@ -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import item_factory from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py index a8fa5c808c..bf25c5c9a1 100644 --- a/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py +++ b/worlds/alttp/test/inverted_minor_glitches/TestInvertedMinor.py @@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory from worlds.alttp.Options import GlitchesRequired from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py index bbdf0f7924..1de22b95e5 100644 --- a/worlds/alttp/test/inverted_owg/TestInvertedOWG.py +++ b/worlds/alttp/test/inverted_owg/TestInvertedOWG.py @@ -6,7 +6,7 @@ from worlds.alttp.Items import item_factory from worlds.alttp.Options import GlitchesRequired from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Shops import create_shops -from test.TestBase import TestBase +from test.bases import TestBase from worlds.alttp.test import LTTPTestBase diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index 93b417ad26..07e86347bd 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from Options import OptionGroup, Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool +from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle, + StartInventoryPool) class CharacterStages(Choice): @@ -521,6 +522,7 @@ class DeathLink(Choice): @dataclass class CV64Options(PerGameCommonOptions): + accessibility: ItemsAccessibility start_inventory_from_pool: StartInventoryPool character_stages: CharacterStages stage_shuffle: StageShuffle diff --git a/worlds/ffmq/Regions.py b/worlds/ffmq/Regions.py index f7b9b9eed4..c1d3d619ff 100644 --- a/worlds/ffmq/Regions.py +++ b/worlds/ffmq/Regions.py @@ -216,7 +216,7 @@ def stage_set_rules(multiworld): multiworld.worlds[player].options.accessibility == "minimal"]) * 3): for player in no_enemies_players: for location in vendor_locations: - if multiworld.worlds[player].options.accessibility == "locations": + if multiworld.worlds[player].options.accessibility == "full": multiworld.get_location(location, player).progress_type = LocationProgressType.EXCLUDED else: multiworld.get_location(location, player).access_rule = lambda state: False diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 1f76dba489..59e694cd39 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -3,15 +3,15 @@ from typing import Dict from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ +from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \ PlandoConnections, Range, StartInventoryPool, Toggle, Visibility from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS -class MessengerAccessibility(Accessibility): - default = Accessibility.option_locations +class MessengerAccessibility(ItemsAccessibility): # defaulting to locations accessibility since items makes certain items self-locking - __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") + default = ItemsAccessibility.option_full + __doc__ = ItemsAccessibility.__doc__ class PortalPlando(PlandoConnections): diff --git a/worlds/minecraft/docs/minecraft_es.md b/worlds/minecraft/docs/minecraft_es.md index 3f2df6e7ba..4f48992122 100644 --- a/worlds/minecraft/docs/minecraft_es.md +++ b/worlds/minecraft/docs/minecraft_es.md @@ -29,7 +29,7 @@ name: TuNombre game: Minecraft # Opciones compartidas por todos los juegos: -accessibility: locations +accessibility: full progression_balancing: 50 # Opciones Especficicas para Minecraft diff --git a/worlds/minecraft/docs/minecraft_sv.md b/worlds/minecraft/docs/minecraft_sv.md index fd89d681ee..ab8c1b5d8e 100644 --- a/worlds/minecraft/docs/minecraft_sv.md +++ b/worlds/minecraft/docs/minecraft_sv.md @@ -79,7 +79,7 @@ description: Template Name # Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns. name: YourName game: Minecraft -accessibility: locations +accessibility: full progression_balancing: 0 advancement_goal: few: 0 diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 5f52703328..277d73b225 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -443,7 +443,7 @@ class PokemonRedBlueWorld(World): self.multiworld.elite_four_pokedex_condition[self.player].total = \ int((len(reachable_mons) / 100) * self.multiworld.elite_four_pokedex_condition[self.player].value) - if self.multiworld.accessibility[self.player] == "locations": + if self.multiworld.accessibility[self.player] == "full": balls = [self.create_item(ball) for ball in ["Poke Ball", "Great Ball", "Ultra Ball"]] traps = [self.create_item(trap) for trap in item_groups["Traps"]] locations = [location for location in self.multiworld.get_locations(self.player) if "Pokedex - " in diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index bd6515913a..54d486a6cf 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -1,4 +1,4 @@ -from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink +from Options import Toggle, Choice, Range, NamedRange, TextChoice, DeathLink, ItemsAccessibility class GameVersion(Choice): @@ -287,7 +287,7 @@ class AllPokemonSeen(Toggle): class DexSanity(NamedRange): """Adds location checks for Pokemon flagged "owned" on your Pokedex. You may specify a percentage of Pokemon to - have checks added. If Accessibility is set to locations, this will be the percentage of all logically reachable + have checks added. If Accessibility is set to full, this will be the percentage of all logically reachable Pokemon that will get a location check added to it. With items or minimal Accessibility, it will be the percentage of all 151 Pokemon. If Pokedex is required, the items for Pokemon acquired before acquiring the Pokedex can be found by talking to @@ -861,6 +861,7 @@ class RandomizePokemonPalettes(Choice): pokemon_rb_options = { + "accessibility": ItemsAccessibility, "game_version": GameVersion, "trainer_name": TrainerName, "rival_name": RivalName, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 21dceb75e8..1d68f31489 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -22,7 +22,7 @@ def set_rules(multiworld, player): item_rules["Celadon Prize Corner - Item Prize 2"] = prize_rule item_rules["Celadon Prize Corner - Item Prize 3"] = prize_rule - if multiworld.accessibility[player] != "locations": + if multiworld.accessibility[player] != "full": multiworld.get_location("Cerulean Bicycle Shop", player).always_allow = (lambda state, item: item.name == "Bike Voucher" and item.player == player) diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index ada463fa36..8c5efc431f 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -1,5 +1,5 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, ItemsAccessibility class SMLogic(Choice): """This option selects what kind of logic to use for item placement inside @@ -128,6 +128,7 @@ class EnergyBeep(DefaultOnToggle): smz3_options: typing.Dict[str, type(Option)] = { + "accessibility": ItemsAccessibility, "sm_logic": SMLogic, "sword_location": SwordLocation, "morph_location": MorphLocation, diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index d78c9f7d82..690e5172a2 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -244,7 +244,7 @@ class SMZ3World(World): set_rule(entrance, lambda state, region=region: region.CanEnter(state.smz3state[self.player])) for loc in region.Locations: l = self.locations[loc.Name] - if self.multiworld.accessibility[self.player] != 'locations': + if self.multiworld.accessibility[self.player] != 'full': l.always_allow = lambda state, item, loc=loc: \ item.game == "SMZ3" and \ loc.alwaysAllow(item.item, state.smz3state[self.player]) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index e663241ac6..cf6f87a150 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -58,7 +58,7 @@ all_random_settings = { easy_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "very rich", @@ -104,7 +104,7 @@ easy_settings = { medium_settings = { "progression_balancing": 25, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "rich", @@ -150,7 +150,7 @@ medium_settings = { hard_settings = { "progression_balancing": 0, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_grandpa_evaluation, FarmType.internal_name: "random", StartingMoney.internal_name: "extra", @@ -196,7 +196,7 @@ hard_settings = { nightmare_settings = { "progression_balancing": 0, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_community_center, FarmType.internal_name: "random", StartingMoney.internal_name: "vanilla", @@ -242,7 +242,7 @@ nightmare_settings = { short_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_items, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.option_bottom_of_the_mines, FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", @@ -334,7 +334,7 @@ minsanity_settings = { allsanity_settings = { "progression_balancing": ProgressionBalancing.default, - "accessibility": Accessibility.option_locations, + "accessibility": Accessibility.option_full, Goal.internal_name: Goal.default, FarmType.internal_name: "random", StartingMoney.internal_name: StartingMoney.default, From 7c8ea34a029e7e2a1683b7a318a65da5028800d3 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:32:17 -0600 Subject: [PATCH 081/222] Shivers: New features and removes two missed options using the old options API (#3287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds an option to have pot pieces placed local/non-local/anywhere Shivers nearly always finishes last in multiworld games due to the fact you need all 20 pot pieces to win and the pot pieces open very few location checks. This option allows the pieces to be placed locally. This should allow Shivers to be finished earlier. * New option: Choose how many ixupi captures are needed for goal completion New option: Choose how many ixupi captures are needed for goal completion * Fixes rule logic for location 'puzzle solved three floor elevator' Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. * Merge branch 'main' of https://github.com/GodlFire/Shivers * Revert "Merge branch 'main' of https://github.com/GodlFire/Shivers" This reverts commit bb08c3f0c2ef148fd24d7c7820cdfe936f7196e2. * Fixes issue with office elevator rule logic. * Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' Bug fix, missing logic requirement for location 'Final Riddle: Guillotine Dropped' * Moves plaque location to front for better tracker referencing. * Tiki should be Shaman. * Hanging should be Gallows. * Merrick spelling. * Clarity change. * Changes new option to use new option API Changes new option to use new option API * Added sub regions for Ixupi -Added sub regions for Ixupi and moved ixupi capture checks into the sub region. -Added missing wax capture possible spot in Shaman room * Adds option for ixupi captures to be priority locations Adds option for ixupi captures to be priority locations * Consistency Consistency * Changes ixupi captures priority to default on toggle Changes ixupi captures priority to default on toggle * Docs update -Updated link to randomizer -Update some text to reflect the latest functionality -Replaced 'setting' with 'option' * New features/bug fixes -Adds an option to have completed pots in the item pool -Moved subterranean world information plaque to maze staircase * Cleanup Cleanup * Fixed name for moved location When moving a location and renaming it I forgot to fix the name in a second spot. * Squashed commit of the following: commit 630a3bdfb9414d8c57154f29253fce0cf67b6436 Merge: 8477d3c8 5e579200 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Mon Apr 1 19:08:48 2024 -0600 Merge pull request #10 from ArchipelagoMW/main Merge main into branch commit 5e5792009cd3089ae61c5fdd208de1b79d183cb4 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 12:08:21 2024 -0500 LttP: delete playerSettings.yaml (#3062) commit 9aeeeb077a9e894cd2ace51b58d537bcf7607d5b Author: CaitSith2 Date: Mon Apr 1 06:07:56 2024 -0700 ALttP: Re-mark light/dark world regions after applying plando connections (#2964) commit 35458380e6e08eab85203942b6415fd964907c84 Author: Bryce Wilson Date: Mon Apr 1 07:07:11 2024 -0600 Pokemon Emerald: Fix wonder trade race condition (#2983) commit 4ac1866689d01dc6693866ee8b1236ad6fea114b Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon Apr 1 08:06:31 2024 -0500 ALTTP: Skull Woods Inverted fix (#2980) commit 4aa03da66e1a8c99fc31c163c1a23fb0bd772c15 Author: Fabian Dill Date: Mon Apr 1 15:06:02 2024 +0200 Factorio: fix attempting to create savegame with not filename safe characters (#2842) commit 24a03bc8b6b406c0925eedf415dcef47e17fdbaa Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon Apr 1 08:02:26 2024 -0500 KDL3: fix shuffled animals not actually being random (#3060) commit f813a7005fadb1c56bb93fee6147b63d9df2b720 Author: Aaron Wagener Date: Sun Mar 31 11:11:10 2024 -0500 The Messenger: update docs formatting and fix outdated info (#3033) * The Messenger: update docs formatting and fix outdated info * address review feedback * 120 chars commit 2a0b7e0def5c00cc2ac273b22581b3cde3b6f6a6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Sun Mar 31 09:55:55 2024 -0600 CV64: A couple of very small docs corrections. (#3057) commit 03d47e460e434b897b313c2ba452d785ecbacebe Author: Ixrec Date: Sun Mar 31 16:55:08 2024 +0100 A Short Hike: Clarify installation instructions (#3058) * Clarify installation instructions * don't mention 'config' folder since it isn't created until the game starts commit e546c0f7ff2456ddb919a1b65a437a1c61b07479 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun Mar 31 10:50:31 2024 -0500 Yoshi's Island: add patch suffix (#3061) commit 2ec93ba82a969865a8addc98feb076898978c8e3 Author: Bryce Wilson Date: Sun Mar 31 09:48:59 2024 -0600 Pokemon Emerald: Fix inconsistent location name (#3065) commit 4e3d3963941934c77573e6e0b699edf9e26cd647 Author: Aaron Wagener Date: Sun Mar 31 10:47:11 2024 -0500 The Messenger: Fix precollected notes not being removed from the itempool (#3066) * The Messenger: fix precollected notes not being properly removed from pool * The Messenger: bump required client version commit 72c53513f8bdab5506ffa972c1bf6f8573f097d7 Author: Fabian Dill Date: Sun Mar 31 03:57:59 2024 +0200 WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063) commit b7ac6a4cbd54d5f8e6672e4a6c6ea708e7e6d4de Author: Aaron Wagener Date: Fri Mar 29 20:14:53 2024 -0500 The Messenger: Fix various portal shuffle issues (#2976) * put constants in a bit more sensical order * fix accidental incorrect scoping * fix plando rules not being respected * add docstrings for the plando functions * fix the portal output pools being overwritten * use shuffle and pop instead of removing by content so plando can go to the same area twice * move portal pool rebuilding outside mapping creation * remove plando_connection cleansing since it isn't shared with transition shuffle commit 5f0112e78365d19f04e22af92d6ad1f52d264b1f Author: Zach Parks Date: Fri Mar 29 19:13:51 2024 -0500 Tracker: Add starting inventory to trackers and received items table. (#3051) commit bb481256de2a511d3b114f164061d440026be4c4 Author: Aaron Wagener Date: Thu Mar 28 21:48:40 2024 -0500 Core: Make fill failure error more human parseable (#3023) commit 301d9de9758e360ccec5399f3f9d922f1c034e45 Author: Aaron Wagener Date: Thu Mar 28 19:31:59 2024 -0500 Docs: adding games rework (#2892) * Docs: complete adding games.md rework * remove all the now unused images * review changes * address medic's review * address more comments commit 9dc708978bd00890afcd3426f829a5ac53cbe136 Author: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Thu Mar 28 18:26:58 2024 -0600 Hylics 2: Fix invalid multiworld data, use `self.random` instead of `self.multiworld.random` (#3001) * Hylics 2: Fixes * Rewrite loop commit 4391d1f4c13cdf2295481d8c51f9ef8f58bf8347 Author: Bryce Wilson Date: Thu Mar 28 18:05:39 2024 -0600 Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025) commit 5d9d4ed9f1e44309f1b53f12413ad260f1b6c983 Author: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri Mar 29 01:01:31 2024 +0100 SoE: update to pyevermizer v0.48.0 (#3050) commit c97215e0e755224593fdd00894731b59aa415e19 Author: Scipio Wright Date: Thu Mar 28 17:23:37 2024 -0400 TUNIC: Minor refactor of the vanilla_portals function (#3009) * Remove unused, change an if to an elif * Remove unused import commit eb66886a908ad75bbe71fac9bb81a0177e05e816 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu Mar 28 16:23:01 2024 -0500 SC2: Don't Filter Excluded Victory Locations (#3018) commit de860623d17d274289e3e4ab13650f2382e2e0b8 Author: Fabian Dill Date: Thu Mar 28 22:21:56 2024 +0100 Core: differentiate between unknown worlds and broken worlds in error message (#2903) commit 74b2bf51613a968eb57a5b138a7ad191324b2dd8 Author: Bryce Wilson Date: Thu Mar 28 15:20:55 2024 -0600 Pokemon Emerald: Exclude norman trainer location during norman goal (#3038) commit 74ac66b03228988d0885cff556f962a04873cc54 Author: BadMagic100 Date: Thu Mar 28 08:49:19 2024 -0700 Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 80d7ac416493a540548aad67981202a1483b5e53 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu Mar 28 09:41:32 2024 -0500 KDL3: RC1 Fixes and Enhancement (#3022) * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs commit 77311719fa0fa5b67fe92f437c3cfed16bd5136f Author: Ziktofel Date: Thu Mar 28 15:38:34 2024 +0100 SC2: Fix HERC upgrades (#3044) commit cfc1541be9e92f1f59b21f4a81f96fc88f4d9f7e Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 28 15:19:32 2024 +0100 Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 4d954afd9b2311248083fc389ac737995985be86 Author: Scipio Wright Date: Thu Mar 28 10:11:20 2024 -0400 TUNIC: Add link to AP plando guide to connection plando section of game page (#2993) commit 17748a4bf1cfd5cc11c6596a09ffc1f01434340f Author: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Thu Mar 28 10:00:10 2024 -0400 Launcher, Docs: Update UI and Set-Up Guide to Reference Options (#2950) commit 9182fe563fc18ed4ccaa8370cfed88407140398e Author: Entropynines <163603868+Entropynines@users.noreply.github.com> Date: Thu Mar 28 06:56:35 2024 -0700 README: Remove outdated information about launchers (#2966) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit bcf223081facd030aa706dc7430a72bcf2fdadc9 Author: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu Mar 28 09:54:56 2024 -0400 TLOZ: Fix markdown issue with game info page (#2985) commit fa93488f3fceac6c2f51851766543cab3ba121e6 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 28 09:46:00 2024 -0400 Docs: Consistent naming for "connection plando" (#2994) commit db15dd4bde442aad99048224bdb0d7dc28c26717 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Thu Mar 28 08:45:19 2024 -0500 A Short Hike: Fix incorrect info in docs (#3016) commit 01cdb0d761a82349afaeb7222b4b59cb1766f4a0 Author: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu Mar 28 09:44:23 2024 -0400 SMW: Update World Doc for v2.0 Features (#3034) Co-authored-by: Scipio Wright commit d0ac2b744eac438570e6a2333e76fa212be66534 Author: panicbit Date: Thu Mar 28 10:11:26 2024 +0100 LADX: fix local and non-local instrument placement (#2987) * LADX: fix local and non-local instrument placement * change confusing variable name commit 14f5f0127eb753eaf0431a54bebc82f5e74a1cb9 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:42:35 2024 -0400 Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002) * fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) * add test to ensure fishing rod need fish shop * fishing rod should be indexed from 0 like a mentally sane person would do. * fishing rod 0 isn't real, but it definitely can hurt you. * reeeeeeeee commit cf133dde7275e171d388fb466b9ed719ab7ed7c8 Author: Bryce Wilson Date: Thu Mar 28 02:32:27 2024 -0600 Pokemon Emerald: Fix typo (#3020) commit ca1812181106a3645e7f7af417590024b377b25e Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Thu Mar 28 04:27:49 2024 -0400 Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970) commit 1d4512590e0b78355e5c10174a9c6749e1098a72 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed Mar 27 21:09:09 2024 +0100 requirements.txt: _ instead of - to make PyCharm happy (#3043) commit f7b415dab00338443b68eba51f42614fc40b9152 Author: agilbert1412 Date: Tue Mar 26 19:40:58 2024 +0300 Stardew valley: Game version documentation (#2990) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit 702f006c848c05b847e85f7dbedeef68b70cdcc6 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Tue Mar 26 07:31:36 2024 -0600 CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015) commit 98ce8f8844fd0c62214a5774609382cf6a6bc829 Author: Yussur Mustafa Oraji Date: Tue Mar 26 14:29:25 2024 +0100 sm64ex: New Options API and WebHost fix (#2979) commit ea47b90367b4a220c346d8057f3aeb4207d226a1 Author: Scipio Wright Date: Tue Mar 26 09:25:41 2024 -0400 TUNIC: You can grapple down here without the ladder, neat (#3019) commit bf3856866c5ea385d0ac58014c71addfdc92637e Author: agilbert1412 Date: Sun Mar 24 23:53:49 2024 +0300 Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014) commit c0368ae0d48b4b2807c5238aeb7b14937282fc3e Author: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun Mar 24 13:53:20 2024 -0700 SC2: Fixed missing upgrade from custom tracker (#3013) commit 36c83073ad8c2ae1912d390ee3976ba0e2eb3f4a Author: Salzkorn Date: Sun Mar 24 21:52:41 2024 +0100 SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992) commit 2b24539ea5b387a3b62063c8177c373e2e3f8389 Author: Ziktofel Date: Sun Mar 24 21:52:16 2024 +0100 SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986) commit 7e904a1c78c91fb502706fe030a1f1765f734de4 Author: Ziktofel Date: Sun Mar 24 21:51:46 2024 +0100 SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978) commit bdd498db2321417374d572bff8beede083fef2b2 Author: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri Mar 22 15:36:27 2024 -0500 ALTTP: Fix #2290's crashes (#2973) commit 355223b8f0af1ee729ffa8b53eb717aa5bf283a4 Author: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Fri Mar 22 15:35:00 2024 -0500 Yoshi's Island: Implement New Game (#2141) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit aaa3472d5d8d8a7a710bd38386d9eb34046a5578 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri Mar 22 21:30:51 2024 +0100 The Witness: Fix seed bleed issue (#3008) commit 96d93c1ae313bb031e983c0d40d8be199b302df1 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Fri Mar 22 15:30:23 2024 -0500 A Short Hike: Add option to customize filler coin count (#3004) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> commit ca549df20a0a07c30ee2e1bbc2498492b919604d Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 15:29:24 2024 -0500 CommonClient: fix hint tab overlapping (#2957) Co-authored-by: Remy Jette commit 44988d430dc7d91eaeac7aad681dc024bc19ccce Author: Star Rauchenberger Date: Fri Mar 22 15:28:41 2024 -0500 Lingo: Add trap weights option (#2837) commit 11b32f17abebc08a6140506a375179f8a46bcfe6 Author: Danaël V <104455676+ReverM@users.noreply.github.com> Date: Fri Mar 22 12:46:14 2024 -0400 Docs: replacing "setting" to "option" in world docs (#2622) * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md Added non-AP World specific information * Update contributing.md Fixed broken link * Some minor touchups * Update Contributing.md Draft for version with picture * Update contributing.md Small word change * Minor updates for conciseness, mostly * Changed all instances of settings to options in info and setup guides I combed through all world docs and swapped "setting" to "option" when this was refering to yaml options. I also changed a leftover "setting" in option.py * Update contributing.md * Update contributing.md * Update setup_en.md Woops I forgot one * Update Options.py Reverted changes regarding options.py * Update worlds/noita/docs/en_Noita.md Co-authored-by: Scipio Wright * Update worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md revert change waiting for that page to be updated * Update worlds/witness/docs/setup_en.md * Update worlds/witness/docs/en_The Witness.md * Update worlds/soe/docs/multiworld_en.md Fixed Typo Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/witness/docs/en_The Witness.md * Update worlds/adventure/docs/en_Adventure.md * Update worlds/witness/docs/setup_en.md * Updated Stardew valley to hopefully get rid of the merge conflicts * Didn't work :dismay: * Delete worlds/sc2wol/docs/setup_en.md I think this will fix the merge issue * Now it should work * Woops --------- Co-authored-by: Scipio Wright Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 218cd45844f9d733618af9088941156cd79b80bc Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri Mar 22 03:02:38 2024 -0500 APProcedurePatch: fix RLE/COPY incorrect sizing (#3006) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead * Update Files.py --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 4196bde597cdbb6186ff614294fd54ff043a0c99 Author: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu Mar 21 16:38:36 2024 -0400 Docs: Fixing special_range_names example (#3005) commit 40f843f54d5970302caeb2a21b76a4845cf5c0ed Author: Star Rauchenberger Date: Thu Mar 21 11:00:53 2024 -0500 Lingo: Minor game data fixes (#3003) commit da333fbb0c88feedd4821a7bade3f56028a02111 Author: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Thu Mar 21 09:52:16 2024 -0600 Shivers: Adds missing logic rule for skull dial door location (#2997) commit 43084da23c719133fcae672e20c9b046e6ef8067 Author: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu Mar 21 16:51:29 2024 +0100 The Witness: Fix newlines in Witness option tooltips (#2971) commit 14816743fca366b52422ccb19add59d4960f17a3 Author: Scipio Wright Date: Thu Mar 21 11:50:07 2024 -0400 TUNIC: Shuffle Ladders option (#2919) commit 30a0aa2c85a7015e2072b5781ed1078965f62f4b Author: Star Rauchenberger Date: Thu Mar 21 10:46:53 2024 -0500 Lingo: Add item/location groups (#2789) commit f4b7c28a33bb163768871616023a8cf3879840b4 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed Mar 20 17:45:32 2024 -0500 APProcedurePatch: hotfix changing class variables to instance variables (#2996) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> commit 12864f7b24028fa56135e599f0fe1642c9d2d377 Author: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Wed Mar 20 22:44:09 2024 +0100 A Short Hike: Implement New Game (#2577) commit db02e9d2aabc0f4c1302ac761b3f5547ef00c7c5 Author: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Wed Mar 20 15:03:25 2024 -0600 Castlevania 64: Implement New Game (#2472) commit 32315776ac0ac1a714eb9d58688c479e2038c658 Author: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Date: Wed Mar 20 16:57:45 2024 -0400 Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967) commit e9620bea777ff1008a09c24a70bf523c94f22c29 Author: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Wed Mar 20 16:56:00 2024 -0400 SM64: Goal Logic and Hint Bugfixes (#2886) commit 183ca35bbaf6c805fdb53396d21d0cba34f9cc5e Author: qwint Date: Wed Mar 20 08:39:37 2024 -0500 CommonClient: Port Casting Bug (#2975) commit fcaaa197a19a3be03965c504ca78dd2c21ce1f84 Author: TheLX5 Date: Wed Mar 20 05:56:19 2024 -0700 SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981) commit 8f7b63a787a0ef05625ae2fad1768251aced0c87 Author: TheLX5 Date: Wed Mar 20 05:56:04 2024 -0700 SMW: Blocksanity logic fixes (#2988) commit 6f64bb98693556ac2635791381cc9651c365b324 Author: Scipio Wright Date: Wed Mar 20 08:46:31 2024 -0400 Noita: Remove newline from option description so it doesn't look bad on webhost (#2969) commit d0a9d0e2d1df641668f4f806b45f9577e69229f6 Author: Bryce Wilson Date: Wed Mar 20 06:43:13 2024 -0600 Pokemon Emerald: Bump required client version (#2963) commit 94650a02de62956eee8e7e41f61e8a41506b5842 Author: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue Mar 19 17:08:29 2024 -0500 Core: implement APProcedurePatch and APTokenMixin (#2536) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * ensure returning bytes, add token type checking * Apply suggestions from code review Co-authored-by: Doug Hoskisson * pep8 --------- Co-authored-by: beauxq Co-authored-by: Doug Hoskisson * Changes pot_completed_list to a instance variable instead of global. Changes pot_completed_list to a instance variable instead of global. The global variable was unintentional and was causing missmatch in pre_fill which would cause generation error. * Removing deprecated options getter * Adds back fix from main branch Adds back fix from main branch * Removing messenger changes that somehow got on my branch? Removing messenger changes that somehow got on my branch? * Removing messenger changes that are somehow on the Shivers branch Removing messenger changes that are somehow on the Shivers branch * Still trying to remove Messenger changes on Shivers branch Still trying to remove Messenger changes on Shivers branch * Review comments addressed. Early lobby access set as default. Review comments addressed. Early lobby access set as default. * Review comments addressed Review comments addressed * Review comments addressed. Option for priority locations removed. Option to have ixupi captures a priority has been removed and can be added again if Priority Fill is changed. See Issues #3467. * Minor Change Minor Change * Fixed ID 10 T Error Fixed ID 10 T Error * Front door option added to slot data Front door option added to slot data * Add missing .value on slot data Add missing .value on slot data * Small change to slot data Small change to slot data * Small change to slot data Why didn't this change get pushed github... * Forgot list Forgot list --------- Co-authored-by: Kory Dondzila Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/shivers/Items.py | 60 ++++++++++++++++-------- worlds/shivers/Options.py | 63 ++++++++++++++++++++++---- worlds/shivers/Rules.py | 55 ++++++++++++---------- worlds/shivers/__init__.py | 73 +++++++++++++++++++++++++----- worlds/shivers/data/locations.json | 46 +++++++++++++------ worlds/shivers/data/regions.json | 72 +++++++++++++++++++++-------- worlds/shivers/docs/en_Shivers.md | 8 ++-- worlds/shivers/docs/setup_en.md | 2 +- 8 files changed, 276 insertions(+), 103 deletions(-) diff --git a/worlds/shivers/Items.py b/worlds/shivers/Items.py index 3b403be5cb..10d234d450 100644 --- a/worlds/shivers/Items.py +++ b/worlds/shivers/Items.py @@ -33,28 +33,38 @@ item_table = { "Lightning Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 17, "pot"), "Sand Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 18, "pot"), "Metal Pot Top": ItemData(SHIVERS_ITEM_ID_OFFSET + 19, "pot"), + "Water Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "pot_type2"), + "Wax Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "pot_type2"), + "Ash Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "pot_type2"), + "Oil Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "pot_type2"), + "Cloth Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "pot_type2"), + "Wood Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "pot_type2"), + "Crystal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "pot_type2"), + "Lightning Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "pot_type2"), + "Sand Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "pot_type2"), + "Metal Pot Complete": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "pot_type2"), #Keys - "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 20, "key"), - "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 21, "key"), - "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 22, "key"), - "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 23, "key"), - "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 24, "key"), - "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 25, "key"), - "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 26, "key"), - "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 27, "key"), - "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 28, "key"), - "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 29, "key"), - "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), - "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), - "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), - "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), - "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), - "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), - "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), - "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), - "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), - "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key-optional"), + "Key for Office Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 30, "key"), + "Key for Bedroom Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 31, "key"), + "Key for Three Floor Elevator": ItemData(SHIVERS_ITEM_ID_OFFSET + 32, "key"), + "Key for Workshop": ItemData(SHIVERS_ITEM_ID_OFFSET + 33, "key"), + "Key for Office": ItemData(SHIVERS_ITEM_ID_OFFSET + 34, "key"), + "Key for Prehistoric Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 35, "key"), + "Key for Greenhouse Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 36, "key"), + "Key for Ocean Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 37, "key"), + "Key for Projector Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 38, "key"), + "Key for Generator Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 39, "key"), + "Key for Egypt Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 40, "key"), + "Key for Library Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 41, "key"), + "Key for Shaman Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 42, "key"), + "Key for UFO Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 43, "key"), + "Key for Torture Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 44, "key"), + "Key for Puzzle Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 45, "key"), + "Key for Bedroom": ItemData(SHIVERS_ITEM_ID_OFFSET + 46, "key"), + "Key for Underground Lake Room": ItemData(SHIVERS_ITEM_ID_OFFSET + 47, "key"), + "Key for Janitor Closet": ItemData(SHIVERS_ITEM_ID_OFFSET + 48, "key"), + "Key for Front Door": ItemData(SHIVERS_ITEM_ID_OFFSET + 49, "key-optional"), #Abilities "Crawling": ItemData(SHIVERS_ITEM_ID_OFFSET + 50, "ability"), @@ -83,6 +93,16 @@ item_table = { "Lightning Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 87, "potduplicate"), "Sand Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 88, "potduplicate"), "Metal Pot Top DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 89, "potduplicate"), + "Water Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 140, "potduplicate_type2"), + "Wax Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 141, "potduplicate_type2"), + "Ash Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 142, "potduplicate_type2"), + "Oil Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 143, "potduplicate_type2"), + "Cloth Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 144, "potduplicate_type2"), + "Wood Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 145, "potduplicate_type2"), + "Crystal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 146, "potduplicate_type2"), + "Lightning Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 147, "potduplicate_type2"), + "Sand Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 148, "potduplicate_type2"), + "Metal Pot Complete DUPE": ItemData(SHIVERS_ITEM_ID_OFFSET + 149, "potduplicate_type2"), #Filler "Empty": ItemData(SHIVERS_ITEM_ID_OFFSET + 90, "filler"), diff --git a/worlds/shivers/Options.py b/worlds/shivers/Options.py index b70882f9a5..2f33eb18e5 100644 --- a/worlds/shivers/Options.py +++ b/worlds/shivers/Options.py @@ -1,21 +1,37 @@ -from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions +from Options import Choice, DefaultOnToggle, Toggle, PerGameCommonOptions, Range from dataclasses import dataclass +class IxupiCapturesNeeded(Range): + """ + Number of Ixupi Captures needed for goal condition. + """ + display_name = "Number of Ixupi Captures Needed" + range_start = 1 + range_end = 10 + default = 10 + class LobbyAccess(Choice): - """Chooses how keys needed to reach the lobby are placed. + """ + Chooses how keys needed to reach the lobby are placed. - Normal: Keys are placed anywhere - Early: Keys are placed early - - Local: Keys are placed locally""" + - Local: Keys are placed locally + """ display_name = "Lobby Access" option_normal = 0 option_early = 1 option_local = 2 + default = 1 class PuzzleHintsRequired(DefaultOnToggle): - """If turned on puzzle hints will be available before the corresponding puzzle is required. For example: The Shaman - Drums puzzle will be placed after access to the security cameras which give you the solution. Turning this off - allows for greater randomization.""" + """ + If turned on puzzle hints/solutions will be available before the corresponding puzzle is required. + + For example: The Red Door puzzle will be logically required only after access to the Beth's Address Book which gives you the solution. + + Turning this off allows for greater randomization. + """ display_name = "Puzzle Hints Required" class InformationPlaques(Toggle): @@ -26,7 +42,9 @@ class InformationPlaques(Toggle): display_name = "Include Information Plaques" class FrontDoorUsable(Toggle): - """Adds a key to unlock the front door of the museum.""" + """ + Adds a key to unlock the front door of the museum. + """ display_name = "Front Door Usable" class ElevatorsStaySolved(DefaultOnToggle): @@ -37,7 +55,9 @@ class ElevatorsStaySolved(DefaultOnToggle): display_name = "Elevators Stay Solved" class EarlyBeth(DefaultOnToggle): - """Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle.""" + """ + Beth's body is open at the start of the game. This allows any pot piece to be placed in the slide and early checks on the second half of the final riddle. + """ display_name = "Early Beth" class EarlyLightning(Toggle): @@ -47,9 +67,34 @@ class EarlyLightning(Toggle): """ display_name = "Early Lightning" +class LocationPotPieces(Choice): + """ + Chooses where pot pieces will be located within the multiworld. + - Own World: Pot pieces will be located within your own world + - Different World: Pot pieces will be located in another world + - Any World: Pot pieces will be located in any world + """ + display_name = "Location of Pot Pieces" + option_own_world = 0 + option_different_world = 1 + option_any_world = 2 + +class FullPots(Choice): + """ + Chooses if pots will be in pieces or already completed + - Pieces: Only pot pieces will be added to the item pool + - Complete: Only completed pots will be added to the item pool + - Mixed: Each pot will be randomly chosen to be pieces or already completed. + """ + display_name = "Full Pots" + option_pieces = 0 + option_complete = 1 + option_mixed = 2 + @dataclass class ShiversOptions(PerGameCommonOptions): + ixupi_captures_needed: IxupiCapturesNeeded lobby_access: LobbyAccess puzzle_hints_required: PuzzleHintsRequired include_information_plaques: InformationPlaques @@ -57,3 +102,5 @@ class ShiversOptions(PerGameCommonOptions): elevators_stay_solved: ElevatorsStaySolved early_beth: EarlyBeth early_lightning: EarlyLightning + location_pot_pieces: LocationPotPieces + full_pots: FullPots diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 3dc4f51c47..5288fa2c9c 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -8,58 +8,58 @@ if TYPE_CHECKING: def water_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or (state.can_reach("Janitor Closet", "Region", player) and cloth_capturable(state, player))) \ - and state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) + return state.has_all({"Water Pot Bottom", "Water Pot Top", "Water Pot Bottom DUPE", "Water Pot Top DUPE"}, player) or \ + state.has_all({"Water Pot Complete", "Water Pot Complete DUPE"}, player) def wax_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Library", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) + return state.has_all({"Wax Pot Bottom", "Wax Pot Top", "Wax Pot Bottom DUPE", "Wax Pot Top DUPE"}, player) or \ + state.has_all({"Wax Pot Complete", "Wax Pot Complete DUPE"}, player) def ash_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Office", "Region", player) or state.can_reach("Burial", "Region", player)) \ - and state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) + return state.has_all({"Ash Pot Bottom", "Ash Pot Top", "Ash Pot Bottom DUPE", "Ash Pot Top DUPE"}, player) or \ + state.has_all({"Ash Pot Complete", "Ash Pot Complete DUPE"}, player) def oil_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Prehistoric", "Region", player) or state.can_reach("Tar River", "Region", player)) \ - and state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) + return state.has_all({"Oil Pot Bottom", "Oil Pot Top", "Oil Pot Bottom DUPE", "Oil Pot Top DUPE"}, player) or \ + state.has_all({"Oil Pot Complete", "Oil Pot Complete DUPE"}, player) def cloth_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Egypt", "Region", player) or state.can_reach("Burial", "Region", player) or state.can_reach("Janitor Closet", "Region", player)) \ - and state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) + return state.has_all({"Cloth Pot Bottom", "Cloth Pot Top", "Cloth Pot Bottom DUPE", "Cloth Pot Top DUPE"}, player) or \ + state.has_all({"Cloth Pot Complete", "Cloth Pot Complete DUPE"}, player) def wood_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Workshop", "Region", player) or state.can_reach("Blue Maze", "Region", player) or state.can_reach("Gods Room", "Region", player) or state.can_reach("Anansi", "Region", player)) \ - and state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) + return state.has_all({"Wood Pot Bottom", "Wood Pot Top", "Wood Pot Bottom DUPE", "Wood Pot Top DUPE"}, player) or \ + state.has_all({"Wood Pot Complete", "Wood Pot Complete DUPE"}, player) def crystal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Lobby", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) + return state.has_all({"Crystal Pot Bottom", "Crystal Pot Top", "Crystal Pot Bottom DUPE", "Crystal Pot Top DUPE"}, player) or \ + state.has_all({"Crystal Pot Complete", "Crystal Pot Complete DUPE"}, player) def sand_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Greenhouse", "Region", player) or state.can_reach("Ocean", "Region", player)) \ - and state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) + return state.has_all({"Sand Pot Bottom", "Sand Pot Top", "Sand Pot Bottom DUPE", "Sand Pot Top DUPE"}, player) or \ + state.has_all({"Sand Pot Complete", "Sand Pot Complete DUPE"}, player) def metal_capturable(state: CollectionState, player: int) -> bool: - return (state.can_reach("Projector Room", "Region", player) or state.can_reach("Prehistoric", "Region", player) or state.can_reach("Bedroom", "Region", player)) \ - and state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) + return state.has_all({"Metal Pot Bottom", "Metal Pot Top", "Metal Pot Bottom DUPE", "Metal Pot Top DUPE"}, player) or \ + state.has_all({"Metal Pot Complete", "Metal Pot Complete DUPE"}, player) def lightning_capturable(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable or state.multiworld.early_lightning[player].value) \ - and state.can_reach("Generator", "Region", player) \ - and state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_lightning.value) \ + and (state.has_all({"Lightning Pot Bottom", "Lightning Pot Top", "Lightning Pot Bottom DUPE", "Lightning Pot Top DUPE"}, player) or \ + state.has_all({"Lightning Pot Complete", "Lightning Pot Complete DUPE"}, player)) def beths_body_available(state: CollectionState, player: int) -> bool: - return (first_nine_ixupi_capturable(state, player) or state.multiworld.early_beth[player].value) \ + return (first_nine_ixupi_capturable(state, player) or state.multiworld.worlds[player].options.early_beth.value) \ and state.can_reach("Generator", "Region", player) @@ -123,7 +123,8 @@ def get_rules_lookup(player: int): "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), "To Slide Room": lambda state: all_skull_dials_available(state, player), - "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) + "To Lobby From Slide Room": lambda state: beths_body_available(state, player), + "To Water Capture From Janitor Closet": lambda state: cloth_capturable(state, player) }, "locations_required": { "Puzzle Solved Anansi Musicbox": lambda state: state.can_reach("Clock Tower", "Region", player), @@ -207,8 +208,10 @@ def set_rules(world: "ShiversWorld") -> None: # forbid cloth in janitor closet and oil in tar river forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Complete DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Top DUPE", player) + forbid_item(multiworld.get_location("Accessible: Storage: Tar River", player), "Oil Pot Complete DUPE", player) # Filler Item Forbids forbid_item(multiworld.get_location("Puzzle Solved Lyre", player), "Easier Lyre", player) @@ -234,4 +237,8 @@ def set_rules(world: "ShiversWorld") -> None: forbid_item(multiworld.get_location("Ixupi Captured Metal", player), "Metal Always Available in Prehistoric", player) # Set completion condition - multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) + multiworld.completion_condition[player] = lambda state: (( + water_capturable(state, player) + wax_capturable(state, player) + ash_capturable(state, player) \ + + oil_capturable(state, player) + cloth_capturable(state, player) + wood_capturable(state, player) \ + + crystal_capturable(state, player) + sand_capturable(state, player) + metal_capturable(state, player) \ + + lightning_capturable(state, player)) >= world.options.ixupi_captures_needed.value) diff --git a/worlds/shivers/__init__.py b/worlds/shivers/__init__.py index e43e91fb5a..a2d7bc1464 100644 --- a/worlds/shivers/__init__.py +++ b/worlds/shivers/__init__.py @@ -1,3 +1,4 @@ +from typing import List from .Items import item_table, ShiversItem from .Rules import set_rules from BaseClasses import Item, Tutorial, Region, Location @@ -22,7 +23,7 @@ class ShiversWorld(World): Shivers is a horror themed point and click adventure. Explore the mysteries of Windlenot's Museum of the Strange and Unusual. """ - game: str = "Shivers" + game = "Shivers" topology_present = False web = ShiversWeb() options_dataclass = ShiversOptions @@ -30,7 +31,13 @@ class ShiversWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = Constants.location_name_to_id - + shivers_item_id_offset = 27000 + pot_completed_list: List[int] + + + def generate_early(self): + self.pot_completed_list = [] + def create_item(self, name: str) -> Item: data = item_table[name] return ShiversItem(name, data.classification, data.code, self.player) @@ -78,9 +85,28 @@ class ShiversWorld(World): #Add items to item pool itempool = [] for name, data in item_table.items(): - if data.type in {"pot", "key", "ability", "filler2"}: + if data.type in {"key", "ability", "filler2"}: itempool.append(self.create_item(name)) + # Pot pieces/Completed/Mixed: + for i in range(10): + if self.options.full_pots == "pieces": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + elif self.options.full_pots == "complete": + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + else: + # Roll for if pieces or a complete pot will be used. + # Pot Pieces + if self.random.randint(0, 1) == 0: + self.pot_completed_list.append(0) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + i])) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 10 + i])) + # Completed Pot + else: + self.pot_completed_list.append(1) + itempool.append(self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 20 + i])) + #Add Filler itempool += [self.create_item("Easier Lyre") for i in range(9)] @@ -88,7 +114,6 @@ class ShiversWorld(World): filler_needed = len(self.multiworld.get_unfilled_locations(self.player)) - 24 - len(itempool) itempool += [self.random.choices([self.create_item("Heal"), self.create_item("Easier Lyre")], weights=[95, 5])[0] for i in range(filler_needed)] - #Place library escape items. Choose a location to place the escape item library_region = self.multiworld.get_region("Library", self.player) librarylocation = self.random.choice([loc for loc in library_region.locations if not loc.name.startswith("Accessible:")]) @@ -123,14 +148,14 @@ class ShiversWorld(World): self.multiworld.itempool += itempool #Lobby acess: - if self.options.lobby_access == 1: + if self.options.lobby_access == "early": if lobby_access_keys == 1: self.multiworld.early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.early_items[self.player]["Key for Office Elevator"] = 1 self.multiworld.early_items[self.player]["Key for Office"] = 1 elif lobby_access_keys == 2: self.multiworld.early_items[self.player]["Key for Front Door"] = 1 - if self.options.lobby_access == 2: + if self.options.lobby_access == "local": if lobby_access_keys == 1: self.multiworld.local_early_items[self.player]["Key for Underground Lake Room"] = 1 self.multiworld.local_early_items[self.player]["Key for Office Elevator"] = 1 @@ -138,6 +163,12 @@ class ShiversWorld(World): elif lobby_access_keys == 2: self.multiworld.local_early_items[self.player]["Key for Front Door"] = 1 + #Pot piece shuffle location: + if self.options.location_pot_pieces == "own_world": + self.options.local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + if self.options.location_pot_pieces == "different_world": + self.options.non_local_items.value |= {name for name, data in item_table.items() if data.type == "pot" or data.type == "pot_type2"} + def pre_fill(self) -> None: # Prefills event storage locations with duplicate pots storagelocs = [] @@ -149,7 +180,23 @@ class ShiversWorld(World): if loc_name.startswith("Accessible: "): storagelocs.append(self.multiworld.get_location(loc_name, self.player)) - storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + #Pot pieces/Completed/Mixed: + if self.options.full_pots == "pieces": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate'] + elif self.options.full_pots == "complete": + storageitems += [self.create_item(name) for name, data in item_table.items() if data.type == 'potduplicate_type2'] + storageitems += [self.create_item("Empty") for i in range(10)] + else: + for i in range(10): + #Pieces + if self.pot_completed_list[i] == 0: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 70 + i])] + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 80 + i])] + #Complete + else: + storageitems += [self.create_item(self.item_id_to_name[self.shivers_item_id_offset + 140 + i])] + storageitems += [self.create_item("Empty")] + storageitems += [self.create_item("Empty") for i in range(3)] state = self.multiworld.get_all_state(True) @@ -166,11 +213,13 @@ class ShiversWorld(World): def fill_slot_data(self) -> dict: return { - "storageplacements": self.storage_placements, - "excludedlocations": {str(excluded_location).replace('ExcludeLocations(', '').replace(')', '') for excluded_location in self.multiworld.exclude_locations.values()}, - "elevatorsstaysolved": {self.options.elevators_stay_solved.value}, - "earlybeth": {self.options.early_beth.value}, - "earlylightning": {self.options.early_lightning.value}, + "StoragePlacements": self.storage_placements, + "ExcludedLocations": list(self.options.exclude_locations.value), + "IxupiCapturesNeeded": self.options.ixupi_captures_needed.value, + "ElevatorsStaySolved": self.options.elevators_stay_solved.value, + "EarlyBeth": self.options.early_beth.value, + "EarlyLightning": self.options.early_lightning.value, + "FrontDoorUsable": self.options.front_door_usable.value } diff --git a/worlds/shivers/data/locations.json b/worlds/shivers/data/locations.json index 1d62f85d2d..64fe364734 100644 --- a/worlds/shivers/data/locations.json +++ b/worlds/shivers/data/locations.json @@ -81,7 +81,7 @@ "Information Plaque: (Ocean) Poseidon", "Information Plaque: (Ocean) Colossus of Rhodes", "Information Plaque: (Ocean) Poseidon's Temple", - "Information Plaque: (Underground Maze) Subterranean World", + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Information Plaque: (Underground Maze) Dero", "Information Plaque: (Egypt) Tomb of the Ixupi", "Information Plaque: (Egypt) The Sphinx", @@ -119,16 +119,6 @@ "Outside": [ "Puzzle Solved Gears", "Puzzle Solved Stone Henge", - "Ixupi Captured Water", - "Ixupi Captured Wax", - "Ixupi Captured Ash", - "Ixupi Captured Oil", - "Ixupi Captured Cloth", - "Ixupi Captured Wood", - "Ixupi Captured Crystal", - "Ixupi Captured Sand", - "Ixupi Captured Metal", - "Ixupi Captured Lightning", "Puzzle Solved Office Elevator", "Puzzle Solved Three Floor Elevator", "Puzzle Hint Found: Combo Lock in Mailbox", @@ -182,7 +172,8 @@ "Accessible: Storage: Transforming Mask" ], "Generator": [ - "Final Riddle: Beth's Body Page 17" + "Final Riddle: Beth's Body Page 17", + "Ixupi Captured Lightning" ], "Theater Back Hallways": [ "Puzzle Solved Clock Tower Door" @@ -210,6 +201,7 @@ "Information Plaque: (Ocean) Poseidon's Temple" ], "Maze Staircase": [ + "Information Plaque: (Underground Maze Staircase) Subterranean World", "Puzzle Solved Maze Door" ], "Egypt": [ @@ -305,7 +297,6 @@ ], "Tar River": [ "Accessible: Storage: Tar River", - "Information Plaque: (Underground Maze) Subterranean World", "Information Plaque: (Underground Maze) Dero" ], "Theater": [ @@ -320,6 +311,33 @@ "Skull Dial Bridge": [ "Accessible: Storage: Skull Bridge", "Puzzle Solved Skull Dial Door" + ], + "Water Capture": [ + "Ixupi Captured Water" + ], + "Wax Capture": [ + "Ixupi Captured Wax" + ], + "Ash Capture": [ + "Ixupi Captured Ash" + ], + "Oil Capture": [ + "Ixupi Captured Oil" + ], + "Cloth Capture": [ + "Ixupi Captured Cloth" + ], + "Wood Capture": [ + "Ixupi Captured Wood" + ], + "Crystal Capture": [ + "Ixupi Captured Crystal" + ], + "Sand Capture": [ + "Ixupi Captured Sand" + ], + "Metal Capture": [ + "Ixupi Captured Metal" ] } -} +} diff --git a/worlds/shivers/data/regions.json b/worlds/shivers/data/regions.json index 963d100fad..aeb5aa7373 100644 --- a/worlds/shivers/data/regions.json +++ b/worlds/shivers/data/regions.json @@ -7,35 +7,35 @@ ["Underground Lake", ["To Underground Tunnels From Underground Lake", "To Underground Blue Tunnels From Underground Lake"]], ["Underground Blue Tunnels", ["To Underground Lake From Underground Blue Tunnels", "To Office Elevator From Underground Blue Tunnels"]], ["Office Elevator", ["To Underground Blue Tunnels From Office Elevator","To Office From Office Elevator"]], - ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office"]], - ["Workshop", ["To Office From Workshop"]], + ["Office", ["To Office Elevator From Office", "To Workshop", "To Lobby From Office", "To Bedroom Elevator From Office", "To Ash Capture From Office"]], + ["Workshop", ["To Office From Workshop", "To Wood Capture From Workshop"]], ["Bedroom Elevator", ["To Office From Bedroom Elevator", "To Bedroom"]], - ["Bedroom", ["To Bedroom Elevator From Bedroom"]], - ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby"]], - ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library"]], + ["Bedroom", ["To Bedroom Elevator From Bedroom", "To Metal Capture From Bedroom"]], + ["Lobby", ["To Office From Lobby", "To Library From Lobby", "To Theater From Lobby", "To Prehistoric From Lobby", "To Egypt From Lobby", "To Tar River From Lobby", "To Outside From Lobby", "To Water Capture From Lobby", "To Crystal Capture From Lobby"]], + ["Library", ["To Lobby From Library", "To Maintenance Tunnels From Library", "To Wax Capture From Library"]], ["Maintenance Tunnels", ["To Library From Maintenance Tunnels", "To Three Floor Elevator From Maintenance Tunnels", "To Generator"]], ["Generator", ["To Maintenance Tunnels From Generator"]], ["Theater", ["To Lobby From Theater", "To Theater Back Hallways From Theater"]], ["Theater Back Hallways", ["To Theater From Theater Back Hallways", "To Clock Tower Staircase From Theater Back Hallways", "To Maintenance Tunnels From Theater Back Hallways", "To Projector Room"]], ["Clock Tower Staircase", ["To Theater Back Hallways From Clock Tower Staircase", "To Clock Tower"]], ["Clock Tower", ["To Clock Tower Staircase From Clock Tower"]], - ["Projector Room", ["To Theater Back Hallways From Projector Room"]], - ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric"]], - ["Greenhouse", ["To Prehistoric From Greenhouse"]], - ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean"]], + ["Projector Room", ["To Theater Back Hallways From Projector Room", "To Metal Capture From Projector Room"]], + ["Prehistoric", ["To Lobby From Prehistoric", "To Greenhouse", "To Ocean From Prehistoric", "To Oil Capture From Prehistoric", "To Metal Capture From Prehistoric"]], + ["Greenhouse", ["To Prehistoric From Greenhouse", "To Sand Capture From Greenhouse"]], + ["Ocean", ["To Prehistoric From Ocean", "To Maze Staircase From Ocean", "To Crystal Capture From Ocean", "To Sand Capture From Ocean"]], ["Maze Staircase", ["To Ocean From Maze Staircase", "To Maze From Maze Staircase"]], ["Maze", ["To Maze Staircase From Maze", "To Tar River"]], - ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River"]], - ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt"]], - ["Burial", ["To Egypt From Burial", "To Shaman From Burial"]], - ["Shaman", ["To Burial From Shaman", "To Gods Room"]], - ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room"]], - ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi"]], + ["Tar River", ["To Maze From Tar River", "To Lobby From Tar River", "To Oil Capture From Tar River"]], + ["Egypt", ["To Lobby From Egypt", "To Burial From Egypt", "To Blue Maze From Egypt", "To Cloth Capture From Egypt"]], + ["Burial", ["To Egypt From Burial", "To Shaman From Burial", "To Ash Capture From Burial", "To Cloth Capture From Burial"]], + ["Shaman", ["To Burial From Shaman", "To Gods Room", "To Wax Capture From Shaman"]], + ["Gods Room", ["To Shaman From Gods Room", "To Anansi From Gods Room", "To Wood Capture From Gods Room"]], + ["Anansi", ["To Gods Room From Anansi", "To Werewolf From Anansi", "To Wax Capture From Anansi", "To Wood Capture From Anansi"]], ["Werewolf", ["To Anansi From Werewolf", "To Night Staircase From Werewolf"]], ["Night Staircase", ["To Werewolf From Night Staircase", "To Janitor Closet", "To UFO"]], - ["Janitor Closet", ["To Night Staircase From Janitor Closet"]], + ["Janitor Closet", ["To Night Staircase From Janitor Closet", "To Water Capture From Janitor Closet", "To Cloth Capture From Janitor Closet"]], ["UFO", ["To Night Staircase From UFO", "To Inventions From UFO"]], - ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze"]], + ["Blue Maze", ["To Egypt From Blue Maze", "To Three Floor Elevator From Blue Maze Bottom", "To Three Floor Elevator From Blue Maze Top", "To Fortune Teller", "To Inventions From Blue Maze", "To Wood Capture From Blue Maze"]], ["Three Floor Elevator", ["To Maintenance Tunnels From Three Floor Elevator", "To Blue Maze From Three Floor Elevator"]], ["Fortune Teller", ["To Blue Maze From Fortune Teller"]], ["Inventions", ["To Blue Maze From Inventions", "To UFO From Inventions", "To Torture From Inventions"]], @@ -43,7 +43,16 @@ ["Puzzle Room Mastermind", ["To Torture", "To Puzzle Room Marbles From Puzzle Room Mastermind"]], ["Puzzle Room Marbles", ["To Puzzle Room Mastermind From Puzzle Room Marbles", "To Skull Dial Bridge From Puzzle Room Marbles"]], ["Skull Dial Bridge", ["To Puzzle Room Marbles From Skull Dial Bridge", "To Slide Room"]], - ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]] + ["Slide Room", ["To Skull Dial Bridge From Slide Room", "To Lobby From Slide Room"]], + ["Water Capture", []], + ["Wax Capture", []], + ["Ash Capture", []], + ["Oil Capture", []], + ["Cloth Capture", []], + ["Wood Capture", []], + ["Crystal Capture", []], + ["Sand Capture", []], + ["Metal Capture", []] ], "mandatory_connections": [ ["To Registry", "Registry"], @@ -140,6 +149,29 @@ ["To Puzzle Room Marbles From Skull Dial Bridge", "Puzzle Room Marbles"], ["To Skull Dial Bridge From Puzzle Room Marbles", "Skull Dial Bridge"], ["To Skull Dial Bridge From Slide Room", "Skull Dial Bridge"], - ["To Slide Room", "Slide Room"] + ["To Slide Room", "Slide Room"], + ["To Wax Capture From Library", "Wax Capture"], + ["To Wax Capture From Shaman", "Wax Capture"], + ["To Wax Capture From Anansi", "Wax Capture"], + ["To Water Capture From Lobby", "Water Capture"], + ["To Water Capture From Janitor Closet", "Water Capture"], + ["To Ash Capture From Office", "Ash Capture"], + ["To Ash Capture From Burial", "Ash Capture"], + ["To Oil Capture From Prehistoric", "Oil Capture"], + ["To Oil Capture From Tar River", "Oil Capture"], + ["To Cloth Capture From Egypt", "Cloth Capture"], + ["To Cloth Capture From Burial", "Cloth Capture"], + ["To Cloth Capture From Janitor Closet", "Cloth Capture"], + ["To Wood Capture From Workshop", "Wood Capture"], + ["To Wood Capture From Gods Room", "Wood Capture"], + ["To Wood Capture From Anansi", "Wood Capture"], + ["To Wood Capture From Blue Maze", "Wood Capture"], + ["To Crystal Capture From Lobby", "Crystal Capture"], + ["To Crystal Capture From Ocean", "Crystal Capture"], + ["To Sand Capture From Greenhouse", "Sand Capture"], + ["To Sand Capture From Ocean", "Sand Capture"], + ["To Metal Capture From Bedroom", "Metal Capture"], + ["To Metal Capture From Projector Room", "Metal Capture"], + ["To Metal Capture From Prehistoric", "Metal Capture"] ] -} \ No newline at end of file +} diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md index a92f8a6b79..2c56152a7a 100644 --- a/worlds/shivers/docs/en_Shivers.md +++ b/worlds/shivers/docs/en_Shivers.md @@ -12,8 +12,8 @@ these are randomized. Crawling has been added and is required to use any crawl s ## What is considered a location check in Shivers? -1. All puzzle solves are location checks excluding elevator puzzles. -2. All Ixupi captures are location checks excluding Lightning. +1. All puzzle solves are location checks. +2. All Ixupi captures are location checks. 3. Puzzle hints/solutions are location checks. For example, looking at the Atlantis map. 4. Optionally information plaques are location checks. @@ -23,9 +23,9 @@ If the player receives a key then the corresponding door will be unlocked. If th ## What is the victory condition? -Victory is achieved when the player captures Lightning in the generator room. +Victory is achieved when the player has captured the required number Ixupi set in their options. ## Encountered a bug? -Please contact GodlFire on Discord for bugs related to Shivers world generation.\ +Please contact GodlFire on Discord for bugs related to Shivers world generation.
Please contact GodlFire or mouse on Discord for bugs related to the Shivers Randomizer. diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md index 187382ef64..c53edcdf2b 100644 --- a/worlds/shivers/docs/setup_en.md +++ b/worlds/shivers/docs/setup_en.md @@ -5,7 +5,7 @@ - [Shivers (GOG version)](https://www.gog.com/en/game/shivers) or original disc - [ScummVM](https://www.scummvm.org/downloads/) version 2.7.0 or later -- [Shivers Randomizer](https://www.speedrun.com/shivers/resources) +- [Shivers Randomizer](https://github.com/GodlFire/Shivers-Randomizer-CSharp/releases/latest) Latest release version ## Setup ScummVM for Shivers From 91f7cf16de7d7ae15bc6a214273fb7f6f878fe0a Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:32:51 -0600 Subject: [PATCH 082/222] Bomb Rush Cyberfunk: Fix Coil quest being in glitched logic too early (#3720) * Update Rules.py * Update Rules.py --- worlds/bomb_rush_cyberfunk/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/bomb_rush_cyberfunk/Rules.py b/worlds/bomb_rush_cyberfunk/Rules.py index f59a428570..8f283ee613 100644 --- a/worlds/bomb_rush_cyberfunk/Rules.py +++ b/worlds/bomb_rush_cyberfunk/Rules.py @@ -1006,6 +1006,8 @@ def rules(brcworld): lambda state: mataan_challenge2(state, player, limit, glitched)) set_rule(multiworld.get_location("Mataan: Score challenge reward", player), lambda state: mataan_challenge3(state, player)) + set_rule(multiworld.get_location("Mataan: Coil joins the crew", player), + lambda state: mataan_deepest(state, player, limit, glitched)) if photos: set_rule(multiworld.get_location("Mataan: Trash Polo", player), lambda state: camera(state, player)) From 53bc4ffa52578d6c6c4c289cb399ed0900ec4f30 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 31 Jul 2024 10:37:52 -0500 Subject: [PATCH 083/222] Options: Always verify keys for VerifyKeys options (#3280) * Options: Always verify keys for VerifyKeys options * fix PlandoTexts * use OptionError and give a slightly better error message for which option it is * add the player name to the error * don't create an unnecessary list --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Options.py | 38 ++++++++++++++++++-------- worlds/stardew_valley/test/__init__.py | 4 +-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Options.py b/Options.py index d5e0ce1a55..d040828509 100644 --- a/Options.py +++ b/Options.py @@ -786,17 +786,22 @@ class VerifyKeys(metaclass=FreezeValidKeys): verify_location_name: bool = False value: typing.Any - @classmethod - def verify_keys(cls, data: typing.Iterable[str]) -> None: - if cls.valid_keys: - data = set(data) - dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) - extra = dataset - cls._valid_keys + def verify_keys(self) -> None: + if self.valid_keys: + data = set(self.value) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys if extra: - raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " - f"Allowed keys: {cls._valid_keys}.") + raise OptionError( + f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed keys: {self._valid_keys}." + ) def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + try: + self.verify_keys() + except OptionError as validation_error: + raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}") if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -833,7 +838,6 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin @classmethod def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict: if type(data) == dict: - cls.verify_keys(data) return cls(data) else: raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") @@ -879,7 +883,6 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -905,7 +908,6 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys): @classmethod def from_any(cls, data: typing.Any): if is_iterable_except_str(data): - cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) @@ -948,6 +950,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): self.value = [] logging.warning(f"The plando texts module is turned off, " f"so text for {player_name} will be ignored.") + else: + super().verify(world, player_name, plando_options) + + def verify_keys(self) -> None: + if self.valid_keys: + data = set(text.at for text in self) + dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data) + extra = dataset - self._valid_keys + if extra: + raise OptionError( + f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. " + f"Allowed placements: {self._valid_keys}." + ) @classmethod def from_any(cls, data: PlandoTextsFromAnyType) -> Self: @@ -971,7 +986,6 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): texts.append(text) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") - cls.verify_keys([text.at for text in texts]) return cls(texts) else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index d077432e24..7e82ea91e4 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -6,7 +6,7 @@ from argparse import Namespace from contextlib import contextmanager from typing import Dict, ClassVar, Iterable, Tuple, Optional, List, Union, Any -from BaseClasses import MultiWorld, CollectionState, get_seed, Location, Item, ItemClassification +from BaseClasses import MultiWorld, CollectionState, PlandoOptions, get_seed, Location, Item, ItemClassification from Options import VerifyKeys from test.bases import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld @@ -365,7 +365,7 @@ def setup_solo_multiworld(test_options: Optional[Dict[Union[str, StardewValleyOp if issubclass(option, VerifyKeys): # Values should already be verified, but just in case... - option.verify_keys(value.value) + value.verify(StardewValleyWorld, "Tester", PlandoOptions.bosses) setattr(args, name, {1: value}) multiworld.set_options(args) From 75b8c7891c65b16dfaa5c06abcc6b53e8299d94e Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Wed, 31 Jul 2024 11:40:45 -0400 Subject: [PATCH 084/222] Docs: Add FFMQ French Setup Guide + Minor fixes to English Guide (#3590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add docs * Fix character * Configuration Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * ajuster Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * inclure Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * doublon Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * remplissage Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * autre Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * pouvoir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * mappemonde Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * virgule Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * fournir Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * snes9x Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 3 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * options Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * lien Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * de laquelle Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Étape de génération Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * apostrophes 4 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * également Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * guillemets 2 Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * adresse Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Connect Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * seed Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> * Changer fichier yaml pour de configuration * Fix capitalization Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix capitalization 2 Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Fix typo+Add link to fr/en info page --------- Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- worlds/ffmq/__init__.py | 15 +- .../docs/en_Final Fantasy Mystic Quest.md | 3 + .../docs/fr_Final Fantasy Mystic Quest.md | 36 ++++ worlds/ffmq/docs/setup_en.md | 17 +- worlds/ffmq/docs/setup_fr.md | 178 ++++++++++++++++++ 5 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md create mode 100644 worlds/ffmq/docs/setup_fr.md diff --git a/worlds/ffmq/__init__.py b/worlds/ffmq/__init__.py index c464203dc6..3c58487265 100644 --- a/worlds/ffmq/__init__.py +++ b/worlds/ffmq/__init__.py @@ -25,14 +25,25 @@ from .Client import FFMQClient class FFMQWebWorld(WebWorld): - tutorials = [Tutorial( + setup_en = Tutorial( "Multiworld Setup Guide", "A guide to playing Final Fantasy Mystic Quest with Archipelago.", "English", "setup_en.md", "setup/en", ["Alchav"] - )] + ) + + setup_fr = Tutorial( + setup_en.tutorial_name, + setup_en.description, + "Français", + "setup_fr.md", + "setup/fr", + ["Artea"] + ) + + tutorials = [setup_en, setup_fr] class FFMQWorld(World): diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md index a652d4e5ad..4e09393073 100644 --- a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -1,5 +1,8 @@ # Final Fantasy Mystic Quest +## Game page in other languages: +* [Français](/games/Final%20Fantasy%20Mystic%20Quest/info/fr) + ## Where is the options page? The [player options page for this game](../player-options) contains all the options you need to configure and export a diff --git a/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md new file mode 100644 index 0000000000..70c2d938bf --- /dev/null +++ b/worlds/ffmq/docs/fr_Final Fantasy Mystic Quest.md @@ -0,0 +1,36 @@ +# Final Fantasy Mystic Quest + +## Page d'info dans d'autres langues : +* [English](/games/Final%20Fantasy%20Mystic%20Quest/info/en) + +## Où se situe la page d'options? + +La [page de configuration](../player-options) contient toutes les options nécessaires pour créer un fichier de configuration. + +## Qu'est-ce qui est rendu aléatoire dans ce jeu? + +Outre les objets mélangés, il y a plusieurs options pour aussi mélanger les villes et donjons, les pièces dans les donjons, les téléporteurs et les champs de bataille. +Il y a aussi plusieurs autres options afin d'ajuster la difficulté du jeu et la vitesse d'une partie. + +## Quels objets et emplacements sont mélangés? + +Les objets normalement reçus des coffres rouges, des PNJ et des champs de bataille sont mélangés. Vous pouvez aussi +inclure les objets des coffres bruns (qui contiennent normalement des consommables) dans les objets mélangés. + +## Quels objets peuvent être dans les mondes des autres joueurs? + +Tous les objets qui ont été déterminés mélangés dans les options peuvent être placés dans d'autres mondes. + +## À quoi ressemblent les objets des autres joueurs dans Final Fantasy Mystic Quest? + +Les emplacements qui étaient à l'origine des coffres (rouges ou bruns si ceux-ci sont inclus) apparaîtront comme des coffres. +Les coffres rouges seront des objets utiles ou de progression, alors que les coffres bruns seront des objets de remplissage. +Les pièges peuvent apparaître comme des coffres rouges ou bruns. +Lorsque vous ouvrirez un coffre contenant un objet d'un autre joueur, vous recevrez l'icône d'Archipelago et +la boîte de dialogue vous indiquera avoir reçu un "Archipelago Item". + + +## Lorsqu'un joueur reçoit un objet, qu'arrive-t-il? + +Une boîte de dialogue apparaîtra pour vous montrer l'objet que vous avez reçu. Vous ne pourrez pas recevoir d'objet si vous êtes +en combat, dans la mappemonde ou dans les menus (à l'exception de lorsque vous fermez le menu). diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md index 35d775f1bc..77569c93f0 100644 --- a/worlds/ffmq/docs/setup_en.md +++ b/worlds/ffmq/docs/setup_en.md @@ -17,6 +17,12 @@ The Archipelago community cannot supply you with this. ## Installation Procedures +### Linux Setup + +1. Download and install [Archipelago](). **The installer + file is located in the assets section at the bottom of the version information. You'll likely be looking for the `.AppImage`.** +2. It is recommended to use either RetroArch or BizHawk if you run on linux, as snes9x-rr isn't compatible. + ### Windows Setup 1. Download and install [Archipelago](). **The installer @@ -75,8 +81,7 @@ Manually launch the SNI Client, and run the patched ROM in your chosen software #### With an emulator -When the client launched automatically, SNI should have also automatically launched in the background. If this is its -first time launching, you may be prompted to allow it to communicate through the Windows Firewall. +If this is the first time SNI launches, you may be prompted to allow it to communicate through the Windows Firewall. ##### snes9x-rr @@ -133,10 +138,10 @@ page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platfor ### Connect to the Archipelago Server -The patch file which launched your client should have automatically connected you to the AP Server. There are a few -reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the -client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it -into the "Server" input field then press enter. +SNI serves as the interface between your emulator and the server. Since you launched it manually, you need to tell it what server to connect to. +If the server is hosted on Archipelago.gg, get the port the server hosts your game on at the top of the game room (last line before the worlds are listed). +In the SNI client, either type `/connect address` (where `address` is the address of the server, for example `/connect archipelago.gg:12345`), or type the address and port on the "Server" input field, then press `Connect`. +If the server is hosted locally, simply ask the host for the address of the server, and copy/paste it into the "Server" input field then press `Connect`. The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". diff --git a/worlds/ffmq/docs/setup_fr.md b/worlds/ffmq/docs/setup_fr.md new file mode 100644 index 0000000000..12ea41c6b3 --- /dev/null +++ b/worlds/ffmq/docs/setup_fr.md @@ -0,0 +1,178 @@ +# Final Fantasy Mystic Quest Setup Guide + +## Logiciels requis + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES + - Un émulateur capable d'éxécuter des scripts Lua + - snes9x-rr de: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html), + - RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Ou, + - Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle + compatible +- Le fichier ROM de la v1.0 ou v1.1 NA de Final Fantasy Mystic Quest obtenu légalement, sûrement nommé `Final Fantasy - Mystic Quest (U) (V1.0).sfc` ou `Final Fantasy - Mystic Quest (U) (V1.1).sfc` +La communauté d'Archipelago ne peut vous fournir avec ce fichier. + +## Procédure d'installation + +### Installation sur Linux + +1. Téléchargez et installez [Archipelago](). +** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version. Vous voulez probablement le `.AppImage`** +2. L'utilisation de RetroArch ou BizHawk est recommandé pour les utilisateurs linux, puisque snes9x-rr n'est pas compatible. + +### Installation sur Windows + +1. Téléchargez et installez [Archipelago](). +** Le fichier d'installation est situé dans la section "assets" dans le bas de la fenêtre d'information de la version.** +2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme + programme par défaut pour ouvrir vos ROMs. + 1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez. + 2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...** + 3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers `.sfc`** + 4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC** + 5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier + devrait se trouver dans le dossier que vous avez extrait à la première étape. + + +## Créer son fichier de configuration (.yaml) + +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? + +Votre fichier de configuration contient un ensemble d'options de configuration pour indiquer au générateur +comment il devrait générer votre seed. Chaque joueur d'un multiworld devra fournir son propre fichier de configuration. Cela permet +à chaque joueur d'apprécier une expérience personalisée. Les différents joueurs d'un même multiworld +pouront avoir des options de génération différentes. +Vous pouvez lire le [guide pour créer un YAML de base](/tutorial/Archipelago/setup/en) en anglais. + +### Où est-ce que j'obtiens un fichier de configuration ? + +La [page d'options sur le site](/games/Final%20Fantasy%20Mystic%20Quest/player-options) vous permet de choisir vos +options de génération et de les exporter vers un fichier de configuration. +Il vous est aussi possible de trouver le fichier de configuration modèle de Mystic Quest dans votre répertoire d'installation d'Archipelago, +dans le dossier Players/Templates. + +### Vérifier son fichier de configuration + +Si vous voulez valider votre fichier de configuration pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du +[Validateur de YAML](/mysterycheck). + +## Générer une partie pour un joueur + +1. Aller sur la page [Génération de partie](/games/Final%20Fantasy%20Mystic%20Quest/player-options), configurez vos options, + et cliquez sur le bouton "Generate Game". +2. Il vous sera alors présenté une page d'informations sur la seed +3. Cliquez sur le lien "Create New Room". +4. Vous verrez s'afficher la page du server, de laquelle vous pourrez télécharger votre fichier patch `.apmq`. +5. Rendez-vous sur le [site FFMQR](https://ffmqrando.net/Archipelago). +Sur cette page, sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File". +Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer. +6. Puisque cette partie est à un seul joueur, vous n'avez plus besoin du client Archipelago ni du serveur, sentez-vous libre de les fermer. + +## Rejoindre un MultiWorld + +### Obtenir son patch et créer sa ROM + +Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier de configuration à celui qui héberge la partie ou +s'occupe de la génération. Une fois cela fait, l'hôte vous fournira soit un lien pour télécharger votre patch, soit un +fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.apmq`. + +Allez au [site FFMQR](https://ffmqrando.net/Archipelago) et sélectionnez votre ROM Final Fantasy Mystic Quest original dans le boîte "ROM", puis votre ficher patch `.apmq` dans la boîte "Load Archipelago Config File". +Cliquez sur "Generate". Un téléchargement avec votre ROM aléatoire devrait s'amorcer. + +Ouvrez le client SNI (sur Windows ArchipelagoSNIClient.exe, sur Linux ouvrez le `.appImage` puis cliquez sur SNI Client), puis ouvrez le ROM téléchargé avec votre émulateur choisi. + +### Se connecter au client + +#### Avec un émulateur + +Quand le client se lance automatiquement, QUsb2Snes devrait également se lancer automatiquement en arrière-plan. Si +c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu +Windows. + +##### snes9x-rr + +1. Chargez votre ROM si ce n'est pas déjà fait. +2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting** +3. Cliquez alors sur **New Lua Script Window...** +4. Dans la nouvelle fenêtre, sélectionnez **Browse...** +5. Sélectionnez le fichier connecteur lua fourni avec votre client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur + est 64-bit ou 32-bit. +6. Si vous obtenez une erreur `socket.dll missing` ou une erreur similaire lorsque vous chargez le script lua, vous devez naviguer dans le dossier +contenant le script lua, puis copier le fichier `socket.dll` dans le dossier d'installation de votre emulateur snes9x. + +##### BizHawk + +1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant + ces options de menu : + `Config --> Cores --> SNES --> BSNES` + Une fois le coeur changé, vous devez redémarrer BizHawk. +2. Chargez votre ROM si ce n'est pas déjà fait. +3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console** +4. Cliquez sur le bouton pour ouvrir un nouveau script Lua, soit par le bouton avec un icône "Ouvrir un dossier", + en cliquant `Open Script...` dans le menu Script ou en appuyant sur `ctrl-O`. +5. Sélectionnez le fichier `Connector.lua` inclus avec le client + - Regardez dans le dossier Archipelago et cherchez `/SNI/lua/x64` ou `/SNI/lua/x86`, dépendemment de si votre emulateur + est 64-bit ou 32-bit. Notez que les versions les plus récentes de BizHawk ne sont que 64-bit. + +##### RetroArch 1.10.1 ou plus récent + +Vous ne devez faire ces étapes qu'une fois. À noter que RetroArch 1.9.x ne fonctionnera pas puisqu'il s'agit d'une version moins récente que 1.10.1. + +1. Entrez dans le menu principal de RetroArch. +2. Allez dans Settings --> User Interface. Activez l'option "Show Advanced Settings". +3. Allez dans Settings --> Network. Activez l'option "Network Commands", qui se trouve sous "Request Device 16". + Laissez le "Network Command Port" à sa valeur par defaut, qui devrait être 55355. + + +![Capture d'écran du menu Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png) +4. Allez dans le Menu Principal --> Online Updater --> Core Downloader. Trouvez et sélectionnez "Nintendo - SNES / SFC (bsnes-mercury + Performance)". + +Lorsque vous chargez un ROM pour Archipelago, assurez vous de toujours sélectionner le coeur **bsnes-mercury**. +Ce sont les seuls coeurs qui permettent à des outils extérieurs de lire les données du ROM. + +#### Avec une solution matérielle + +Ce guide suppose que vous avez téléchargé le bon micro-logiciel pour votre appareil. Si ce n'est pas déjà le cas, faites +le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logiciel approprié +[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée +[sur cette page](http://usb2snes.com/#supported-platforms). + +1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement. +2. Ouvrez votre appareil et chargez le ROM. + +### Se connecter au MultiServer + +Puisque vous avez lancé SNI manuellement, vous devrez probablement lui indiquer l'adresse à laquelle il doit se connecter. +Si le serveur est hébergé sur le site d'Archipelago, vous verrez l'adresse à laquelle vous connecter dans le haut de la page, dernière ligne avant la liste des mondes. +Tapez `/connect adresse` (ou le "adresse" est remplacé par l'adresse archipelago, par exemple `/connect archipelago.gg:12345`) dans la boîte de commande au bas de votre client SNI, ou encore écrivez l'adresse dans la boîte "server" dans le haut du client, puis cliquez `Connect`. +Si le serveur n'est pas hébergé sur le site d'Archipelago, demandez à l'hôte l'adresse du serveur, puis tapez `/connect adresse` (ou "adresse" est remplacé par l'adresse fourni par l'hôte) ou copiez/collez cette adresse dans le champ "Server" puis appuyez sur "Connect". + +Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status: +Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-être rafraîchir la page de +l'interface Web. + +### Jouer au jeu + +Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations +pour avoir rejoint un multiworld ! + +## Héberger un MultiWorld + +La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par +Archipelago. Le processus est relativement simple : + +1. Récupérez les fichiers de configuration (.yaml) des joueurs. +2. Créez une archive zip contenant ces fichiers de configuration. +3. Téléversez l'archive zip sur le lien ci-dessous. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Attendez un moment que la seed soit générée. +5. Lorsque la seed est générée, vous serez redirigé vers une page d'informations "Seed Info". +6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres + joueurs afin qu'ils puissent récupérer leurs patchs. +7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également + fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quelle personne voulant + observer devrait avoir accès à ce lien. +8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer. From 4620493828b73657633f4ec8d94dd91d4049c4e4 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:27:35 -0400 Subject: [PATCH 085/222] Spire: Convert options, clean up random calls, and add DeathLink (#3704) * Convert StS options * probably a bad idea * Update worlds/spire/Options.py Co-authored-by: Scipio Wright --------- Co-authored-by: Kono Tyran Co-authored-by: Scipio Wright --- worlds/spire/Options.py | 25 ++++++++++++++++++------- worlds/spire/__init__.py | 13 ++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/worlds/spire/Options.py b/worlds/spire/Options.py index 76cbc4cf37..9c94756600 100644 --- a/worlds/spire/Options.py +++ b/worlds/spire/Options.py @@ -1,5 +1,7 @@ import typing -from Options import TextChoice, Option, Range, Toggle +from dataclasses import dataclass + +from Options import TextChoice, Range, Toggle, PerGameCommonOptions class Character(TextChoice): @@ -55,9 +57,18 @@ class Downfall(Toggle): default = 0 -spire_options: typing.Dict[str, type(Option)] = { - "character": Character, - "ascension": Ascension, - "final_act": FinalAct, - "downfall": Downfall, -} +class DeathLink(Range): + """Percentage of health to lose when a death link is received.""" + display_name = "Death Link %" + range_start = 0 + range_end = 100 + default = 0 + + +@dataclass +class SpireOptions(PerGameCommonOptions): + character: Character + ascension: Ascension + final_act: FinalAct + downfall: Downfall + death_link: DeathLink diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 5b0e1e17f2..a0a6a794d8 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -3,7 +3,7 @@ import string from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial from .Items import event_item_pairs, item_pool, item_table from .Locations import location_table -from .Options import spire_options +from .Options import SpireOptions from .Regions import create_regions from .Rules import set_rules from ..AutoWorld import WebWorld, World @@ -27,7 +27,8 @@ class SpireWorld(World): immense power, and Slay the Spire! """ - option_definitions = spire_options + options_dataclass = SpireOptions + options: SpireOptions game = "Slay the Spire" topology_present = False web = SpireWeb() @@ -63,15 +64,13 @@ class SpireWorld(World): def fill_slot_data(self) -> dict: slot_data = { - 'seed': "".join(self.multiworld.per_slot_randoms[self.player].choice(string.ascii_letters) for i in range(16)) + 'seed': "".join(self.random.choice(string.ascii_letters) for i in range(16)) } - for option_name in spire_options: - option = getattr(self.multiworld, option_name)[self.player] - slot_data[option_name] = option.value + slot_data.update(self.options.as_dict("character", "ascension", "final_act", "downfall", "death_link")) return slot_data def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) + return self.random.choice(["Card Draw", "Card Draw", "Card Draw", "Relic", "Relic"]) def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): From c0ef02d6faaaae07c467e007133e1dc5408f6e64 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 4 Aug 2024 06:55:34 -0500 Subject: [PATCH 086/222] Core: fix missing import for `MultiWorld.link_items()` (#3731) --- BaseClasses.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a0c243c0fd..81601506d0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -290,6 +290,8 @@ class MultiWorld(): def link_items(self) -> None: """Called to link together items in the itempool related to the registered item link groups.""" + from worlds import AutoWorld + for group_id, group in self.groups.items(): def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]] @@ -300,15 +302,15 @@ class MultiWorld(): if item.player in counters and item.name in shared_pool: counters[item.player][item.name] += 1 classifications[item.name] |= item.classification - + for player in players.copy(): if all([counters[player][item] == 0 for item in shared_pool]): players.remove(player) del (counters[player]) - + if not players: return None, None - + for item in shared_pool: count = min(counters[player][item] for player in players) if count: @@ -318,11 +320,11 @@ class MultiWorld(): for player in players: del (counters[player][item]) return counters, classifications - + common_item_count, classifications = find_common_pool(group["players"], group["item_pool"]) if not common_item_count: continue - + new_itempool: List[Item] = [] for item_name, item_count in next(iter(common_item_count.values())).items(): for _ in range(item_count): @@ -330,7 +332,7 @@ class MultiWorld(): # mangle together all original classification bits new_item.classification |= classifications[item_name] new_itempool.append(new_item) - + region = Region("Menu", group_id, self, "ItemLink") self.regions.append(region) locations = region.locations @@ -341,16 +343,16 @@ class MultiWorld(): None, region) loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \ state.has(item_name, group_id_, count_) - + locations.append(loc) loc.place_locked_item(item) common_item_count[item.player][item.name] -= 1 else: new_itempool.append(item) - + itemcount = len(self.itempool) self.itempool = new_itempool - + while itemcount > len(self.itempool): items_to_add = [] for player in group["players"]: From 203c8f4d89d2740ed98e4f3c1358eb7208d96dfa Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:40:16 -0400 Subject: [PATCH 087/222] Pokemon R/B: Removing Floats from NamedRange #3717 --- worlds/pokemon_rb/options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/pokemon_rb/options.py b/worlds/pokemon_rb/options.py index 54d486a6cf..9f217e82e6 100644 --- a/worlds/pokemon_rb/options.py +++ b/worlds/pokemon_rb/options.py @@ -418,10 +418,10 @@ class ExpModifier(NamedRange): """Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16.""" display_name = "Exp Modifier" default = 16 - range_start = default / 4 + range_start = default // 4 range_end = 255 special_range_names = { - "half": default / 2, + "half": default // 2, "normal": default, "double": default * 2, "triple": default * 3, @@ -960,4 +960,4 @@ pokemon_rb_options = { "ice_trap_weight": IceTrapWeight, "randomize_pokemon_palettes": RandomizePokemonPalettes, "death_link": DeathLink -} \ No newline at end of file +} From 98bb8517e1d40ed6f3b4e9a06024c1470e25c014 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:00:33 -0400 Subject: [PATCH 088/222] Docs: Missed Full Accessibility mention/conversion #3734 --- worlds/generic/docs/advanced_settings_en.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/generic/docs/advanced_settings_en.md b/worlds/generic/docs/advanced_settings_en.md index 37467eeb46..2197c0708e 100644 --- a/worlds/generic/docs/advanced_settings_en.md +++ b/worlds/generic/docs/advanced_settings_en.md @@ -102,10 +102,10 @@ See the plando guide for more info on plando options. Plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en) * `accessibility` determines the level of access to the game the generation will expect you to have in order to reach - your completion goal. This supports `items`, `locations`, and `minimal` and is set to `locations` by default. - * `locations` will guarantee all locations are accessible in your world. + your completion goal. This supports `full`, `items`, and `minimal` and is set to `full` by default. + * `full` will guarantee all locations are accessible in your world. * `items` will guarantee you can acquire all logically relevant items in your world. Some items, such as keys, may - be self-locking. + be self-locking. This value only exists in and affects some worlds. * `minimal` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but may not be able to access all locations or acquire all items. A good example of this is having a big key in the big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon. From 90446ad1750034c03521884acb22a084995f4e7c Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:39:56 -0400 Subject: [PATCH 089/222] ChecksFinder: Refactor/Cleaning (#3725) * Update ChecksFinder * minor cleanup * Check for compatible name * Enable APWorld * Update setup_en.md * Update en_ChecksFinder.md * The client is getting updated instead * Qwint suggestions, ' -> ", streamline fill_slot_data * Oops, too many refactors --------- Co-authored-by: SunCat --- setup.py | 1 - worlds/checksfinder/Items.py | 19 ++--- worlds/checksfinder/Locations.py | 44 ++---------- worlds/checksfinder/Options.py | 6 -- worlds/checksfinder/Rules.py | 52 +++++--------- worlds/checksfinder/__init__.py | 78 ++++++++------------- worlds/checksfinder/docs/en_ChecksFinder.md | 5 -- worlds/checksfinder/docs/setup_en.md | 34 +++------ 8 files changed, 69 insertions(+), 170 deletions(-) delete mode 100644 worlds/checksfinder/Options.py diff --git a/setup.py b/setup.py index cb4d1a7511..0c9ee2c293 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ non_apworlds: set = { "Adventure", "ArchipIDLE", "Archipelago", - "ChecksFinder", "Clique", "Final Fantasy", "Lufia II Ancient Cave", diff --git a/worlds/checksfinder/Items.py b/worlds/checksfinder/Items.py index 2e86267396..5f9be79598 100644 --- a/worlds/checksfinder/Items.py +++ b/worlds/checksfinder/Items.py @@ -3,8 +3,8 @@ import typing class ItemData(typing.NamedTuple): - code: typing.Optional[int] - progression: bool + code: int + progression: bool = True class ChecksFinderItem(Item): @@ -12,16 +12,9 @@ class ChecksFinderItem(Item): item_table = { - "Map Width": ItemData(80000, True), - "Map Height": ItemData(80001, True), - "Map Bombs": ItemData(80002, True), + "Map Width": ItemData(80000), + "Map Height": ItemData(80001), + "Map Bombs": ItemData(80002), } -required_items = { -} - -item_frequencies = { - -} - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items()} diff --git a/worlds/checksfinder/Locations.py b/worlds/checksfinder/Locations.py index 59a96c83ea..aefdc38381 100644 --- a/worlds/checksfinder/Locations.py +++ b/worlds/checksfinder/Locations.py @@ -3,46 +3,14 @@ import typing class AdvData(typing.NamedTuple): - id: typing.Optional[int] - region: str + id: int + region: str = "Board" -class ChecksFinderAdvancement(Location): +class ChecksFinderLocation(Location): game: str = "ChecksFinder" -advancement_table = { - "Tile 1": AdvData(81000, 'Board'), - "Tile 2": AdvData(81001, 'Board'), - "Tile 3": AdvData(81002, 'Board'), - "Tile 4": AdvData(81003, 'Board'), - "Tile 5": AdvData(81004, 'Board'), - "Tile 6": AdvData(81005, 'Board'), - "Tile 7": AdvData(81006, 'Board'), - "Tile 8": AdvData(81007, 'Board'), - "Tile 9": AdvData(81008, 'Board'), - "Tile 10": AdvData(81009, 'Board'), - "Tile 11": AdvData(81010, 'Board'), - "Tile 12": AdvData(81011, 'Board'), - "Tile 13": AdvData(81012, 'Board'), - "Tile 14": AdvData(81013, 'Board'), - "Tile 15": AdvData(81014, 'Board'), - "Tile 16": AdvData(81015, 'Board'), - "Tile 17": AdvData(81016, 'Board'), - "Tile 18": AdvData(81017, 'Board'), - "Tile 19": AdvData(81018, 'Board'), - "Tile 20": AdvData(81019, 'Board'), - "Tile 21": AdvData(81020, 'Board'), - "Tile 22": AdvData(81021, 'Board'), - "Tile 23": AdvData(81022, 'Board'), - "Tile 24": AdvData(81023, 'Board'), - "Tile 25": AdvData(81024, 'Board'), -} - -exclusion_table = { -} - -events_table = { -} - -lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items() if data.id} \ No newline at end of file +base_id = 81000 +advancement_table = {f"Tile {i+1}": AdvData(base_id+i) for i in range(25)} +lookup_id_to_name: typing.Dict[int, str] = {data.id: item_name for item_name, data in advancement_table.items()} diff --git a/worlds/checksfinder/Options.py b/worlds/checksfinder/Options.py deleted file mode 100644 index a670109362..0000000000 --- a/worlds/checksfinder/Options.py +++ /dev/null @@ -1,6 +0,0 @@ -import typing -from Options import Option - - -checksfinder_options: typing.Dict[str, type(Option)] = { -} diff --git a/worlds/checksfinder/Rules.py b/worlds/checksfinder/Rules.py index 38d7d77ad3..8e8809be5c 100644 --- a/worlds/checksfinder/Rules.py +++ b/worlds/checksfinder/Rules.py @@ -1,44 +1,24 @@ -from ..generic.Rules import set_rule -from BaseClasses import MultiWorld, CollectionState +from worlds.generic.Rules import set_rule +from BaseClasses import MultiWorld -def _has_total(state: CollectionState, player: int, total: int): - return (state.count('Map Width', player) + state.count('Map Height', player) + - state.count('Map Bombs', player)) >= total +items = ["Map Width", "Map Height", "Map Bombs"] # Sets rules on entrances and advancements that are always applied -def set_rules(world: MultiWorld, player: int): - set_rule(world.get_location("Tile 6", player), lambda state: _has_total(state, player, 1)) - set_rule(world.get_location("Tile 7", player), lambda state: _has_total(state, player, 2)) - set_rule(world.get_location("Tile 8", player), lambda state: _has_total(state, player, 3)) - set_rule(world.get_location("Tile 9", player), lambda state: _has_total(state, player, 4)) - set_rule(world.get_location("Tile 10", player), lambda state: _has_total(state, player, 5)) - set_rule(world.get_location("Tile 11", player), lambda state: _has_total(state, player, 6)) - set_rule(world.get_location("Tile 12", player), lambda state: _has_total(state, player, 7)) - set_rule(world.get_location("Tile 13", player), lambda state: _has_total(state, player, 8)) - set_rule(world.get_location("Tile 14", player), lambda state: _has_total(state, player, 9)) - set_rule(world.get_location("Tile 15", player), lambda state: _has_total(state, player, 10)) - set_rule(world.get_location("Tile 16", player), lambda state: _has_total(state, player, 11)) - set_rule(world.get_location("Tile 17", player), lambda state: _has_total(state, player, 12)) - set_rule(world.get_location("Tile 18", player), lambda state: _has_total(state, player, 13)) - set_rule(world.get_location("Tile 19", player), lambda state: _has_total(state, player, 14)) - set_rule(world.get_location("Tile 20", player), lambda state: _has_total(state, player, 15)) - set_rule(world.get_location("Tile 21", player), lambda state: _has_total(state, player, 16)) - set_rule(world.get_location("Tile 22", player), lambda state: _has_total(state, player, 17)) - set_rule(world.get_location("Tile 23", player), lambda state: _has_total(state, player, 18)) - set_rule(world.get_location("Tile 24", player), lambda state: _has_total(state, player, 19)) - set_rule(world.get_location("Tile 25", player), lambda state: _has_total(state, player, 20)) +def set_rules(multiworld: MultiWorld, player: int): + for i in range(20): + set_rule(multiworld.get_location(f"Tile {i+6}", player), lambda state, i=i: state.has_from_list(items, player, i+1)) # Sets rules on completion condition -def set_completion_rules(world: MultiWorld, player: int): - - width_req = 10-5 - height_req = 10-5 - bomb_req = 20-5 - completion_requirements = lambda state: \ - state.has("Map Width", player, width_req) and \ - state.has("Map Height", player, height_req) and \ - state.has("Map Bombs", player, bomb_req) - world.completion_condition[player] = lambda state: completion_requirements(state) +def set_completion_rules(multiworld: MultiWorld, player: int): + width_req = 5 # 10 - 5 + height_req = 5 # 10 - 5 + bomb_req = 15 # 20 - 5 + multiworld.completion_condition[player] = lambda state: state.has_all_counts( + { + "Map Width": width_req, + "Map Height": height_req, + "Map Bombs": bomb_req, + }, player) diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index c8b9587f85..e064a1c419 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -1,9 +1,9 @@ -from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification -from .Items import ChecksFinderItem, item_table, required_items -from .Locations import ChecksFinderAdvancement, advancement_table, exclusion_table -from .Options import checksfinder_options +from BaseClasses import Region, Entrance, Tutorial, ItemClassification +from .Items import ChecksFinderItem, item_table +from .Locations import ChecksFinderLocation, advancement_table +from Options import PerGameCommonOptions from .Rules import set_rules, set_completion_rules -from ..AutoWorld import World, WebWorld +from worlds.AutoWorld import World, WebWorld client_version = 7 @@ -25,38 +25,34 @@ class ChecksFinderWorld(World): ChecksFinder is a game where you avoid mines and find checks inside the board with the mines! You win when you get all your items and beat the board! """ - game: str = "ChecksFinder" - option_definitions = checksfinder_options - topology_present = True + game = "ChecksFinder" + options_dataclass = PerGameCommonOptions web = ChecksFinderWeb() item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.id for name, data in advancement_table.items()} - def _get_checksfinder_data(self): - return { - 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), - 'seed_name': self.multiworld.seed_name, - 'player_name': self.multiworld.get_player_name(self.player), - 'player_id': self.player, - 'client_version': client_version, - 'race': self.multiworld.is_race, - } + def create_regions(self): + menu = Region("Menu", self.player, self.multiworld) + board = Region("Board", self.player, self.multiworld) + board.locations += [ChecksFinderLocation(self.player, loc_name, loc_data.id, board) + for loc_name, loc_data in advancement_table.items()] + + connection = Entrance(self.player, "New Board", menu) + menu.exits.append(connection) + connection.connect(board) + self.multiworld.regions += [menu, board] def create_items(self): - # Generate item pool itempool = [] - # Add all required progression items - for (name, num) in required_items.items(): - itempool += [name] * num # Add the map width and height stuff - itempool += ["Map Width"] * (10-5) - itempool += ["Map Height"] * (10-5) + itempool += ["Map Width"] * 5 # 10 - 5 + itempool += ["Map Height"] * 5 # 10 - 5 # Add the map bombs - itempool += ["Map Bombs"] * (20-5) + itempool += ["Map Bombs"] * 15 # 20 - 5 # Convert itempool into real items - itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + itempool = [self.create_item(item) for item in itempool] self.multiworld.itempool += itempool @@ -64,28 +60,16 @@ class ChecksFinderWorld(World): set_rules(self.multiworld, self.player) set_completion_rules(self.multiworld, self.player) - def create_regions(self): - menu = Region("Menu", self.player, self.multiworld) - board = Region("Board", self.player, self.multiworld) - board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) - for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] - - connection = Entrance(self.player, "New Board", menu) - menu.exits.append(connection) - connection.connect(board) - self.multiworld.regions += [menu, board] - def fill_slot_data(self): - slot_data = self._get_checksfinder_data() - for option_name in checksfinder_options: - option = getattr(self.multiworld, option_name)[self.player] - if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: - slot_data[option_name] = int(option.value) - return slot_data + return { + "world_seed": self.random.getrandbits(32), + "seed_name": self.multiworld.seed_name, + "player_name": self.player_name, + "player_id": self.player, + "client_version": client_version, + "race": self.multiworld.is_race, + } - def create_item(self, name: str) -> Item: + def create_item(self, name: str) -> ChecksFinderItem: item_data = item_table[name] - item = ChecksFinderItem(name, - ItemClassification.progression if item_data.progression else ItemClassification.filler, - item_data.code, self.player) - return item + return ChecksFinderItem(name, ItemClassification.progression, item_data.code, self.player) diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index c9569376c5..cb33ab3959 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -24,8 +24,3 @@ next to an icon, the number is how many you have gotten and the icon represents Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. -## Unique Local Commands - -The following command is only available when using the ChecksFinderClient to play with Archipelago. - -- `/resync` Manually trigger a resync. diff --git a/worlds/checksfinder/docs/setup_en.md b/worlds/checksfinder/docs/setup_en.md index 673b34900a..e15763ab31 100644 --- a/worlds/checksfinder/docs/setup_en.md +++ b/worlds/checksfinder/docs/setup_en.md @@ -4,7 +4,6 @@ - ChecksFinder from the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version) -- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) ## Configuring your YAML file @@ -17,28 +16,15 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options) -### Generating a ChecksFinder game +## Joining a MultiWorld Game -**ChecksFinder is meant to be played _alongside_ another game! You may not be playing it for long periods of time if -you play it by itself with another person!** - -When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done, -the host will provide you with either a link to download your data file, or with a zip file containing everyone's data -files. You do not have a file inside that zip though! - -You need to start ChecksFinder client yourself, it is located within the Archipelago folder. - -### Connect to the MultiServer - -First start ChecksFinder. - -Once both ChecksFinder and the client are started. In the client at the top type in the spot labeled `Server` type the -`Ip Address` and `Port` separated with a `:` symbol. - -The client will then ask for the username you chose, input that in the text box at the bottom of the client. - -### Play the game - -When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a -multiworld game! +1. Start ChecksFinder +2. Enter the following information: + - Enter the server url (starting from `wss://` for https connection like archipelago.gg, and starting from `ws://` for http connection and local multiserver) + - Enter server port + - Enter the name of the slot you wish to connect to + - Enter the room password (optional) + - Press `Play Online` to connect +3. Start playing! +Game options and controls are described in the readme on the github repository for the game From 8ddb49f0710244945bc9b378368f691e0810fc15 Mon Sep 17 00:00:00 2001 From: digiholic Date: Tue, 6 Aug 2024 15:13:11 -0600 Subject: [PATCH 090/222] OSRS: Implement New Game (#1976) * MMBN3: Press program now has proper color index when received remotely * Initial commit of OSRS untangled from MMBN3 branch * Fixes some broken region connections * Removes some locations * Rearranges locations to fill in slots left by removed locations * Adds starting area rando * Moves Oak and Willow trees to resource regions * Fixes various PEP8 violations * Refactor of regions * Fixes variable capture issue with region rules * Partial completion of brutal grind logic * Finishes can_reach_skill function * Adds skill requirements to location rules, fixes regions rules * Adds documentation for OSRS * Removes match statement * Updates Data Version to test mode to prevent item name caching * Fixes starting spawn logic for east varrock * Fixes river lum crossing logic to not assume you can phase across water * Prevents equipping items when you haven't unlocked them * Changes canoe logic to not require huge levels * Skeletoning out some data I'll need for variable task system * Adds csvs and parser for logic * Adds Items parsing * Fixes the spawning logic to not default to Chunksanity when you didn't pick it * Begins adding generation rules for data-driven logic * Moves region handling and location creating to different methods * Adds logic limits to Options * Begun the location generation has * Randomly generates tasks for each skill until populated * Mopping up improper names, adding custom logic, and fixes location rolling * Drastically cleans up the location rolling loop * Modifies generation to properly use local variables and pass unit tests * Game is now generating, but rules don't seem to work * Lambda capture, my old nemesis. We meet again * Fixes issue with Corsair Cove item requirement causing logic loop * Okay one more fix, another variable capture * On second thought lets not have skull sceptre tasks. 'Tis a silly place * Removes QP from item pool (they're events not items) * Removes Stronghold floor tasks, no varbit to track them * Loads CSV with pkutil so it can be used in apworld * Fixes logic of skill tasks and adds QP requirements to long grinds * Fixes pathing in pkgutil call * Better handling for empty task categories, no longer throws errors * Fixes order for progressive tasks, removes un-checkable spider task * Fixes logic issues related to stew and the Blurite caves * Fixes issues generating causing tests to sporadically fail * Adds missing task that caused off-by-one error * Updates to new Options API * Updates generation to function properly with the Universal Tracker (Thanks Faris) * Replaces runtime CSV parsing with pre-made python files generated from CSVs * Switches to self.random and uses random.choice instead of doing it manually * Fixes to typing, variable names, iterators, and continue conditions * Replaces Name classes with Enums * Fixes parse error on region special rules * Skill requirements check now returns an accessrule instead of being one that checks options * Updates documentation and setup guide * Adjusts maximum numbers for combat and general tasks * Fixes region names so dictionary lookup works for chunksanity * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Update worlds/osrs/docs/en_Old School Runescape.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Updates readme.md and codeowners doc * Removes erroneous East Varrock -> Al Kharid connection * Changes to canoe logic to account for woodcutting level options * Fixes embarassing typo on 'Edgeville' * Moves Logic CSVs to separate repository, addresses suggested changes on PR * Fixes logic error in east/west lumbridge regions. Fixes incorrect List typing in main * Removes task types with weight 0 from the list of rollable tasks * Missed another place that the task type had to be removed if 0 weight * Prevents adding an empty task weight if levels are too restrictive for tasks to be added * Removes giant blank space in error message * Adds player name to error for not having enough available tasks --------- Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/osrs/Items.py | 85 +++ worlds/osrs/Locations.py | 21 + worlds/osrs/LogicCSV/LogicCSVToPython.py | 144 +++++ worlds/osrs/LogicCSV/items_generated.py | 43 ++ worlds/osrs/LogicCSV/locations_generated.py | 127 ++++ worlds/osrs/LogicCSV/regions_generated.py | 47 ++ worlds/osrs/LogicCSV/resources_generated.py | 54 ++ worlds/osrs/Names.py | 212 +++++++ worlds/osrs/Options.py | 474 ++++++++++++++ worlds/osrs/Regions.py | 12 + worlds/osrs/__init__.py | 657 ++++++++++++++++++++ worlds/osrs/docs/en_Old School Runescape.md | 114 ++++ worlds/osrs/docs/setup_en.md | 58 ++ 15 files changed, 2052 insertions(+) create mode 100644 worlds/osrs/Items.py create mode 100644 worlds/osrs/Locations.py create mode 100644 worlds/osrs/LogicCSV/LogicCSVToPython.py create mode 100644 worlds/osrs/LogicCSV/items_generated.py create mode 100644 worlds/osrs/LogicCSV/locations_generated.py create mode 100644 worlds/osrs/LogicCSV/regions_generated.py create mode 100644 worlds/osrs/LogicCSV/resources_generated.py create mode 100644 worlds/osrs/Names.py create mode 100644 worlds/osrs/Options.py create mode 100644 worlds/osrs/Regions.py create mode 100644 worlds/osrs/__init__.py create mode 100644 worlds/osrs/docs/en_Old School Runescape.md create mode 100644 worlds/osrs/docs/setup_en.md diff --git a/README.md b/README.md index cebd4f7e75..5b66e3db87 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Currently, the following games are supported: * Aquaria * Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006 * A Hat in Time +* Old School Runescape For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index ab841e65ee..4f012c306b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -115,6 +115,9 @@ # Ocarina of Time /worlds/oot/ @espeon65536 +# Old School Runescape +/worlds/osrs @digiholic + # Overcooked! 2 /worlds/overcooked2/ @toasterparty diff --git a/worlds/osrs/Items.py b/worlds/osrs/Items.py new file mode 100644 index 0000000000..0679c964e7 --- /dev/null +++ b/worlds/osrs/Items.py @@ -0,0 +1,85 @@ +import typing + +from BaseClasses import Item, ItemClassification +from .Names import ItemNames + + +class ItemRow(typing.NamedTuple): + name: str + amount: int + progression: ItemClassification + + +class OSRSItem(Item): + game: str = "Old School Runescape" + + +QP_Items: typing.List[str] = [ + ItemNames.QP_Cooks_Assistant, + ItemNames.QP_Demon_Slayer, + ItemNames.QP_Restless_Ghost, + ItemNames.QP_Romeo_Juliet, + ItemNames.QP_Sheep_Shearer, + ItemNames.QP_Shield_of_Arrav, + ItemNames.QP_Ernest_the_Chicken, + ItemNames.QP_Vampyre_Slayer, + ItemNames.QP_Imp_Catcher, + ItemNames.QP_Prince_Ali_Rescue, + ItemNames.QP_Dorics_Quest, + ItemNames.QP_Black_Knights_Fortress, + ItemNames.QP_Witchs_Potion, + ItemNames.QP_Knights_Sword, + ItemNames.QP_Goblin_Diplomacy, + ItemNames.QP_Pirates_Treasure, + ItemNames.QP_Rune_Mysteries, + ItemNames.QP_Misthalin_Mystery, + ItemNames.QP_Corsair_Curse, + ItemNames.QP_X_Marks_the_Spot, + ItemNames.QP_Below_Ice_Mountain +] + +starting_area_dict: typing.Dict[int, str] = { + 0: ItemNames.Lumbridge, + 1: ItemNames.Al_Kharid, + 2: ItemNames.Central_Varrock, + 3: ItemNames.West_Varrock, + 4: ItemNames.Edgeville, + 5: ItemNames.Falador, + 6: ItemNames.Draynor_Village, + 7: ItemNames.Wilderness, +} + +chunksanity_starting_chunks: typing.List[str] = [ + ItemNames.Lumbridge, + ItemNames.Lumbridge_Swamp, + ItemNames.Lumbridge_Farms, + ItemNames.HAM_Hideout, + ItemNames.Draynor_Village, + ItemNames.Draynor_Manor, + ItemNames.Wizards_Tower, + ItemNames.Al_Kharid, + ItemNames.Citharede_Abbey, + ItemNames.South_Of_Varrock, + ItemNames.Central_Varrock, + ItemNames.Varrock_Palace, + ItemNames.East_Of_Varrock, + ItemNames.West_Varrock, + ItemNames.Edgeville, + ItemNames.Barbarian_Village, + ItemNames.Monastery, + ItemNames.Ice_Mountain, + ItemNames.Dwarven_Mines, + ItemNames.Falador, + ItemNames.Falador_Farm, + ItemNames.Crafting_Guild, + ItemNames.Rimmington, + ItemNames.Port_Sarim, + ItemNames.Mudskipper_Point, + ItemNames.Wilderness +] + +# Some starting areas contain multiple regions, so if that area is rolled for Chunksanity, we need to map it to one +chunksanity_special_region_names: typing.Dict[str, str] = { + ItemNames.Lumbridge_Farms: 'Lumbridge Farms East', + ItemNames.Crafting_Guild: 'Crafting Guild Outskirts', +} diff --git a/worlds/osrs/Locations.py b/worlds/osrs/Locations.py new file mode 100644 index 0000000000..b5827d60f2 --- /dev/null +++ b/worlds/osrs/Locations.py @@ -0,0 +1,21 @@ +import typing + +from BaseClasses import Location + + +class SkillRequirement(typing.NamedTuple): + skill: str + level: int + + +class LocationRow(typing.NamedTuple): + name: str + category: str + regions: typing.List[str] + skills: typing.List[SkillRequirement] + items: typing.List[str] + qp: int + + +class OSRSLocation(Location): + game: str = "Old School Runescape" diff --git a/worlds/osrs/LogicCSV/LogicCSVToPython.py b/worlds/osrs/LogicCSV/LogicCSVToPython.py new file mode 100644 index 0000000000..ed8bd8172a --- /dev/null +++ b/worlds/osrs/LogicCSV/LogicCSVToPython.py @@ -0,0 +1,144 @@ +""" +This is a utility file that converts logic in the form of CSV files into Python files that can be imported and used +directly by the world implementation. Whenever the logic files are updated, this script should be run to re-generate +the python files containing the data. +""" +import requests + +# The CSVs are updated at this repository to be shared between generator and client. +data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/" +# The Github tag of the CSVs this was generated with +data_csv_tag = "v1.5" + +if __name__ == "__main__": + import sys + import os + import csv + import typing + + # makes this module runnable from its world folder. Shamelessly stolen from Subnautica + sys.path.remove(os.path.dirname(__file__)) + 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) + + + def load_location_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile: + locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n") + locPyFile.write("\n") + locPyFile.write("location_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req: + locations_reader = csv.reader(req.text.splitlines()) + for row in locations_reader: + row_line = "LocationRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1].lower()) + + region_strings = row[2].split(", ") if row[2] else [] + row_line += f"{str_list_to_py(region_strings)}, " + + skill_strings = row[3].split(", ") + row_line += "[" + if skill_strings: + split_skills = [skill.split(" ") for skill in skill_strings if skill != ""] + if split_skills: + for split in split_skills: + row_line += f"SkillRequirement('{split[0]}', {split[1]}), " + row_line += "], " + + item_strings = row[4].split(", ") if row[4] else [] + row_line += f"{str_list_to_py(item_strings)}, " + row_line += f"{row[5]})" if row[5] != "" else "0)" + locPyFile.write(f"\t{row_line},\n") + locPyFile.write("]\n") + + def load_region_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile: + regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + regPyFile.write("from ..Regions import RegionRow\n") + regPyFile.write("\n") + regPyFile.write("region_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req: + regions_reader = csv.reader(req.text.splitlines()) + for row in regions_reader: + row_line = "RegionRow(" + row_line += str_format(row[0]) + row_line += str_format(row[1]) + connections = row[2].replace("'", "\\'") + row_line += f"{str_list_to_py(connections.split(', '))}, " + resources = row[3].replace("'", "\\'") + row_line += f"{str_list_to_py(resources.split(', '))})" + regPyFile.write(f"\t{row_line},\n") + regPyFile.write("]\n") + + def load_resource_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile: + resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + resPyFile.write("from ..Regions import ResourceRow\n") + resPyFile.write("\n") + resPyFile.write("resource_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req: + resource_reader = csv.reader(req.text.splitlines()) + for row in resource_reader: + name = row[0].replace("'", "\\'") + row_line = f"ResourceRow('{name}')" + resPyFile.write(f"\t{row_line},\n") + resPyFile.write("]\n") + + + def load_item_csv(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + + with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile: + itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n') + itemPyfile.write("from BaseClasses import ItemClassification\n") + itemPyfile.write("from ..Items import ItemRow\n") + itemPyfile.write("\n") + itemPyfile.write("item_rows = [\n") + + with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req: + item_reader = csv.reader(req.text.splitlines()) + for row in item_reader: + row_line = "ItemRow(" + row_line += str_format(row[0]) + row_line += f"{row[1]}, " + + row_line += f"ItemClassification.{row[2]})" + + itemPyfile.write(f"\t{row_line},\n") + itemPyfile.write("]\n") + + + def str_format(s) -> str: + ret_str = s.replace("'", "\\'") + return f"'{ret_str}', " + + + def str_list_to_py(str_list) -> str: + ret_str = "[" + for s in str_list: + ret_str += f"'{s}', " + ret_str += "]" + return ret_str + + + + load_location_csv() + print("Generated locations py") + load_region_csv() + print("Generated regions py") + load_resource_csv() + print("Generated resource py") + load_item_csv() + print("Generated item py") diff --git a/worlds/osrs/LogicCSV/items_generated.py b/worlds/osrs/LogicCSV/items_generated.py new file mode 100644 index 0000000000..b5e610a6e3 --- /dev/null +++ b/worlds/osrs/LogicCSV/items_generated.py @@ -0,0 +1,43 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from BaseClasses import ItemClassification +from ..Items import ItemRow + +item_rows = [ + ItemRow('Area: Lumbridge', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Swamp', 1, ItemClassification.progression), + ItemRow('Area: HAM Hideout', 1, ItemClassification.progression), + ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression), + ItemRow('Area: South of Varrock', 1, ItemClassification.progression), + ItemRow('Area: East Varrock', 1, ItemClassification.progression), + ItemRow('Area: Central Varrock', 1, ItemClassification.progression), + ItemRow('Area: Varrock Palace', 1, ItemClassification.progression), + ItemRow('Area: West Varrock', 1, ItemClassification.progression), + ItemRow('Area: Edgeville', 1, ItemClassification.progression), + ItemRow('Area: Barbarian Village', 1, ItemClassification.progression), + ItemRow('Area: Draynor Manor', 1, ItemClassification.progression), + ItemRow('Area: Falador', 1, ItemClassification.progression), + ItemRow('Area: Dwarven Mines', 1, ItemClassification.progression), + ItemRow('Area: Ice Mountain', 1, ItemClassification.progression), + ItemRow('Area: Monastery', 1, ItemClassification.progression), + ItemRow('Area: Falador Farms', 1, ItemClassification.progression), + ItemRow('Area: Port Sarim', 1, ItemClassification.progression), + ItemRow('Area: Mudskipper Point', 1, ItemClassification.progression), + ItemRow('Area: Karamja', 1, ItemClassification.progression), + ItemRow('Area: Crandor', 1, ItemClassification.progression), + ItemRow('Area: Rimmington', 1, ItemClassification.progression), + ItemRow('Area: Crafting Guild', 1, ItemClassification.progression), + ItemRow('Area: Draynor Village', 1, ItemClassification.progression), + ItemRow('Area: Wizard Tower', 1, ItemClassification.progression), + ItemRow('Area: Corsair Cove', 1, ItemClassification.progression), + ItemRow('Area: Al Kharid', 1, ItemClassification.progression), + ItemRow('Area: Citharede Abbey', 1, ItemClassification.progression), + ItemRow('Area: Wilderness', 1, ItemClassification.progression), + ItemRow('Progressive Armor', 6, ItemClassification.progression), + ItemRow('Progressive Weapons', 6, ItemClassification.progression), + ItemRow('Progressive Tools', 6, ItemClassification.useful), + ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful), + ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful), + ItemRow('Progressive Magic', 2, ItemClassification.useful), +] diff --git a/worlds/osrs/LogicCSV/locations_generated.py b/worlds/osrs/LogicCSV/locations_generated.py new file mode 100644 index 0000000000..073e505ad8 --- /dev/null +++ b/worlds/osrs/LogicCSV/locations_generated.py @@ -0,0 +1,127 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Locations import LocationRow, SkillRequirement + +location_rows = [ + LocationRow('Quest: Cook\'s Assistant', 'quest', ['Lumbridge', 'Wheat', 'Windmill', 'Egg', 'Milk', ], [], [], 0), + LocationRow('Quest: Demon Slayer', 'quest', ['Central Varrock', 'Varrock Palace', 'Wizard Tower', 'South of Varrock', ], [], [], 0), + LocationRow('Quest: The Restless Ghost', 'quest', ['Lumbridge', 'Lumbridge Swamp', 'Wizard Tower', ], [], [], 0), + LocationRow('Quest: Romeo & Juliet', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Sheep Shearer', 'quest', ['Lumbridge Farms West', 'Spinning Wheel', ], [], [], 0), + LocationRow('Quest: Shield of Arrav', 'quest', ['Central Varrock', 'Varrock Palace', 'South of Varrock', 'West Varrock', ], [], [], 0), + LocationRow('Quest: Ernest the Chicken', 'quest', ['Draynor Manor', ], [], [], 0), + LocationRow('Quest: Vampyre Slayer', 'quest', ['Draynor Village', 'Central Varrock', 'Draynor Manor', ], [], [], 0), + LocationRow('Quest: Imp Catcher', 'quest', ['Wizard Tower', 'Imps', ], [], [], 0), + LocationRow('Quest: Prince Ali Rescue', 'quest', ['Al Kharid', 'Central Varrock', 'Bronze Ores', 'Clay Ore', 'Sheep', 'Spinning Wheel', 'Draynor Village', ], [], [], 0), + LocationRow('Quest: Doric\'s Quest', 'quest', ['Dwarven Mountain Pass', 'Clay Ore', 'Iron Ore', 'Bronze Ores', ], [SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Quest: Black Knights\' Fortress', 'quest', ['Dwarven Mines', 'Falador', 'Monastery', 'Ice Mountain', 'Falador Farms', ], [], ['Progressive Armor', ], 12), + LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0), + LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0), + LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0), + LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0), + LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0), + LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0), + LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16), + LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32), + LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0), + LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2), + LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6), + LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0), + LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0), + LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2), + LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6), + LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0), + LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0), + LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0), + LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0), + LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0), + LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0), + LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4), + LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8), + LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0), + LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0), + LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0), + LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0), + LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2), + LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6), + LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0), + LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0), + LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2), + LocationRow('Catch a Lobster', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 40), ], [], 6), + LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12), + LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0), + LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0), + LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2), + LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6), + LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8), + LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0), + LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0), + LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0), + LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0), + LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0), + LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0), + LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0), + LocationRow('Kill a Barbarian', 'combat', ['Barbarian', ], [SkillRequirement('Combat', 10), ], [], 0), + LocationRow('Kill a Giant Frog', 'combat', ['Lumbridge Swamp', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Zombie', 'combat', ['Zombie', ], [SkillRequirement('Combat', 13), ], [], 0), + LocationRow('Kill a Guard', 'combat', ['Guard', ], [SkillRequirement('Combat', 21), ], [], 0), + LocationRow('Kill a Hill Giant', 'combat', ['Hill Giant', ], [SkillRequirement('Combat', 28), ], [], 2), + LocationRow('Kill a Deadly Red Spider', 'combat', ['Deadly Red Spider', ], [SkillRequirement('Combat', 34), ], [], 2), + LocationRow('Kill a Moss Giant', 'combat', ['Moss Giant', ], [SkillRequirement('Combat', 42), ], [], 2), + LocationRow('Kill a Catablepon', 'combat', ['Barbarian Village', ], [SkillRequirement('Combat', 49), ], [], 4), + LocationRow('Kill an Ice Giant', 'combat', ['Ice Giant', ], [SkillRequirement('Combat', 53), ], [], 4), + LocationRow('Kill a Lesser Demon', 'combat', ['Lesser Demon', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8), + LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28), + LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28), + LocationRow('Total XP 5,000', 'general', [], [], [], 0), + LocationRow('Combat Level 5', 'general', [], [], [], 0), + LocationRow('Total XP 10,000', 'general', [], [], [], 0), + LocationRow('Total Level 50', 'general', [], [], [], 0), + LocationRow('Total XP 25,000', 'general', [], [], [], 0), + LocationRow('Total Level 100', 'general', [], [], [], 0), + LocationRow('Total XP 50,000', 'general', [], [], [], 0), + LocationRow('Combat Level 15', 'general', [], [], [], 0), + LocationRow('Total Level 150', 'general', [], [], [], 2), + LocationRow('Total XP 75,000', 'general', [], [], [], 2), + LocationRow('Combat Level 25', 'general', [], [], [], 2), + LocationRow('Total XP 100,000', 'general', [], [], [], 6), + LocationRow('Total Level 200', 'general', [], [], [], 6), + LocationRow('Total XP 125,000', 'general', [], [], [], 6), + LocationRow('Combat Level 30', 'general', [], [], [], 10), + LocationRow('Total Level 250', 'general', [], [], [], 10), + LocationRow('Total XP 150,000', 'general', [], [], [], 10), + LocationRow('Total Level 300', 'general', [], [], [], 16), + LocationRow('Combat Level 40', 'general', [], [], [], 16), + LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0), + LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0), + LocationRow('Points: Demon Slayer', 'points', [], [], [], 0), + LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0), + LocationRow('Points: Romeo & Juliet', 'points', [], [], [], 0), + LocationRow('Points: Sheep Shearer', 'points', [], [], [], 0), + LocationRow('Points: Shield of Arrav', 'points', [], [], [], 0), + LocationRow('Points: Ernest the Chicken', 'points', [], [], [], 0), + LocationRow('Points: Vampyre Slayer', 'points', [], [], [], 0), + LocationRow('Points: Imp Catcher', 'points', [], [], [], 0), + LocationRow('Points: Prince Ali Rescue', 'points', [], [], [], 0), + LocationRow('Points: Doric\'s Quest', 'points', [], [], [], 0), + LocationRow('Points: Black Knights\' Fortress', 'points', [], [], [], 0), + LocationRow('Points: Witch\'s Potion', 'points', [], [], [], 0), + LocationRow('Points: The Knight\'s Sword', 'points', [], [], [], 0), + LocationRow('Points: Goblin Diplomacy', 'points', [], [], [], 0), + LocationRow('Points: Pirate\'s Treasure', 'points', [], [], [], 0), + LocationRow('Points: Rune Mysteries', 'points', [], [], [], 0), + LocationRow('Points: Misthalin Mystery', 'points', [], [], [], 0), + LocationRow('Points: The Corsair Curse', 'points', [], [], [], 0), + LocationRow('Points: X Marks the Spot', 'points', [], [], [], 0), + LocationRow('Points: Below Ice Mountain', 'points', [], [], [], 0), +] diff --git a/worlds/osrs/LogicCSV/regions_generated.py b/worlds/osrs/LogicCSV/regions_generated.py new file mode 100644 index 0000000000..87b3747d93 --- /dev/null +++ b/worlds/osrs/LogicCSV/regions_generated.py @@ -0,0 +1,47 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import RegionRow + +region_rows = [ + RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]), + RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]), + RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]), + RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]), + RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]), + RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]), + RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]), + RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]), + RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]), + RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]), + RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]), + RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]), + RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]), + RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]), + RegionRow('Falador East Outskirts', 'Area: Falador', ['Dwarven Mountain Pass', 'Draynor Manor Outskirts', 'Falador Farms', ], ['', ]), + RegionRow('Dwarven Mountain Pass', 'Area: Dwarven Mines', ['Goblin Village', 'Monastery', 'Barbarian Village', 'Falador East Outskirts', 'Falador', ], ['Anvil*', 'Wheat', ]), + RegionRow('Dwarven Mines', 'Area: Dwarven Mines', ['Monastery', 'Ice Mountain', 'Falador', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Gold Ore', 'Anvil', 'Pie Dish', 'Clay Ore', ]), + RegionRow('Goblin Village', 'Area: Ice Mountain', ['Wilderness', 'Dwarven Mountain Pass', ], ['Meat', ]), + RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]), + RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]), + RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]), + RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]), + RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]), + RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]), + RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]), + RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]), + RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), + RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]), + RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]), + RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]), + RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]), + RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]), + RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]), + RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]), + RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]), + RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]), + RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]), +] diff --git a/worlds/osrs/LogicCSV/resources_generated.py b/worlds/osrs/LogicCSV/resources_generated.py new file mode 100644 index 0000000000..18c2ebe2f3 --- /dev/null +++ b/worlds/osrs/LogicCSV/resources_generated.py @@ -0,0 +1,54 @@ +""" +This file was auto generated by LogicCSVToPython.py +""" +from ..Regions import ResourceRow + +resource_rows = [ + ResourceRow('Mind Runes'), + ResourceRow('Spinning Wheel'), + ResourceRow('Sheep'), + ResourceRow('Furnace'), + ResourceRow('Chisel'), + ResourceRow('Bronze Ores'), + ResourceRow('Iron Ore'), + ResourceRow('Silver Ore'), + ResourceRow('Coal Ore'), + ResourceRow('Gold Ore'), + ResourceRow('Bronze Anvil'), + ResourceRow('Anvil'), + ResourceRow('Shrimp Spot'), + ResourceRow('Fly Fishing Spot'), + ResourceRow('Lobster Spot'), + ResourceRow('Redberry Bush'), + ResourceRow('Bowl'), + ResourceRow('Meat'), + ResourceRow('Cooking Apple'), + ResourceRow('Pie Dish'), + ResourceRow('Cake Tin'), + ResourceRow('Wheat'), + ResourceRow('Windmill'), + ResourceRow('Egg'), + ResourceRow('Milk'), + ResourceRow('Cheese'), + ResourceRow('Tomato'), + ResourceRow('Oak Tree'), + ResourceRow('Willow Tree'), + ResourceRow('Canoe Tree'), + ResourceRow('Goblin'), + ResourceRow('Barbarian'), + ResourceRow('Zombie'), + ResourceRow('Guard'), + ResourceRow('Hill Giant'), + ResourceRow('Deadly Red Spider'), + ResourceRow('Moss Giant'), + ResourceRow('Ice Giant'), + ResourceRow('Lesser Demon'), + ResourceRow('Rune Essence'), + ResourceRow('Crafting Moulds'), + ResourceRow('Nature Runes'), + ResourceRow('Law Runes'), + ResourceRow('Imps'), + ResourceRow('Clay Ore'), + ResourceRow('Onion'), + ResourceRow('Potato'), +] diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py new file mode 100644 index 0000000000..95aed742b6 --- /dev/null +++ b/worlds/osrs/Names.py @@ -0,0 +1,212 @@ +from enum import Enum + + +class RegionNames(str, Enum): + Lumbridge = "Lumbridge" + Lumbridge_Swamp = "Lumbridge Swamp" + Lumbridge_Farms_East = "Lumbridge Farms East" + Lumbridge_Farms_West = "Lumbridge Farms West" + HAM_Hideout = "HAM Hideout" + Draynor_Village = "Draynor Village" + Draynor_Manor = "Draynor Manor" + Wizards_Tower = "Wizard Tower" + Al_Kharid = "Al Kharid" + Citharede_Abbey = "Citharede Abbey" + South_Of_Varrock = "South of Varrock" + Central_Varrock = "Central Varrock" + Varrock_Palace = "Varrock Palace" + East_Of_Varrock = "East Varrock" + West_Varrock = "West Varrock" + Edgeville = "Edgeville" + Barbarian_Village = "Barbarian Village" + Monastery = "Monastery" + Ice_Mountain = "Ice Mountain" + Dwarven_Mines = "Dwarven Mines" + Falador = "Falador" + Falador_Farm = "Falador Farms" + Crafting_Guild = "Crafting Guild" + Cooks_Guild = "Cook's Guild" + Rimmington = "Rimmington" + Port_Sarim = "Port Sarim" + Mudskipper_Point = "Mudskipper Point" + Karamja = "Karamja" + Corsair_Cove = "Corsair Cove" + Wilderness = "The Wilderness" + Crandor = "Crandor" + # Resource Regions + Egg = "Egg" + Sheep = "Sheep" + Milk = "Milk" + Wheat = "Wheat" + Windmill = "Windmill" + Spinning_Wheel = "Spinning Wheel" + Imp = "Imp" + Bronze_Ores = "Bronze Ores" + Clay_Rock = "Clay Ore" + Coal_Rock = "Coal Ore" + Iron_Rock = "Iron Ore" + Silver_Rock = "Silver Ore" + Gold_Rock = "Gold Ore" + Furnace = "Furnace" + Anvil = "Anvil" + Oak_Tree = "Oak Tree" + Willow_Tree = "Willow Tree" + Shrimp = "Shrimp Spot" + Fly_Fish = "Fly Fishing Spot" + Lobster = "Lobster Spot" + Mind_Runes = "Mind Runes" + Canoe_Tree = "Canoe Tree" + + __str__ = str.__str__ + + +class ItemNames(str, Enum): + Lumbridge = "Area: Lumbridge" + Lumbridge_Swamp = "Area: Lumbridge Swamp" + Lumbridge_Farms = "Area: Lumbridge Farms" + HAM_Hideout = "Area: HAM Hideout" + Draynor_Village = "Area: Draynor Village" + Draynor_Manor = "Area: Draynor Manor" + Wizards_Tower = "Area: Wizard Tower" + Al_Kharid = "Area: Al Kharid" + Citharede_Abbey = "Area: Citharede Abbey" + South_Of_Varrock = "Area: South of Varrock" + Central_Varrock = "Area: Central Varrock" + Varrock_Palace = "Area: Varrock Palace" + East_Of_Varrock = "Area: East Varrock" + West_Varrock = "Area: West Varrock" + Edgeville = "Area: Edgeville" + Barbarian_Village = "Area: Barbarian Village" + Monastery = "Area: Monastery" + Ice_Mountain = "Area: Ice Mountain" + Dwarven_Mines = "Area: Dwarven Mines" + Falador = "Area: Falador" + Falador_Farm = "Area: Falador Farms" + Crafting_Guild = "Area: Crafting Guild" + Rimmington = "Area: Rimmington" + Port_Sarim = "Area: Port Sarim" + Mudskipper_Point = "Area: Mudskipper Point" + Karamja = "Area: Karamja" + Crandor = "Area: Crandor" + Corsair_Cove = "Area: Corsair Cove" + Wilderness = "Area: Wilderness" + Progressive_Armor = "Progressive Armor" + Progressive_Weapons = "Progressive Weapons" + Progressive_Tools = "Progressive Tools" + Progressive_Range_Armor = "Progressive Range Armor" + Progressive_Range_Weapon = "Progressive Range Weapon" + Progressive_Magic = "Progressive Magic Spell" + Lobsters = "10 Lobsters" + Swordfish = "5 Swordfish" + Energy_Potions = "10 Energy Potions" + Coins = "5,000 Coins" + Mind_Runes = "50 Mind Runes" + Chaos_Runes = "25 Chaos Runes" + Death_Runes = "10 Death Runes" + Law_Runes = "10 Law Runes" + QP_Cooks_Assistant = "1 QP (Cook's Assistant)" + QP_Demon_Slayer = "3 QP (Demon Slayer)" + QP_Restless_Ghost = "1 QP (The Restless Ghost)" + QP_Romeo_Juliet = "5 QP (Romeo & Juliet)" + QP_Sheep_Shearer = "1 QP (Sheep Shearer)" + QP_Shield_of_Arrav = "1 QP (Shield of Arrav)" + QP_Ernest_the_Chicken = "4 QP (Ernest the Chicken)" + QP_Vampyre_Slayer = "3 QP (Vampyre Slayer)" + QP_Imp_Catcher = "1 QP (Imp Catcher)" + QP_Prince_Ali_Rescue = "3 QP (Prince Ali Rescue)" + QP_Dorics_Quest = "1 QP (Doric's Quest)" + QP_Black_Knights_Fortress = "3 QP (Black Knights' Fortress)" + QP_Witchs_Potion = "1 QP (Witch's Potion)" + QP_Knights_Sword = "1 QP (The Knight's Sword)" + QP_Goblin_Diplomacy = "5 QP (Goblin Diplomacy)" + QP_Pirates_Treasure = "2 QP (Pirate's Treasure)" + QP_Rune_Mysteries = "1 QP (Rune Mysteries)" + QP_Misthalin_Mystery = "1 QP (Misthalin Mystery)" + QP_Corsair_Curse = "2 QP (The Corsair Curse)" + QP_X_Marks_the_Spot = "1 QP (X Marks The Spot)" + QP_Below_Ice_Mountain = "1 QP (Below Ice Mountain)" + + __str__ = str.__str__ + + +class LocationNames(str, Enum): + Q_Cooks_Assistant = "Quest: Cook's Assistant" + Q_Demon_Slayer = "Quest: Demon Slayer" + Q_Restless_Ghost = "Quest: The Restless Ghost" + Q_Romeo_Juliet = "Quest: Romeo & Juliet" + Q_Sheep_Shearer = "Quest: Sheep Shearer" + Q_Shield_of_Arrav = "Quest: Shield of Arrav" + Q_Ernest_the_Chicken = "Quest: Ernest the Chicken" + Q_Vampyre_Slayer = "Quest: Vampyre Slayer" + Q_Imp_Catcher = "Quest: Imp Catcher" + Q_Prince_Ali_Rescue = "Quest: Prince Ali Rescue" + Q_Dorics_Quest = "Quest: Doric's Quest" + Q_Black_Knights_Fortress = "Quest: Black Knights' Fortress" + Q_Witchs_Potion = "Quest: Witch's Potion" + Q_Knights_Sword = "Quest: The Knight's Sword" + Q_Goblin_Diplomacy = "Quest: Goblin Diplomacy" + Q_Pirates_Treasure = "Quest: Pirate's Treasure" + Q_Rune_Mysteries = "Quest: Rune Mysteries" + Q_Misthalin_Mystery = "Quest: Misthalin Mystery" + Q_Corsair_Curse = "Quest: The Corsair Curse" + Q_X_Marks_the_Spot = "Quest: X Marks the Spot" + Q_Below_Ice_Mountain = "Quest: Below Ice Mountain" + QP_Cooks_Assistant = "Points: Cook's Assistant" + QP_Demon_Slayer = "Points: Demon Slayer" + QP_Restless_Ghost = "Points: The Restless Ghost" + QP_Romeo_Juliet = "Points: Romeo & Juliet" + QP_Sheep_Shearer = "Points: Sheep Shearer" + QP_Shield_of_Arrav = "Points: Shield of Arrav" + QP_Ernest_the_Chicken = "Points: Ernest the Chicken" + QP_Vampyre_Slayer = "Points: Vampyre Slayer" + QP_Imp_Catcher = "Points: Imp Catcher" + QP_Prince_Ali_Rescue = "Points: Prince Ali Rescue" + QP_Dorics_Quest = "Points: Doric's Quest" + QP_Black_Knights_Fortress = "Points: Black Knights' Fortress" + QP_Witchs_Potion = "Points: Witch's Potion" + QP_Knights_Sword = "Points: The Knight's Sword" + QP_Goblin_Diplomacy = "Points: Goblin Diplomacy" + QP_Pirates_Treasure = "Points: Pirate's Treasure" + QP_Rune_Mysteries = "Points: Rune Mysteries" + QP_Misthalin_Mystery = "Points: Misthalin Mystery" + QP_Corsair_Curse = "Points: The Corsair Curse" + QP_X_Marks_the_Spot = "Points: X Marks the Spot" + QP_Below_Ice_Mountain = "Points: Below Ice Mountain" + Guppy = "Prepare a Guppy" + Cavefish = "Prepare a Cavefish" + Tetra = "Prepare a Tetra" + Barronite_Deposit = "Crush a Barronite Deposit" + Oak_Log = "Cut an Oak Log" + Willow_Log = "Cut a Willow Log" + Catch_Lobster = "Catch a Lobster" + Mine_Silver = "Mine Silver" + Mine_Coal = "Mine Coal" + Mine_Gold = "Mine Gold" + Smelt_Silver = "Smelt a Silver Bar" + Smelt_Steel = "Smelt a Steel Bar" + Smelt_Gold = "Smelt a Gold Bar" + Cut_Sapphire = "Cut a Sapphire" + Cut_Emerald = "Cut an Emerald" + Cut_Ruby = "Cut a Ruby" + Cut_Diamond = "Cut a Diamond" + K_Lesser_Demon = "Kill a Lesser Demon" + K_Ogress_Shaman = "Kill an Ogress Shaman" + Bake_Apple_Pie = "Bake an Apple Pie" + Bake_Cake = "Bake a Cake" + Bake_Meat_Pizza = "Bake a Meat Pizza" + Total_XP_5000 = "5,000 Total XP" + Total_XP_10000 = "10,000 Total XP" + Total_XP_25000 = "25,000 Total XP" + Total_XP_50000 = "50,000 Total XP" + Total_XP_100000 = "100,000 Total XP" + Total_Level_50 = "Total Level 50" + Total_Level_100 = "Total Level 100" + Total_Level_150 = "Total Level 150" + Total_Level_200 = "Total Level 200" + Combat_Level_5 = "Combat Level 5" + Combat_Level_15 = "Combat Level 15" + Combat_Level_25 = "Combat Level 25" + Travel_on_a_Canoe = "Travel on a Canoe" + Q_Dragon_Slayer = "Quest: Dragon Slayer" + + __str__ = str.__str__ diff --git a/worlds/osrs/Options.py b/worlds/osrs/Options.py new file mode 100644 index 0000000000..520cd8e8b0 --- /dev/null +++ b/worlds/osrs/Options.py @@ -0,0 +1,474 @@ +from dataclasses import dataclass + +from Options import Choice, Toggle, Range, PerGameCommonOptions + +MAX_COMBAT_TASKS = 16 +MAX_PRAYER_TASKS = 3 +MAX_MAGIC_TASKS = 4 +MAX_RUNECRAFT_TASKS = 3 +MAX_CRAFTING_TASKS = 5 +MAX_MINING_TASKS = 5 +MAX_SMITHING_TASKS = 4 +MAX_FISHING_TASKS = 5 +MAX_COOKING_TASKS = 5 +MAX_FIREMAKING_TASKS = 2 +MAX_WOODCUTTING_TASKS = 3 + +NON_QUEST_LOCATION_COUNT = 22 + + +class StartingArea(Choice): + """ + Which chunks are available at the start. The player may need to move through locked chunks to reach the starting + area, but any areas that require quests, skills, or coins are not available as a starting location. + + "Any Bank" rolls a random region that contains a bank. + Chunksanity can start you in any chunk. Hope you like woodcutting! + """ + display_name = "Starting Region" + option_lumbridge = 0 + option_al_kharid = 1 + option_varrock_east = 2 + option_varrock_west = 3 + option_edgeville = 4 + option_falador = 5 + option_draynor = 6 + option_wilderness = 7 + option_any_bank = 8 + option_chunksanity = 9 + default = 0 + + +class BrutalGrinds(Toggle): + """ + Whether to allow skill tasks without having reasonable access to the usual skill training path. + For example, if enabled, you could be forced to train smithing without an anvil purely by smelting bars, + or training fishing to high levels entirely on shrimp. + """ + display_name = "Allow Brutal Grinds" + + +class ProgressiveTasks(Toggle): + """ + Whether skill tasks should always be generated in order of easiest to hardest. + If enabled, you would not be assigned "Mine Gold" without also being assigned + "Mine Silver", "Mine Coal", and "Mine Iron". Enabling this will result in a generally shorter seed, but with + a lower variety of tasks. + """ + display_name = "Progressive Tasks" + + +class MaxCombatLevel(Range): + """ + The highest combat level of monster to possibly be assigned as a task. + If set to 0, no combat tasks will be generated. + """ + range_start = 0 + range_end = 1520 + default = 50 + + +class MaxCombatTasks(Range): + """ + The maximum number of Combat Tasks to possibly be assigned. + If set to 0, no combat tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_COMBAT_TASKS + default = MAX_COMBAT_TASKS + + +class CombatTaskWeight(Range): + """ + How much to favor generating combat tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxPrayerLevel(Range): + """ + The highest Prayer requirement of any task generated. + If set to 0, no Prayer tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxPrayerTasks(Range): + """ + The maximum number of Prayer Tasks to possibly be assigned. + If set to 0, no Prayer tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_PRAYER_TASKS + default = MAX_PRAYER_TASKS + + +class PrayerTaskWeight(Range): + """ + How much to favor generating Prayer tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMagicLevel(Range): + """ + The highest Magic requirement of any task generated. + If set to 0, no Magic tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMagicTasks(Range): + """ + The maximum number of Magic Tasks to possibly be assigned. + If set to 0, no Magic tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_MAGIC_TASKS + default = MAX_MAGIC_TASKS + + +class MagicTaskWeight(Range): + """ + How much to favor generating Magic tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxRunecraftLevel(Range): + """ + The highest Runecraft requirement of any task generated. + If set to 0, no Runecraft tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxRunecraftTasks(Range): + """ + The maximum number of Runecraft Tasks to possibly be assigned. + If set to 0, no Runecraft tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_RUNECRAFT_TASKS + default = MAX_RUNECRAFT_TASKS + + +class RunecraftTaskWeight(Range): + """ + How much to favor generating Runecraft tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCraftingLevel(Range): + """ + The highest Crafting requirement of any task generated. + If set to 0, no Crafting tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCraftingTasks(Range): + """ + The maximum number of Crafting Tasks to possibly be assigned. + If set to 0, no Crafting tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_CRAFTING_TASKS + default = MAX_CRAFTING_TASKS + + +class CraftingTaskWeight(Range): + """ + How much to favor generating Crafting tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMiningLevel(Range): + """ + The highest Mining requirement of any task generated. + If set to 0, no Mining tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxMiningTasks(Range): + """ + The maximum number of Mining Tasks to possibly be assigned. + If set to 0, no Mining tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_MINING_TASKS + default = MAX_MINING_TASKS + + +class MiningTaskWeight(Range): + """ + How much to favor generating Mining tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxSmithingLevel(Range): + """ + The highest Smithing requirement of any task generated. + If set to 0, no Smithing tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxSmithingTasks(Range): + """ + The maximum number of Smithing Tasks to possibly be assigned. + If set to 0, no Smithing tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_SMITHING_TASKS + default = MAX_SMITHING_TASKS + + +class SmithingTaskWeight(Range): + """ + How much to favor generating Smithing tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFishingLevel(Range): + """ + The highest Fishing requirement of any task generated. + If set to 0, no Fishing tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFishingTasks(Range): + """ + The maximum number of Fishing Tasks to possibly be assigned. + If set to 0, no Fishing tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_FISHING_TASKS + default = MAX_FISHING_TASKS + + +class FishingTaskWeight(Range): + """ + How much to favor generating Fishing tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCookingLevel(Range): + """ + The highest Cooking requirement of any task generated. + If set to 0, no Cooking tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxCookingTasks(Range): + """ + The maximum number of Cooking Tasks to possibly be assigned. + If set to 0, no Cooking tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_COOKING_TASKS + default = MAX_COOKING_TASKS + + +class CookingTaskWeight(Range): + """ + How much to favor generating Cooking tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFiremakingLevel(Range): + """ + The highest Firemaking requirement of any task generated. + If set to 0, no Firemaking tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxFiremakingTasks(Range): + """ + The maximum number of Firemaking Tasks to possibly be assigned. + If set to 0, no Firemaking tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_FIREMAKING_TASKS + default = MAX_FIREMAKING_TASKS + + +class FiremakingTaskWeight(Range): + """ + How much to favor generating Firemaking tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxWoodcuttingLevel(Range): + """ + The highest Woodcutting requirement of any task generated. + If set to 0, no Woodcutting tasks will be generated. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MaxWoodcuttingTasks(Range): + """ + The maximum number of Woodcutting Tasks to possibly be assigned. + If set to 0, no Woodcutting tasks will be generated. + This only determines the maximum possible, fewer than the maximum could be assigned. + """ + range_start = 0 + range_end = MAX_WOODCUTTING_TASKS + default = MAX_WOODCUTTING_TASKS + + +class WoodcuttingTaskWeight(Range): + """ + How much to favor generating Woodcutting tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +class MinimumGeneralTasks(Range): + """ + How many guaranteed general progression tasks to be assigned (total level, total XP, etc.). + General progression tasks will be used to fill out any holes caused by having fewer possible tasks than needed, so + there is no maximum. + """ + range_start = 0 + range_end = NON_QUEST_LOCATION_COUNT + default = 10 + + +class GeneralTaskWeight(Range): + """ + How much to favor generating General tasks over other types of task. + Weights of all Task Types will be compared against each other, a task with 50 weight + is twice as likely to appear as one with 25. + """ + range_start = 0 + range_end = 99 + default = 50 + + +@dataclass +class OSRSOptions(PerGameCommonOptions): + starting_area: StartingArea + brutal_grinds: BrutalGrinds + progressive_tasks: ProgressiveTasks + max_combat_level: MaxCombatLevel + max_combat_tasks: MaxCombatTasks + combat_task_weight: CombatTaskWeight + max_prayer_level: MaxPrayerLevel + max_prayer_tasks: MaxPrayerTasks + prayer_task_weight: PrayerTaskWeight + max_magic_level: MaxMagicLevel + max_magic_tasks: MaxMagicTasks + magic_task_weight: MagicTaskWeight + max_runecraft_level: MaxRunecraftLevel + max_runecraft_tasks: MaxRunecraftTasks + runecraft_task_weight: RunecraftTaskWeight + max_crafting_level: MaxCraftingLevel + max_crafting_tasks: MaxCraftingTasks + crafting_task_weight: CraftingTaskWeight + max_mining_level: MaxMiningLevel + max_mining_tasks: MaxMiningTasks + mining_task_weight: MiningTaskWeight + max_smithing_level: MaxSmithingLevel + max_smithing_tasks: MaxSmithingTasks + smithing_task_weight: SmithingTaskWeight + max_fishing_level: MaxFishingLevel + max_fishing_tasks: MaxFishingTasks + fishing_task_weight: FishingTaskWeight + max_cooking_level: MaxCookingLevel + max_cooking_tasks: MaxCookingTasks + cooking_task_weight: CookingTaskWeight + max_firemaking_level: MaxFiremakingLevel + max_firemaking_tasks: MaxFiremakingTasks + firemaking_task_weight: FiremakingTaskWeight + max_woodcutting_level: MaxWoodcuttingLevel + max_woodcutting_tasks: MaxWoodcuttingTasks + woodcutting_task_weight: WoodcuttingTaskWeight + minimum_general_tasks: MinimumGeneralTasks + general_task_weight: GeneralTaskWeight diff --git a/worlds/osrs/Regions.py b/worlds/osrs/Regions.py new file mode 100644 index 0000000000..436cdf3c7c --- /dev/null +++ b/worlds/osrs/Regions.py @@ -0,0 +1,12 @@ +import typing + + +class RegionRow(typing.NamedTuple): + name: str + itemReq: str + connections: typing.List[str] + resources: typing.List[str] + + +class ResourceRow(typing.NamedTuple): + name: str diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py new file mode 100644 index 0000000000..f726b4b81b --- /dev/null +++ b/worlds/osrs/__init__.py @@ -0,0 +1,657 @@ +import typing + +from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_rule, CollectionRule +from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \ + chunksanity_special_region_names +from .Locations import OSRSLocation, LocationRow + +from .Options import OSRSOptions, StartingArea +from .Names import LocationNames, ItemNames, RegionNames + +from .LogicCSV.LogicCSVToPython import data_csv_tag +from .LogicCSV.items_generated import item_rows +from .LogicCSV.locations_generated import location_rows +from .LogicCSV.regions_generated import region_rows +from .LogicCSV.resources_generated import resource_rows +from .Regions import RegionRow, ResourceRow + + +class OSRSWeb(WebWorld): + theme = "stone" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld", + "English", + "docs/setup_en.md", + "setup/en", + ["digiholic"] + ) + tutorials = [setup_en] + + +class OSRSWorld(World): + game = "Old School Runescape" + options_dataclass = OSRSOptions + options: OSRSOptions + topology_present = True + web = OSRSWeb() + base_id = 0x070000 + data_version = 1 + + item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))} + location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))} + + region_name_to_data: typing.Dict[str, Region] + location_name_to_data: typing.Dict[str, OSRSLocation] + + location_rows_by_name: typing.Dict[str, LocationRow] + region_rows_by_name: typing.Dict[str, RegionRow] + resource_rows_by_name: typing.Dict[str, ResourceRow] + item_rows_by_name: typing.Dict[str, ItemRow] + + starting_area_item: str + + locations_by_category: typing.Dict[str, typing.List[LocationRow]] + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.region_name_to_data = {} + self.location_name_to_data = {} + + self.location_rows_by_name = {} + self.region_rows_by_name = {} + self.resource_rows_by_name = {} + self.item_rows_by_name = {} + + self.starting_area_item = "" + + self.locations_by_category = {} + + def generate_early(self) -> None: + location_categories = [location_row.category for location_row in location_rows] + self.locations_by_category = {category: + [location_row for location_row in location_rows if + location_row.category == category] + for category in location_categories} + + self.location_rows_by_name = {loc_row.name: loc_row for loc_row in location_rows} + self.region_rows_by_name = {reg_row.name: reg_row for reg_row in region_rows} + self.resource_rows_by_name = {rec_row.name: rec_row for rec_row in resource_rows} + self.item_rows_by_name = {it_row.name: it_row for it_row in item_rows} + + rnd = self.random + starting_area = self.options.starting_area + + if starting_area.value == StartingArea.option_any_bank: + self.starting_area_item = rnd.choice(starting_area_dict) + elif starting_area.value < StartingArea.option_chunksanity: + self.starting_area_item = starting_area_dict[starting_area.value] + else: + self.starting_area_item = rnd.choice(chunksanity_starting_chunks) + + # Set Starting Chunk + self.multiworld.push_precollected(self.create_item(self.starting_area_item)) + + """ + This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client. + _Make sure to update that value whenever the CSVs change!_ + """ + + def fill_slot_data(self): + data = self.options.as_dict("brutal_grinds") + data["data_csv_tag"] = data_csv_tag + return data + + def create_regions(self) -> None: + """ + called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done + during generate_early or basic as well. + """ + + # First, create the "Menu" region to start + menu_region = self.create_region("Menu") + + for region_row in region_rows: + self.create_region(region_row.name) + + for resource_row in resource_rows: + self.create_region(resource_row.name) + + # Removes the word "Area: " from the item name to get the region it applies to. + # I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse + if self.starting_area_item in chunksanity_special_region_names: + starting_area_region = chunksanity_special_region_names[self.starting_area_item] + else: + starting_area_region = self.starting_area_item[6:] # len("Area: ") + starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}") + starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player) + starting_entrance.connect(self.region_name_to_data[starting_area_region]) + + # Create entrances between regions + for region_row in region_rows: + region = self.region_name_to_data[region_row.name] + + for outbound_region_name in region_row.connections: + parsed_outbound = outbound_region_name.replace('*', '') + entrance = region.create_exit(f"{region_row.name}->{parsed_outbound}") + entrance.connect(self.region_name_to_data[parsed_outbound]) + + item_name = self.region_rows_by_name[parsed_outbound].itemReq + if "*" not in outbound_region_name and "*" not in item_name: + entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player) + continue + + self.generate_special_rules_for(entrance, region_row, outbound_region_name) + + for resource_region in region_row.resources: + if not resource_region: + continue + + entrance = region.create_exit(f"{region_row.name}->{resource_region.replace('*', '')}") + if "*" not in resource_region: + entrance.connect(self.region_name_to_data[resource_region]) + else: + self.generate_special_rules_for(entrance, region_row, resource_region) + entrance.connect(self.region_name_to_data[resource_region.replace('*', '')]) + + self.roll_locations() + + def generate_special_rules_for(self, entrance, region_row, outbound_region_name): + # print(f"Special rules required to access region {outbound_region_name} from {region_row.name}") + if outbound_region_name == RegionNames.Cooks_Guild: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + cooking_level_rule = self.get_skill_rule("cooking", 32) + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + cooking_level_rule(state) + return + if outbound_region_name == RegionNames.Crafting_Guild: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + crafting_level_rule = self.get_skill_rule("crafting", 40) + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + crafting_level_rule(state) + return + if outbound_region_name == RegionNames.Corsair_Cove: + item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '') + # Need to be able to start Corsair Curse in addition to having the item + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + state.can_reach(RegionNames.Falador_Farm, "Region", self.player) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance) + + return + if outbound_region_name == "Camdozaal*": + item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq + entrance.access_rule = lambda state: state.has(item_name, self.player) and \ + state.has(ItemNames.QP_Below_Ice_Mountain, self.player) + return + if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*": + entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player) + return + # Special logic for canoes + canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village, + RegionNames.Edgeville, RegionNames.Wilderness] + if region_row.name in canoe_regions: + # Skill rules for greater distances + woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12) + woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27) + woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42) + woodcutting_rule_all = self.get_skill_rule("woodcutting", 57) + + if region_row.name == RegionNames.Lumbridge: + # Canoe Tree access for the Location + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + # South of Varrock does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.Edgeville: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 57 + + if region_row.name == RegionNames.South_Of_Varrock: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + # Lumbridge does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ + and self.options.max_woodcutting_level >= 12 + if outbound_region_name == RegionNames.Edgeville: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 42 + if region_row.name == RegionNames.Barbarian_Village: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d1(state) \ + and self.options.max_woodcutting_level >= 12 + # Edgeville does not need to be checked, because it's already adjacent + if outbound_region_name == RegionNames.Wilderness: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if region_row.name == RegionNames.Edgeville: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \ + (state.can_reach_region(RegionNames.Wilderness) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + # Barbarian Village does not need to be checked, because it's already adjacent + # Wilderness does not need to be checked, because it's already adjacent + if region_row.name == RegionNames.Wilderness: + if outbound_region_name == RegionNames.Canoe_Tree: + entrance.access_rule = \ + lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player) + and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \ + (state.can_reach_region(RegionNames.South_Of_Varrock) + and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \ + (state.can_reach_region(RegionNames.Barbarian_Village) + and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \ + (state.can_reach_region(RegionNames.Edgeville) + and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance) + self.multiworld.register_indirect_condition( + self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance) + # Access to other chunks based on woodcutting settings + if outbound_region_name == RegionNames.Lumbridge: + entrance.access_rule = lambda state: woodcutting_rule_all(state) \ + and self.options.max_woodcutting_level >= 57 + if outbound_region_name == RegionNames.South_Of_Varrock: + entrance.access_rule = lambda state: woodcutting_rule_d3(state) \ + and self.options.max_woodcutting_level >= 42 + if outbound_region_name == RegionNames.Barbarian_Village: + entrance.access_rule = lambda state: woodcutting_rule_d2(state) \ + and self.options.max_woodcutting_level >= 27 + # Edgeville does not need to be checked, because it's already adjacent + + def roll_locations(self): + locations_required = 0 + generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override + for item_row in item_rows: + locations_required += item_row.amount + + locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0 + + # Quests are always added + for i, location_row in enumerate(location_rows): + if location_row.category in {"quest", "points", "goal"}: + self.create_and_add_location(i) + if location_row.category == "quest": + locations_added += 1 + + # Build up the weighted Task Pool + rnd = self.random + + # Start with the minimum general tasks + general_tasks = [task for task in self.locations_by_category["general"]] + if not self.options.progressive_tasks: + rnd.shuffle(general_tasks) + else: + general_tasks.reverse() + for i in range(self.options.minimum_general_tasks): + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0 + + tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {} + weights_per_task_type: typing.Dict[str, int] = {} + + task_types = ["prayer", "magic", "runecraft", "mining", "crafting", + "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"] + for task_type in task_types: + max_level_for_task_type = getattr(self.options, f"max_{task_type}_level") + max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks") + tasks_for_this_type = [task for task in self.locations_by_category[task_type] + if task.skills[0].level <= max_level_for_task_type] + if not self.options.progressive_tasks: + rnd.shuffle(tasks_for_this_type) + else: + tasks_for_this_type.reverse() + + tasks_for_this_type = tasks_for_this_type[:max_amount_for_task_type] + weight_for_this_type = getattr(self.options, + f"{task_type}_task_weight") + if weight_for_this_type > 0 and tasks_for_this_type: + tasks_per_task_type[task_type] = tasks_for_this_type + weights_per_task_type[task_type] = weight_for_this_type + + # Build a list of collections and weights in a matching order for rnd.choices later + all_tasks = [] + all_weights = [] + for task_type in task_types: + if task_type in tasks_per_task_type: + all_tasks.append(tasks_per_task_type[task_type]) + all_weights.append(weights_per_task_type[task_type]) + + # Even after the initial forced generals, they can still be rolled randomly + if general_weight > 0: + all_tasks.append(general_tasks) + all_weights.append(general_weight) + + while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0): + if all_tasks: + chosen_task = rnd.choices(all_tasks, all_weights)[0] + if chosen_task: + task = chosen_task.pop() + self.add_location(task) + locations_added += 1 + + # This isn't an else because chosen_task can become empty in the process of resolving the above block + # We still want to clear this list out while we're doing that + if not chosen_task: + index = all_tasks.index(chosen_task) + del all_tasks[index] + del all_weights[index] + + else: + if len(general_tasks) == 0: + raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " + + f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.") + task = general_tasks.pop() + self.add_location(task) + locations_added += 1 + + def add_location(self, location): + index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0] + self.create_and_add_location(index) + + def create_items(self) -> None: + for item_row in item_rows: + if item_row.name != self.starting_area_item: + for c in range(item_row.amount): + item = self.create_item(item_row.name) + self.multiworld.itempool.append(item) + + def get_filler_item_name(self) -> str: + return self.random.choice( + [ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic, + ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon]) + + def create_and_add_location(self, row_index) -> None: + location_row = location_rows[row_index] + # print(f"Adding task {location_row.name}") + + # Create Location + location_id = self.base_id + row_index + if location_row.category == "points" or location_row.category == "goal": + location_id = None + location = OSRSLocation(self.player, location_row.name, location_id) + self.location_name_to_data[location_row.name] = location + + # Add the location to its first region, or if it doesn't belong to one, to Menu + region = self.region_name_to_data["Menu"] + if location_row.regions: + region = self.region_name_to_data[location_row.regions[0]] + location.parent_region = region + region.locations.append(location) + + def set_rules(self) -> None: + """ + called to set access and item rules on locations and entrances. + """ + quest_attr_names = ["Cooks_Assistant", "Demon_Slayer", "Restless_Ghost", "Romeo_Juliet", + "Sheep_Shearer", "Shield_of_Arrav", "Ernest_the_Chicken", "Vampyre_Slayer", + "Imp_Catcher", "Prince_Ali_Rescue", "Dorics_Quest", "Black_Knights_Fortress", + "Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure", + "Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot", + "Below_Ice_Mountain"] + for qp_attr_name in quest_attr_names: + loc_name = getattr(LocationNames, f"QP_{qp_attr_name}") + item_name = getattr(ItemNames, f"QP_{qp_attr_name}") + self.multiworld.get_location(loc_name, self.player) \ + .place_locked_item(self.create_event(item_name)) + + for quest_attr_name in quest_attr_names: + qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}") + q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}") + add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: ( + self.multiworld.get_location(q_loc_name, self.player).can_reach(state) + )) + + # place "Victory" at "Dragon Slayer" and set collection as win condition + self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \ + .place_locked_item(self.create_event("Victory")) + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Victory", self.player)) + + for location_name, location in self.location_name_to_data.items(): + location_row = self.location_rows_by_name[location_name] + # Set up requirements for region + for region_required_name in location_row.regions: + region_required = self.region_name_to_data[region_required_name] + add_rule(location, + lambda state, region_required=region_required: state.can_reach(region_required, "Region", + self.player)) + for skill_req in location_row.skills: + add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level)) + for item_req in location_row.items: + add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player)) + if location_row.qp: + add_rule(location, lambda state, location_row=location_row: self.quest_points(state) > location_row.qp) + + def create_region(self, name: str) -> "Region": + region = Region(name, self.player, self.multiworld) + self.region_name_to_data[name] = region + self.multiworld.regions.append(region) + return region + + def create_item(self, item_name: str) -> "Item": + item = [item for item in item_rows if item.name == item_name][0] + index = item_rows.index(item) + return OSRSItem(item.name, item.progression, self.base_id + index, self.player) + + def create_event(self, event: str): + # while we are at it, we can also add a helper to create events + return OSRSItem(event, ItemClassification.progression, None, self.player) + + def quest_points(self, state): + qp = 0 + for qp_event in QP_Items: + if state.has(qp_event, self.player): + qp += int(qp_event[0]) + return qp + + """ + Ensures a target level can be reached with available resources + """ + + def get_skill_rule(self, skill, level) -> CollectionRule: + if skill.lower() == "fishing": + if self.options.brutal_grinds or level < 5: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) + if level < 20: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ + state.can_reach(RegionNames.Port_Sarim, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \ + state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \ + state.can_reach(RegionNames.Fly_Fish, "Region", self.player) + if skill.lower() == "mining": + if self.options.brutal_grinds or level < 15: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \ + state.can_reach(RegionNames.Clay_Rock, "Region", self.player) + else: + # Iron is the best way to train all the way to 99, so having access to iron is all you need to check for + return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or + state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) + if skill.lower() == "woodcutting": + if self.options.brutal_grinds or level < 15: + # I've checked. There is not a single chunk in the f2p that does not have at least one normal tree. + # Even the desert. + return lambda state: True + if level < 30: + return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \ + state.can_reach(RegionNames.Willow_Tree, "Region", self.player) + if skill.lower() == "smithing": + if self.options.brutal_grinds: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) + if level < 15: + # Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included + # in the "Anvil" resource region. We still need to check for it though. + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + (state.can_reach(RegionNames.Anvil, "Region", self.player) or + state.can_reach(RegionNames.Lumbridge, "Region", self.player)) + if level < 30: + # For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + state.can_reach(RegionNames.Anvil, "Region", self.player) + else: + return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \ + state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and \ + state.can_reach(RegionNames.Anvil, "Region", self.player) + if skill.lower() == "crafting": + # Crafting is really complex. Need a lot of sub-rules to make this even remotely readable + def can_spin(state): + return state.can_reach(RegionNames.Sheep, "Region", self.player) and \ + state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player) + + def can_pot(state): + return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Barbarian_Village, "Region", self.player) + + def can_tan(state): + return state.can_reach(RegionNames.Milk, "Region", self.player) and \ + state.can_reach(RegionNames.Al_Kharid, "Region", self.player) + + def mould_access(state): + return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \ + state.can_reach(RegionNames.Rimmington, "Region", self.player) + + def can_silver(state): + + return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) + + def can_gold(state): + return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \ + state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state) + + if self.options.brutal_grinds or level < 5: + return lambda state: can_spin(state) or can_pot(state) or can_tan(state) + + can_smelt_gold = self.get_skill_rule("smithing", 40) + can_smelt_silver = self.get_skill_rule("smithing", 20) + if level < 16: + return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state)) + else: + return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \ + (can_gold(state) and can_smelt_gold(state)) + if skill.lower() == "Cooking": + if self.options.brutal_grinds or level < 15: + return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \ + state.can_reach(RegionNames.Egg, "Region", self.player) or \ + state.can_reach(RegionNames.Shrimp, "Region", self.player) or \ + (state.can_reach(RegionNames.Wheat, "Region", self.player) and + state.can_reach(RegionNames.Windmill, "Region", self.player)) + else: + can_catch_fly_fish = self.get_skill_rule("fishing", 20) + return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \ + can_catch_fly_fish(state) and \ + (state.can_reach(RegionNames.Milk, "Region", self.player) or + state.can_reach(RegionNames.Egg, "Region", self.player) or + state.can_reach(RegionNames.Shrimp, "Region", self.player) or + (state.can_reach(RegionNames.Wheat, "Region", self.player) and + state.can_reach(RegionNames.Windmill, "Region", self.player))) + if skill.lower() == "runecraft": + return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player) + if skill.lower() == "magic": + return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player) + + return lambda state: True diff --git a/worlds/osrs/docs/en_Old School Runescape.md b/worlds/osrs/docs/en_Old School Runescape.md new file mode 100644 index 0000000000..d367082b22 --- /dev/null +++ b/worlds/osrs/docs/en_Old School Runescape.md @@ -0,0 +1,114 @@ +# Old School Runescape + +## What is the Goal of this Randomizer? +The goal is to complete the quest "Dragon Slayer I" with limited access to gear and map chunks while following normal +Ironman/Group Ironman restrictions on a fresh free-to-play account. + +## Where is the options page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a +config file. OSRS contains many options for a highly customizable experience. The options available to you are: + +* **Starting Area** - The starting region of your run. This is the first region you will have available, and you can always +freely return to it (see the section below for when it is allowed to cross locked regions to access it) + * You may select a starting city from the list of Lumbridge, Al Kharid, Varrock (East or West), Edgeville, Falador, +Draynor Village, or The Wilderness (Ferox Enclave) + * The option "Any Bank" will choose one of the above regions at random + * The option "Chunksanity" can start you in _any_ chunk, regardless of whether it has access to a bank. +* **Brutal Grinds** - If enabled, the logic will assume you are willing to go to great lengths to train skills. + * As an example, when enabled, it might be in logic to obtain tin and copper from mob drops and smelt bronze bars to +reach Smithing Level 40 to smelt gold for a task. + * If left disabled, the logic will always ensure you have a reasonable method for training a skill to reach a specific +task, such as having access to intermediate-level training options +* **Progressive Tasks** - If enabled, tasks for a skill are generated in order from earliest to latest. + * For example, your first Smithing task would always be "Smelt an Iron Bar", then "Smelt a Silver Bar", and so on. +You would never have the task "Smelt a Gold Bar" without having every previous Smithing task as well. +This can lead to a more consistent length of run, and is generally shorter than disabling it, but with less variety. +* **Skill Category Weighting Options** + * These are available in each task category (all trainable skills plus "Combat" and "General") + * **Max [Category] Level** - The highest level you intend to have to reach in order to complete all tasks for this +category. For the Combat category, this is the max level of monster you are willing to fight. +General tasks do not have a level and thus do not have this option. + * **Max [Category] Tasks** - The highest number of tasks in this category you are willing to be assigned. +Note that you can end up with _less_ than this amount, but never more. The "General" category is used to fill remaining +spots so a maximum is not specified, instead it has a _minimum_ count. + * **[Category] Task Weighting** - The relative weighting of this category to all of the others. Increase this to make +tasks in this category more likely. + +## What does randomization do to this game? +The OSRS Archipelago Randomizer takes the form of a "Chunkman" account, a form of challenge account +where you are limited to specific regions of the map (known as "chunks") until you complete tasks to unlock +more. The plugin will interface with the [Region Locker Plugin](https://github.com/slaytostay/region-locker) to +visually display these chunk borders and highlight them as locked or unlocked. The optional included GPU plugin for the +Region Locker can tint the locked areas gray, but is incompatible with other GPU plugins such as 117's HD OSRS. +If you choose not to include it, the world map will show locked and unlocked regions instead. + +In order to access a region, you will need to access it entirely through unlocked regions. At no point are you +ever allowed to cross through locked regions, with the following exceptions: +* If your starting region is not Lumbridge, when you complete Tutorial Island, you will need to traverse locked regions +to reach your intended starting location. +* If your starting region is not Lumbridge, you are allowed to "Home Teleport" to your starting region by using the +Lumbridge Home Teleport Spell and then walking to your start location. This is to prevent you from getting "stuck" after +using one-way transportation such as the Port Sarim Jail Teleport from Shantay Pass and being locked out of progression. +* All of your starting Tutorial Island items are assumed to be available at all times. If you have lost an important +item such as a Tinderbox, and cannot re-obtain it in your unlocked region, you are allowed to enter locked regions to +replace it in the least obtrusive way possible. +* If you need to adjust Group Ironman settings, such as adding or removing a member, you may freely access The Node +to do so. + +When passing through locked regions for such exceptions, do not interact with any NPCs, items, or enemies and attempt +to spend as little time in them as possible. + +The plugin will prevent equipping items that you have not unlocked the ability to wield. For example, attempting +to equip an Iron Platebody before the first Progressive Armor unlock will display a chat message and will not +equip the item. + +The plugin will show a list of your current tasks in the sidebar. The plugin will be able to detect the completion +of most tasks, but in the case that a task cannot be detected (for example, killing an enemy with no +drop table such as Deadly Red Spiders), the task can be marked as complete manually by clicking +on the button. This button can also be used to mark completed tasks you have done while playing OSRS mobile or +on a different client without having the plugin available. Simply click the button the next time you are logged in to +Runelite and connected to send the check. + +Due to the nature of randomizing a live MMO with no ability to freely edit the character or adjust game logic or +balancing, this randomizer relies heavily on **the honor system**. The plugin cannot prevent you from walking through +locked regions or equipping locked items with the plugin disabled before connecting. It is important +to acknowledge before starting that the entire purpose of the randomizer is a self-imposed challenge, and there +is little point in cheating by circumventing the plugin's restrictions or marking a task complete without actually +completing it. If you wish to play OSRS with no restrictions, that is always available without the plugin. + +In order to access the AP Text Client commands (such as `!hint` or to chat with other players in the seed), enter your +command in chat prefaced by the string `!ap`. Example commands: + +`!ap buying gf 100k` -> Sends the message "buying gf 100k" to the server +`!ap !hint Area: Lumbridge` -> Attempts to hint for the "Area: Lumbridge" item. Results will appear in your chat box. + +Other server messages, such as chat, will appear in your chat box, prefaced by the Archipelago icon. + +## What items and locations get shuffled? +Items: +- Every map region (at least one chunk but sometimes more) +- Weapon tiers from iron to Rune (bronze is available from the start) +- Armor tiers from iron to Rune (bronze is available from the start) +- Two Spell Tiers (bolt and blast spells) +- Three tiers of Ranged Armor (leather, studded leather + vambraces, green dragonhide) +- Three tiers of Ranged Weapons (oak, willow, maple bows and their respective highest tier of arrows) + +Locations: +* Every Quest is a location that will always be included in every seed +* A random assortment of tasks, separated into categories based on the skill required. +These task categories can have different weights, minimums, and maximums based on your options. + * For a full list of Locations, items, and regions, see the +[Logic Document](https://docs.google.com/spreadsheets/d/1R8Cm8L6YkRWeiN7uYrdru8Vc1DlJ0aFAinH_fwhV8aU/edit?usp=sharing) + +## Which items can be in another player's world? +Any item or region unlock can be found in any player's world. + +## What does another world's item look like in Old School Runescape? +Upon completing a task, the item and recipient will be listed in the player's chatbox. + +## When the player receives an item, what happens? +In addition to the message appearing in the chatbox, a UI window will appear listing the item and who sent it. +These boxes also appear when connecting to a seed already in progress to list the items you have acquired while offline. +The sidebar will list all received items below the task list, starting with regions, then showing the highest tier of +equipment in each category. \ No newline at end of file diff --git a/worlds/osrs/docs/setup_en.md b/worlds/osrs/docs/setup_en.md new file mode 100644 index 0000000000..47c1c8f16f --- /dev/null +++ b/worlds/osrs/docs/setup_en.md @@ -0,0 +1,58 @@ +# Setup Guide for Old School Runescape + +## Required Software + +- [RuneLite](https://runelite.net/) +- If the account being used has been migrated to a Jagex Account, the [Jagex Launcher](https://www.jagex.com/en-GB/launcher) +will also be necessary to run RuneLite + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +Your YAML file contains a set of configuration options which provide the generator with information about how it should +generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy +an experience customized for their taste, and different players in the same multiworld can all have different options. + +### Where do I get a YAML file? + +You can customize your settings by visiting the +[Old School Runescape Player Options Page](/games/Old%20School%20Runescape/player-options). + +## Joining a MultiWorld Game + +### Install the RuneLite Plugins +Open RuneLite and click on the wrench icon on the right side. From there, click on the plug icon to access the +Plugin Hub. You will need to install the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) +and [Region Locker Plugin](https://github.com/slaytostay/region-locker). The Region Locker plugin +will include three plugins; only the `Region Locker` plugin itself is required. The `Region Locker GPU` plugin can be +used to display locked chunks in gray, but is incompatible with other GPU plugins such as 117's HD OSRS and can be +disabled. + +### Create a new OSRS Account +The OSRS Randomizer assumes you are playing on a newly created f2p Ironman account. As such, you will need to [create a +new Runescape account](https://secure.runescape.com/m=account-creation/create_account?theme=oldschool). + +If you already have a [Jagex Account](https://www.jagex.com/en-GB/accounts) you can add up to 20 characters on +one account through the Jagex Launcher. Note that there is currently no way to _remove_ characters +from a Jagex Account, as such, you might want to create a separate account to hold your Archipelago +characters if you intend to use your main Jagex account for more characters in the future. + +**Protip**: In order to avoid having to remember random email addresses for many accounts, take advantage of an email +alias, a feature supported by most email providers. Any text after a `+` in your email address will redirect to your +normal address, but the email will be recognized by the Jagex login as a new email address. For example, if your email +were `Archipelago@gmail.com`, entering `Archipelago+OSRSRandomizer@gmail.com` would cause the confirmation email to +be sent to your primary address, but the alias can be used to create a new account. One recommendation would be to +include the date of generation in the account, such as `Archipelago+APYYMMDD@gmail.com` for easy memorability. + +After creating an account, you may run through Tutorial Island without connecting; the randomizer has no +effect on the Tutorial. + +### Connect to the Multiserver +In the Archipelago Plugin, enter your server information. The `Auto Reconnect on Login For` field should remain blank; +it will be populated by the character name you first connect with, and it will reconnect to the AP server whenever that +character logs in. Open the Archipelago panel on the right-hand side to connect to the multiworld while logged in to +a game world to associate this character to the randomizer. + +For further information about how to connect to the server in the RuneLite plugin, +please see the [Archipelago Plugin](https://github.com/digiholic/osrs-archipelago) instructions. \ No newline at end of file From 6297a4efa552173f8a83f6dfa3b7f4fca828f4e4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 7 Aug 2024 12:01:41 -0400 Subject: [PATCH 091/222] TUNIC: Fix missing traversal req #3740 --- worlds/tunic/er_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index f49e7dff3e..78a934b490 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1183,6 +1183,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], "Library Hero's Grave Region": [], + "Library Hall to Rotunda": + [], }, "Library Hero's Grave Region": { "Library Hall": From cf6661439e006d17aaca3fb814da927e7a0bae09 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 7 Aug 2024 12:18:50 -0400 Subject: [PATCH 092/222] TUNIC: Sort entrances in the spoiler log (#3733) * Sort entrances in spoiler log * Rearrange portal list to closer match the vanilla game order, for better spoiler and because I already did this mod-side * Add break (thanks vi) --- worlds/tunic/er_data.py | 230 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 32 +++++- 2 files changed, 144 insertions(+), 118 deletions(-) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 78a934b490..e999026dec 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -169,7 +169,61 @@ portal_mapping: List[Portal] = [ destination="Overworld Redux", tag="_rafters"), Portal(name="Temple Door Exit", region="Sealed Temple", destination="Overworld Redux", tag="_main"), - + + Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", + destination="Fortress Courtyard", tag="_"), + Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", + destination="East Forest Redux", tag="_"), + Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", + destination="Overworld Redux", tag="_"), + Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", + destination="Forest Boss Room", tag="_"), + + Portal(name="Forest to Belltower", region="East Forest", + destination="Forest Belltower", tag="_"), + Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", + destination="East Forest Redux Laddercave", tag="_lower"), + Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", + destination="East Forest Redux Laddercave", tag="_gate"), + Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", + destination="East Forest Redux Laddercave", tag="_upper"), + Portal(name="Forest to Far Shore", region="East Forest Portal", + destination="Transit", tag="_teleporter_forest teleporter"), + Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest", + destination="East Forest Redux Interior", tag="_lower"), + Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", + destination="East Forest Redux Interior", tag="_upper"), + Portal(name="Forest Grave Path Lower Entrance", region="East Forest", + destination="Sword Access", tag="_lower"), + Portal(name="Forest Grave Path Upper Entrance", region="East Forest", + destination="Sword Access", tag="_upper"), + + Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", + destination="East Forest Redux", tag="_upper"), + Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", + destination="East Forest Redux", tag="_lower"), + Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", + destination="RelicVoid", tag="_teleporter_relic plinth"), + + Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", + destination="East Forest Redux", tag="_upper"), + Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", + destination="East Forest Redux", tag="_lower"), + Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", + destination="East Forest Redux", tag="_gate"), + Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", + destination="Forest Boss Room", tag="_"), + + Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", + destination="East Forest Redux", tag="_lower"), + Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", + destination="East Forest Redux", tag="_upper"), + + Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", + destination="East Forest Redux Laddercave", tag="_"), + Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", + destination="Forest Belltower", tag="_"), + Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", destination="Overworld Redux", tag="_entrance"), Portal(name="Well to Well Boss", region="Beneath the Well Back", @@ -206,7 +260,66 @@ portal_mapping: List[Portal] = [ Portal(name="Magic Dagger House Exit", region="Magic Dagger House", destination="Archipelagos Redux", tag="_"), - + + Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", + destination="Fortress Reliquary", tag="_Lower"), + Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", + destination="Fortress Reliquary", tag="_Upper"), + Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", + destination="Fortress Main", tag="_Big Door"), + Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", + destination="Fortress East", tag="_"), + Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry", + destination="Fortress Basement", tag="_"), + Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", + destination="Forest Belltower", tag="_"), + Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", + destination="Overworld Redux", tag="_"), + Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", + destination="Shop", tag="_"), + + Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", + destination="Fortress Main", tag="_"), + Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", + destination="Fortress Courtyard", tag="_"), + + Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", + destination="Fortress Courtyard", tag="_Big Door"), + Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", + destination="Fortress Basement", tag="_"), + Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", + destination="Fortress Arena", tag="_"), + Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", + destination="Shop", tag="_"), + Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", + destination="Fortress East", tag="_upper"), + Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", + destination="Fortress East", tag="_lower"), + + Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", + destination="Fortress Main", tag="_lower"), + Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", + destination="Fortress Courtyard", tag="_"), + Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", + destination="Fortress Main", tag="_upper"), + + Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", + destination="Fortress Courtyard", tag="_Lower"), + Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", + destination="Fortress Courtyard", tag="_Upper"), + Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region", + destination="Dusty", tag="_"), + + Portal(name="Dusty Exit", region="Fortress Leaf Piles", + destination="Fortress Reliquary", tag="_"), + + Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", + destination="Fortress Main", tag="_"), + Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", + destination="Transit", tag="_teleporter_spidertank"), + Portal(name="Atoll Upper Exit", region="Ruined Atoll", destination="Overworld Redux", tag="_upper"), Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", @@ -263,119 +376,6 @@ portal_mapping: List[Portal] = [ Portal(name="Librarian Arena Exit", region="Library Arena", destination="Library Lab", tag="_"), - Portal(name="Forest to Belltower", region="East Forest", - destination="Forest Belltower", tag="_"), - Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", - destination="East Forest Redux Laddercave", tag="_lower"), - Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", - destination="East Forest Redux Laddercave", tag="_gate"), - Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", - destination="East Forest Redux Laddercave", tag="_upper"), - Portal(name="Forest to Far Shore", region="East Forest Portal", - destination="Transit", tag="_teleporter_forest teleporter"), - Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest", - destination="East Forest Redux Interior", tag="_lower"), - Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", - destination="East Forest Redux Interior", tag="_upper"), - Portal(name="Forest Grave Path Lower Entrance", region="East Forest", - destination="Sword Access", tag="_lower"), - Portal(name="Forest Grave Path Upper Entrance", region="East Forest", - destination="Sword Access", tag="_upper"), - - Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", - destination="East Forest Redux", tag="_upper"), - Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", - destination="East Forest Redux", tag="_lower"), - Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", - destination="East Forest Redux", tag="_gate"), - Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", - destination="Forest Boss Room", tag="_"), - - Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", - destination="East Forest Redux", tag="_upper"), - Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", - destination="East Forest Redux", tag="_lower"), - Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", - destination="RelicVoid", tag="_teleporter_relic plinth"), - - Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", - destination="East Forest Redux", tag="_lower"), - Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", - destination="East Forest Redux", tag="_upper"), - - Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", - destination="East Forest Redux Laddercave", tag="_"), - Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", - destination="Forest Belltower", tag="_"), - - Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", - destination="Fortress Courtyard", tag="_"), - Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", - destination="East Forest Redux", tag="_"), - Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", - destination="Overworld Redux", tag="_"), - Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", - destination="Forest Boss Room", tag="_"), - - Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", - destination="Fortress Reliquary", tag="_Lower"), - Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", - destination="Fortress Reliquary", tag="_Upper"), - Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", - destination="Fortress Main", tag="_Big Door"), - Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", - destination="Fortress East", tag="_"), - Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry", - destination="Fortress Basement", tag="_"), - Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", - destination="Forest Belltower", tag="_"), - Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", - destination="Overworld Redux", tag="_"), - Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", - destination="Shop", tag="_"), - - Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", - destination="Fortress Main", tag="_"), - Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", - destination="Fortress Courtyard", tag="_"), - - Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", - destination="Fortress Courtyard", tag="_Big Door"), - Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", - destination="Fortress Basement", tag="_"), - Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", - destination="Fortress Arena", tag="_"), - Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", - destination="Shop", tag="_"), - Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", - destination="Fortress East", tag="_upper"), - Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", - destination="Fortress East", tag="_lower"), - - Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", - destination="Fortress Main", tag="_lower"), - Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", - destination="Fortress Courtyard", tag="_"), - Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", - destination="Fortress Main", tag="_upper"), - - Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", - destination="Fortress Courtyard", tag="_Lower"), - Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", - destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", - destination="Fortress Courtyard", tag="_Upper"), - Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region", - destination="Dusty", tag="_"), - - Portal(name="Dusty Exit", region="Fortress Leaf Piles", - destination="Fortress Reliquary", tag="_"), - - Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", - destination="Fortress Main", tag="_"), - Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", - destination="Transit", tag="_teleporter_spidertank"), - Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", destination="Mountaintop", tag="_"), Portal(name="Mountain to Quarry", region="Lower Mountain", diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 0bd8c5e806..a4295cf9f2 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -24,10 +24,10 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} if world.options.entrance_rando: portal_pairs = pair_portals(world) - # output the entrances to the spoiler log here for convenience - for portal1, portal2 in portal_pairs.items(): - world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + sorted_portal_pairs = sort_portals(portal_pairs) + for portal1, portal2 in sorted_portal_pairs.items(): + world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) else: portal_pairs = vanilla_portals() @@ -504,3 +504,29 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) return connected_regions + + +# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list +def sort_portals(portal_pairs: Dict[Portal, Portal]) -> Dict[str, str]: + sorted_pairs: Dict[str, str] = {} + reference_list: List[str] = [portal.name for portal in portal_mapping] + reference_list.append("Shop Portal") + + # note: this is not necessary yet since the shop portals aren't numbered yet -- they will be when decoupled happens + # due to plando, there can be a variable number of shops + # I could either do it like this, or just go up to like 200, this seemed better + # shop_count = 0 + # for portal1, portal2 in portal_pairs.items(): + # if portal1.name.startswith("Shop"): + # shop_count += 1 + # if portal2.name.startswith("Shop"): + # shop_count += 1 + # reference_list.extend([f"Shop Portal {i + 1}" for i in range(shop_count)]) + + for name in reference_list: + for portal1, portal2 in portal_pairs.items(): + if name == portal1.name: + sorted_pairs[portal1.name] = portal2.name + break + return sorted_pairs + From 74697b679ea4bd376647c69107891bb79b7b9c56 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:56:22 -0400 Subject: [PATCH 093/222] KH2: Update the docs to support steam in the setup guide (#3711) * doc updates * add steam link * Update worlds/kh2/docs/setup_en.md Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update setup_en.md * Forgot to include these * Consistent styling * :) * version 3.3.0 --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/kh2/docs/setup_en.md | 57 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index c6fdb020b8..ed4d90bb54 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -1,22 +1,25 @@ # Kingdom Hearts 2 Archipelago Setup Guide +

Quick Links

- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en) - [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)

Required Software:

- `Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) -- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
- 1. `3.2.0 OpenKH Mod Manager with Panacea`
- 2. `Lua Backend from the OpenKH Mod Manager` - 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`
+`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/) + +- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) + 1. `Version 3.3.0 or greater OpenKH Mod Manager with Panacea` + 2. `Lua Backend from the OpenKH Mod Manager` + 3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager` - Needed for Archipelago - 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
- 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`
- 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager`
- 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`
+ 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases) + 2. `Install the Archipelago Companion mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager` + 3. `Install the Archipelago Quality Of Life mod from JaredWeakStrike/AP_QOL using OpenKH Mod Manager` + 4. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager` 5. `AP Randomizer Seed` +

Required: Archipelago Companion Mod

Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`
@@ -24,6 +27,7 @@ Have this mod second-highest priority below the .zip seed.
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.

Required: Auto Save Mod

+ Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save

Installing A Seed

@@ -33,33 +37,33 @@ Make sure the seed is on the top of the list (Highest Priority)
After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.

What the Mod Manager Should Look Like.

+ ![image](https://i.imgur.com/Si4oZ8w.png)

Using the KH2 Client

-Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`
Most checks will be sent to you anywhere outside a load or cutscene.
`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.` -
+

KH2 Client should look like this:

+ ![image](https://i.imgur.com/qP6CmV8.png) -
+ Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected -

Common Pitfalls

-- Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder. - - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) -
-- Not having Lua Backend Configured Correctly. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. -
-- Loading into Simulated Twilight Town Instead of the GOA. - - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps. +- Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder. + - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) +- Not having Lua Backend Configured Correctly. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. + +- Loading into Simulated Twilight Town Instead of the GOA. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.

Best Practices

@@ -70,8 +74,11 @@ Enter `The room's port number` into the top box where the x's are and pr - Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed

Logic Sheet

+ Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing) +

F.A.Q.

+ - Why is my Client giving me a "Cannot Open Process: " error? - Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin. - Why is my HP/MP continuously increasing without stopping? @@ -83,11 +90,13 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Why did I not load into the correct visit? - You need to trigger a cutscene or visit The World That Never Was for it to register that you have received the item. - What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported: version `1.0.0.8_WW`. + - Currently the `only` supported versions are `Epic Games Version 1.0.0.9_WW` and `Steam Build Version 14716933`. - Why am I getting wallpapered while going into a world for the first time? - - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. + - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. - Why am I not getting magic? - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why did I crash after picking my dream weapon? + - This is normally caused by having an outdated GOA mod or having an outdated panacea and/or luabackend. To fix this rerun the setup wizard and reinstall luabackend and panacea. Also make sure all your mods are up-to-date. - Why did I crash? - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify @@ -99,5 +108,3 @@ Have any questions on what's in logic? This spreadsheet made by Bulcon has the a - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. - How do I load an auto save? - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. - - From 05ce29f7dcad5af14cd2ffb89798695fc1c7c688 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Wed, 7 Aug 2024 22:57:07 +0100 Subject: [PATCH 094/222] RoR2: Remove recursion from explore mode access rules (#3681) The access rules for " Chest n", " Shrine n" etc. locations recursively called state.can_reach() for the n-1 location name, with the n=1 location being the only location to have the actual access rule set. This patch removes the recursion, instead setting the actual access rule directly on each location, increasing the performance of checking accessibility of n>1 locations. Risk of Rain 2 was already quite fast to generate despite the recursion in the access rules, but with this patch, generating a multiworld with 200 copies of the template RoR2 yaml (and progression balancing disabled through a meta.yaml) goes from about 18s to about 6s for me. From generating the same seed before and after this patch, the same result is produced. --- worlds/ror2/rules.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/worlds/ror2/rules.py b/worlds/ror2/rules.py index 2e6b018f42..f0ab9f2831 100644 --- a/worlds/ror2/rules.py +++ b/worlds/ror2/rules.py @@ -31,23 +31,17 @@ def has_all_items(multiworld: MultiWorld, items: Set[str], region: str, player: # Checks to see if chest/shrine are accessible def has_location_access_rule(multiworld: MultiWorld, environment: str, player: int, item_number: int, item_type: str)\ -> None: - if item_number == 1: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) + location_name = f"{environment}: {item_type} {item_number}" + if item_type == "Scavenger": # scavengers need to be locked till after a full loop since that is when they are capable of spawning. # (While technically the requirement is just beating 5 stages, this will ensure that the player will have # a long enough run to have enough director credits for scavengers and # help prevent being stuck in the same stages until that point). - if item_type == "Scavenger": - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: state.has(environment, player) and state.has("Stage 5", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) and state.has("Stage 5", player) else: - multiworld.get_location(f"{environment}: {item_type} {item_number}", player).access_rule = \ - lambda state: check_location(state, environment, player, item_number, item_type) - - -def check_location(state, environment: str, player: int, item_number: int, item_name: str) -> bool: - return state.can_reach(f"{environment}: {item_name} {item_number - 1}", "Location", player) + multiworld.get_location(location_name, player).access_rule = \ + lambda state: state.has(environment, player) def set_rules(ror2_world: "RiskOfRainWorld") -> None: From 575c338aa3c895aa4d10824be5380b4e094b55e1 Mon Sep 17 00:00:00 2001 From: Louis M Date: Wed, 7 Aug 2024 18:19:52 -0400 Subject: [PATCH 095/222] Aquaria: Logic bug fixes (#3679) * Fixing logic bugs * Require energy attack in the cathedral and energy form in the body * King Jelly can be beaten easily with only the Dual Form * I think that I have a problem with my left and right... * There is a monster that is blocking the path, soo need attack to pass * The Li cage is not accessible without the Sunken city boss * Removing useless space. Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Two more minors logic modification * Adapting tests to af9b6cd * Reformat the Region file --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/aquaria/Items.py | 2 +- worlds/aquaria/Locations.py | 20 +- worlds/aquaria/Regions.py | 446 +++++++++--------- worlds/aquaria/test/__init__.py | 2 +- worlds/aquaria/test/test_beast_form_access.py | 24 +- ...test_beast_form_or_arnassi_armor_access.py | 39 ++ .../aquaria/test/test_energy_form_access.py | 53 +-- .../test_energy_form_or_dual_form_access.py | 92 ++++ worlds/aquaria/test/test_fish_form_access.py | 4 +- worlds/aquaria/test/test_light_access.py | 1 - .../aquaria/test/test_spirit_form_access.py | 1 - 11 files changed, 396 insertions(+), 288 deletions(-) create mode 100644 worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py create mode 100644 worlds/aquaria/test/test_energy_form_or_dual_form_access.py diff --git a/worlds/aquaria/Items.py b/worlds/aquaria/Items.py index 34557d95d0..f822d675e6 100644 --- a/worlds/aquaria/Items.py +++ b/worlds/aquaria/Items.py @@ -99,7 +99,7 @@ item_table = { "Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume "Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus "Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha - "Arnassi Armor": ItemData(698023, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_seahorse_costume + "Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume "Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag "King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull "Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index 2eb9d1e9a2..f6e098103f 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -45,7 +45,7 @@ class AquariaLocations: "Home Water, bulb below the grouper fish": 698058, "Home Water, bulb in the path below Nautilus Prime": 698059, "Home Water, bulb in the little room above the grouper fish": 698060, - "Home Water, bulb in the end of the left path from the Verse Cave": 698061, + "Home Water, bulb in the end of the path close to the Verse Cave": 698061, "Home Water, bulb in the top left path": 698062, "Home Water, bulb in the bottom left room": 698063, "Home Water, bulb close to Naija's Home": 698064, @@ -67,7 +67,7 @@ class AquariaLocations: locations_song_cave = { "Song Cave, Erulian spirit": 698206, - "Song Cave, bulb in the top left part": 698071, + "Song Cave, bulb in the top right part": 698071, "Song Cave, bulb in the big anemone room": 698072, "Song Cave, bulb in the path to the singing statues": 698073, "Song Cave, bulb under the rock in the path to the singing statues": 698074, @@ -152,6 +152,9 @@ class AquariaLocations: locations_arnassi_path = { "Arnassi Ruins, Arnassi Statue": 698164, + } + + locations_arnassi_cave_transturtle = { "Arnassi Ruins, Transturtle": 698217, } @@ -269,9 +272,12 @@ class AquariaLocations: } locations_forest_bl = { + "Kelp Forest bottom left area, Transturtle": 698212, + } + + locations_forest_bl_sc = { "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, Transturtle": 698212, } locations_forest_br = { @@ -370,7 +376,7 @@ class AquariaLocations: locations_sun_temple_r = { "Sun Temple, first bulb of the temple": 698091, - "Sun Temple, bulb on the left part": 698092, + "Sun Temple, bulb on the right part": 698092, "Sun Temple, bulb in the hidden room of the right part": 698093, "Sun Temple, Sun Key": 698182, } @@ -402,6 +408,9 @@ class AquariaLocations: "Abyss right area, bulb in the middle path": 698110, "Abyss right area, bulb behind the rock in the middle path": 698111, "Abyss right area, bulb in the left green room": 698112, + } + + locations_abyss_r_transturtle = { "Abyss right area, Transturtle": 698214, } @@ -499,6 +508,7 @@ location_table = { **AquariaLocations.locations_skeleton_path_sc, **AquariaLocations.locations_arnassi, **AquariaLocations.locations_arnassi_path, + **AquariaLocations.locations_arnassi_cave_transturtle, **AquariaLocations.locations_arnassi_crab_boss, **AquariaLocations.locations_sun_temple_l, **AquariaLocations.locations_sun_temple_r, @@ -509,6 +519,7 @@ location_table = { **AquariaLocations.locations_abyss_l, **AquariaLocations.locations_abyss_lb, **AquariaLocations.locations_abyss_r, + **AquariaLocations.locations_abyss_r_transturtle, **AquariaLocations.locations_energy_temple_1, **AquariaLocations.locations_energy_temple_2, **AquariaLocations.locations_energy_temple_3, @@ -530,6 +541,7 @@ location_table = { **AquariaLocations.locations_forest_tr, **AquariaLocations.locations_forest_tr_fp, **AquariaLocations.locations_forest_bl, + **AquariaLocations.locations_forest_bl_sc, **AquariaLocations.locations_forest_br, **AquariaLocations.locations_forest_boss, **AquariaLocations.locations_forest_boss_entrance, diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index 93c02d4e67..3ec1fb880e 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -14,97 +14,112 @@ from worlds.generic.Rules import add_rule, set_rule # Every condition to connect regions -def _has_hot_soup(state:CollectionState, player: int) -> bool: +def _has_hot_soup(state: CollectionState, player: int) -> bool: """`player` in `state` has the hotsoup item""" - return state.has("Hot soup", player) + return state.has_any({"Hot soup", "Hot soup x 2"}, player) -def _has_tongue_cleared(state:CollectionState, player: int) -> bool: +def _has_tongue_cleared(state: CollectionState, player: int) -> bool: """`player` in `state` has the Body tongue cleared item""" return state.has("Body tongue cleared", player) -def _has_sun_crystal(state:CollectionState, player: int) -> bool: +def _has_sun_crystal(state: CollectionState, player: int) -> bool: """`player` in `state` has the Sun crystal item""" return state.has("Has sun crystal", player) and _has_bind_song(state, player) -def _has_li(state:CollectionState, player: int) -> bool: +def _has_li(state: CollectionState, player: int) -> bool: """`player` in `state` has Li in its team""" return state.has("Li and Li song", player) -def _has_damaging_item(state:CollectionState, player: int) -> bool: +def _has_damaging_item(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" - return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", - "Baby Piranha", "Baby Blaster"}, player) + return state.has_any({"Energy form", "Nature form", "Beast form", "Li and Li song", "Baby Nautilus", + "Baby Piranha", "Baby Blaster"}, player) -def _has_shield_song(state:CollectionState, player: int) -> bool: +def _has_energy_attack_item(state: CollectionState, player: int) -> bool: + """`player` in `state` has items that can do a lot of damage (enough to beat bosses)""" + return _has_energy_form(state, player) or _has_dual_form(state, player) + + +def _has_shield_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the shield song item""" return state.has("Shield song", player) -def _has_bind_song(state:CollectionState, player: int) -> bool: +def _has_bind_song(state: CollectionState, player: int) -> bool: """`player` in `state` has the bind song item""" return state.has("Bind song", player) -def _has_energy_form(state:CollectionState, player: int) -> bool: +def _has_energy_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the energy form item""" return state.has("Energy form", player) -def _has_beast_form(state:CollectionState, player: int) -> bool: +def _has_beast_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the beast form item""" return state.has("Beast form", player) -def _has_nature_form(state:CollectionState, player: int) -> bool: +def _has_beast_and_soup_form(state: CollectionState, player: int) -> bool: + """`player` in `state` has the beast form item""" + return _has_beast_form(state, player) and _has_hot_soup(state, player) + + +def _has_beast_form_or_arnassi_armor(state: CollectionState, player: int) -> bool: + """`player` in `state` has the beast form item""" + return _has_beast_form(state, player) or state.has("Arnassi Armor", player) + + +def _has_nature_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the nature form item""" return state.has("Nature form", player) -def _has_sun_form(state:CollectionState, player: int) -> bool: +def _has_sun_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the sun form item""" return state.has("Sun form", player) -def _has_light(state:CollectionState, player: int) -> bool: +def _has_light(state: CollectionState, player: int) -> bool: """`player` in `state` has the light item""" return state.has("Baby Dumbo", player) or _has_sun_form(state, player) -def _has_dual_form(state:CollectionState, player: int) -> bool: +def _has_dual_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the dual form item""" return _has_li(state, player) and state.has("Dual form", player) -def _has_fish_form(state:CollectionState, player: int) -> bool: +def _has_fish_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the fish form item""" return state.has("Fish form", player) -def _has_spirit_form(state:CollectionState, player: int) -> bool: +def _has_spirit_form(state: CollectionState, player: int) -> bool: """`player` in `state` has the spirit form item""" return state.has("Spirit form", player) -def _has_big_bosses(state:CollectionState, player: int) -> bool: +def _has_big_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" return state.has_all({"Fallen God beated", "Mithalan God beated", "Drunian God beated", - "Sun God beated", "The Golem beated"}, player) + "Sun God beated", "The Golem beated"}, player) -def _has_mini_bosses(state:CollectionState, player: int) -> bool: +def _has_mini_bosses(state: CollectionState, player: int) -> bool: """`player` in `state` has beated every big bosses""" return state.has_all({"Nautilus Prime beated", "Blaster Peg Prime beated", "Mergog beated", - "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated", - "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player) + "Mithalan priests beated", "Octopus Prime beated", "Crabbius Maximus beated", + "Mantis Shrimp Prime beated", "King Jellyfish God Prime beated"}, player) -def _has_secrets(state:CollectionState, player: int) -> bool: - return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"},player) +def _has_secrets(state: CollectionState, player: int) -> bool: + return state.has_all({"First secret obtained", "Second secret obtained", "Third secret obtained"}, player) class AquariaRegions: @@ -134,6 +149,7 @@ class AquariaRegions: skeleton_path: Region skeleton_path_sc: Region arnassi: Region + arnassi_cave_transturtle: Region arnassi_path: Region arnassi_crab_boss: Region simon: Region @@ -152,6 +168,7 @@ class AquariaRegions: forest_tr: Region forest_tr_fp: Region forest_bl: Region + forest_bl_sc: Region forest_br: Region forest_boss: Region forest_boss_entrance: Region @@ -179,6 +196,7 @@ class AquariaRegions: abyss_l: Region abyss_lb: Region abyss_r: Region + abyss_r_transturtle: Region ice_cave: Region bubble_cave: Region bubble_cave_boss: Region @@ -213,7 +231,7 @@ class AquariaRegions: """ def __add_region(self, hint: str, - locations: Optional[Dict[str, Optional[int]]]) -> Region: + locations: Optional[Dict[str, int]]) -> Region: """ Create a new Region, add it to the `world` regions and return it. Be aware that this function have a side effect on ``world`.`regions` @@ -236,7 +254,7 @@ class AquariaRegions: self.home_water_nautilus = self.__add_region("Home Water, Nautilus nest", AquariaLocations.locations_home_water_nautilus) self.home_water_transturtle = self.__add_region("Home Water, turtle room", - AquariaLocations.locations_home_water_transturtle) + AquariaLocations.locations_home_water_transturtle) self.naija_home = self.__add_region("Naija's Home", AquariaLocations.locations_naija_home) self.song_cave = self.__add_region("Song Cave", AquariaLocations.locations_song_cave) @@ -280,6 +298,8 @@ class AquariaRegions: self.arnassi = self.__add_region("Arnassi Ruins", AquariaLocations.locations_arnassi) self.arnassi_path = self.__add_region("Arnassi Ruins, back entrance path", AquariaLocations.locations_arnassi_path) + self.arnassi_cave_transturtle = self.__add_region("Arnassi Ruins, transturtle area", + AquariaLocations.locations_arnassi_cave_transturtle) self.arnassi_crab_boss = self.__add_region("Arnassi Ruins, Crabbius Maximus lair", AquariaLocations.locations_arnassi_crab_boss) @@ -302,9 +322,9 @@ class AquariaRegions: AquariaLocations.locations_cathedral_r) 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", + self.cathedral_boss_r = self.__add_region("Mithalas Cathedral, Mithalan God room", None) + self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", AquariaLocations.locations_cathedral_boss) - self.cathedral_boss_l = self.__add_region("Mithalas Cathedral, after Mithalan God room", None) def __create_forest(self) -> None: """ @@ -320,6 +340,8 @@ class AquariaRegions: AquariaLocations.locations_forest_tr_fp) self.forest_bl = self.__add_region("Kelp Forest bottom left area", AquariaLocations.locations_forest_bl) + self.forest_bl_sc = self.__add_region("Kelp Forest bottom left area, spirit crystals", + AquariaLocations.locations_forest_bl_sc) self.forest_br = self.__add_region("Kelp Forest bottom right area", AquariaLocations.locations_forest_br) self.forest_sprite_cave = self.__add_region("Kelp Forest spirit cave", @@ -375,9 +397,9 @@ class AquariaRegions: self.sun_temple_r = self.__add_region("Sun Temple right area", AquariaLocations.locations_sun_temple_r) self.sun_temple_boss_path = self.__add_region("Sun Temple before boss area", - AquariaLocations.locations_sun_temple_boss_path) + AquariaLocations.locations_sun_temple_boss_path) self.sun_temple_boss = self.__add_region("Sun Temple boss area", - AquariaLocations.locations_sun_temple_boss) + AquariaLocations.locations_sun_temple_boss) def __create_abyss(self) -> None: """ @@ -388,6 +410,8 @@ class AquariaRegions: AquariaLocations.locations_abyss_l) self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb) self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r) + self.abyss_r_transturtle = self.__add_region("Abyss right area, transturtle", + AquariaLocations.locations_abyss_r_transturtle) self.ice_cave = self.__add_region("Ice Cave", AquariaLocations.locations_ice_cave) self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave) self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss) @@ -407,7 +431,7 @@ class AquariaRegions: self.sunken_city_r = self.__add_region("Sunken City right area", AquariaLocations.locations_sunken_city_r) self.sunken_city_boss = self.__add_region("Sunken City boss area", - AquariaLocations.locations_sunken_city_boss) + AquariaLocations.locations_sunken_city_boss) def __create_body(self) -> None: """ @@ -427,7 +451,7 @@ class AquariaRegions: self.final_boss_tube = self.__add_region("The Body, final boss area turtle room", AquariaLocations.locations_final_boss_tube) self.final_boss = self.__add_region("The Body, final boss", - AquariaLocations.locations_final_boss) + AquariaLocations.locations_final_boss) self.final_boss_end = self.__add_region("The Body, final boss area", None) def __connect_one_way_regions(self, source_name: str, destination_name: str, @@ -455,8 +479,8 @@ class AquariaRegions: """ Connect entrances of the different regions around `home_water` """ - self.__connect_regions("Menu", "Verse Cave right area", - self.menu, self.verse_cave_r) + self.__connect_one_way_regions("Menu", "Verse Cave right area", + self.menu, self.verse_cave_r) self.__connect_regions("Verse Cave left area", "Verse Cave right area", self.verse_cave_l, self.verse_cave_r) self.__connect_regions("Verse Cave", "Home Water", self.verse_cave_l, self.home_water) @@ -464,7 +488,8 @@ class AquariaRegions: self.__connect_regions("Home Water", "Song Cave", self.home_water, self.song_cave) self.__connect_regions("Home Water", "Home Water, nautilus nest", self.home_water, self.home_water_nautilus, - lambda state: _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player) and + _has_bind_song(state, self.player)) self.__connect_regions("Home Water", "Home Water transturtle room", self.home_water, self.home_water_transturtle) self.__connect_regions("Home Water", "Energy Temple first area", @@ -472,7 +497,7 @@ class AquariaRegions: lambda state: _has_bind_song(state, self.player)) self.__connect_regions("Home Water", "Energy Temple_altar", self.home_water, self.energy_temple_altar, - lambda state: _has_energy_form(state, self.player) and + lambda state: _has_energy_attack_item(state, self.player) and _has_bind_song(state, self.player)) self.__connect_regions("Energy Temple first area", "Energy Temple second area", self.energy_temple_1, self.energy_temple_2, @@ -482,28 +507,28 @@ class AquariaRegions: lambda state: _has_fish_form(state, self.player)) self.__connect_regions("Energy Temple idol room", "Energy Temple boss area", self.energy_temple_idol, self.energy_temple_boss, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player) and + _has_fish_form(state, self.player)) self.__connect_one_way_regions("Energy Temple first area", "Energy Temple boss area", self.energy_temple_1, self.energy_temple_boss, lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Energy Temple boss area", "Energy Temple first area", self.energy_temple_boss, self.energy_temple_1, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_regions("Energy Temple second area", "Energy Temple third area", self.energy_temple_2, self.energy_temple_3, - lambda state: _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Energy Temple boss area", "Energy Temple blaster room", self.energy_temple_boss, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Energy Temple first area", "Energy Temple blaster room", self.energy_temple_1, self.energy_temple_blaster_room, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_beast_form(state, self.player)) self.__connect_regions("Home Water", "Open Water top left area", self.home_water, self.openwater_tl) @@ -520,7 +545,7 @@ class AquariaRegions: self.openwater_tl, self.forest_br) self.__connect_regions("Open Water top right area", "Open Water top right area, turtle room", self.openwater_tr, self.openwater_tr_turtle, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_regions("Open Water top right area", "Open Water bottom right area", self.openwater_tr, self.openwater_br) self.__connect_regions("Open Water top right area", "Mithalas City", @@ -529,10 +554,9 @@ class AquariaRegions: self.openwater_tr, self.veil_bl) self.__connect_one_way_regions("Open Water top right area", "Veil bottom right", self.openwater_tr, self.veil_br, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_one_way_regions("Veil bottom right", "Open Water top right area", - self.veil_br, self.openwater_tr, - lambda state: _has_beast_form(state, self.player)) + self.veil_br, self.openwater_tr) self.__connect_regions("Open Water bottom left area", "Open Water bottom right area", self.openwater_bl, self.openwater_br) self.__connect_regions("Open Water bottom left area", "Skeleton path", @@ -551,10 +575,14 @@ class AquariaRegions: self.arnassi, self.openwater_br) self.__connect_regions("Arnassi", "Arnassi path", self.arnassi, self.arnassi_path) + self.__connect_regions("Arnassi ruins, transturtle area", "Arnassi path", + self.arnassi_cave_transturtle, self.arnassi_path, + lambda state: _has_fish_form(state, self.player)) self.__connect_one_way_regions("Arnassi path", "Arnassi crab boss area", self.arnassi_path, self.arnassi_crab_boss, - lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player) and + (_has_energy_attack_item(state, self.player) or + _has_nature_form(state, self.player))) self.__connect_one_way_regions("Arnassi crab boss area", "Arnassi path", self.arnassi_crab_boss, self.arnassi_path) @@ -564,61 +592,62 @@ class AquariaRegions: """ self.__connect_one_way_regions("Mithalas City", "Mithalas City top path", self.mithalas_city, self.mithalas_city_top_path, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) self.__connect_one_way_regions("Mithalas City_top_path", "Mithalas City", self.mithalas_city_top_path, self.mithalas_city) self.__connect_regions("Mithalas City", "Mithalas City home with fishpass", self.mithalas_city, self.mithalas_city_fishpass, lambda state: _has_fish_form(state, self.player)) self.__connect_regions("Mithalas City", "Mithalas castle", - self.mithalas_city, self.cathedral_l, - lambda state: _has_fish_form(state, self.player)) + self.mithalas_city, self.cathedral_l) self.__connect_one_way_regions("Mithalas City top path", "Mithalas castle, flower tube", self.mithalas_city_top_path, self.cathedral_l_tube, lambda state: _has_nature_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Mithalas castle, flower tube area", "Mithalas City top path", self.cathedral_l_tube, self.mithalas_city_top_path, - lambda state: _has_beast_form(state, self.player) and - _has_nature_form(state, self.player)) + lambda state: _has_nature_form(state, self.player)) self.__connect_one_way_regions("Mithalas castle flower tube area", "Mithalas castle, spirit crystals", - self.cathedral_l_tube, self.cathedral_l_sc, - lambda state: _has_spirit_form(state, self.player)) + self.cathedral_l_tube, self.cathedral_l_sc, + lambda state: _has_spirit_form(state, self.player)) self.__connect_one_way_regions("Mithalas castle_flower tube area", "Mithalas castle", - self.cathedral_l_tube, self.cathedral_l, - lambda state: _has_spirit_form(state, self.player)) + self.cathedral_l_tube, self.cathedral_l, + lambda state: _has_spirit_form(state, self.player)) self.__connect_regions("Mithalas castle", "Mithalas castle, spirit crystals", self.cathedral_l, self.cathedral_l_sc, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Mithalas castle", "Cathedral boss left area", - self.cathedral_l, self.cathedral_boss_l, - lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player) and - _has_bind_song(state, self.player)) + self.__connect_one_way_regions("Mithalas castle", "Cathedral boss right area", + self.cathedral_l, self.cathedral_boss_r, + lambda state: _has_beast_form(state, self.player)) + self.__connect_one_way_regions("Cathedral boss left area", "Mithalas castle", + self.cathedral_boss_l, self.cathedral_l, + lambda state: _has_beast_form(state, self.player)) 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", "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("Mithalas Cathedral", "Mithalas Cathedral underground", - self.cathedral_r, self.cathedral_underground, - lambda state: _has_energy_form(state, self.player)) - 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", "Mithalas Cathedral underground", + lambda state: _has_beast_form(state, self.player)) + self.__connect_one_way_regions("Mithalas castle", "Mithalas Cathedral", + self.cathedral_l, self.cathedral_r, + lambda state: _has_bind_song(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Mithalas Cathedral", "Mithalas Cathedral underground", + self.cathedral_r, self.cathedral_underground) + self.__connect_one_way_regions("Mithalas Cathedral underground", "Mithalas Cathedral", + self.cathedral_underground, self.cathedral_r, + lambda state: _has_beast_form(state, self.player) and + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Mithalas Cathedral underground", "Cathedral boss right area", + self.cathedral_underground, self.cathedral_boss_r) + self.__connect_one_way_regions("Cathedral boss right 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", + self.__connect_one_way_regions("Cathedral boss right area", "Cathedral boss left area", self.cathedral_boss_r, self.cathedral_boss_l, lambda state: _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) + self.__connect_one_way_regions("Cathedral boss left area", "Cathedral boss right area", + self.cathedral_boss_l, self.cathedral_boss_r) def __connect_forest_regions(self) -> None: """ @@ -628,6 +657,12 @@ class AquariaRegions: self.forest_br, self.veil_bl) self.__connect_regions("Forest bottom right", "Forest bottom left area", self.forest_br, self.forest_bl) + self.__connect_one_way_regions("Forest bottom left area", "Forest bottom left area, spirit crystals", + self.forest_bl, self.forest_bl_sc, + lambda state: _has_energy_attack_item(state, self.player) or + _has_fish_form(state, self.player)) + self.__connect_one_way_regions("Forest bottom left area, spirit crystals", "Forest bottom left area", + self.forest_bl_sc, self.forest_bl) self.__connect_regions("Forest bottom right", "Forest top right area", self.forest_br, self.forest_tr) self.__connect_regions("Forest bottom left area", "Forest fish cave", @@ -641,7 +676,7 @@ class AquariaRegions: self.forest_tl, self.forest_tl_fp, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_fish_form(state, self.player)) self.__connect_regions("Forest top left area", "Forest top right area", self.forest_tl, self.forest_tr) @@ -649,7 +684,7 @@ class AquariaRegions: self.forest_tl, self.forest_boss_entrance) self.__connect_regions("Forest boss area", "Forest boss entrance", self.forest_boss, self.forest_boss_entrance, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_regions("Forest top right area", "Forest top right area fish pass", self.forest_tr, self.forest_tr_fp, lambda state: _has_fish_form(state, self.player)) @@ -663,7 +698,7 @@ class AquariaRegions: self.__connect_regions("Fermog cave", "Fermog boss", self.mermog_cave, self.mermog_boss, lambda state: _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) def __connect_veil_regions(self) -> None: """ @@ -681,8 +716,7 @@ class AquariaRegions: self.veil_b_sc, self.veil_br, lambda state: _has_spirit_form(state, self.player)) self.__connect_regions("Veil bottom right", "Veil top left area", - self.veil_br, self.veil_tl, - lambda state: _has_beast_form(state, self.player)) + self.veil_br, self.veil_tl) self.__connect_regions("Veil top left area", "Veil_top left area, fish pass", self.veil_tl, self.veil_tl_fp, lambda state: _has_fish_form(state, self.player)) @@ -691,20 +725,25 @@ class AquariaRegions: self.__connect_regions("Veil top left area", "Turtle cave", self.veil_tl, self.turtle_cave) self.__connect_regions("Turtle cave", "Turtle cave Bubble Cliff", - self.turtle_cave, self.turtle_cave_bubble, - lambda state: _has_beast_form(state, self.player)) + self.turtle_cave, self.turtle_cave_bubble) self.__connect_regions("Veil right of sun temple", "Sun Temple right area", self.veil_tr_r, self.sun_temple_r) - self.__connect_regions("Sun Temple right area", "Sun Temple left area", - self.sun_temple_r, self.sun_temple_l, - lambda state: _has_bind_song(state, self.player)) + self.__connect_one_way_regions("Sun Temple right area", "Sun Temple left area", + self.sun_temple_r, self.sun_temple_l, + lambda state: _has_bind_song(state, self.player) or + _has_light(state, self.player)) + self.__connect_one_way_regions("Sun Temple left area", "Sun Temple right area", + self.sun_temple_l, self.sun_temple_r, + lambda state: _has_light(state, self.player)) self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.sun_temple_l, self.veil_tr_l) self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", - self.sun_temple_l, self.sun_temple_boss_path) + self.sun_temple_l, self.sun_temple_boss_path, + lambda state: _has_light(state, self.player) or + _has_sun_crystal(state, self.player)) self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.sun_temple_boss_path, self.sun_temple_boss, - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) self.__connect_one_way_regions("Sun Temple boss area", "Veil left of sun temple", self.sun_temple_boss, self.veil_tr_l) self.__connect_regions("Veil left of sun temple", "Octo cave top path", @@ -712,7 +751,7 @@ class AquariaRegions: lambda state: _has_fish_form(state, self.player) and _has_sun_form(state, self.player) and _has_beast_form(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Veil left of sun temple", "Octo cave bottom path", self.veil_tr_l, self.octo_cave_b, lambda state: _has_fish_form(state, self.player)) @@ -728,16 +767,22 @@ class AquariaRegions: self.abyss_lb, self.sunken_city_r, lambda state: _has_li(state, self.player)) self.__connect_one_way_regions("Abyss left bottom area", "Body center area", - self.abyss_lb, self.body_c, - lambda state: _has_tongue_cleared(state, self.player)) + self.abyss_lb, self.body_c, + lambda state: _has_tongue_cleared(state, self.player)) self.__connect_one_way_regions("Body center area", "Abyss left bottom area", - self.body_c, self.abyss_lb) + self.body_c, self.abyss_lb) self.__connect_regions("Abyss left area", "King jellyfish cave", self.abyss_l, self.king_jellyfish_cave, - lambda state: _has_energy_form(state, self.player) and - _has_beast_form(state, self.player)) + lambda state: (_has_energy_form(state, self.player) and + _has_beast_form(state, self.player)) or + _has_dual_form(state, self.player)) self.__connect_regions("Abyss left area", "Abyss right area", self.abyss_l, self.abyss_r) + self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle", + self.abyss_r, self.abyss_r_transturtle) + self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area", + self.abyss_r_transturtle, self.abyss_r, + lambda state: _has_light(state, self.player)) self.__connect_regions("Abyss right area", "Inside the whale", self.abyss_r, self.whale, lambda state: _has_spirit_form(state, self.player) and @@ -747,13 +792,14 @@ class AquariaRegions: lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player) and _has_bind_song(state, self.player) and - _has_energy_form(state, self.player)) + _has_energy_attack_item(state, self.player)) self.__connect_regions("Abyss right area", "Ice Cave", self.abyss_r, self.ice_cave, lambda state: _has_spirit_form(state, self.player)) - self.__connect_regions("Abyss right area", "Bubble Cave", + self.__connect_regions("Ice cave", "Bubble Cave", self.ice_cave, self.bubble_cave, - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form(state, self.player) or + _has_hot_soup(state, self.player)) self.__connect_regions("Bubble Cave boss area", "Bubble Cave", self.bubble_cave, self.bubble_cave_boss, lambda state: _has_nature_form(state, self.player) and _has_bind_song(state, self.player) @@ -772,7 +818,7 @@ class AquariaRegions: self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and _has_sun_form(state, self.player) and - _has_energy_form(state, self.player) and + _has_energy_attack_item(state, self.player) and _has_bind_song(state, self.player)) def __connect_body_regions(self) -> None: @@ -780,11 +826,13 @@ class AquariaRegions: Connect entrances of the different regions around The Body """ self.__connect_regions("Body center area", "Body left area", - self.body_c, self.body_l) + self.body_c, self.body_l, + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Body center area", "Body right area top path", self.body_c, self.body_rt) self.__connect_regions("Body center area", "Body right area bottom path", - self.body_c, self.body_rb) + self.body_c, self.body_rb, + lambda state: _has_energy_form(state, self.player)) self.__connect_regions("Body center area", "Body bottom area", self.body_c, self.body_b, lambda state: _has_dual_form(state, self.player)) @@ -803,22 +851,12 @@ class AquariaRegions: self.__connect_one_way_regions("final boss third form area", "final boss end", self.final_boss, self.final_boss_end) - def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, region_target: Region, - rule=None) -> None: + def __connect_transturtle(self, item_source: str, item_target: str, region_source: Region, + region_target: Region) -> None: """Connect a single transturtle to another one""" if item_source != item_target: - if rule is None: - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, - lambda state: state.has(item_target, self.player)) - else: - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, rule) - - def __connect_arnassi_path_transturtle(self, item_source: str, item_target: str, region_source: Region, - region_target: Region) -> None: - """Connect the Arnassi Ruins transturtle to another one""" - self.__connect_one_way_regions(item_source, item_target, region_source, region_target, - lambda state: state.has(item_target, self.player) and - _has_fish_form(state, self.player)) + self.__connect_one_way_regions(item_source, item_target, region_source, region_target, + lambda state: state.has(item_target, self.player)) def _connect_transturtle_to_other(self, item: str, region: Region) -> None: """Connect a single transturtle to all others""" @@ -827,24 +865,10 @@ class AquariaRegions: self.__connect_transturtle(item, "Transturtle Open Water top right", region, self.openwater_tr_turtle) self.__connect_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) self.__connect_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) - self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) + self.__connect_transturtle(item, "Transturtle Abyss right", region, self.abyss_r_transturtle) self.__connect_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) self.__connect_transturtle(item, "Transturtle Simon Says", region, self.simon) - self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_path, - lambda state: state.has("Transturtle Arnassi Ruins", self.player) and - _has_fish_form(state, self.player)) - - def _connect_arnassi_path_transturtle_to_other(self, item: str, region: Region) -> None: - """Connect the Arnassi Ruins transturtle to all others""" - self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top left", region, self.veil_tl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Veil top right", region, self.veil_tr_l) - self.__connect_arnassi_path_transturtle(item, "Transturtle Open Water top right", region, - self.openwater_tr_turtle) - self.__connect_arnassi_path_transturtle(item, "Transturtle Forest bottom left", region, self.forest_bl) - self.__connect_arnassi_path_transturtle(item, "Transturtle Home Water", region, self.home_water_transturtle) - self.__connect_arnassi_path_transturtle(item, "Transturtle Abyss right", region, self.abyss_r) - self.__connect_arnassi_path_transturtle(item, "Transturtle Final Boss", region, self.final_boss_tube) - self.__connect_arnassi_path_transturtle(item, "Transturtle Simon Says", region, self.simon) + self.__connect_transturtle(item, "Transturtle Arnassi Ruins", region, self.arnassi_cave_transturtle) def __connect_transturtles(self) -> None: """Connect every transturtle with others""" @@ -853,10 +877,10 @@ class AquariaRegions: self._connect_transturtle_to_other("Transturtle Open Water top right", self.openwater_tr_turtle) self._connect_transturtle_to_other("Transturtle Forest bottom left", self.forest_bl) self._connect_transturtle_to_other("Transturtle Home Water", self.home_water_transturtle) - self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r) + self._connect_transturtle_to_other("Transturtle Abyss right", self.abyss_r_transturtle) self._connect_transturtle_to_other("Transturtle Final Boss", self.final_boss_tube) self._connect_transturtle_to_other("Transturtle Simon Says", self.simon) - self._connect_arnassi_path_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_path) + self._connect_transturtle_to_other("Transturtle Arnassi Ruins", self.arnassi_cave_transturtle) def connect_regions(self) -> None: """ @@ -893,7 +917,7 @@ class AquariaRegions: self.__add_event_location(self.energy_temple_boss, "Beating Fallen God", "Fallen God beated") - self.__add_event_location(self.cathedral_boss_r, + self.__add_event_location(self.cathedral_boss_l, "Beating Mithalan God", "Mithalan God beated") self.__add_event_location(self.forest_boss, @@ -970,8 +994,9 @@ class AquariaRegions: """Since Urns need to be broken, add a damaging item to rules""" add_rule(self.multiworld.get_location("Open Water top right area, first urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), - lambda state: _has_damaging_item(state, self.player)) + add_rule( + self.multiworld.get_location("Open Water top right area, second urn in the Mithalas exit", self.player), + lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Open Water top right area, third urn in the Mithalas exit", self.player), lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Mithalas City, first urn in one of the homes", self.player), @@ -1019,66 +1044,46 @@ class AquariaRegions: Modify rules for location that need soup """ add_rule(self.multiworld.get_location("Turtle cave, Urchin Costume", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) + lambda state: _has_hot_soup(state, self.player)) add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player), - lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) + lambda state: _has_beast_and_soup_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: """ Modify rules implying bind song needed for bulb under rocks """ add_rule(self.multiworld.get_location("Home Water, bulb under the rock in the left path from the Verse Cave", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Verse Cave left area, bulb under the rock at the end of the path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Naija's Home, bulb under the rock at the right of the main path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Song Cave, bulb under the rock in the path to the singing statues", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Song Cave, bulb under the rock close to the song door", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Energy Temple second area, bulb under the rock", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Open Water top left area, bulb under the rock in the left path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest top right area, bulb under the rock in the right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb in the middle path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("The Veil top left area, bulb under the rock in the top right path", - self.player), lambda state: _has_bind_song(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) def __adjusting_light_in_dark_place_rules(self) -> None: add_rule(self.multiworld.get_location("Kelp Forest top right area, Black Pearl", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_location("Kelp Forest bottom right area, Odd Container", self.player), lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Veil top left to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Open Water top right to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Veil top right to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Forest bottom left to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Home Water to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Final Boss to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Simon Says to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) - add_rule(self.multiworld.get_entrance("Transturtle Arnassi Ruins to Transturtle Abyss right", self.player), - lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Body center area to Abyss left bottom area", self.player), lambda state: _has_light(state, self.player)) add_rule(self.multiworld.get_entrance("Veil left of sun temple to Octo cave top path", self.player), @@ -1097,12 +1102,14 @@ class AquariaRegions: def __adjusting_manual_rules(self) -> None: add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), 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("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), 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)) + 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)) add_rule(self.multiworld.get_location("Turtle cave, Turtle Egg", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Abyss left area, bulb in the bottom fish pass", self.player), @@ -1114,103 +1121,119 @@ class AquariaRegions: add_rule(self.multiworld.get_location("Verse Cave right area, Big Seed", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Arnassi Ruins, Song Plant Spore", self.player), - lambda state: _has_beast_form(state, self.player)) + lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) add_rule(self.multiworld.get_location("Energy Temple first area, bulb in the bottom room blocked by a rock", - self.player), lambda state: _has_energy_form(state, self.player)) + self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Home Water, bulb in the bottom left room", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Home Water, bulb in the path below Nautilus Prime", self.player), lambda state: _has_bind_song(state, self.player)) add_rule(self.multiworld.get_location("Naija's Home, bulb after the energy door", self.player), - lambda state: _has_energy_form(state, self.player)) + lambda state: _has_energy_attack_item(state, self.player)) add_rule(self.multiworld.get_location("Abyss right area, bulb behind the rock in the whale room", self.player), lambda state: _has_spirit_form(state, self.player) and _has_sun_form(state, self.player)) add_rule(self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", self.player), - lambda state: _has_fish_form(state, self.player) and - _has_spirit_form(state, self.player)) + lambda state: _has_fish_form(state, self.player) or + _has_beast_and_soup_form(state, self.player)) + add_rule(self.multiworld.get_location("Mithalas City, urn inside a home fish pass", self.player), + lambda state: _has_damaging_item(state, self.player)) + add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), + lambda state: _has_damaging_item(state, self.player)) + add_rule(self.multiworld.get_location( + "The Veil top right area, bulb in the middle of the wall jump cliff", self.player + ), lambda state: _has_beast_form_or_arnassi_armor(state, self.player)) + add_rule(self.multiworld.get_location("Kelp Forest top left area, Jelly Egg", self.player), + lambda state: _has_beast_form(state, self.player)) + add_rule(self.multiworld.get_location("Sun Worm path, first cliff bulb", self.player), + lambda state: state.has("Sun God beated", self.player)) + add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), + lambda state: state.has("Sun God beated", self.player)) + add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), + lambda state: _has_tongue_cleared(state, self.player)) def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mithalas boss area, beating Mithalan God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest boss area, beating Drunian God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple boss area, beating Sun God", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sunken City, bulb on top of the boss area", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Home Water, Nautilus Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Energy Temple blaster room, Blaster Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mithalas City Castle, beating the Priests", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Mermog cave, Piranha Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Octopus Cave, Dumbo Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Final Boss area, bulb in the boss third form room", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, first cliff bulb", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Worm path, second cliff bulb", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, Verse Egg", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, Sun Key", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("The Body bottom area, Mutant Costume", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Arnassi Ruins, Arnassi Armor", - self.player).item_rule =\ + self.player).item_rule = \ lambda item: item.classification != ItemClassification.progression def adjusting_rules(self, options: AquariaOptions) -> None: """ Modify rules for single location or optional rules """ + self.multiworld.get_entrance("Before Final Boss to Final Boss", self.player) self.__adjusting_urns_rules() self.__adjusting_crates_rules() self.__adjusting_soup_rules() @@ -1234,7 +1257,7 @@ class AquariaRegions: lambda state: _has_bind_song(state, self.player)) if options.unconfine_home_water.value in [0, 2]: add_rule(self.multiworld.get_entrance("Home Water to Open Water top left area", self.player), - lambda state: _has_bind_song(state, self.player) and _has_energy_form(state, self.player)) + lambda state: _has_bind_song(state, self.player) and _has_energy_attack_item(state, self.player)) if options.early_energy_form: self.multiworld.early_items[self.player]["Energy form"] = 1 @@ -1274,6 +1297,7 @@ class AquariaRegions: self.multiworld.regions.append(self.arnassi) self.multiworld.regions.append(self.arnassi_path) self.multiworld.regions.append(self.arnassi_crab_boss) + self.multiworld.regions.append(self.arnassi_cave_transturtle) self.multiworld.regions.append(self.simon) def __add_mithalas_regions_to_world(self) -> None: @@ -1300,6 +1324,7 @@ class AquariaRegions: self.multiworld.regions.append(self.forest_tr) self.multiworld.regions.append(self.forest_tr_fp) self.multiworld.regions.append(self.forest_bl) + self.multiworld.regions.append(self.forest_bl_sc) self.multiworld.regions.append(self.forest_br) self.multiworld.regions.append(self.forest_boss) self.multiworld.regions.append(self.forest_boss_entrance) @@ -1337,6 +1362,7 @@ class AquariaRegions: self.multiworld.regions.append(self.abyss_l) self.multiworld.regions.append(self.abyss_lb) self.multiworld.regions.append(self.abyss_r) + self.multiworld.regions.append(self.abyss_r_transturtle) self.multiworld.regions.append(self.ice_cave) self.multiworld.regions.append(self.bubble_cave) self.multiworld.regions.append(self.bubble_cave_boss) diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 029db691b6..8c4f64c345 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -141,7 +141,7 @@ after_home_water_locations = [ "Sun Temple, bulb at the top of the high dark room", "Sun Temple, Golden Gear", "Sun Temple, first bulb of the temple", - "Sun Temple, bulb on the left part", + "Sun Temple, bulb on the right part", "Sun Temple, bulb in the hidden room of the right part", "Sun Temple, Sun Key", "Sun Worm path, first path bulb", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index 0efc3e7388..c09586269d 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -13,36 +13,16 @@ class BeastFormAccessTest(AquariaTestBase): def test_beast_form_location(self) -> None: """Test locations that require beast form""" locations = [ - "Mithalas City Castle, beating the Priests", - "Arnassi Ruins, Crab Armor", - "Arnassi Ruins, Song Plant Spore", - "Mithalas City, first bulb at the end of the top path", - "Mithalas City, second bulb at the end of the top path", - "Mithalas City, bulb in the top path", - "Mithalas City, Mithalas Pot", - "Mithalas City, urn in the Castle flower tube entrance", "Mermog cave, Piranha Egg", + "Kelp Forest top left area, Jelly Egg", "Mithalas Cathedral, Mithalan Dress", - "Turtle cave, bulb in Bubble Cliff", - "Turtle cave, Urchin Costume", - "Sun Worm path, first cliff bulb", - "Sun Worm path, second cliff bulb", "The Veil top right area, bulb at the top of the waterfall", - "Bubble Cave, bulb in the left cave wall", - "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", - "Bubble Cave, Verse Egg", "Sunken City, bulb on top of the boss area", "Octopus Cave, Dumbo Egg", "Beating the Golem", "Beating Mergog", - "Beating Crabbius Maximus", "Beating Octopus Prime", - "Beating Mantis Shrimp Prime", - "King Jellyfish Cave, Jellyfish Costume", - "King Jellyfish Cave, bulb in the right path from King Jelly", - "Beating King Jellyfish God Prime", - "Beating Mithalan priests", - "Sunken City cleared" + "Sunken City cleared", ] items = [["Beast form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py new file mode 100644 index 0000000000..fa4c692340 --- /dev/null +++ b/worlds/aquaria/test/test_beast_form_or_arnassi_armor_access.py @@ -0,0 +1,39 @@ +""" +Author: Louis M +Date: Thu, 18 Apr 2024 18:45:56 +0000 +Description: Unit test used to test accessibility of locations with and without the beast form or arnassi armor +""" + +from . import AquariaTestBase + + +class BeastForArnassiArmormAccessTest(AquariaTestBase): + """Unit test used to test accessibility of locations with and without the beast form or arnassi armor""" + + def test_beast_form_arnassi_armor_location(self) -> None: + """Test locations that require beast form or arnassi armor""" + locations = [ + "Mithalas City Castle, beating the Priests", + "Arnassi Ruins, Crab Armor", + "Arnassi Ruins, Song Plant Spore", + "Mithalas City, first bulb at the end of the top path", + "Mithalas City, second bulb at the end of the top path", + "Mithalas City, bulb in the top path", + "Mithalas City, Mithalas Pot", + "Mithalas City, urn in the Castle flower tube entrance", + "Mermog cave, Piranha Egg", + "Mithalas Cathedral, Mithalan Dress", + "Kelp Forest top left area, Jelly Egg", + "The Veil top right area, bulb in the middle of the wall jump cliff", + "The Veil top right area, bulb at the top of the waterfall", + "Sunken City, bulb on top of the boss area", + "Octopus Cave, Dumbo Egg", + "Beating the Golem", + "Beating Mergog", + "Beating Crabbius Maximus", + "Beating Octopus Prime", + "Beating Mithalan priests", + "Sunken City cleared" + ] + items = [["Beast form", "Arnassi Armor"]] + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_energy_form_access.py b/worlds/aquaria/test/test_energy_form_access.py index 82d8e89a00..b443166823 100644 --- a/worlds/aquaria/test/test_energy_form_access.py +++ b/worlds/aquaria/test/test_energy_form_access.py @@ -17,55 +17,16 @@ class EnergyFormAccessTest(AquariaTestBase): def test_energy_form_location(self) -> None: """Test locations that require Energy form""" locations = [ - "Home Water, Nautilus Egg", - "Naija's Home, bulb after the energy door", - "Energy Temple first area, bulb in the bottom room blocked by a rock", "Energy Temple second area, bulb under the rock", - "Energy Temple bottom entrance, Krotite Armor", "Energy Temple third area, bulb in the bottom path", - "Energy Temple boss area, Fallen God Tooth", - "Energy Temple blaster room, Blaster Egg", - "Mithalas City Castle, beating the Priests", - "Mithalas Cathedral, first urn in the top right room", - "Mithalas Cathedral, second urn in the top right room", - "Mithalas Cathedral, third urn in the top right room", - "Mithalas Cathedral, urn in the flesh room with fleas", - "Mithalas Cathedral, first urn in the bottom right path", - "Mithalas Cathedral, second urn in the bottom right path", - "Mithalas Cathedral, urn behind the flesh vein", - "Mithalas Cathedral, urn in the top left eyes boss room", - "Mithalas Cathedral, first urn in the path behind the flesh vein", - "Mithalas Cathedral, second urn in the path behind the flesh vein", - "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, 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", - "Mermog cave, Piranha Egg", - "Octopus Cave, Dumbo Egg", - "Sun Temple boss area, beating Sun God", - "Arnassi Ruins, Crab Armor", - "King Jellyfish Cave, bulb in the right path from King Jelly", - "King Jellyfish Cave, Jellyfish Costume", - "Sunken City, bulb on top of the boss area", + "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", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", "Final Boss area, bulb in the boss third form room", - "Beating Fallen God", - "Beating Mithalan God", - "Beating Drunian God", - "Beating Sun God", - "Beating the Golem", - "Beating Nautilus Prime", - "Beating Blaster Peg Prime", - "Beating Mergog", - "Beating Mithalan priests", - "Beating Octopus Prime", - "Beating Crabbius Maximus", - "Beating King Jellyfish God Prime", - "First secret", - "Sunken City cleared", "Objective complete", ] items = [["Energy form"]] diff --git a/worlds/aquaria/test/test_energy_form_or_dual_form_access.py b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py new file mode 100644 index 0000000000..8a765bc4e4 --- /dev/null +++ b/worlds/aquaria/test/test_energy_form_or_dual_form_access.py @@ -0,0 +1,92 @@ +""" +Author: Louis M +Date: Thu, 18 Apr 2024 18:45:56 +0000 +Description: Unit test used to test accessibility of locations with and without the energy form and dual form (and Li) +""" + +from . import AquariaTestBase + + +class EnergyFormDualFormAccessTest(AquariaTestBase): + """Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)""" + options = { + "early_energy_form": False, + } + + def test_energy_form_or_dual_form_location(self) -> None: + """Test locations that require Energy form or dual form""" + locations = [ + "Naija's Home, bulb after the energy door", + "Home Water, Nautilus Egg", + "Energy Temple second area, bulb under the rock", + "Energy Temple bottom entrance, Krotite Armor", + "Energy Temple third area, bulb in the bottom path", + "Energy Temple blaster room, Blaster Egg", + "Energy Temple boss area, Fallen God Tooth", + "Mithalas City Castle, beating the Priests", + "Mithalas boss area, beating Mithalan God", + "Mithalas Cathedral, first urn in the top right room", + "Mithalas Cathedral, second urn in the top right room", + "Mithalas Cathedral, third urn in the top right room", + "Mithalas Cathedral, urn in the flesh room with fleas", + "Mithalas Cathedral, first urn in the bottom right path", + "Mithalas Cathedral, second urn in the bottom right path", + "Mithalas Cathedral, urn behind the flesh vein", + "Mithalas Cathedral, urn in the top left eyes boss room", + "Mithalas Cathedral, first urn in the path behind the flesh vein", + "Mithalas Cathedral, second urn in the path behind the flesh vein", + "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, urn below the left entrance", + "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", + "Mermog cave, Piranha Egg", + "Octopus Cave, Dumbo Egg", + "Sun Temple boss area, beating Sun God", + "King Jellyfish Cave, bulb in the right path from King Jelly", + "King Jellyfish Cave, Jellyfish Costume", + "Sunken City right area, crate close to the save crystal", + "Sunken City right area, crate in the left bottom room", + "Sunken City left area, crate in the little pipe room", + "Sunken City left area, crate close to the save crystal", + "Sunken City left area, crate before the bedroom", + "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 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", + "The Body left area, bulb in the top path to the top face room", + "The Body left area, bulb in the bottom face room", + "The Body right area, bulb in the top face room", + "The Body right area, bulb in the top path to the bottom face room", + "The Body right area, bulb in the bottom face room", + "The Body bottom area, bulb in the Jelly Zap room", + "The Body bottom area, bulb in the nautilus room", + "The Body bottom area, Mutant Costume", + "Final Boss area, bulb in the boss third form room", + "Final Boss area, first bulb in the turtle room", + "Final Boss area, second bulb in the turtle room", + "Final Boss area, third bulb in the turtle room", + "Final Boss area, Transturtle", + "Beating Fallen God", + "Beating Blaster Peg Prime", + "Beating Mithalan God", + "Beating Drunian God", + "Beating Sun God", + "Beating the Golem", + "Beating Nautilus Prime", + "Beating Mergog", + "Beating Mithalan priests", + "Beating Octopus Prime", + "Beating King Jellyfish God Prime", + "Beating the Golem", + "Sunken City cleared", + "First secret", + "Objective complete" + ] + items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]] + self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_fish_form_access.py b/worlds/aquaria/test/test_fish_form_access.py index c98a53e924..40b15a87cd 100644 --- a/worlds/aquaria/test/test_fish_form_access.py +++ b/worlds/aquaria/test/test_fish_form_access.py @@ -17,6 +17,7 @@ class FishFormAccessTest(AquariaTestBase): """Test locations that require fish form""" locations = [ "The Veil top left area, bulb inside the fish pass", + "Energy Temple first area, Energy Idol", "Mithalas City, Doll", "Mithalas City, urn inside a home fish pass", "Kelp Forest top right area, bulb in the top fish pass", @@ -30,8 +31,7 @@ class FishFormAccessTest(AquariaTestBase): "Octopus Cave, Dumbo Egg", "Octopus Cave, bulb in the path below the Octopus Cave path", "Beating Octopus Prime", - "Abyss left area, bulb in the bottom fish pass", - "Arnassi Ruins, Arnassi Armor" + "Abyss left area, bulb in the bottom fish pass" ] items = [["Fish form"]] self.assertAccessDependency(locations, items) diff --git a/worlds/aquaria/test/test_light_access.py b/worlds/aquaria/test/test_light_access.py index b5d7cf99fe..29d37d790b 100644 --- a/worlds/aquaria/test/test_light_access.py +++ b/worlds/aquaria/test/test_light_access.py @@ -39,7 +39,6 @@ class LightAccessTest(AquariaTestBase): "Abyss right area, bulb in the middle path", "Abyss right area, bulb behind the rock in the middle path", "Abyss right area, bulb in the left green room", - "Abyss right area, Transturtle", "Ice Cave, bulb in the room to the right", "Ice Cave, first bulb in the top exit room", "Ice Cave, second bulb in the top exit room", diff --git a/worlds/aquaria/test/test_spirit_form_access.py b/worlds/aquaria/test/test_spirit_form_access.py index 3bcbd7d72e..7e31de9905 100644 --- a/worlds/aquaria/test/test_spirit_form_access.py +++ b/worlds/aquaria/test/test_spirit_form_access.py @@ -30,7 +30,6 @@ class SpiritFormAccessTest(AquariaTestBase): "Sunken City left area, Girl Costume", "Beating Mantis Shrimp Prime", "First secret", - "Arnassi Ruins, Arnassi Armor", ] items = [["Spirit form"]] self.assertAccessDependency(locations, items) From 6803c373e5ff738914c362b5e7a158fd528f54f7 Mon Sep 17 00:00:00 2001 From: qwint Date: Thu, 8 Aug 2024 13:33:13 -0500 Subject: [PATCH 096/222] HK: add grub hunt goal (#3203) * makes grub hunt goal option that calculates the total available grubs (including item link replacements) and requires all of them to be gathered for goal completion * update slot data name for grub count * add option to set number needed for grub hub * updates to grub hunt goal based on review * copy/paste fix * account for 'any' goal and fix overriding non-grub goals * making sure godhome is in logic for any and removing redundancy on completion condition * fix typing * i hate typing * move to stage_pre_fill * modify "any" goal so all goals are in logic under minimal settings * rewrite grub counting to create lookups for grubs and groups that can be reused * use generator instead of list comprehension * fix whitespace merging wrong * minor code cleanup --- worlds/hk/Options.py | 13 ++++++++- worlds/hk/__init__.py | 68 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index e2602036a2..c1206d41ee 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -405,9 +405,20 @@ class Goal(Choice): option_radiance = 3 option_godhome = 4 option_godhome_flower = 5 + option_grub_hunt = 6 default = 0 +class GrubHuntGoal(NamedRange): + """The amount of grubs required to finish Grub Hunt. + On 'All' any grubs from item links replacements etc. will be counted""" + display_name = "Grub Hunt Goal" + range_start = 1 + range_end = 46 + special_range_names = {"all": -1} + default = 46 + + class WhitePalace(Choice): """ Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be @@ -522,7 +533,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, + StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index e5065876dd..99277378a1 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,6 +5,7 @@ import typing from copy import deepcopy import itertools import operator +from collections import defaultdict, Counter logger = logging.getLogger("Hollow Knight") @@ -12,12 +13,12 @@ from .Items import item_table, lookup_type_to_names, item_name_groups from .Regions import create_regions from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ - shop_to_option, HKOptions + shop_to_option, HKOptions, GrubHuntGoal from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs from .Charms import names as charm_names -from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification +from BaseClasses import Region, Location, MultiWorld, Item, LocationProgressType, Tutorial, ItemClassification, CollectionState from worlds.AutoWorld import World, LogicMixin, WebWorld path_of_pain_locations = { @@ -155,6 +156,7 @@ class HKWorld(World): ranges: typing.Dict[str, typing.Tuple[int, int]] charm_costs: typing.List[int] cached_filler_items = {} + grub_count: int def __init__(self, multiworld, player): super(HKWorld, self).__init__(multiworld, player) @@ -164,6 +166,7 @@ class HKWorld(World): self.ranges = {} self.created_shop_items = 0 self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) + self.grub_count = 0 def generate_early(self): options = self.options @@ -201,7 +204,7 @@ class HKWorld(World): # check for any goal that godhome events are relevant to all_event_names = event_names.copy() - if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]: + if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower, Goal.option_any]: from .GodhomeData import godhome_event_names all_event_names.update(set(godhome_event_names)) @@ -441,12 +444,67 @@ class HKWorld(World): multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) elif goal == Goal.option_godhome_flower: multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) + elif goal == Goal.option_grub_hunt: + pass # will set in stage_pre_fill() else: # Any goal - multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player) + multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player) and \ + _hk_can_beat_radiance(state, player) and state.count("Godhome_Flower_Quest", player) set_rules(self) + @classmethod + def stage_pre_fill(cls, multiworld: "MultiWorld"): + def set_goal(player, grub_rule: typing.Callable[[CollectionState], bool]): + world = multiworld.worlds[player] + + if world.options.Goal == "grub_hunt": + multiworld.completion_condition[player] = grub_rule + else: + old_rule = multiworld.completion_condition[player] + multiworld.completion_condition[player] = lambda state: old_rule(state) and grub_rule(state) + + worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] + if worlds: + grubs = [item for item in multiworld.get_items() if item.name == "Grub"] + all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + + if all_grub_players: + group_lookup = defaultdict(set) + for group_id, group in multiworld.groups.items(): + for player in group["players"]: + group_lookup[group_id].add(player) + + grub_count_per_player = Counter() + per_player_grubs_per_player = defaultdict(Counter) + + for grub in grubs: + player = grub.player + if player in group_lookup: + for real_player in group_lookup[player]: + per_player_grubs_per_player[real_player][player] += 1 + else: + per_player_grubs_per_player[player][player] += 1 + + if grub.location and grub.location.player in group_lookup.keys(): + for real_player in group_lookup[grub.location.player]: + grub_count_per_player[real_player] += 1 + else: + grub_count_per_player[player] += 1 + + for player, count in grub_count_per_player.items(): + multiworld.worlds[player].grub_count = count + + for player, grub_player_count in per_player_grubs_per_player.items(): + if player in all_grub_players: + set_goal(player, lambda state, g=grub_player_count: all(state.has("Grub", owner, count) for owner, count in g.items())) + + for world in worlds: + if world.player not in all_grub_players: + world.grub_count = world.options.GrubHuntGoal.value + player = world.player + set_goal(player, lambda state, p=player, c=world.grub_count: state.has("Grub", p, c)) + def fill_slot_data(self): slot_data = {} @@ -484,6 +542,8 @@ class HKWorld(World): slot_data["notch_costs"] = self.charm_costs + slot_data["grub_count"] = self.grub_count + return slot_data def create_item(self, name: str) -> HKItem: From 5efb3fd2b0450f68dc95f3b79a0f48746b5e732d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 9 Aug 2024 03:14:26 -0700 Subject: [PATCH 097/222] DS3: Version 3.0.0 (#3128) * Update worlds/dark_souls_3/Locations.py Co-authored-by: Scipio Wright * Fix Covetous Silver Serpent Ring location * Update location groups This should cover pretty much all of the seriously hidden items. It also splits out miniboss drops, mimic drops, and hostile NPC drops. * Remove the "Guarded by Keys" group On reflection, I don't think this is actually that useful. It'll also get a lot muddier once we can randomize shops and ashes become pseudo-"keys". * Restore Knight Slayer's Ring classification * Support infusions/upgrades in the new DS3 mod system * Support random starting loadouts * Make an item's NPC status orthogonal to its category * Track location groups with flags * Track Archipelago/Offline mismatches on the server Also fix a few incorrect item names. * Add additional locations that are now randomizable * Don't put soul and multiple items in shops * Add an option to enable whether NG+ items/locations are included * Clean up useful item categorization There are so many weapons in the game now, it doesn't make sense to treat them all as useful * Add more variety to filler items * Iron out a few bugs and incompatibilities * Fix more silly bugs * Get tests passing * Update options to cover new item types Also recategorize some items. * Verify the default values of `Option`s. Since `Option.verify()` can handle normalization of option names, this allows options to define defaults which rely on that normalization. For example, it allows a world to exclude certain locations by default. This also makes it easier to catch errors if a world author accidentally sets an invalid default. * Make a few more improvements and fixes * Randomize Path of the Dragon * Mark items that unlock checks as useful These items all unlock missable checks, but they're still good to ahve in the game for variety's sake. * Guarantee more NPC quests are completable * Fix a syntax error * Fix rule definition * Support enemy randomization * Support online Yhorm randomization * Remove a completed TODO * Fix tests * Fix force_unique * Add an option to smooth out upgrade item progression * Add helpers for setting location/entrance rules * Support smoother soul item progression * Fill extra smoothing items into conditional locations as well as other worlds * Add health item smoothing * Handle infusions at item generation time * Handle item upgrades at genreation time * Fix Grave Warden's Ashes * Don't overwrite old rules * Randomize items based on spheres instead of DS3 locations * Add a smoothing option for weapon upgrades * Add rules for crow trades * Small fixes * Fix a few more bugs * Fix more bugs * Try to prevent Path of the Dragon from going somewhere it doesn't work * Add the ability to provide enemy presets * Various fixes and features * Bug fixes * Better Coiled Sword placement * Structure DarkSouls3Location more like DarkSouls3Item * Add events to make DS3's spheres more even * Restructure locations to work like items do now * Add rules for more missable locations * Don't add two Storm Rulers * Place Hawk Ring in Farron Keep * Mark the Grass Crest Shield as useful * Mark new progression items * Fix a bug * Support newer better Path of the Dragon code * Don't lock the player out of Coiled Sword * Don't create events for missable locations * Don't throw strings * Don't smooth event items * Properly categorize Butcher Knife * Be more careful about placing Yhorm in low-randomization scenarios * Don't try to smooth DLC items with DLC disabled * Fix another Yhorm bug * Fix upgrade/infusion logic * Remove the PoolType option This distinction is no longer meaningful now that every location in the game of each type is randomized * Categorize HWL: Red Eye Orb as an NPC location * Don't place Storm Ruler on CA: Coiled Sword * Define flatten() locally to make this APWorld capable * Fix some more Leonhard weirdness * Fix unique item randomization * Don't double Twin Dragon Greatshield * Remove debugging print * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Don't add double Storm Ruler Also remove now-redundant item sorting by category in create_items. * Add a missing dlc_enabled check * Use nicer options syntax * Bump data_version * Mention where Yhorm is in which world * Better handle excluded events * Add a newline to Yhorm location * Better way of handling excluded unradomized progression locations * Fix a squidge of nondeterminism * Only smooth items from this world * Don't smooth progression weapons * Remove a location that doesn't actually exist in-game * Classify Power Within as useful * Clarify location names * Fix location requirements * Clean up randomization options * Properly name Coiled Sword location * Add an option for configuring how missable items are handled * Fix some bugs from location name updates * Fix location guide link * Fix a couple locations that were busted offline * Update detailed location descriptions * Fix some bugs when generating for a multiworld * Inject Large Leather Shield * Fix a few location issues * Don't allow progression_skip_balancing for unnecessary locs * Update some location info * Don't uniquify the wrong items * Fix some more location issues * More location fixes * Use hyphens instead of parens for location descriptions * Update and fix more locations * Fix Soul of Cinder boss name * Fix some logic issues * Add item groups and document item/location groups * Fix the display name for "Impatient Mimics" * Properly handle Transposing Kiln and Pyromancer's Flame * Testing * Some fixes to NPC quests, late basin, and transposing kiln * Improve a couple location names * Split out and improve missable NPC item logic * Don't allow crow trades to have foreign items * Fix a variable capture bug * Make sure early items are accessible early even with early Castle * Mark ID giant slave drops as missable * Make sure late basin means that early items aren't behind it * Make is_location_available explicitly private * Add an _add_item_rule utility that checks availability * Clear excluded items if excluded_locations == "unnecessary" * Don't allow upgrades/infusions in crow trades * Fix the documentation for deprecated options * Create events for all excluded locations This allows `can_reach` logic to work even if the locations are randomized. * Fix up Patches' and Siegward's logic based on some manual testing * Factor out more sub-methods for setting location rules * Oops, left these in * Fixing name * Left that in too * Changing to NamedRange to support special_range_names * Alphabetizing * Don't call _is_location_available on foreign locations * Add missing Leonhard items * Changing late basin to have a post-small-doll option * Update basin option, add logic for some of Leonhard Hawkwood and Orbeck * Simplifying an option, fixing a copy-paste error * Removing trailing whitespace * Changing lost items to go into start inventory * Revert Basin changes * Oops * Update Options.py * Reverting small doll changes * Farron Keep boss requirement logic * Add Scroll for late_dlc * Fixing excluded unnecessary locations * Adding Priestess Ring as being after UG boss * Removing missable from Corvian Titanite Slab * Adding KFF Yhorm boss locks * Screams about Creighton * Elite Knight Set isn't permanently missable * Adding Kiln requirement to KFF * fixing valid_keys and item groups * Fixing an option-checker * Throwing unplaceable Storm Ruler into start inventory * Update locations * Refactor item injection * Update setup doc * Small fixes * Fix another location name * Fix injection calculation * Inject guaranteed items along with progression items * Mark boss souls as required for access to regions This allows us to set quest requirements for boss souls and have them automatically propagated to regions, means we need less machinery for Yhorm bosses, and allows us to get rid of a few region-transition events. * Make sure Sirris's quest can be completed before Pontiff * Removing unused list * Changing dict to list * Removing unused test * Update __init__.py * self.multiworld.random -> self.random (#9) * Fix some miscellaneous location issues * Rewrite the DS3 intro page/FAQ * Removing modifying the itempool after fill (#7) Co-authored-by: Natalie Weizenbaum * Small fixes to the setup guide (#10) Small fixes, adding an example for connecting * Expanded Late Basin of Vows and Late DLC (#6) * Add proper requirements for CD: Black Eye Orb * Fix Aldrich's name * Document the differences with the 2.x.x branch * Don't crash if there are more items than locations in smoothing * Apply suggestions from code review Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Code review * Fix _replace_with_filler * Don't use the shared flatten function in SM * Track local items separately rather than iterating the multiworld * Various formatting/docs changes suggested by PyCharm (#12) * Drop deprecated options * Rename "offline randomizer" to "static randomizer" which is clearer * Move `enable_*_locations` under removed options. * Avoid excluded locations for locally-filled items * Adding Removed options to error (#14) * Changes for WebHost options display and the options overhaul * unpack iterators in item list (#13) * Allow worlds to add options to prebuilt groups Previously, this crashed because `typing.NamedTuple` fields such as `group.name` aren't assignable. Now it will only fail for group names that are actually incorrectly cased, and will fail with a better error message. * Style changes, rename exclude behavior options, remove guaranteed items option * Spacing/Formatting (#18) * Various Fixes (#19) * Universally Track Yhorm (#20) * Account for excluded and missable * These are behaviors now * This is singular, apparently * Oops * Fleshing out the priority process * Missable Titanite Lizards and excluded locations (#22) * Small style/efficiency changes * Final passthrough fixes (#24) * Use rich option formatting * Make the behavior option values actual behaviors (#25) * Use != * Remove unused flatten utility * Some changes from review (#28) * Fixing determinism and making smooth faster (#29) * Style change * PyCharm and Mypy fixes (#26) Co-authored-by: Scipio Wright * Change yhorm default (#30) * Add indirect condition (#27) * Update worlds/dark_souls_3/docs/locations_en.md Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> * Ship all item IDs to the client This avoids issues where items might get skipped if, for instance, they're only in the starting inventory. * Make sure to send AP IDs for infused/upgraded weapons * Make `RandomEnemyPresetOption` compatible with ArchipelagoMW/Archipelago#3280 (#31) * Fix cast * More typing and small fixes (#32) --------- Co-authored-by: Scipio Wright Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Co-authored-by: Doug Hoskisson Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- worlds/dark_souls_3/Bosses.py | 264 ++ worlds/dark_souls_3/Items.py | 2810 +++++++------ worlds/dark_souls_3/Locations.py | 3666 ++++++++++++++--- worlds/dark_souls_3/Options.py | 517 ++- worlds/dark_souls_3/__init__.py | 1739 ++++++-- .../detailed_location_descriptions.py | 97 + worlds/dark_souls_3/docs/en_Dark Souls III.md | 203 +- worlds/dark_souls_3/docs/items_en.md | 24 + worlds/dark_souls_3/docs/locations_en.md | 2276 ++++++++++ worlds/dark_souls_3/docs/setup_en.md | 61 +- worlds/dark_souls_3/test/TestDarkSouls3.py | 27 + worlds/dark_souls_3/test/__init__.py | 0 12 files changed, 9325 insertions(+), 2359 deletions(-) create mode 100644 worlds/dark_souls_3/Bosses.py create mode 100644 worlds/dark_souls_3/detailed_location_descriptions.py create mode 100644 worlds/dark_souls_3/docs/items_en.md create mode 100644 worlds/dark_souls_3/docs/locations_en.md create mode 100644 worlds/dark_souls_3/test/TestDarkSouls3.py create mode 100644 worlds/dark_souls_3/test/__init__.py diff --git a/worlds/dark_souls_3/Bosses.py b/worlds/dark_souls_3/Bosses.py new file mode 100644 index 0000000000..008a297132 --- /dev/null +++ b/worlds/dark_souls_3/Bosses.py @@ -0,0 +1,264 @@ +# In almost all cases, we leave boss and enemy randomization up to the static randomizer. But for +# Yhorm specifically we need to know where he ends up in order to ensure that the Storm Ruler is +# available before his fight. + +from dataclasses import dataclass, field +from typing import Set + + +@dataclass +class DS3BossInfo: + """The set of locations a given boss location blocks access to.""" + + name: str + """The boss's name.""" + + id: int + """The game's ID for this particular boss.""" + + dlc: bool = False + """This boss appears in one of the game's DLCs.""" + + before_storm_ruler: bool = False + """Whether this location appears before it's possible to get Storm Ruler in vanilla. + + This is used to determine whether it's safe to place Yhorm here if weapons + aren't randomized. + """ + + locations: Set[str] = field(default_factory=set) + """Additional individual locations that can't be accessed until the boss is dead.""" + + +# Note: the static randomizer splits up some bosses into separate fights for separate phases, each +# of which can be individually replaced by Yhorm. +all_bosses = [ + DS3BossInfo("Iudex Gundyr", 4000800, before_storm_ruler = True, locations = { + "CA: Coiled Sword - boss drop" + }), + DS3BossInfo("Vordt of the Boreal Valley", 3000800, before_storm_ruler = True, locations = { + "HWL: Soul of Boreal Valley Vordt" + }), + DS3BossInfo("Curse-rotted Greatwood", 3100800, locations = { + "US: Soul of the Rotted Greatwood", + "US: Transposing Kiln - boss drop", + "US: Wargod Wooden Shield - Pit of Hollows", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + "FS: Sunless Talisman - Sirris, kill GA boss", + "FS: Sunless Veil - shop, Sirris quest, kill GA boss", + "FS: Sunless Armor - shop, Sirris quest, kill GA boss", + "FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + }), + DS3BossInfo("Crystal Sage", 3300850, locations = { + "RS: Soul of a Crystal Sage", + "FS: Sage's Big Hat - shop after killing RS boss", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("Deacons of the Deep", 3500800, locations = { + "CD: Soul of the Deacons of the Deep", + "CD: Small Doll - boss drop", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { + "FK: Soul of the Blood of the Wolf", + "FK: Cinders of a Lord - Abyss Watcher", + "FS: Undead Legion Helm - shop after killing FK boss", + "FS: Undead Legion Armor - shop after killing FK boss", + "FS: Undead Legion Gauntlet - shop after killing FK boss", + "FS: Undead Legion Leggings - shop after killing FK boss", + "FS: Farron Ring - Hawkwood", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + }), + DS3BossInfo("High Lord Wolnir", 3800800, before_storm_ruler = True, locations = { + "CC: Soul of High Lord Wolnir", + "FS: Wolnir's Crown - shop after killing CC boss", + "CC: Homeward Bone - Irithyll bridge", + "CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop", + }), + DS3BossInfo("Pontiff Sulyvahn", 3700850, locations = { + "IBV: Soul of Pontiff Sulyvahn", + }), + DS3BossInfo("Old Demon King", 3800830, locations = { + "SL: Soul of the Old Demon King", + }), + DS3BossInfo("Aldrich, Devourer of Gods", 3700800, locations = { + "AL: Soul of Aldrich", + "AL: Cinders of a Lord - Aldrich", + "FS: Smough's Helm - shop after killing AL boss", + "FS: Smough's Armor - shop after killing AL boss", + "FS: Smough's Gauntlets - shop after killing AL boss", + "FS: Smough's Leggings - shop after killing AL boss", + "AL: Sun Princess Ring - dark cathedral, after boss", + "FS: Leonhard's Garb - shop after killing Leonhard", + "FS: Leonhard's Gauntlets - shop after killing Leonhard", + "FS: Leonhard's Trousers - shop after killing Leonhard", + }), + DS3BossInfo("Dancer of the Boreal Valley", 3000899, locations = { + "HWL: Soul of the Dancer", + "FS: Dancer's Crown - shop after killing LC entry boss", + "FS: Dancer's Armor - shop after killing LC entry boss", + "FS: Dancer's Gauntlets - shop after killing LC entry boss", + "FS: Dancer's Leggings - shop after killing LC entry boss", + }), + DS3BossInfo("Dragonslayer Armour", 3010800, locations = { + "LC: Soul of Dragonslayer Armour", + "FS: Morne's Helm - shop after killing Eygon or LC boss", + "FS: Morne's Armor - shop after killing Eygon or LC boss", + "FS: Morne's Gauntlets - shop after killing Eygon or LC boss", + "FS: Morne's Leggings - shop after killing Eygon or LC boss", + "LC: Titanite Chunk - down stairs after boss", + }), + DS3BossInfo("Consumed King Oceiros", 3000830, locations = { + "CKG: Soul of Consumed Oceiros", + "CKG: Titanite Scale - tomb, chest #1", + "CKG: Titanite Scale - tomb, chest #2", + "CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + }), + DS3BossInfo("Champion Gundyr", 4000830, locations = { + "UG: Soul of Champion Gundyr", + "FS: Gundyr's Helm - shop after killing UG boss", + "FS: Gundyr's Armor - shop after killing UG boss", + "FS: Gundyr's Gauntlets - shop after killing UG boss", + "FS: Gundyr's Leggings - shop after killing UG boss", + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Chaos Blade - environs, left of shrine", + "UG: Blacksmith Hammer - shrine, Andre's room", + "UG: Eyes of a Fire Keeper - shrine, Irina's room", + "UG: Coiled Sword Fragment - shrine, dead bonfire", + "UG: Soul of a Crestfallen Knight - environs, above shrine entrance", + "UG: Life Ring+3 - shrine, behind big throne", + "UG: Ring of Steel Protection+1 - environs, behind bell tower", + "FS: Ring of Sacrifice - Yuria shop", + "UG: Ember - shop", + "UG: Priestess Ring - shop", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss", + }), + DS3BossInfo("Ancient Wyvern", 3200800), + DS3BossInfo("King of the Storm", 3200850, locations = { + "AP: Soul of the Nameless King", + "FS: Golden Crown - shop after killing AP boss", + "FS: Dragonscale Armor - shop after killing AP boss", + "FS: Golden Bracelets - shop after killing AP boss", + "FS: Dragonscale Waistcloth - shop after killing AP boss", + "AP: Titanite Slab - plaza", + "AP: Covetous Gold Serpent Ring+2 - plaza", + "AP: Dragonslayer Helm - plaza", + "AP: Dragonslayer Armor - plaza", + "AP: Dragonslayer Gauntlets - plaza", + "AP: Dragonslayer Leggings - plaza", + }), + DS3BossInfo("Nameless King", 3200851, locations = { + "AP: Soul of the Nameless King", + "FS: Golden Crown - shop after killing AP boss", + "FS: Dragonscale Armor - shop after killing AP boss", + "FS: Golden Bracelets - shop after killing AP boss", + "FS: Dragonscale Waistcloth - shop after killing AP boss", + "AP: Titanite Slab - plaza", + "AP: Covetous Gold Serpent Ring+2 - plaza", + "AP: Dragonslayer Helm - plaza", + "AP: Dragonslayer Armor - plaza", + "AP: Dragonslayer Gauntlets - plaza", + "AP: Dragonslayer Leggings - plaza", + }), + DS3BossInfo("Lothric, Younger Prince", 3410830, locations = { + "GA: Soul of the Twin Princes", + "GA: Cinders of a Lord - Lothric Prince", + }), + DS3BossInfo("Lorian, Elder Prince", 3410832, locations = { + "GA: Soul of the Twin Princes", + "GA: Cinders of a Lord - Lothric Prince", + "FS: Lorian's Helm - shop after killing GA boss", + "FS: Lorian's Armor - shop after killing GA boss", + "FS: Lorian's Gauntlets - shop after killing GA boss", + "FS: Lorian's Leggings - shop after killing GA boss", + }), + DS3BossInfo("Champion's Gravetender and Gravetender Greatwolf", 4500860, dlc = True, + locations = {"PW1: Valorheart - boss drop"}), + DS3BossInfo("Sister Friede", 4500801, dlc = True, locations = { + "PW2: Soul of Sister Friede", + "PW2: Titanite Slab - boss drop", + "PW1: Titanite Slab - Corvian", + "FS: Ordained Hood - shop after killing PW2 boss", + "FS: Ordained Dress - shop after killing PW2 boss", + "FS: Ordained Trousers - shop after killing PW2 boss", + }), + DS3BossInfo("Blackflame Friede", 4500800, dlc = True, locations = { + "PW2: Soul of Sister Friede", + "PW1: Titanite Slab - Corvian", + "FS: Ordained Hood - shop after killing PW2 boss", + "FS: Ordained Dress - shop after killing PW2 boss", + "FS: Ordained Trousers - shop after killing PW2 boss", + }), + DS3BossInfo("Demon Prince", 5000801, dlc = True, locations = { + "DH: Soul of the Demon Prince", + "DH: Small Envoy Banner - boss drop", + }), + DS3BossInfo("Halflight, Spear of the Church", 5100800, dlc = True, locations = { + "RC: Titanite Slab - mid boss drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Titanite Slab - ashes, mob drop", + "RC: Filianore's Spear Ornament - mid boss drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Darkeater Midir", 5100850, dlc = True, locations = { + "RC: Soul of Darkeater Midir", + "RC: Spears of the Church - hidden boss drop", + }), + DS3BossInfo("Slave Knight Gael 1", 5110801, dlc = True, locations = { + "RC: Soul of Slave Knight Gael", + "RC: Blood of the Dark Soul - end boss drop", + # These are accessible before you trigger the boss, but once you do you + # have to beat it before getting them. + "RC: Titanite Slab - ashes, mob drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Sacred Chime of Filianore - ashes, NPC drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Slave Knight Gael 2", 5110800, dlc = True, locations = { + "RC: Soul of Slave Knight Gael", + "RC: Blood of the Dark Soul - end boss drop", + # These are accessible before you trigger the boss, but once you do you + # have to beat it before getting them. + "RC: Titanite Slab - ashes, mob drop", + "RC: Titanite Slab - ashes, NPC drop", + "RC: Sacred Chime of Filianore - ashes, NPC drop", + "RC: Crucifix of the Mad King - ashes, NPC drop", + "RC: Shira's Crown - Shira's room after killing ashes NPC", + "RC: Shira's Armor - Shira's room after killing ashes NPC", + "RC: Shira's Gloves - Shira's room after killing ashes NPC", + "RC: Shira's Trousers - Shira's room after killing ashes NPC", + }), + DS3BossInfo("Lords of Cinder", 4100800, locations = { + "KFF: Soul of the Lords", + "FS: Billed Mask - Yuria after killing KFF boss", + "FS: Black Dress - Yuria after killing KFF boss", + "FS: Black Gauntlets - Yuria after killing KFF boss", + "FS: Black Leggings - Yuria after killing KFF boss" + }), +] + +default_yhorm_location = DS3BossInfo("Yhorm the Giant", 3900800, locations = { + "PC: Soul of Yhorm the Giant", + "PC: Cinders of a Lord - Yhorm the Giant", + "PC: Siegbräu - Siegward after killing boss", +}) diff --git a/worlds/dark_souls_3/Items.py b/worlds/dark_souls_3/Items.py index 3dd5cb2d3c..19cd79a994 100644 --- a/worlds/dark_souls_3/Items.py +++ b/worlds/dark_souls_3/Items.py @@ -1,7 +1,9 @@ +from dataclasses import dataclass +import dataclasses from enum import IntEnum -from typing import NamedTuple +from typing import Any, cast, ClassVar, Dict, Generator, List, Optional, Set -from BaseClasses import Item +from BaseClasses import Item, ItemClassification class DS3ItemCategory(IntEnum): @@ -14,1267 +16,1677 @@ class DS3ItemCategory(IntEnum): RING = 6 SPELL = 7 MISC = 8 - KEY = 9 + UNIQUE = 9 BOSS = 10 - SKIP = 11 + SOUL = 11 + UPGRADE = 12 + HEALING = 13 + + @property + def is_infusible(self) -> bool: + """Returns whether this category can be infused.""" + return self in [ + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, + DS3ItemCategory.SHIELD_INFUSIBLE + ] + + @property + def upgrade_level(self) -> Optional[int]: + """The maximum upgrade level for this category, or None if it's not upgradable.""" + if self == DS3ItemCategory.WEAPON_UPGRADE_5: return 5 + if self in [ + DS3ItemCategory.WEAPON_UPGRADE_10, + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE + ]: return 10 + return None -class DS3ItemData(NamedTuple): +@dataclass +class Infusion(IntEnum): + """Infusions supported by Dark Souls III. + + The value of each infusion is the number added to the base weapon's ID to get the infused ID. + """ + + HEAVY = 100 + SHARP = 200 + REFINED = 300 + SIMPLE = 400 + CRYSTAL = 500 + FIRE = 600 + CHAOS = 700 + LIGHTNING = 800 + DEEP = 900 + DARK = 1000 + POISON = 1100 + BLOOD = 1200 + RAW = 1300 + BLESSED = 1400 + HOLLOW = 1500 + + @property + def prefix(self): + """The prefix to add to a weapon name with this infusion.""" + return self.name.title() + + +class UsefulIf(IntEnum): + """An enum that indicates when an item should be upgraded to ItemClassification.useful. + + This is used for rings with +x variants that may or may not be the best in class depending on + the player's settings. + """ + + DEFAULT = 0 + """Follows DS3ItemData.classification as written.""" + + BASE = 1 + """Useful only if the DLC and NG+ locations are disabled.""" + + NO_DLC = 2 + """Useful if the DLC is disabled, whether or not NG+ locations are.""" + + NO_NGP = 3 + """Useful if NG+ locations is disabled, whether or not the DLC is.""" + + +@dataclass +class DS3ItemData: + __item_id: ClassVar[int] = 100000 + """The next item ID to use when creating item data.""" + name: str - ds3_code: int - is_dlc: bool + ds3_code: Optional[int] category: DS3ItemCategory + base_ds3_code: Optional[int] = None + """If this is an upgradable weapon, the base ID of the weapon it upgrades from. + + Otherwise, or if the weapon isn't upgraded, this is the same as ds3_code. + """ + + base_name: Optional[str] = None + """The name of the individual item, if this is a multi-item group.""" + + classification: ItemClassification = ItemClassification.filler + """How important this item is to the game progression.""" + + ap_code: Optional[int] = None + """The Archipelago ID for this item.""" + + is_dlc: bool = False + """Whether this item is only found in one of the two DLC packs.""" + + count: int = 1 + """The number of copies of this item included in each drop.""" + + inject: bool = False + """If this is set, the randomizer will try to inject this item into the game. + + This is used for items such as covenant rewards that aren't realistically reachable in a + randomizer run, but are still fun to have available to the player. If there are more locations + available than there are items in the item pool, these items will be used to help make up the + difference. + """ + + souls: Optional[int] = None + """If this is a consumable item that gives souls, the number of souls it gives.""" + + useful_if: UsefulIf = UsefulIf.DEFAULT + """Whether and when this item should be marked as "useful".""" + + filler: bool = False + """Whether this is a candidate for a filler item to be added to fill out extra locations.""" + + skip: bool = False + """Whether to omit this item from randomization and replace it with filler or unique items.""" + + @property + def unique(self): + """Whether this item should be unique, appearing only once in the randomizer.""" + return self.category not in { + DS3ItemCategory.MISC, DS3ItemCategory.SOUL, DS3ItemCategory.UPGRADE, + DS3ItemCategory.HEALING, + } + + def __post_init__(self): + self.ap_code = self.ap_code or DS3ItemData.__item_id + if not self.base_name: self.base_name = self.name + if not self.base_ds3_code: self.base_ds3_code = self.ds3_code + DS3ItemData.__item_id += 1 + + def item_groups(self) -> List[str]: + """The names of item groups this item should appear in. + + This is computed from the properties assigned to this item.""" + names = [] + if self.classification == ItemClassification.progression: names.append("Progression") + if self.name.startswith("Cinders of a Lord -"): names.append("Cinders") + + names.append({ + DS3ItemCategory.WEAPON_UPGRADE_5: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE: "Weapons", + DS3ItemCategory.SHIELD: "Shields", + DS3ItemCategory.SHIELD_INFUSIBLE: "Shields", + DS3ItemCategory.ARMOR: "Armor", + DS3ItemCategory.RING: "Rings", + DS3ItemCategory.SPELL: "Spells", + DS3ItemCategory.MISC: "Miscellaneous", + DS3ItemCategory.UNIQUE: "Unique", + DS3ItemCategory.BOSS: "Boss Souls", + DS3ItemCategory.SOUL: "Small Souls", + DS3ItemCategory.UPGRADE: "Upgrade", + DS3ItemCategory.HEALING: "Healing", + }[self.category]) + + return names + + def counts(self, counts: List[int]) -> Generator["DS3ItemData", None, None]: + """Returns an iterable of copies of this item with the given counts.""" + yield self + for count in counts: + yield dataclasses.replace( + self, + ap_code = None, + name = "{} x{}".format(self.base_name, count), + base_name = self.base_name, + count = count, + filler = False, # Don't count multiples as filler by default + ) + + @property + def is_infused(self) -> bool: + """Returns whether this item is an infused weapon.""" + return cast(int, self.ds3_code) - cast(int, self.base_ds3_code) >= 100 + + def infuse(self, infusion: Infusion) -> "DS3ItemData": + """Returns this item with the given infusion applied.""" + if not self.category.is_infusible: raise RuntimeError(f"{self.name} is not infusible.") + if self.is_infused: + raise RuntimeError(f"{self.name} is already infused.") + + # We can't change the name or AP code when infusing/upgrading weapons, because they both + # need to match what's in item_name_to_id. We don't want to add every possible + # infusion/upgrade combination to that map because it's way too many items. + return dataclasses.replace( + self, + name = self.name, + ds3_code = cast(int, self.ds3_code) + infusion.value, + filler = False, + ) + + @property + def is_upgraded(self) -> bool: + """Returns whether this item is a weapon that's upgraded beyond level 0.""" + return (cast(int, self.ds3_code) - cast(int, self.base_ds3_code)) % 100 != 0 + + def upgrade(self, level: int) -> "DS3ItemData": + """Upgrades this item to the given level.""" + if not self.category.upgrade_level: raise RuntimeError(f"{self.name} is not upgradable.") + if level > self.category.upgrade_level: + raise RuntimeError(f"{self.name} can't be upgraded to +{level}.") + if self.is_upgraded: + raise RuntimeError(f"{self.name} is already upgraded.") + + # We can't change the name or AP code when infusing/upgrading weapons, because they both + # need to match what's in item_name_to_id. We don't want to add every possible + # infusion/upgrade combination to that map because it's way too many items. + return dataclasses.replace( + self, + name = self.name, + ds3_code = cast(int, self.ds3_code) + level, + filler = False, + ) + + def __hash__(self) -> int: + return (self.name, self.ds3_code).__hash__() + + def __eq__(self, other: Any) -> bool: + if isinstance(other, self.__class__): + return self.name == other.name and self.ds3_code == other.ds3_code + else: + return False + class DarkSouls3Item(Item): game: str = "Dark Souls III" + data: DS3ItemData + + @property + def level(self) -> Optional[int]: + """This item's upgrade level, if it's a weapon.""" + return cast(int, self.data.ds3_code) % 100 if self.data.category.upgrade_level else None + + def __init__( + self, + player: int, + data: DS3ItemData, + classification = None): + super().__init__(data.name, classification or data.classification, data.ap_code, player) + self.data = data @staticmethod - def get_name_to_id() -> dict: - base_id = 100000 - return {item_data.name: id for id, item_data in enumerate(_all_items, base_id)} + def event(name: str, player: int) -> "DarkSouls3Item": + data = DS3ItemData(name, None, DS3ItemCategory.MISC, + skip = True, classification = ItemClassification.progression) + data.ap_code = None + return DarkSouls3Item(player, data) -key_item_names = { - "Small Lothric Banner", - "Basin of Vows", - "Small Doll", - "Storm Ruler", - "Grand Archives Key", - "Cinders of a Lord - Abyss Watcher", - "Cinders of a Lord - Yhorm the Giant", - "Cinders of a Lord - Aldrich", - "Cinders of a Lord - Lothric Prince", - "Mortician's Ashes", - "Cell Key", - #"Tower Key", #Not a relevant key item atm - "Jailbreaker's Key", - "Prisoner Chief's Ashes", - "Old Cell Key", - "Jailer's Key Ring", - "Contraption Key", - "Small Envoy Banner" -} - - -_vanilla_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ +_vanilla_items = [ # Ammunition - ("Standard Arrow", 0x00061A80, DS3ItemCategory.SKIP), - ("Fire Arrow", 0x00061AE4, DS3ItemCategory.SKIP), - ("Poison Arrow", 0x00061B48, DS3ItemCategory.SKIP), - ("Large Arrow", 0x00061BAC, DS3ItemCategory.SKIP), - ("Feather Arrow", 0x00061C10, DS3ItemCategory.SKIP), - ("Moonlight Arrow", 0x00061C74, DS3ItemCategory.SKIP), - ("Wood Arrow", 0x00061CD8, DS3ItemCategory.SKIP), - ("Dark Arrow", 0x00061D3C, DS3ItemCategory.SKIP), - ("Dragonslayer Greatarrow", 0x00062250, DS3ItemCategory.SKIP), - ("Dragonslayer Lightning Arrow", 0x00062318, DS3ItemCategory.SKIP), - ("Onislayer Greatarrow", 0x0006237C, DS3ItemCategory.SKIP), - ("Standard Bolt", 0x00062A20, DS3ItemCategory.SKIP), - ("Heavy Bolt", 0x00062A84, DS3ItemCategory.SKIP), - ("Sniper Bolt", 0x00062AE8, DS3ItemCategory.SKIP), - ("Wood Bolt", 0x00062B4C, DS3ItemCategory.SKIP), - ("Lightning Bolt", 0x00062BB0, DS3ItemCategory.SKIP), - ("Splintering Bolt", 0x00062C14, DS3ItemCategory.SKIP), - ("Exploding Bolt", 0x00062C78, DS3ItemCategory.SKIP), + *DS3ItemData("Standard Arrow", 0x00061A80, DS3ItemCategory.MISC).counts([12]), + DS3ItemData("Standard Arrow x8", 0x00061A80, DS3ItemCategory.MISC, count = 8, filler = True), + DS3ItemData("Fire Arrow", 0x00061AE4, DS3ItemCategory.MISC), + DS3ItemData("Fire Arrow x8", 0x00061AE4, DS3ItemCategory.MISC, count = 8, filler = True), + *DS3ItemData("Poison Arrow", 0x00061B48, DS3ItemCategory.MISC).counts([18]), + DS3ItemData("Poison Arrow x8", 0x00061B48, DS3ItemCategory.MISC, count = 8, filler = True), + DS3ItemData("Large Arrow", 0x00061BAC, DS3ItemCategory.MISC), + DS3ItemData("Feather Arrow", 0x00061C10, DS3ItemCategory.MISC), + *DS3ItemData("Moonlight Arrow", 0x00061C74, DS3ItemCategory.MISC).counts([6]), + DS3ItemData("Wood Arrow", 0x00061CD8, DS3ItemCategory.MISC), + DS3ItemData("Dark Arrow", 0x00061D3C, DS3ItemCategory.MISC), + *DS3ItemData("Dragonslayer Greatarrow", 0x00062250, DS3ItemCategory.MISC).counts([5]), + *DS3ItemData("Dragonslayer Lightning Arrow", 0x00062318, DS3ItemCategory.MISC).counts([10]), + *DS3ItemData("Onislayer Greatarrow", 0x0006237C, DS3ItemCategory.MISC).counts([8]), + DS3ItemData("Standard Bolt", 0x00062A20, DS3ItemCategory.MISC), + DS3ItemData("Heavy Bolt", 0x00062A84, DS3ItemCategory.MISC), + *DS3ItemData("Sniper Bolt", 0x00062AE8, DS3ItemCategory.MISC).counts([11]), + DS3ItemData("Wood Bolt", 0x00062B4C, DS3ItemCategory.MISC), + *DS3ItemData("Lightning Bolt", 0x00062BB0, DS3ItemCategory.MISC).counts([9]), + *DS3ItemData("Lightning Bolt", 0x00062BB0, DS3ItemCategory.MISC).counts([12]), + DS3ItemData("Splintering Bolt", 0x00062C14, DS3ItemCategory.MISC), + *DS3ItemData("Exploding Bolt", 0x00062C78, DS3ItemCategory.MISC).counts([6]), # Weapons - ("Dagger", 0x000F4240, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Bandit's Knife", 0x000F6950, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Parrying Dagger", 0x000F9060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rotten Ghru Dagger", 0x000FDE80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Harpe", 0x00102CA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Scholar's Candlestick", 0x001053B0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Tailbone Short Sword", 0x00107AC0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Corvian Greatknife", 0x0010A1D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Handmaid's Dagger", 0x00111700, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Shortsword", 0x001E8480, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Longsword", 0x001EAB90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Broadsword", 0x001ED2A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Broken Straight Sword", 0x001EF9B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Sword", 0x001F6EE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Sunlight Straight Sword", 0x00203230, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Rotten Ghru Curved Sword", 0x00205940, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Irithyll Straight Sword", 0x0020A760, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cleric's Candlestick", 0x0020F580, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Morion Blade", 0x002143A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Astora's Straight Sword", 0x002191C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Barbed Straight Sword", 0x0021B8D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Executioner's Greatsword", 0x0021DFE0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Anri's Straight Sword", 0x002206F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Estoc", 0x002DC6C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Mail Breaker", 0x002DEDD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rapier", 0x002E14E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ricard's Rapier", 0x002E3BF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crystal Sage's Rapier", 0x002E6300, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Irithyll Rapier", 0x002E8A10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Shotel", 0x003D3010, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Scimitar", 0x003D7E30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Falchion", 0x003DA540, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Carthus Curved Sword", 0x003DCC50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Carthus Curved Greatsword", 0x003DF360, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Pontiff Knight Curved Sword", 0x003E1A70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Storm Curved Sword", 0x003E4180, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Painting Guardian's Curved Sword", 0x003E6890, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crescent Moon Sword", 0x003E8FA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Carthus Shotel", 0x003EB6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Uchigatana", 0x004C4B40, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Washing Pole", 0x004C7250, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Chaos Blade", 0x004C9960, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Black Blade", 0x004CC070, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Bloodlust", 0x004CE780, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Darkdrift", 0x004D0E90, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Bastard Sword", 0x005B8D80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Claymore", 0x005BDBA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Zweihander", 0x005C29C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatsword", 0x005C50D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Astora Greatsword", 0x005C9EF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Murakumo", 0x005CC600, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Greatsword", 0x005D1420, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Flamberge", 0x005DB060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Exile Greatsword", 0x005DD770, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatsword of Judgment", 0x005E2590, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Profaned Greatsword", 0x005E4CA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cathedral Knight Greatsword", 0x005E73B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Farron Greatsword", 0x005E9AC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Yhorm's Great Machete", 0x005F0FF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Dark Sword", 0x005F3700, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Sword", 0x005F5E10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lorian's Greatsword", 0x005F8520, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Twin Princes' Greatsword", 0x005FAC30, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lothric's Holy Sword", 0x005FD340, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Wolnir's Holy Sword", 0x005FFA50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Wolf Knight's Greatsword", 0x00602160, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Greatsword of Artorias", 0x0060216A, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Hollowslayer Greatsword", 0x00604870, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Moonlight Greatsword", 0x00606F80, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Drakeblood Greatsword", 0x00609690, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Firelink Greatsword", 0x0060BDA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Fume Ultra Greatsword", 0x0060E4B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Old Wolf Curved Sword", 0x00610BC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Storm Ruler", 0x006132D0, DS3ItemCategory.KEY), - ("Hand Axe", 0x006ACFC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Battle Axe", 0x006AF6D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Deep Battle Axe", 0x006AFA54, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Brigand Axe", 0x006B1DE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crescent Axe", 0x006B6C00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Axe", 0x006B9310, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Butcher Knife", 0x006BE130, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonslayer's Axe", 0x006C0840, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Thrall Axe", 0x006C5660, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragonslayer Greataxe", 0x006C7D70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Demon's Greataxe", 0x006CA480, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Eleonora", 0x006CCB90, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Man Serpent Hatchet", 0x006D19B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Club", 0x007A1200, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Mace", 0x007A3910, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Morning Star", 0x007A6020, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Reinforced Club", 0x007A8730, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Large Club", 0x007AFC60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Club", 0x007B4A80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Mace", 0x007BBFB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Great Wooden Hammer", 0x007C8300, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gargoyle Flame Hammer", 0x007CAA10, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Vordt's Great Hammer", 0x007CD120, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Old King's Great Hammer", 0x007CF830, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Heysel Pick", 0x007D6D60, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Warpick", 0x007DBB80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Pickaxe", 0x007DE290, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragon Tooth", 0x007E09A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Smough's Great Hammer", 0x007E30B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Blacksmith Hammer", 0x007E57C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Morne's Great Hammer", 0x007E7ED0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Spiked Mace", 0x007EA5E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Spear", 0x00895440, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Spear", 0x00897B50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Partizan", 0x0089C970, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Greatlance", 0x008A8CC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lothric Knight Long Spear", 0x008AB3D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gargoyle Flame Spear", 0x008B01F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Rotten Ghru Spear", 0x008B2900, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Tailbone Spear", 0x008B5010, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Soldering Iron", 0x008B7720, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Arstor's Spear", 0x008BEC50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Saint Bident", 0x008C1360, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Yorshka's Spear", 0x008C3A70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pike", 0x008C6180, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Heavy Four-pronged Plow", 0x008ADAE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dragonslayer Spear", 0x008CAFA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Scythe", 0x00989680, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Lucerne", 0x0098BD90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Glaive", 0x0098E4A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Halberd", 0x00990BB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Greataxe", 0x009959D0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pontiff Knight Great Scythe", 0x0099A7F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Corvian Scythe", 0x0099CF00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Knight Halberd", 0x0099F610, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gundyr's Halberd", 0x009A1D20, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Red Hilted Halberd", 0x009AB960, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Black Knight Glaive", 0x009AE070, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Immolation Tinder", 0x009B0780, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Claw", 0x00A7D8C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Caestus", 0x00A7FFD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Manikin Claws", 0x00A826E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Demon's Fist", 0x00A84DF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Dark Hand", 0x00A87500, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Whip", 0x00B71B00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Witch's Locks", 0x00B7B740, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Notched Whip", 0x00B7DE50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Spotted Whip", 0x00B80560, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Talisman", 0x00C72090, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sorcerer's Staff", 0x00C747A0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Storyteller's Staff", 0x00C76EB0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Mendicant's Staff", 0x00C795C0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Man-grub's Staff", 0x00C7E3E0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Archdeacon's Great Staff", 0x00C80AF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Golden Ritual Spear", 0x00C83200, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Yorshka's Chime", 0x00C88020, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sage's Crystal Staff", 0x00C8CE40, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Heretic's Staff", 0x00C8F550, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Court Sorcerer's Staff", 0x00C91C60, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Witchtree Branch", 0x00C94370, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Izalith Staff", 0x00C96A80, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Cleric's Sacred Chime", 0x00C99190, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Priest's Chime", 0x00C9B8A0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Saint-tree Bellvine", 0x00C9DFB0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Caitha's Chime", 0x00CA06C0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crystal Chime", 0x00CA2DD0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Sunlight Talisman", 0x00CA54E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Canvas Talisman", 0x00CA7BF0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sunless Talisman", 0x00CAA300, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Saint's Talisman", 0x00CACA10, DS3ItemCategory.WEAPON_UPGRADE_10), - ("White Hair Talisman", 0x00CAF120, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pyromancy Flame", 0x00CC77C0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonslayer Greatbow", 0x00CF8500, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Short Bow", 0x00D5C690, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Composite Bow", 0x00D5EDA0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Light Crossbow", 0x00D63BC0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Arbalest", 0x00D662D0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Longbow", 0x00D689E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Dragonrider Bow", 0x00D6B0F0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Avelyn", 0x00D6FF10, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Knight's Crossbow", 0x00D72620, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Heavy Crossbow", 0x00D74D30, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Darkmoon Longbow", 0x00D79B50, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Onislayer Greatbow", 0x00D7C260, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Black Bow of Pharis", 0x00D7E970, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sniper Crossbow", 0x00D83790, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sellsword Twinblades", 0x00F42400, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Warden Twinblades", 0x00F47220, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Winged Knight Twinaxes", 0x00F49930, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Dancer's Enchanted Swords", 0x00F4C040, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Great Machete", 0x00F4E750, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Brigand Twindaggers", 0x00F50E60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Gotthard Twinswords", 0x00F53570, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Onikiri and Ubadachi", 0x00F58390, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Drang Twinspears", 0x00F5AAA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Drang Hammers", 0x00F61FD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dagger", 0x000F4240, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Bandit's Knife", 0x000F6950, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Parrying Dagger", 0x000F9060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rotten Ghru Dagger", 0x000FDE80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Harpe", 0x00102CA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Scholar's Candlestick", 0x001053B0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Tailbone Short Sword", 0x00107AC0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Corvian Greatknife", 0x0010A1D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Handmaid's Dagger", 0x00111700, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Shortsword", 0x001E8480, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Longsword", 0x001EAB90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Broadsword", 0x001ED2A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Broken Straight Sword", 0x001EF9B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Sword", 0x001F6EE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Sunlight Straight Sword", 0x00203230, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Rotten Ghru Curved Sword", 0x00205940, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Irithyll Straight Sword", 0x0020A760, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cleric's Candlestick", 0x0020F580, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Morion Blade", 0x002143A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Astora Straight Sword", 0x002191C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Barbed Straight Sword", 0x0021B8D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Executioner's Greatsword", 0x0021DFE0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Anri's Straight Sword", 0x002206F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Estoc", 0x002DC6C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Mail Breaker", 0x002DEDD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rapier", 0x002E14E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ricard's Rapier", 0x002E3BF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crystal Sage's Rapier", 0x002E6300, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Irithyll Rapier", 0x002E8A10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Shotel", 0x003D3010, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Scimitar", 0x003D7E30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Falchion", 0x003DA540, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Carthus Curved Sword", 0x003DCC50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Carthus Curved Greatsword", 0x003DF360, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Pontiff Knight Curved Sword", 0x003E1A70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Storm Curved Sword", 0x003E4180, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Painting Guardian's Curved Sword", 0x003E6890, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crescent Moon Sword", 0x003E8FA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Carthus Shotel", 0x003EB6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Uchigatana", 0x004C4B40, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Washing Pole", 0x004C7250, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Chaos Blade", 0x004C9960, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Black Blade", 0x004CC070, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Bloodlust", 0x004CE780, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Darkdrift", 0x004D0E90, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Bastard Sword", 0x005B8D80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Claymore", 0x005BDBA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Zweihander", 0x005C29C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatsword", 0x005C50D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Astora Greatsword", 0x005C9EF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Murakumo", 0x005CC600, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Greatsword", 0x005D1420, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Flamberge", 0x005DB060, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Exile Greatsword", 0x005DD770, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatsword of Judgment", 0x005E2590, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Profaned Greatsword", 0x005E4CA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cathedral Knight Greatsword", 0x005E73B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Farron Greatsword", 0x005E9AC0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Yhorm's Great Machete", 0x005F0FF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Dark Sword", 0x005F3700, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Sword", 0x005F5E10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lorian's Greatsword", 0x005F8520, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Twin Princes' Greatsword", 0x005FAC30, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lothric's Holy Sword", 0x005FD340, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Wolnir's Holy Sword", 0x005FFA50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Wolf Knight's Greatsword", 0x00602160, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Greatsword of Artorias", 0x0060216A, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Hollowslayer Greatsword", 0x00604870, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Moonlight Greatsword", 0x00606F80, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Drakeblood Greatsword", 0x00609690, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Firelink Greatsword", 0x0060BDA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Fume Ultra Greatsword", 0x0060E4B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Old Wolf Curved Sword", 0x00610BC0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Storm Ruler", 0x006132D0, DS3ItemCategory.WEAPON_UPGRADE_5, + classification = ItemClassification.progression), + DS3ItemData("Hand Axe", 0x006ACFC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Battle Axe", 0x006AF6D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Deep Battle Axe", 0x006AFA54, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Brigand Axe", 0x006B1DE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crescent Axe", 0x006B6C00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greataxe", 0x006B9310, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Butcher Knife", 0x006BE130, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonslayer's Axe", 0x006C0840, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Thrall Axe", 0x006C5660, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragonslayer Greataxe", 0x006C7D70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Demon's Greataxe", 0x006CA480, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Eleonora", 0x006CCB90, DS3ItemCategory.WEAPON_UPGRADE_5, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Man Serpent Hatchet", 0x006D19B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Club", 0x007A1200, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Mace", 0x007A3910, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Morning Star", 0x007A6020, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Reinforced Club", 0x007A8730, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Large Club", 0x007AFC60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Club", 0x007B4A80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Mace", 0x007BBFB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Great Wooden Hammer", 0x007C8300, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gargoyle Flame Hammer", 0x007CAA10, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Vordt's Great Hammer", 0x007CD120, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Old King's Great Hammer", 0x007CF830, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Heysel Pick", 0x007D6D60, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Warpick", 0x007DBB80, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Pickaxe", 0x007DE290, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragon Tooth", 0x007E09A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Smough's Great Hammer", 0x007E30B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Blacksmith Hammer", 0x007E57C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Morne's Great Hammer", 0x007E7ED0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Spiked Mace", 0x007EA5E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Spear", 0x00895440, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Spear", 0x00897B50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Partizan", 0x0089C970, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Greatlance", 0x008A8CC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lothric Knight Long Spear", 0x008AB3D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gargoyle Flame Spear", 0x008B01F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Rotten Ghru Spear", 0x008B2900, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Tailbone Spear", 0x008B5010, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Soldering Iron", 0x008B7720, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonslayer Swordspear", 0x008BC540, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Arstor's Spear", 0x008BEC50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Saint Bident", 0x008C1360, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Yorshka's Spear", 0x008C3A70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pike", 0x008C6180, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Heavy Four-pronged Plow", 0x008ADAE0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dragonslayer Spear", 0x008CAFA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Scythe", 0x00989680, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Lucerne", 0x0098BD90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Glaive", 0x0098E4A0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Halberd", 0x00990BB0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Greataxe", 0x009959D0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pontiff Knight Great Scythe", 0x0099A7F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Corvian Scythe", 0x0099CF00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Knight Halberd", 0x0099F610, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gundyr's Halberd", 0x009A1D20, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Red Hilted Halberd", 0x009AB960, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Black Knight Glaive", 0x009AE070, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Immolation Tinder", 0x009B0780, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Claw", 0x00A7D8C0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Caestus", 0x00A7FFD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Manikin Claws", 0x00A826E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Demon's Fist", 0x00A84DF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Dark Hand", 0x00A87500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Whip", 0x00B71B00, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Witch's Locks", 0x00B7B740, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Notched Whip", 0x00B7DE50, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Spotted Whip", 0x00B80560, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Talisman", 0x00C72090, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sorcerer's Staff", 0x00C747A0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Storyteller's Staff", 0x00C76EB0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Mendicant's Staff", 0x00C795C0, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression, # Crow trade + inject = True), # This is just a random drop normally, but we need it in-logic + DS3ItemData("Man-grub's Staff", 0x00C7E3E0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Archdeacon's Great Staff", 0x00C80AF0, DS3ItemCategory.WEAPON_UPGRADE_5, + inject = True), # Covenant reward + DS3ItemData("Golden Ritual Spear", 0x00C83200, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Yorshka's Chime", 0x00C88020, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sage's Crystal Staff", 0x00C8CE40, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Heretic's Staff", 0x00C8F550, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Court Sorcerer's Staff", 0x00C91C60, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Witchtree Branch", 0x00C94370, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Izalith Staff", 0x00C96A80, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Cleric's Sacred Chime", 0x00C99190, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Priest's Chime", 0x00C9B8A0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Saint-tree Bellvine", 0x00C9DFB0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Caitha's Chime", 0x00CA06C0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crystal Chime", 0x00CA2DD0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Sunlight Talisman", 0x00CA54E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Canvas Talisman", 0x00CA7BF0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sunless Talisman", 0x00CAA300, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Saint's Talisman", 0x00CACA10, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("White Hair Talisman", 0x00CAF120, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pyromancy Flame", 0x00CC77C0, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression), + DS3ItemData("Dragonslayer Greatbow", 0x00CF8500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Short Bow", 0x00D5C690, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Composite Bow", 0x00D5EDA0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Light Crossbow", 0x00D63BC0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Arbalest", 0x00D662D0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Longbow", 0x00D689E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Dragonrider Bow", 0x00D6B0F0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Avelyn", 0x00D6FF10, DS3ItemCategory.WEAPON_UPGRADE_10, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Knight's Crossbow", 0x00D72620, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Heavy Crossbow", 0x00D74D30, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Darkmoon Longbow", 0x00D79B50, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Onislayer Greatbow", 0x00D7C260, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Black Bow of Pharis", 0x00D7E970, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sniper Crossbow", 0x00D83790, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sellsword Twinblades", 0x00F42400, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Warden Twinblades", 0x00F47220, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Winged Knight Twinaxes", 0x00F49930, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Dancer's Enchanted Swords", 0x00F4C040, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Great Machete", 0x00F4E750, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Brigand Twindaggers", 0x00F50E60, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Gotthard Twinswords", 0x00F53570, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Onikiri and Ubadachi", 0x00F58390, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Drang Twinspears", 0x00F5AAA0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Drang Hammers", 0x00F61FD0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), # Shields - ("Buckler", 0x01312D00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Small Leather Shield", 0x01315410, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Round Shield", 0x0131A230, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Large Leather Shield", 0x0131C940, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Hawkwood's Shield", 0x01323E70, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Iron Round Shield", 0x01326580, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Wooden Shield", 0x0132DAB0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Kite Shield", 0x013301C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Ghru Rotshield", 0x013328D0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Havel's Greatshield", 0x013376F0, DS3ItemCategory.SHIELD), - ("Target Shield", 0x01339E00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Elkhorn Round Shield", 0x0133C510, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Warrior's Round Shield", 0x0133EC20, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Caduceus Round Shield", 0x01341330, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Red and White Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Blessed Red and White Shield+1", 0x01343FB9, DS3ItemCategory.SHIELD), - ("Plank Shield", 0x01346150, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Leather Shield", 0x01348860, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Crimson Parma", 0x0134AF70, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Eastern Iron Shield", 0x0134D680, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Llewellyn Shield", 0x0134FD90, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Golden Falcon Shield", 0x01354BB0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Sacred Bloom Shield", 0x013572C0, DS3ItemCategory.SHIELD), - ("Ancient Dragon Greatshield", 0x013599D0, DS3ItemCategory.SHIELD), - ("Lothric Knight Shield", 0x01409650, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Knight Shield", 0x01410B80, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Pontiff Knight Shield", 0x014159A0, DS3ItemCategory.SHIELD), - ("Carthus Shield", 0x014180B0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Black Knight Shield", 0x0141F5E0, DS3ItemCategory.SHIELD), - ("Silver Knight Shield", 0x01424400, DS3ItemCategory.SHIELD), - ("Spiked Shield", 0x01426B10, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Pierce Shield", 0x01429220, DS3ItemCategory.SHIELD_INFUSIBLE), - ("East-West Shield", 0x0142B930, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Sunlight Shield", 0x0142E040, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Crest Shield", 0x01430750, DS3ItemCategory.SHIELD), - ("Dragon Crest Shield", 0x01432E60, DS3ItemCategory.SHIELD), - ("Spider Shield", 0x01435570, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Grass Crest Shield", 0x01437C80, DS3ItemCategory.SHIELD), - ("Sunset Shield", 0x0143A390, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Golden Wing Crest Shield", 0x0143CAA0, DS3ItemCategory.SHIELD), - ("Blue Wooden Shield", 0x0143F1B0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Silver Eagle Kite Shield", 0x014418C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Stone Parma", 0x01443FD0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Spirit Tree Crest Shield", 0x014466E0, DS3ItemCategory.SHIELD), - ("Porcine Shield", 0x01448DF0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Shield of Want", 0x0144B500, DS3ItemCategory.SHIELD), - ("Wargod Wooden Shield", 0x0144DC10, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Lothric Knight Greatshield", 0x014FD890, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Cathedral Knight Greatshield", 0x014FFFA0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Dragonslayer Greatshield", 0x01504DC0, DS3ItemCategory.SHIELD), - ("Moaning Shield", 0x015074D0, DS3ItemCategory.SHIELD), - ("Yhorm's Greatshield", 0x0150C2F0, DS3ItemCategory.SHIELD), - ("Black Iron Greatshield", 0x0150EA00, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Wolf Knight's Greatshield", 0x01511110, DS3ItemCategory.SHIELD), - ("Twin Dragon Greatshield", 0x01513820, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Greatshield of Glory", 0x01515F30, DS3ItemCategory.SHIELD), - ("Curse Ward Greatshield", 0x01518640, DS3ItemCategory.SHIELD), - ("Bonewheel Shield", 0x0151AD50, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Stone Greatshield", 0x0151D460, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Buckler", 0x01312D00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Small Leather Shield", 0x01315410, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Round Shield", 0x0131A230, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Large Leather Shield", 0x0131C940, DS3ItemCategory.SHIELD_INFUSIBLE, + classification = ItemClassification.progression, # Crow trade + inject = True), # This is a shop/infinite drop item, but we need it in logic + DS3ItemData("Hawkwood's Shield", 0x01323E70, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Iron Round Shield", 0x01326580, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Wooden Shield", 0x0132DAB0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Kite Shield", 0x013301C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Ghru Rotshield", 0x013328D0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Havel's Greatshield", 0x013376F0, DS3ItemCategory.SHIELD), + DS3ItemData("Target Shield", 0x01339E00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Elkhorn Round Shield", 0x0133C510, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Warrior's Round Shield", 0x0133EC20, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Caduceus Round Shield", 0x01341330, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Red and White Shield", 0x01343A40, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Blessed Red and White Shield+1", 0x01343FB9, DS3ItemCategory.SHIELD), + DS3ItemData("Plank Shield", 0x01346150, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Leather Shield", 0x01348860, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Crimson Parma", 0x0134AF70, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Eastern Iron Shield", 0x0134D680, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Llewellyn Shield", 0x0134FD90, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Golden Falcon Shield", 0x01354BB0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Sacred Bloom Shield", 0x013572C0, DS3ItemCategory.SHIELD), + DS3ItemData("Ancient Dragon Greatshield", 0x013599D0, DS3ItemCategory.SHIELD), + DS3ItemData("Lothric Knight Shield", 0x01409650, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Knight Shield", 0x01410B80, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Pontiff Knight Shield", 0x014159A0, DS3ItemCategory.SHIELD), + DS3ItemData("Carthus Shield", 0x014180B0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Black Knight Shield", 0x0141F5E0, DS3ItemCategory.SHIELD), + DS3ItemData("Silver Knight Shield", 0x01424400, DS3ItemCategory.SHIELD), + DS3ItemData("Spiked Shield", 0x01426B10, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Pierce Shield", 0x01429220, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("East-West Shield", 0x0142B930, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Sunlight Shield", 0x0142E040, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Crest Shield", 0x01430750, DS3ItemCategory.SHIELD), + DS3ItemData("Dragon Crest Shield", 0x01432E60, DS3ItemCategory.SHIELD), + DS3ItemData("Spider Shield", 0x01435570, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Grass Crest Shield", 0x01437C80, DS3ItemCategory.SHIELD, + classification = ItemClassification.useful), + DS3ItemData("Sunset Shield", 0x0143A390, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Golden Wing Crest Shield", 0x0143CAA0, DS3ItemCategory.SHIELD), + DS3ItemData("Blue Wooden Shield", 0x0143F1B0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Silver Eagle Kite Shield", 0x014418C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Stone Parma", 0x01443FD0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Spirit Tree Crest Shield", 0x014466E0, DS3ItemCategory.SHIELD), + DS3ItemData("Porcine Shield", 0x01448DF0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Shield of Want", 0x0144B500, DS3ItemCategory.SHIELD), + DS3ItemData("Wargod Wooden Shield", 0x0144DC10, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Lothric Knight Greatshield", 0x014FD890, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Cathedral Knight Greatshield", 0x014FFFA0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Dragonslayer Greatshield", 0x01504DC0, DS3ItemCategory.SHIELD), + DS3ItemData("Moaning Shield", 0x015074D0, DS3ItemCategory.SHIELD, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Yhorm's Greatshield", 0x0150C2F0, DS3ItemCategory.SHIELD), + DS3ItemData("Black Iron Greatshield", 0x0150EA00, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Wolf Knight's Greatshield", 0x01511110, DS3ItemCategory.SHIELD, + inject = True), # Covenant reward + DS3ItemData("Twin Dragon Greatshield", 0x01513820, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Greatshield of Glory", 0x01515F30, DS3ItemCategory.SHIELD), + DS3ItemData("Curse Ward Greatshield", 0x01518640, DS3ItemCategory.SHIELD), + DS3ItemData("Bonewheel Shield", 0x0151AD50, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Stone Greatshield", 0x0151D460, DS3ItemCategory.SHIELD_INFUSIBLE), # Armor - ("Fallen Knight Helm", 0x1121EAC0, DS3ItemCategory.ARMOR), - ("Fallen Knight Armor", 0x1121EEA8, DS3ItemCategory.ARMOR), - ("Fallen Knight Gauntlets", 0x1121F290, DS3ItemCategory.ARMOR), - ("Fallen Knight Trousers", 0x1121F678, DS3ItemCategory.ARMOR), - ("Knight Helm", 0x11298BE0, DS3ItemCategory.ARMOR), - ("Knight Armor", 0x11298FC8, DS3ItemCategory.ARMOR), - ("Knight Gauntlets", 0x112993B0, DS3ItemCategory.ARMOR), - ("Knight Leggings", 0x11299798, DS3ItemCategory.ARMOR), - ("Firelink Helm", 0x11406F40, DS3ItemCategory.ARMOR), - ("Firelink Armor", 0x11407328, DS3ItemCategory.ARMOR), - ("Firelink Gauntlets", 0x11407710, DS3ItemCategory.ARMOR), - ("Firelink Leggings", 0x11407AF8, DS3ItemCategory.ARMOR), - ("Sellsword Helm", 0x11481060, DS3ItemCategory.ARMOR), - ("Sellsword Armor", 0x11481448, DS3ItemCategory.ARMOR), - ("Sellsword Gauntlet", 0x11481830, DS3ItemCategory.ARMOR), - ("Sellsword Trousers", 0x11481C18, DS3ItemCategory.ARMOR), - ("Herald Helm", 0x114FB180, DS3ItemCategory.ARMOR), - ("Herald Armor", 0x114FB568, DS3ItemCategory.ARMOR), - ("Herald Gloves", 0x114FB950, DS3ItemCategory.ARMOR), - ("Herald Trousers", 0x114FBD38, DS3ItemCategory.ARMOR), - ("Sunless Veil", 0x115752A0, DS3ItemCategory.ARMOR), - ("Sunless Armor", 0x11575688, DS3ItemCategory.ARMOR), - ("Sunless Gauntlets", 0x11575A70, DS3ItemCategory.ARMOR), - ("Sunless Leggings", 0x11575E58, DS3ItemCategory.ARMOR), - ("Black Hand Hat", 0x115EF3C0, DS3ItemCategory.ARMOR), - ("Black Hand Armor", 0x115EF7A8, DS3ItemCategory.ARMOR), - ("Assassin Gloves", 0x115EFB90, DS3ItemCategory.ARMOR), - ("Assassin Trousers", 0x115EFF78, DS3ItemCategory.ARMOR), - ("Assassin Hood", 0x11607A60, DS3ItemCategory.ARMOR), - ("Assassin Armor", 0x11607E48, DS3ItemCategory.ARMOR), - ("Xanthous Crown", 0x116694E0, DS3ItemCategory.ARMOR), - ("Xanthous Overcoat", 0x116698C8, DS3ItemCategory.ARMOR), - ("Xanthous Gloves", 0x11669CB0, DS3ItemCategory.ARMOR), - ("Xanthous Trousers", 0x1166A098, DS3ItemCategory.ARMOR), - ("Northern Helm", 0x116E3600, DS3ItemCategory.ARMOR), - ("Northern Armor", 0x116E39E8, DS3ItemCategory.ARMOR), - ("Northern Gloves", 0x116E3DD0, DS3ItemCategory.ARMOR), - ("Northern Trousers", 0x116E41B8, DS3ItemCategory.ARMOR), - ("Morne's Helm", 0x1175D720, DS3ItemCategory.ARMOR), - ("Morne's Armor", 0x1175DB08, DS3ItemCategory.ARMOR), - ("Morne's Gauntlets", 0x1175DEF0, DS3ItemCategory.ARMOR), - ("Morne's Leggings", 0x1175E2D8, DS3ItemCategory.ARMOR), - ("Silver Mask", 0x117D7840, DS3ItemCategory.ARMOR), - ("Leonhard's Garb", 0x117D7C28, DS3ItemCategory.ARMOR), - ("Leonhard's Gauntlets", 0x117D8010, DS3ItemCategory.ARMOR), - ("Leonhard's Trousers", 0x117D83F8, DS3ItemCategory.ARMOR), - ("Sneering Mask", 0x11851960, DS3ItemCategory.ARMOR), - ("Pale Shade Robe", 0x11851D48, DS3ItemCategory.ARMOR), - ("Pale Shade Gloves", 0x11852130, DS3ItemCategory.ARMOR), - ("Pale Shade Trousers", 0x11852518, DS3ItemCategory.ARMOR), - ("Sunset Helm", 0x118CBA80, DS3ItemCategory.ARMOR), - ("Sunset Armor", 0x118CBE68, DS3ItemCategory.ARMOR), - ("Sunset Gauntlets", 0x118CC250, DS3ItemCategory.ARMOR), - ("Sunset Leggings", 0x118CC638, DS3ItemCategory.ARMOR), - ("Old Sage's Blindfold", 0x11945BA0, DS3ItemCategory.ARMOR), - ("Cornyx's Garb", 0x11945F88, DS3ItemCategory.ARMOR), - ("Cornyx's Wrap", 0x11946370, DS3ItemCategory.ARMOR), - ("Cornyx's Skirt", 0x11946758, DS3ItemCategory.ARMOR), - ("Executioner Helm", 0x119BFCC0, DS3ItemCategory.ARMOR), - ("Executioner Armor", 0x119C00A8, DS3ItemCategory.ARMOR), - ("Executioner Gauntlets", 0x119C0490, DS3ItemCategory.ARMOR), - ("Executioner Leggings", 0x119C0878, DS3ItemCategory.ARMOR), - ("Billed Mask", 0x11A39DE0, DS3ItemCategory.ARMOR), - ("Black Dress", 0x11A3A1C8, DS3ItemCategory.ARMOR), - ("Black Gauntlets", 0x11A3A5B0, DS3ItemCategory.ARMOR), - ("Black Leggings", 0x11A3A998, DS3ItemCategory.ARMOR), - ("Pyromancer Crown", 0x11AB3F00, DS3ItemCategory.ARMOR), - ("Pyromancer Garb", 0x11AB42E8, DS3ItemCategory.ARMOR), - ("Pyromancer Wrap", 0x11AB46D0, DS3ItemCategory.ARMOR), - ("Pyromancer Trousers", 0x11AB4AB8, DS3ItemCategory.ARMOR), - ("Court Sorcerer Hood", 0x11BA8140, DS3ItemCategory.ARMOR), - ("Court Sorcerer Robe", 0x11BA8528, DS3ItemCategory.ARMOR), - ("Court Sorcerer Gloves", 0x11BA8910, DS3ItemCategory.ARMOR), - ("Court Sorcerer Trousers", 0x11BA8CF8, DS3ItemCategory.ARMOR), - ("Sorcerer Hood", 0x11C9C380, DS3ItemCategory.ARMOR), - ("Sorcerer Robe", 0x11C9C768, DS3ItemCategory.ARMOR), - ("Sorcerer Gloves", 0x11C9CB50, DS3ItemCategory.ARMOR), - ("Sorcerer Trousers", 0x11C9CF38, DS3ItemCategory.ARMOR), - ("Clandestine Coat", 0x11CB4E08, DS3ItemCategory.ARMOR), - ("Cleric Hat", 0x11D905C0, DS3ItemCategory.ARMOR), - ("Cleric Blue Robe", 0x11D909A8, DS3ItemCategory.ARMOR), - ("Cleric Gloves", 0x11D90D90, DS3ItemCategory.ARMOR), - ("Cleric Trousers", 0x11D91178, DS3ItemCategory.ARMOR), - ("Steel Soldier Helm", 0x12625A00, DS3ItemCategory.ARMOR), - ("Deserter Armor", 0x12625DE8, DS3ItemCategory.ARMOR), - ("Deserter Trousers", 0x126265B8, DS3ItemCategory.ARMOR), - ("Thief Mask", 0x12656740, DS3ItemCategory.ARMOR), - ("Sage's Big Hat", 0x129020C0, DS3ItemCategory.ARMOR), - ("Aristocrat's Mask", 0x129F6300, DS3ItemCategory.ARMOR), - ("Jailer Robe", 0x129F66E8, DS3ItemCategory.ARMOR), - ("Jailer Gloves", 0x129F6AD0, DS3ItemCategory.ARMOR), - ("Jailer Trousers", 0x129F6EB8, DS3ItemCategory.ARMOR), - ("Grave Warden Hood", 0x12BDE780, DS3ItemCategory.ARMOR), - ("Grave Warden Robe", 0x12BDEB68, DS3ItemCategory.ARMOR), - ("Grave Warden Wrap", 0x12BDEF50, DS3ItemCategory.ARMOR), - ("Grave Warden Skirt", 0x12BDF338, DS3ItemCategory.ARMOR), - ("Worker Hat", 0x12CD29C0, DS3ItemCategory.ARMOR), - ("Worker Garb", 0x12CD2DA8, DS3ItemCategory.ARMOR), - ("Worker Gloves", 0x12CD3190, DS3ItemCategory.ARMOR), - ("Worker Trousers", 0x12CD3578, DS3ItemCategory.ARMOR), - ("Thrall Hood", 0x12D4CAE0, DS3ItemCategory.ARMOR), - ("Evangelist Hat", 0x12DC6C00, DS3ItemCategory.ARMOR), - ("Evangelist Robe", 0x12DC6FE8, DS3ItemCategory.ARMOR), - ("Evangelist Gloves", 0x12DC73D0, DS3ItemCategory.ARMOR), - ("Evangelist Trousers", 0x12DC77B8, DS3ItemCategory.ARMOR), - ("Scholar's Robe", 0x12E41108, DS3ItemCategory.ARMOR), - ("Winged Knight Helm", 0x12EBAE40, DS3ItemCategory.ARMOR), - ("Winged Knight Armor", 0x12EBB228, DS3ItemCategory.ARMOR), - ("Winged Knight Gauntlets", 0x12EBB610, DS3ItemCategory.ARMOR), - ("Winged Knight Leggings", 0x12EBB9F8, DS3ItemCategory.ARMOR), - ("Cathedral Knight Helm", 0x130291A0, DS3ItemCategory.ARMOR), - ("Cathedral Knight Armor", 0x13029588, DS3ItemCategory.ARMOR), - ("Cathedral Knight Gauntlets", 0x13029970, DS3ItemCategory.ARMOR), - ("Cathedral Knight Leggings", 0x13029D58, DS3ItemCategory.ARMOR), - ("Lothric Knight Helm", 0x13197500, DS3ItemCategory.ARMOR), - ("Lothric Knight Armor", 0x131978E8, DS3ItemCategory.ARMOR), - ("Lothric Knight Gauntlets", 0x13197CD0, DS3ItemCategory.ARMOR), - ("Lothric Knight Leggings", 0x131980B8, DS3ItemCategory.ARMOR), - ("Outrider Knight Helm", 0x1328B740, DS3ItemCategory.ARMOR), - ("Outrider Knight Armor", 0x1328BB28, DS3ItemCategory.ARMOR), - ("Outrider Knight Gauntlets", 0x1328BF10, DS3ItemCategory.ARMOR), - ("Outrider Knight Leggings", 0x1328C2F8, DS3ItemCategory.ARMOR), - ("Black Knight Helm", 0x1337F980, DS3ItemCategory.ARMOR), - ("Black Knight Armor", 0x1337FD68, DS3ItemCategory.ARMOR), - ("Black Knight Gauntlets", 0x13380150, DS3ItemCategory.ARMOR), - ("Black Knight Leggings", 0x13380538, DS3ItemCategory.ARMOR), - ("Dark Mask", 0x133F9AA0, DS3ItemCategory.ARMOR), - ("Dark Armor", 0x133F9E88, DS3ItemCategory.ARMOR), - ("Dark Gauntlets", 0x133FA270, DS3ItemCategory.ARMOR), - ("Dark Leggings", 0x133FA658, DS3ItemCategory.ARMOR), - ("Exile Mask", 0x13473BC0, DS3ItemCategory.ARMOR), - ("Exile Armor", 0x13473FA8, DS3ItemCategory.ARMOR), - ("Exile Gauntlets", 0x13474390, DS3ItemCategory.ARMOR), - ("Exile Leggings", 0x13474778, DS3ItemCategory.ARMOR), - ("Pontiff Knight Crown", 0x13567E00, DS3ItemCategory.ARMOR), - ("Pontiff Knight Armor", 0x135681E8, DS3ItemCategory.ARMOR), - ("Pontiff Knight Gauntlets", 0x135685D0, DS3ItemCategory.ARMOR), - ("Pontiff Knight Leggings", 0x135689B8, DS3ItemCategory.ARMOR), - ("Golden Crown", 0x1365C040, DS3ItemCategory.ARMOR), - ("Dragonscale Armor", 0x1365C428, DS3ItemCategory.ARMOR), - ("Golden Bracelets", 0x1365C810, DS3ItemCategory.ARMOR), - ("Dragonscale Waistcloth", 0x1365CBF8, DS3ItemCategory.ARMOR), - ("Wolnir's Crown", 0x136D6160, DS3ItemCategory.ARMOR), - ("Undead Legion Helm", 0x13750280, DS3ItemCategory.ARMOR), - ("Undead Legion Armor", 0x13750668, DS3ItemCategory.ARMOR), - ("Undead Legion Gauntlets", 0x13750A50, DS3ItemCategory.ARMOR), - ("Undead Legion Leggings", 0x13750E38, DS3ItemCategory.ARMOR), - ("Fire Witch Helm", 0x13938700, DS3ItemCategory.ARMOR), - ("Fire Witch Armor", 0x13938AE8, DS3ItemCategory.ARMOR), - ("Fire Witch Gauntlets", 0x13938ED0, DS3ItemCategory.ARMOR), - ("Fire Witch Leggings", 0x139392B8, DS3ItemCategory.ARMOR), - ("Lorian's Helm", 0x13A2C940, DS3ItemCategory.ARMOR), - ("Lorian's Armor", 0x13A2CD28, DS3ItemCategory.ARMOR), - ("Lorian's Gauntlets", 0x13A2D110, DS3ItemCategory.ARMOR), - ("Lorian's Leggings", 0x13A2D4F8, DS3ItemCategory.ARMOR), - ("Hood of Prayer", 0x13AA6A60, DS3ItemCategory.ARMOR), - ("Robe of Prayer", 0x13AA6E48, DS3ItemCategory.ARMOR), - ("Skirt of Prayer", 0x13AA7618, DS3ItemCategory.ARMOR), - ("Dancer's Crown", 0x13C14DC0, DS3ItemCategory.ARMOR), - ("Dancer's Armor", 0x13C151A8, DS3ItemCategory.ARMOR), - ("Dancer's Gauntlets", 0x13C15590, DS3ItemCategory.ARMOR), - ("Dancer's Leggings", 0x13C15978, DS3ItemCategory.ARMOR), - ("Gundyr's Helm", 0x13D09000, DS3ItemCategory.ARMOR), - ("Gundyr's Armor", 0x13D093E8, DS3ItemCategory.ARMOR), - ("Gundyr's Gauntlets", 0x13D097D0, DS3ItemCategory.ARMOR), - ("Gundyr's Leggings", 0x13D09BB8, DS3ItemCategory.ARMOR), - ("Archdeacon White Crown", 0x13EF1480, DS3ItemCategory.ARMOR), - ("Archdeacon Holy Garb", 0x13EF1868, DS3ItemCategory.ARMOR), - ("Archdeacon Skirt", 0x13EF2038, DS3ItemCategory.ARMOR), - ("Deacon Robe", 0x13F6B988, DS3ItemCategory.ARMOR), - ("Deacon Skirt", 0x13F6C158, DS3ItemCategory.ARMOR), - ("Fire Keeper Robe", 0x140D9CE8, DS3ItemCategory.ARMOR), - ("Fire Keeper Gloves", 0x140DA0D0, DS3ItemCategory.ARMOR), - ("Fire Keeper Skirt", 0x140DA4B8, DS3ItemCategory.ARMOR), - ("Chain Helm", 0x142C1D80, DS3ItemCategory.ARMOR), - ("Chain Armor", 0x142C2168, DS3ItemCategory.ARMOR), - ("Leather Gauntlets", 0x142C2550, DS3ItemCategory.ARMOR), - ("Chain Leggings", 0x142C2938, DS3ItemCategory.ARMOR), - ("Nameless Knight Helm", 0x143B5FC0, DS3ItemCategory.ARMOR), - ("Nameless Knight Armor", 0x143B63A8, DS3ItemCategory.ARMOR), - ("Nameless Knight Gauntlets", 0x143B6790, DS3ItemCategory.ARMOR), - ("Nameless Knight Leggings", 0x143B6B78, DS3ItemCategory.ARMOR), - ("Elite Knight Helm", 0x144AA200, DS3ItemCategory.ARMOR), - ("Elite Knight Armor", 0x144AA5E8, DS3ItemCategory.ARMOR), - ("Elite Knight Gauntlets", 0x144AA9D0, DS3ItemCategory.ARMOR), - ("Elite Knight Leggings", 0x144AADB8, DS3ItemCategory.ARMOR), - ("Faraam Helm", 0x1459E440, DS3ItemCategory.ARMOR), - ("Faraam Armor", 0x1459E828, DS3ItemCategory.ARMOR), - ("Faraam Gauntlets", 0x1459EC10, DS3ItemCategory.ARMOR), - ("Faraam Boots", 0x1459EFF8, DS3ItemCategory.ARMOR), - ("Catarina Helm", 0x14692680, DS3ItemCategory.ARMOR), - ("Catarina Armor", 0x14692A68, DS3ItemCategory.ARMOR), - ("Catarina Gauntlets", 0x14692E50, DS3ItemCategory.ARMOR), - ("Catarina Leggings", 0x14693238, DS3ItemCategory.ARMOR), - ("Standard Helm", 0x1470C7A0, DS3ItemCategory.ARMOR), - ("Hard Leather Armor", 0x1470CB88, DS3ItemCategory.ARMOR), - ("Hard Leather Gauntlets", 0x1470CF70, DS3ItemCategory.ARMOR), - ("Hard Leather Boots", 0x1470D358, DS3ItemCategory.ARMOR), - ("Havel's Helm", 0x147868C0, DS3ItemCategory.ARMOR), - ("Havel's Armor", 0x14786CA8, DS3ItemCategory.ARMOR), - ("Havel's Gauntlets", 0x14787090, DS3ItemCategory.ARMOR), - ("Havel's Leggings", 0x14787478, DS3ItemCategory.ARMOR), - ("Brigand Hood", 0x148009E0, DS3ItemCategory.ARMOR), - ("Brigand Armor", 0x14800DC8, DS3ItemCategory.ARMOR), - ("Brigand Gauntlets", 0x148011B0, DS3ItemCategory.ARMOR), - ("Brigand Trousers", 0x14801598, DS3ItemCategory.ARMOR), - ("Pharis's Hat", 0x1487AB00, DS3ItemCategory.ARMOR), - ("Leather Armor", 0x1487AEE8, DS3ItemCategory.ARMOR), - ("Leather Gloves", 0x1487B2D0, DS3ItemCategory.ARMOR), - ("Leather Boots", 0x1487B6B8, DS3ItemCategory.ARMOR), - ("Ragged Mask", 0x148F4C20, DS3ItemCategory.ARMOR), - ("Master's Attire", 0x148F5008, DS3ItemCategory.ARMOR), - ("Master's Gloves", 0x148F53F0, DS3ItemCategory.ARMOR), - ("Loincloth", 0x148F57D8, DS3ItemCategory.ARMOR), - ("Old Sorcerer Hat", 0x1496ED40, DS3ItemCategory.ARMOR), - ("Old Sorcerer Coat", 0x1496F128, DS3ItemCategory.ARMOR), - ("Old Sorcerer Gauntlets", 0x1496F510, DS3ItemCategory.ARMOR), - ("Old Sorcerer Boots", 0x1496F8F8, DS3ItemCategory.ARMOR), - ("Conjurator Hood", 0x149E8E60, DS3ItemCategory.ARMOR), - ("Conjurator Robe", 0x149E9248, DS3ItemCategory.ARMOR), - ("Conjurator Manchettes", 0x149E9630, DS3ItemCategory.ARMOR), - ("Conjurator Boots", 0x149E9A18, DS3ItemCategory.ARMOR), - ("Black Leather Armor", 0x14A63368, DS3ItemCategory.ARMOR), - ("Black Leather Gloves", 0x14A63750, DS3ItemCategory.ARMOR), - ("Black Leather Boots", 0x14A63B38, DS3ItemCategory.ARMOR), - ("Symbol of Avarice", 0x14ADD0A0, DS3ItemCategory.ARMOR), - ("Creighton's Steel Mask", 0x14B571C0, DS3ItemCategory.ARMOR), - ("Mirrah Chain Mail", 0x14B575A8, DS3ItemCategory.ARMOR), - ("Mirrah Chain Gloves", 0x14B57990, DS3ItemCategory.ARMOR), - ("Mirrah Chain Leggings", 0x14B57D78, DS3ItemCategory.ARMOR), - ("Maiden Hood", 0x14BD12E0, DS3ItemCategory.ARMOR), - ("Maiden Robe", 0x14BD16C8, DS3ItemCategory.ARMOR), - ("Maiden Gloves", 0x14BD1AB0, DS3ItemCategory.ARMOR), - ("Maiden Skirt", 0x14BD1E98, DS3ItemCategory.ARMOR), - ("Alva Helm", 0x14C4B400, DS3ItemCategory.ARMOR), - ("Alva Armor", 0x14C4B7E8, DS3ItemCategory.ARMOR), - ("Alva Gauntlets", 0x14C4BBD0, DS3ItemCategory.ARMOR), - ("Alva Leggings", 0x14C4BFB8, DS3ItemCategory.ARMOR), - ("Shadow Mask", 0x14D3F640, DS3ItemCategory.ARMOR), - ("Shadow Garb", 0x14D3FA28, DS3ItemCategory.ARMOR), - ("Shadow Gauntlets", 0x14D3FE10, DS3ItemCategory.ARMOR), - ("Shadow Leggings", 0x14D401F8, DS3ItemCategory.ARMOR), - ("Eastern Helm", 0x14E33880, DS3ItemCategory.ARMOR), - ("Eastern Armor", 0x14E33C68, DS3ItemCategory.ARMOR), - ("Eastern Gauntlets", 0x14E34050, DS3ItemCategory.ARMOR), - ("Eastern Leggings", 0x14E34438, DS3ItemCategory.ARMOR), - ("Helm of Favor", 0x14F27AC0, DS3ItemCategory.ARMOR), - ("Embraced Armor of Favor", 0x14F27EA8, DS3ItemCategory.ARMOR), - ("Gauntlets of Favor", 0x14F28290, DS3ItemCategory.ARMOR), - ("Leggings of Favor", 0x14F28678, DS3ItemCategory.ARMOR), - ("Brass Helm", 0x1501BD00, DS3ItemCategory.ARMOR), - ("Brass Armor", 0x1501C0E8, DS3ItemCategory.ARMOR), - ("Brass Gauntlets", 0x1501C4D0, DS3ItemCategory.ARMOR), - ("Brass Leggings", 0x1501C8B8, DS3ItemCategory.ARMOR), - ("Silver Knight Helm", 0x1510FF40, DS3ItemCategory.ARMOR), - ("Silver Knight Armor", 0x15110328, DS3ItemCategory.ARMOR), - ("Silver Knight Gauntlets", 0x15110710, DS3ItemCategory.ARMOR), - ("Silver Knight Leggings", 0x15110AF8, DS3ItemCategory.ARMOR), - ("Lucatiel's Mask", 0x15204180, DS3ItemCategory.ARMOR), - ("Mirrah Vest", 0x15204568, DS3ItemCategory.ARMOR), - ("Mirrah Gloves", 0x15204950, DS3ItemCategory.ARMOR), - ("Mirrah Trousers", 0x15204D38, DS3ItemCategory.ARMOR), - ("Iron Helm", 0x152F83C0, DS3ItemCategory.ARMOR), - ("Armor of the Sun", 0x152F87A8, DS3ItemCategory.ARMOR), - ("Iron Bracelets", 0x152F8B90, DS3ItemCategory.ARMOR), - ("Iron Leggings", 0x152F8F78, DS3ItemCategory.ARMOR), - ("Drakeblood Helm", 0x153EC600, DS3ItemCategory.ARMOR), - ("Drakeblood Armor", 0x153EC9E8, DS3ItemCategory.ARMOR), - ("Drakeblood Gauntlets", 0x153ECDD0, DS3ItemCategory.ARMOR), - ("Drakeblood Leggings", 0x153ED1B8, DS3ItemCategory.ARMOR), - ("Drang Armor", 0x154E0C28, DS3ItemCategory.ARMOR), - ("Drang Gauntlets", 0x154E1010, DS3ItemCategory.ARMOR), - ("Drang Shoes", 0x154E13F8, DS3ItemCategory.ARMOR), - ("Black Iron Helm", 0x155D4A80, DS3ItemCategory.ARMOR), - ("Black Iron Armor", 0x155D4E68, DS3ItemCategory.ARMOR), - ("Black Iron Gauntlets", 0x155D5250, DS3ItemCategory.ARMOR), - ("Black Iron Leggings", 0x155D5638, DS3ItemCategory.ARMOR), - ("Painting Guardian Hood", 0x156C8CC0, DS3ItemCategory.ARMOR), - ("Painting Guardian Gown", 0x156C90A8, DS3ItemCategory.ARMOR), - ("Painting Guardian Gloves", 0x156C9490, DS3ItemCategory.ARMOR), - ("Painting Guardian Waistcloth", 0x156C9878, DS3ItemCategory.ARMOR), - ("Wolf Knight Helm", 0x157BCF00, DS3ItemCategory.ARMOR), - ("Wolf Knight Armor", 0x157BD2E8, DS3ItemCategory.ARMOR), - ("Wolf Knight Gauntlets", 0x157BD6D0, DS3ItemCategory.ARMOR), - ("Wolf Knight Leggings", 0x157BDAB8, DS3ItemCategory.ARMOR), - ("Dragonslayer Helm", 0x158B1140, DS3ItemCategory.ARMOR), - ("Dragonslayer Armor", 0x158B1528, DS3ItemCategory.ARMOR), - ("Dragonslayer Gauntlets", 0x158B1910, DS3ItemCategory.ARMOR), - ("Dragonslayer Leggings", 0x158B1CF8, DS3ItemCategory.ARMOR), - ("Smough's Helm", 0x159A5380, DS3ItemCategory.ARMOR), - ("Smough's Armor", 0x159A5768, DS3ItemCategory.ARMOR), - ("Smough's Gauntlets", 0x159A5B50, DS3ItemCategory.ARMOR), - ("Smough's Leggings", 0x159A5F38, DS3ItemCategory.ARMOR), - ("Helm of Thorns", 0x15B8D800, DS3ItemCategory.ARMOR), - ("Armor of Thorns", 0x15B8DBE8, DS3ItemCategory.ARMOR), - ("Gauntlets of Thorns", 0x15B8DFD0, DS3ItemCategory.ARMOR), - ("Leggings of Thorns", 0x15B8E3B8, DS3ItemCategory.ARMOR), - ("Crown of Dusk", 0x15D75C80, DS3ItemCategory.ARMOR), - ("Antiquated Dress", 0x15D76068, DS3ItemCategory.ARMOR), - ("Antiquated Gloves", 0x15D76450, DS3ItemCategory.ARMOR), - ("Antiquated Skirt", 0x15D76838, DS3ItemCategory.ARMOR), - ("Karla's Pointed Hat", 0x15E69EC0, DS3ItemCategory.ARMOR), - ("Karla's Coat", 0x15E6A2A8, DS3ItemCategory.ARMOR), - ("Karla's Gloves", 0x15E6A690, DS3ItemCategory.ARMOR), - ("Karla's Trousers", 0x15E6AA78, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Helm", 0x1121EAC0, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Armor", 0x1121EEA8, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Gauntlets", 0x1121F290, DS3ItemCategory.ARMOR), + DS3ItemData("Fallen Knight Trousers", 0x1121F678, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Helm", 0x11298BE0, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Armor", 0x11298FC8, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Gauntlets", 0x112993B0, DS3ItemCategory.ARMOR), + DS3ItemData("Knight Leggings", 0x11299798, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Helm", 0x11406F40, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Armor", 0x11407328, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Gauntlets", 0x11407710, DS3ItemCategory.ARMOR), + DS3ItemData("Firelink Leggings", 0x11407AF8, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Helm", 0x11481060, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Armor", 0x11481448, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Gauntlet", 0x11481830, DS3ItemCategory.ARMOR), + DS3ItemData("Sellsword Trousers", 0x11481C18, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Helm", 0x114FB180, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Armor", 0x114FB568, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Gloves", 0x114FB950, DS3ItemCategory.ARMOR), + DS3ItemData("Herald Trousers", 0x114FBD38, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Veil", 0x115752A0, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Armor", 0x11575688, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Gauntlets", 0x11575A70, DS3ItemCategory.ARMOR), + DS3ItemData("Sunless Leggings", 0x11575E58, DS3ItemCategory.ARMOR), + DS3ItemData("Black Hand Hat", 0x115EF3C0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Hand Armor", 0x115EF7A8, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Gloves", 0x115EFB90, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Trousers", 0x115EFF78, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Hood", 0x11607A60, DS3ItemCategory.ARMOR), + DS3ItemData("Assassin Armor", 0x11607E48, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Crown", 0x116694E0, DS3ItemCategory.ARMOR, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Xanthous Overcoat", 0x116698C8, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Gloves", 0x11669CB0, DS3ItemCategory.ARMOR), + DS3ItemData("Xanthous Trousers", 0x1166A098, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Helm", 0x116E3600, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Armor", 0x116E39E8, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Gloves", 0x116E3DD0, DS3ItemCategory.ARMOR), + DS3ItemData("Northern Trousers", 0x116E41B8, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Helm", 0x1175D720, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Armor", 0x1175DB08, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Gauntlets", 0x1175DEF0, DS3ItemCategory.ARMOR), + DS3ItemData("Morne's Leggings", 0x1175E2D8, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Mask", 0x117D7840, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Garb", 0x117D7C28, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Gauntlets", 0x117D8010, DS3ItemCategory.ARMOR), + DS3ItemData("Leonhard's Trousers", 0x117D83F8, DS3ItemCategory.ARMOR), + DS3ItemData("Sneering Mask", 0x11851960, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Robe", 0x11851D48, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Gloves", 0x11852130, DS3ItemCategory.ARMOR), + DS3ItemData("Pale Shade Trousers", 0x11852518, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Helm", 0x118CBA80, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Armor", 0x118CBE68, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Gauntlets", 0x118CC250, DS3ItemCategory.ARMOR), + DS3ItemData("Sunset Leggings", 0x118CC638, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sage's Blindfold", 0x11945BA0, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Garb", 0x11945F88, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Wrap", 0x11946370, DS3ItemCategory.ARMOR), + DS3ItemData("Cornyx's Skirt", 0x11946758, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Helm", 0x119BFCC0, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Armor", 0x119C00A8, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Gauntlets", 0x119C0490, DS3ItemCategory.ARMOR), + DS3ItemData("Executioner Leggings", 0x119C0878, DS3ItemCategory.ARMOR), + DS3ItemData("Billed Mask", 0x11A39DE0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Dress", 0x11A3A1C8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Gauntlets", 0x11A3A5B0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leggings", 0x11A3A998, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Crown", 0x11AB3F00, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Garb", 0x11AB42E8, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Wrap", 0x11AB46D0, DS3ItemCategory.ARMOR), + DS3ItemData("Pyromancer Trousers", 0x11AB4AB8, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Hood", 0x11BA8140, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Robe", 0x11BA8528, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Gloves", 0x11BA8910, DS3ItemCategory.ARMOR), + DS3ItemData("Court Sorcerer Trousers", 0x11BA8CF8, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Hood", 0x11C9C380, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Robe", 0x11C9C768, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Gloves", 0x11C9CB50, DS3ItemCategory.ARMOR), + DS3ItemData("Sorcerer Trousers", 0x11C9CF38, DS3ItemCategory.ARMOR), + DS3ItemData("Clandestine Coat", 0x11CB4E08, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Hat", 0x11D905C0, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Blue Robe", 0x11D909A8, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Gloves", 0x11D90D90, DS3ItemCategory.ARMOR), + DS3ItemData("Cleric Trousers", 0x11D91178, DS3ItemCategory.ARMOR), + DS3ItemData("Steel Soldier Helm", 0x12625A00, DS3ItemCategory.ARMOR), + DS3ItemData("Deserter Armor", 0x12625DE8, DS3ItemCategory.ARMOR), + DS3ItemData("Deserter Trousers", 0x126265B8, DS3ItemCategory.ARMOR), + DS3ItemData("Thief Mask", 0x12656740, DS3ItemCategory.ARMOR), + DS3ItemData("Sage's Big Hat", 0x129020C0, DS3ItemCategory.ARMOR), + DS3ItemData("Aristocrat's Mask", 0x129F6300, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Robe", 0x129F66E8, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Gloves", 0x129F6AD0, DS3ItemCategory.ARMOR), + DS3ItemData("Jailer Trousers", 0x129F6EB8, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Hood", 0x12BDE780, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Robe", 0x12BDEB68, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Wrap", 0x12BDEF50, DS3ItemCategory.ARMOR), + DS3ItemData("Grave Warden Skirt", 0x12BDF338, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Hat", 0x12CD29C0, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Garb", 0x12CD2DA8, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Gloves", 0x12CD3190, DS3ItemCategory.ARMOR), + DS3ItemData("Worker Trousers", 0x12CD3578, DS3ItemCategory.ARMOR), + DS3ItemData("Thrall Hood", 0x12D4CAE0, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Hat", 0x12DC6C00, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Robe", 0x12DC6FE8, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Gloves", 0x12DC73D0, DS3ItemCategory.ARMOR), + DS3ItemData("Evangelist Trousers", 0x12DC77B8, DS3ItemCategory.ARMOR), + DS3ItemData("Scholar's Robe", 0x12E41108, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Helm", 0x12EBAE40, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Armor", 0x12EBB228, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Gauntlets", 0x12EBB610, DS3ItemCategory.ARMOR), + DS3ItemData("Winged Knight Leggings", 0x12EBB9F8, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Helm", 0x130291A0, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Armor", 0x13029588, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Gauntlets", 0x13029970, DS3ItemCategory.ARMOR), + DS3ItemData("Cathedral Knight Leggings", 0x13029D58, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Helm", 0x13197500, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Armor", 0x131978E8, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Gauntlets", 0x13197CD0, DS3ItemCategory.ARMOR), + DS3ItemData("Lothric Knight Leggings", 0x131980B8, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Helm", 0x1328B740, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Armor", 0x1328BB28, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Gauntlets", 0x1328BF10, DS3ItemCategory.ARMOR), + DS3ItemData("Outrider Knight Leggings", 0x1328C2F8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Helm", 0x1337F980, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Armor", 0x1337FD68, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Gauntlets", 0x13380150, DS3ItemCategory.ARMOR), + DS3ItemData("Black Knight Leggings", 0x13380538, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Mask", 0x133F9AA0, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Armor", 0x133F9E88, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Gauntlets", 0x133FA270, DS3ItemCategory.ARMOR), + DS3ItemData("Dark Leggings", 0x133FA658, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Mask", 0x13473BC0, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Armor", 0x13473FA8, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Gauntlets", 0x13474390, DS3ItemCategory.ARMOR), + DS3ItemData("Exile Leggings", 0x13474778, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Crown", 0x13567E00, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Armor", 0x135681E8, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Gauntlets", 0x135685D0, DS3ItemCategory.ARMOR), + DS3ItemData("Pontiff Knight Leggings", 0x135689B8, DS3ItemCategory.ARMOR), + DS3ItemData("Golden Crown", 0x1365C040, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonscale Armor", 0x1365C428, DS3ItemCategory.ARMOR), + DS3ItemData("Golden Bracelets", 0x1365C810, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonscale Waistcloth", 0x1365CBF8, DS3ItemCategory.ARMOR), + DS3ItemData("Wolnir's Crown", 0x136D6160, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Helm", 0x13750280, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Armor", 0x13750668, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Gauntlet", 0x13750A50, DS3ItemCategory.ARMOR), + DS3ItemData("Undead Legion Leggings", 0x13750E38, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Helm", 0x13938700, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Armor", 0x13938AE8, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Gauntlets", 0x13938ED0, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Witch Leggings", 0x139392B8, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Helm", 0x13A2C940, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Armor", 0x13A2CD28, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Gauntlets", 0x13A2D110, DS3ItemCategory.ARMOR), + DS3ItemData("Lorian's Leggings", 0x13A2D4F8, DS3ItemCategory.ARMOR), + DS3ItemData("Hood of Prayer", 0x13AA6A60, DS3ItemCategory.ARMOR), + DS3ItemData("Robe of Prayer", 0x13AA6E48, DS3ItemCategory.ARMOR), + DS3ItemData("Skirt of Prayer", 0x13AA7618, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Crown", 0x13C14DC0, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Armor", 0x13C151A8, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Gauntlets", 0x13C15590, DS3ItemCategory.ARMOR), + DS3ItemData("Dancer's Leggings", 0x13C15978, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Helm", 0x13D09000, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Armor", 0x13D093E8, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Gauntlets", 0x13D097D0, DS3ItemCategory.ARMOR), + DS3ItemData("Gundyr's Leggings", 0x13D09BB8, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon White Crown", 0x13EF1480, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon Holy Garb", 0x13EF1868, DS3ItemCategory.ARMOR), + DS3ItemData("Archdeacon Skirt", 0x13EF2038, DS3ItemCategory.ARMOR), + DS3ItemData("Deacon Robe", 0x13F6B988, DS3ItemCategory.ARMOR), + DS3ItemData("Deacon Skirt", 0x13F6C158, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Robe", 0x140D9CE8, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Gloves", 0x140DA0D0, DS3ItemCategory.ARMOR), + DS3ItemData("Fire Keeper Skirt", 0x140DA4B8, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Helm", 0x142C1D80, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Armor", 0x142C2168, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Gauntlets", 0x142C2550, DS3ItemCategory.ARMOR), + DS3ItemData("Chain Leggings", 0x142C2938, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Helm", 0x143B5FC0, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Armor", 0x143B63A8, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Gauntlets", 0x143B6790, DS3ItemCategory.ARMOR), + DS3ItemData("Nameless Knight Leggings", 0x143B6B78, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Helm", 0x144AA200, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Armor", 0x144AA5E8, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Gauntlets", 0x144AA9D0, DS3ItemCategory.ARMOR), + DS3ItemData("Elite Knight Leggings", 0x144AADB8, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Helm", 0x1459E440, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Armor", 0x1459E828, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Gauntlets", 0x1459EC10, DS3ItemCategory.ARMOR), + DS3ItemData("Faraam Boots", 0x1459EFF8, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Helm", 0x14692680, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Armor", 0x14692A68, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Gauntlets", 0x14692E50, DS3ItemCategory.ARMOR), + DS3ItemData("Catarina Leggings", 0x14693238, DS3ItemCategory.ARMOR), + DS3ItemData("Standard Helm", 0x1470C7A0, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Armor", 0x1470CB88, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Gauntlets", 0x1470CF70, DS3ItemCategory.ARMOR), + DS3ItemData("Hard Leather Boots", 0x1470D358, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Helm", 0x147868C0, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Armor", 0x14786CA8, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Gauntlets", 0x14787090, DS3ItemCategory.ARMOR), + DS3ItemData("Havel's Leggings", 0x14787478, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Hood", 0x148009E0, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Armor", 0x14800DC8, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Gauntlets", 0x148011B0, DS3ItemCategory.ARMOR), + DS3ItemData("Brigand Trousers", 0x14801598, DS3ItemCategory.ARMOR), + DS3ItemData("Pharis's Hat", 0x1487AB00, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Armor", 0x1487AEE8, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Gloves", 0x1487B2D0, DS3ItemCategory.ARMOR), + DS3ItemData("Leather Boots", 0x1487B6B8, DS3ItemCategory.ARMOR), + DS3ItemData("Ragged Mask", 0x148F4C20, DS3ItemCategory.ARMOR), + DS3ItemData("Master's Attire", 0x148F5008, DS3ItemCategory.ARMOR), + DS3ItemData("Master's Gloves", 0x148F53F0, DS3ItemCategory.ARMOR), + DS3ItemData("Loincloth", 0x148F57D8, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Hat", 0x1496ED40, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Coat", 0x1496F128, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Gauntlets", 0x1496F510, DS3ItemCategory.ARMOR), + DS3ItemData("Old Sorcerer Boots", 0x1496F8F8, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Hood", 0x149E8E60, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Robe", 0x149E9248, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Manchettes", 0x149E9630, DS3ItemCategory.ARMOR), + DS3ItemData("Conjurator Boots", 0x149E9A18, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Armor", 0x14A63368, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Gloves", 0x14A63750, DS3ItemCategory.ARMOR), + DS3ItemData("Black Leather Boots", 0x14A63B38, DS3ItemCategory.ARMOR), + DS3ItemData("Symbol of Avarice", 0x14ADD0A0, DS3ItemCategory.ARMOR), + DS3ItemData("Creighton's Steel Mask", 0x14B571C0, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Mail", 0x14B575A8, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Gloves", 0x14B57990, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Chain Leggings", 0x14B57D78, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Hood", 0x14BD12E0, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Robe", 0x14BD16C8, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Gloves", 0x14BD1AB0, DS3ItemCategory.ARMOR), + DS3ItemData("Maiden Skirt", 0x14BD1E98, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Helm", 0x14C4B400, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Armor", 0x14C4B7E8, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Gauntlets", 0x14C4BBD0, DS3ItemCategory.ARMOR), + DS3ItemData("Alva Leggings", 0x14C4BFB8, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Mask", 0x14D3F640, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Garb", 0x14D3FA28, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Gauntlets", 0x14D3FE10, DS3ItemCategory.ARMOR), + DS3ItemData("Shadow Leggings", 0x14D401F8, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Helm", 0x14E33880, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Armor", 0x14E33C68, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Gauntlets", 0x14E34050, DS3ItemCategory.ARMOR), + DS3ItemData("Eastern Leggings", 0x14E34438, DS3ItemCategory.ARMOR), + DS3ItemData("Helm of Favor", 0x14F27AC0, DS3ItemCategory.ARMOR), + DS3ItemData("Embraced Armor of Favor", 0x14F27EA8, DS3ItemCategory.ARMOR), + DS3ItemData("Gauntlets of Favor", 0x14F28290, DS3ItemCategory.ARMOR), + DS3ItemData("Leggings of Favor", 0x14F28678, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Helm", 0x1501BD00, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Armor", 0x1501C0E8, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Gauntlets", 0x1501C4D0, DS3ItemCategory.ARMOR), + DS3ItemData("Brass Leggings", 0x1501C8B8, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Helm", 0x1510FF40, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Armor", 0x15110328, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Gauntlets", 0x15110710, DS3ItemCategory.ARMOR), + DS3ItemData("Silver Knight Leggings", 0x15110AF8, DS3ItemCategory.ARMOR), + DS3ItemData("Lucatiel's Mask", 0x15204180, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Vest", 0x15204568, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Gloves", 0x15204950, DS3ItemCategory.ARMOR), + DS3ItemData("Mirrah Trousers", 0x15204D38, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Helm", 0x152F83C0, DS3ItemCategory.ARMOR), + DS3ItemData("Armor of the Sun", 0x152F87A8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Bracelets", 0x152F8B90, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Leggings", 0x152F8F78, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Helm", 0x153EC600, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Armor", 0x153EC9E8, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Gauntlets", 0x153ECDD0, DS3ItemCategory.ARMOR), + DS3ItemData("Drakeblood Leggings", 0x153ED1B8, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Armor", 0x154E0C28, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Gauntlets", 0x154E1010, DS3ItemCategory.ARMOR), + DS3ItemData("Drang Shoes", 0x154E13F8, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Helm", 0x155D4A80, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Armor", 0x155D4E68, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Gauntlets", 0x155D5250, DS3ItemCategory.ARMOR), + DS3ItemData("Black Iron Leggings", 0x155D5638, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Hood", 0x156C8CC0, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Gown", 0x156C90A8, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Gloves", 0x156C9490, DS3ItemCategory.ARMOR), + DS3ItemData("Painting Guardian Waistcloth", 0x156C9878, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Helm", 0x157BCF00, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Armor", 0x157BD2E8, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Gauntlets", 0x157BD6D0, DS3ItemCategory.ARMOR), + DS3ItemData("Wolf Knight Leggings", 0x157BDAB8, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Helm", 0x158B1140, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Armor", 0x158B1528, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Gauntlets", 0x158B1910, DS3ItemCategory.ARMOR), + DS3ItemData("Dragonslayer Leggings", 0x158B1CF8, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Helm", 0x159A5380, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Armor", 0x159A5768, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Gauntlets", 0x159A5B50, DS3ItemCategory.ARMOR), + DS3ItemData("Smough's Leggings", 0x159A5F38, DS3ItemCategory.ARMOR), + DS3ItemData("Helm of Thorns", 0x15B8D800, DS3ItemCategory.ARMOR), + DS3ItemData("Armor of Thorns", 0x15B8DBE8, DS3ItemCategory.ARMOR), + DS3ItemData("Gauntlets of Thorns", 0x15B8DFD0, DS3ItemCategory.ARMOR), + DS3ItemData("Leggings of Thorns", 0x15B8E3B8, DS3ItemCategory.ARMOR), + DS3ItemData("Crown of Dusk", 0x15D75C80, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Dress", 0x15D76068, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Gloves", 0x15D76450, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Skirt", 0x15D76838, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Pointed Hat", 0x15E69EC0, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Coat", 0x15E6A2A8, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Gloves", 0x15E6A690, DS3ItemCategory.ARMOR), + DS3ItemData("Karla's Trousers", 0x15E6AA78, DS3ItemCategory.ARMOR), # Covenants - ("Blade of the Darkmoon", 0x20002710, DS3ItemCategory.SKIP), - ("Watchdogs of Farron", 0x20002724, DS3ItemCategory.SKIP), - ("Aldrich Faithful", 0x2000272E, DS3ItemCategory.SKIP), - ("Warrior of Sunlight", 0x20002738, DS3ItemCategory.SKIP), - ("Mound-makers", 0x20002742, DS3ItemCategory.SKIP), - ("Way of Blue", 0x2000274C, DS3ItemCategory.SKIP), - ("Blue Sentinels", 0x20002756, DS3ItemCategory.SKIP), - ("Rosaria's Fingers", 0x20002760, DS3ItemCategory.SKIP), + DS3ItemData("Blade of the Darkmoon", 0x20002710, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Watchdogs of Farron", 0x20002724, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Aldrich Faithful", 0x2000272E, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Warrior of Sunlight", 0x20002738, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Mound-makers", 0x20002742, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Way of Blue", 0x2000274C, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Blue Sentinels", 0x20002756, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Rosaria's Fingers", 0x20002760, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Spears of the Church", 0x2000276A, DS3ItemCategory.UNIQUE, skip = True), # Rings - ("Life Ring", 0x20004E20, DS3ItemCategory.RING), - ("Life Ring+1", 0x20004E21, DS3ItemCategory.RING), - ("Life Ring+2", 0x20004E22, DS3ItemCategory.RING), - ("Life Ring+3", 0x20004E23, DS3ItemCategory.RING), - ("Chloranthy Ring", 0x20004E2A, DS3ItemCategory.RING), - ("Chloranthy Ring+1", 0x20004E2B, DS3ItemCategory.RING), - ("Chloranthy Ring+2", 0x20004E2C, DS3ItemCategory.RING), - ("Havel's Ring", 0x20004E34, DS3ItemCategory.RING), - ("Havel's Ring+1", 0x20004E35, DS3ItemCategory.RING), - ("Havel's Ring+2", 0x20004E36, DS3ItemCategory.RING), - ("Ring of Favor", 0x20004E3E, DS3ItemCategory.RING), - ("Ring of Favor+1", 0x20004E3F, DS3ItemCategory.RING), - ("Ring of Favor+2", 0x20004E40, DS3ItemCategory.RING), - ("Ring of Steel Protection", 0x20004E48, DS3ItemCategory.RING), - ("Ring of Steel Protection+1", 0x20004E49, DS3ItemCategory.RING), - ("Ring of Steel Protection+2", 0x20004E4A, DS3ItemCategory.RING), - ("Flame Stoneplate Ring", 0x20004E52, DS3ItemCategory.RING), - ("Flame Stoneplate Ring+1", 0x20004E53, DS3ItemCategory.RING), - ("Flame Stoneplate Ring+2", 0x20004E54, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring", 0x20004E5C, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring+1", 0x20004E5D, DS3ItemCategory.RING), - ("Thunder Stoneplate Ring+2", 0x20004E5E, DS3ItemCategory.RING), - ("Magic Stoneplate Ring", 0x20004E66, DS3ItemCategory.RING), - ("Magic Stoneplate Ring+1", 0x20004E67, DS3ItemCategory.RING), - ("Magic Stoneplate Ring+2", 0x20004E68, DS3ItemCategory.RING), - ("Dark Stoneplate Ring", 0x20004E70, DS3ItemCategory.RING), - ("Dark Stoneplate Ring+1", 0x20004E71, DS3ItemCategory.RING), - ("Dark Stoneplate Ring+2", 0x20004E72, DS3ItemCategory.RING), - ("Speckled Stoneplate Ring", 0x20004E7A, DS3ItemCategory.RING), - ("Speckled Stoneplate Ring+1", 0x20004E7B, DS3ItemCategory.RING), - ("Bloodbite Ring", 0x20004E84, DS3ItemCategory.RING), - ("Bloodbite Ring+1", 0x20004E85, DS3ItemCategory.RING), - ("Poisonbite Ring", 0x20004E8E, DS3ItemCategory.RING), - ("Poisonbite Ring+1", 0x20004E8F, DS3ItemCategory.RING), - ("Cursebite Ring", 0x20004E98, DS3ItemCategory.RING), - ("Fleshbite Ring", 0x20004EA2, DS3ItemCategory.RING), - ("Fleshbite Ring+1", 0x20004EA3, DS3ItemCategory.RING), - ("Wood Grain Ring", 0x20004EAC, DS3ItemCategory.RING), - ("Wood Grain Ring+1", 0x20004EAD, DS3ItemCategory.RING), - ("Wood Grain Ring+2", 0x20004EAE, DS3ItemCategory.RING), - ("Scholar Ring", 0x20004EB6, DS3ItemCategory.RING), - ("Priestess Ring", 0x20004EC0, DS3ItemCategory.RING), - ("Red Tearstone Ring", 0x20004ECA, DS3ItemCategory.RING), - ("Blue Tearstone Ring", 0x20004ED4, DS3ItemCategory.RING), - ("Wolf Ring", 0x20004EDE, DS3ItemCategory.RING), - ("Wolf Ring+1", 0x20004EDF, DS3ItemCategory.RING), - ("Wolf Ring+2", 0x20004EE0, DS3ItemCategory.RING), - ("Leo Ring", 0x20004EE8, DS3ItemCategory.RING), - ("Ring of Sacrifice", 0x20004EF2, DS3ItemCategory.RING), - ("Young Dragon Ring", 0x20004F06, DS3ItemCategory.RING), - ("Bellowing Dragoncrest Ring", 0x20004F07, DS3ItemCategory.RING), - ("Great Swamp Ring", 0x20004F10, DS3ItemCategory.RING), - ("Witch's Ring", 0x20004F11, DS3ItemCategory.RING), - ("Morne's Ring", 0x20004F1A, DS3ItemCategory.RING), - ("Ring of the Sun's First Born", 0x20004F1B, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring", 0x20004F2E, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring+1", 0x20004F2F, DS3ItemCategory.RING), - ("Lingering Dragoncrest Ring+2", 0x20004F30, DS3ItemCategory.RING), - ("Sage Ring", 0x20004F38, DS3ItemCategory.RING), - ("Sage Ring+1", 0x20004F39, DS3ItemCategory.RING), - ("Sage Ring+2", 0x20004F3A, DS3ItemCategory.RING), - ("Slumbering Dragoncrest Ring", 0x20004F42, DS3ItemCategory.RING), - ("Dusk Crown Ring", 0x20004F4C, DS3ItemCategory.RING), - ("Saint's Ring", 0x20004F56, DS3ItemCategory.RING), - ("Deep Ring", 0x20004F60, DS3ItemCategory.RING), - ("Darkmoon Ring", 0x20004F6A, DS3ItemCategory.RING), - ("Hawk Ring", 0x20004F92, DS3ItemCategory.RING), - ("Hornet Ring", 0x20004F9C, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring", 0x20004FA6, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+1", 0x20004FA7, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+2", 0x20004FA8, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring", 0x20004FB0, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+1", 0x20004FB1, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+2", 0x20004FB2, DS3ItemCategory.RING), - ("Sun Princess Ring", 0x20004FBA, DS3ItemCategory.RING), - ("Silvercat Ring", 0x20004FC4, DS3ItemCategory.RING), - ("Skull Ring", 0x20004FCE, DS3ItemCategory.RING), - ("Untrue White Ring", 0x20004FD8, DS3ItemCategory.RING), - ("Carthus Milkring", 0x20004FE2, DS3ItemCategory.RING), - ("Knight's Ring", 0x20004FEC, DS3ItemCategory.RING), - ("Hunter's Ring", 0x20004FF6, DS3ItemCategory.RING), - ("Knight Slayer's Ring", 0x20005000, DS3ItemCategory.RING), - ("Magic Clutch Ring", 0x2000500A, DS3ItemCategory.RING), - ("Lightning Clutch Ring", 0x20005014, DS3ItemCategory.RING), - ("Fire Clutch Ring", 0x2000501E, DS3ItemCategory.RING), - ("Dark Clutch Ring", 0x20005028, DS3ItemCategory.RING), - ("Flynn's Ring", 0x2000503C, DS3ItemCategory.RING), - ("Prisoner's Chain", 0x20005046, DS3ItemCategory.RING), - ("Untrue Dark Ring", 0x20005050, DS3ItemCategory.RING), - ("Obscuring Ring", 0x20005064, DS3ItemCategory.RING), - ("Ring of the Evil Eye", 0x2000506E, DS3ItemCategory.RING), - ("Ring of the Evil Eye+1", 0x2000506F, DS3ItemCategory.RING), - ("Ring of the Evil Eye+2", 0x20005070, DS3ItemCategory.RING), - ("Calamity Ring", 0x20005078, DS3ItemCategory.RING), - ("Farron Ring", 0x20005082, DS3ItemCategory.RING), - ("Aldrich's Ruby", 0x2000508C, DS3ItemCategory.RING), - ("Aldrich's Sapphire", 0x20005096, DS3ItemCategory.RING), - ("Lloyd's Sword Ring", 0x200050B4, DS3ItemCategory.RING), - ("Lloyd's Shield Ring", 0x200050BE, DS3ItemCategory.RING), - ("Estus Ring", 0x200050DC, DS3ItemCategory.RING), - ("Ashen Estus Ring", 0x200050E6, DS3ItemCategory.RING), - ("Carthus Bloodring", 0x200050FA, DS3ItemCategory.RING), - ("Reversal Ring", 0x20005104, DS3ItemCategory.RING), - ("Pontiff's Right Eye", 0x2000510E, DS3ItemCategory.RING), - ("Pontiff's Left Eye", 0x20005136, DS3ItemCategory.RING), - ("Dragonscale Ring", 0x2000515E, DS3ItemCategory.RING), + DS3ItemData("Life Ring", 0x20004E20, DS3ItemCategory.RING), + DS3ItemData("Life Ring+1", 0x20004E21, DS3ItemCategory.RING), + DS3ItemData("Life Ring+2", 0x20004E22, DS3ItemCategory.RING), + DS3ItemData("Life Ring+3", 0x20004E23, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring", 0x20004E2A, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Chloranthy Ring+1", 0x20004E2B, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring+2", 0x20004E2C, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Havel's Ring", 0x20004E34, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Havel's Ring+1", 0x20004E35, DS3ItemCategory.RING), + DS3ItemData("Havel's Ring+2", 0x20004E36, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Ring of Favor", 0x20004E3E, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Ring of Favor+1", 0x20004E3F, DS3ItemCategory.RING), + DS3ItemData("Ring of Favor+2", 0x20004E40, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Ring of Steel Protection", 0x20004E48, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Ring of Steel Protection+1", 0x20004E49, DS3ItemCategory.RING), + DS3ItemData("Ring of Steel Protection+2", 0x20004E4A, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Flame Stoneplate Ring", 0x20004E52, DS3ItemCategory.RING), + DS3ItemData("Flame Stoneplate Ring+1", 0x20004E53, DS3ItemCategory.RING), + DS3ItemData("Flame Stoneplate Ring+2", 0x20004E54, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring", 0x20004E5C, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring+1", 0x20004E5D, DS3ItemCategory.RING), + DS3ItemData("Thunder Stoneplate Ring+2", 0x20004E5E, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring", 0x20004E66, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring+1", 0x20004E67, DS3ItemCategory.RING), + DS3ItemData("Magic Stoneplate Ring+2", 0x20004E68, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring", 0x20004E70, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring+1", 0x20004E71, DS3ItemCategory.RING), + DS3ItemData("Dark Stoneplate Ring+2", 0x20004E72, DS3ItemCategory.RING), + DS3ItemData("Speckled Stoneplate Ring", 0x20004E7A, DS3ItemCategory.RING), + DS3ItemData("Speckled Stoneplate Ring+1", 0x20004E7B, DS3ItemCategory.RING), + DS3ItemData("Bloodbite Ring", 0x20004E84, DS3ItemCategory.RING), + DS3ItemData("Bloodbite Ring+1", 0x20004E85, DS3ItemCategory.RING), + DS3ItemData("Poisonbite Ring", 0x20004E8E, DS3ItemCategory.RING), + DS3ItemData("Poisonbite Ring+1", 0x20004E8F, DS3ItemCategory.RING), + DS3ItemData("Cursebite Ring", 0x20004E98, DS3ItemCategory.RING), + DS3ItemData("Fleshbite Ring", 0x20004EA2, DS3ItemCategory.RING), + DS3ItemData("Fleshbite Ring+1", 0x20004EA3, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring", 0x20004EAC, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring+1", 0x20004EAD, DS3ItemCategory.RING), + DS3ItemData("Wood Grain Ring+2", 0x20004EAE, DS3ItemCategory.RING), + DS3ItemData("Scholar Ring", 0x20004EB6, DS3ItemCategory.RING), + DS3ItemData("Priestess Ring", 0x20004EC0, DS3ItemCategory.RING), + DS3ItemData("Red Tearstone Ring", 0x20004ECA, DS3ItemCategory.RING), + DS3ItemData("Blue Tearstone Ring", 0x20004ED4, DS3ItemCategory.RING), + DS3ItemData("Wolf Ring", 0x20004EDE, DS3ItemCategory.RING, + inject = True), # Covenant reward + DS3ItemData("Wolf Ring+1", 0x20004EDF, DS3ItemCategory.RING), + DS3ItemData("Wolf Ring+2", 0x20004EE0, DS3ItemCategory.RING), + DS3ItemData("Leo Ring", 0x20004EE8, DS3ItemCategory.RING), + DS3ItemData("Ring of Sacrifice", 0x20004EF2, DS3ItemCategory.RING, filler = True), + DS3ItemData("Young Dragon Ring", 0x20004F06, DS3ItemCategory.RING), + DS3ItemData("Bellowing Dragoncrest Ring", 0x20004F07, DS3ItemCategory.RING), + DS3ItemData("Great Swamp Ring", 0x20004F10, DS3ItemCategory.RING), + DS3ItemData("Witch's Ring", 0x20004F11, DS3ItemCategory.RING), + DS3ItemData("Morne's Ring", 0x20004F1A, DS3ItemCategory.RING), + DS3ItemData("Ring of the Sun's First Born", 0x20004F1B, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring", 0x20004F2E, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring+1", 0x20004F2F, DS3ItemCategory.RING), + DS3ItemData("Lingering Dragoncrest Ring+2", 0x20004F30, DS3ItemCategory.RING), + DS3ItemData("Sage Ring", 0x20004F38, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_NGP), + DS3ItemData("Sage Ring+1", 0x20004F39, DS3ItemCategory.RING), + DS3ItemData("Sage Ring+2", 0x20004F3A, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Slumbering Dragoncrest Ring", 0x20004F42, DS3ItemCategory.RING), + DS3ItemData("Dusk Crown Ring", 0x20004F4C, DS3ItemCategory.RING), + DS3ItemData("Saint's Ring", 0x20004F56, DS3ItemCategory.RING), + DS3ItemData("Deep Ring", 0x20004F60, DS3ItemCategory.RING), + DS3ItemData("Darkmoon Ring", 0x20004F6A, DS3ItemCategory.RING, + inject = True), # Covenant reward + DS3ItemData("Hawk Ring", 0x20004F92, DS3ItemCategory.RING), + DS3ItemData("Hornet Ring", 0x20004F9C, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring", 0x20004FA6, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+1", 0x20004FA7, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+2", 0x20004FA8, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring", 0x20004FB0, DS3ItemCategory.RING, + useful_if = UsefulIf.BASE), + DS3ItemData("Covetous Silver Serpent Ring+1", 0x20004FB1, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring+2", 0x20004FB2, DS3ItemCategory.RING, + useful_if = UsefulIf.NO_DLC), + DS3ItemData("Sun Princess Ring", 0x20004FBA, DS3ItemCategory.RING), + DS3ItemData("Silvercat Ring", 0x20004FC4, DS3ItemCategory.RING), + DS3ItemData("Skull Ring", 0x20004FCE, DS3ItemCategory.RING), + DS3ItemData("Untrue White Ring", 0x20004FD8, DS3ItemCategory.RING, skip = True), + DS3ItemData("Carthus Milkring", 0x20004FE2, DS3ItemCategory.RING), + DS3ItemData("Knight's Ring", 0x20004FEC, DS3ItemCategory.RING), + DS3ItemData("Hunter's Ring", 0x20004FF6, DS3ItemCategory.RING), + DS3ItemData("Knight Slayer's Ring", 0x20005000, DS3ItemCategory.RING), + DS3ItemData("Magic Clutch Ring", 0x2000500A, DS3ItemCategory.RING), + DS3ItemData("Lightning Clutch Ring", 0x20005014, DS3ItemCategory.RING), + DS3ItemData("Fire Clutch Ring", 0x2000501E, DS3ItemCategory.RING), + DS3ItemData("Dark Clutch Ring", 0x20005028, DS3ItemCategory.RING), + DS3ItemData("Flynn's Ring", 0x2000503C, DS3ItemCategory.RING), + DS3ItemData("Prisoner's Chain", 0x20005046, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Untrue Dark Ring", 0x20005050, DS3ItemCategory.RING), + DS3ItemData("Obscuring Ring", 0x20005064, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye", 0x2000506E, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye+1", 0x2000506F, DS3ItemCategory.RING), + DS3ItemData("Ring of the Evil Eye+2", 0x20005070, DS3ItemCategory.RING), + DS3ItemData("Calamity Ring", 0x20005078, DS3ItemCategory.RING), + DS3ItemData("Farron Ring", 0x20005082, DS3ItemCategory.RING), + DS3ItemData("Aldrich's Ruby", 0x2000508C, DS3ItemCategory.RING), + DS3ItemData("Aldrich's Sapphire", 0x20005096, DS3ItemCategory.RING), + DS3ItemData("Lloyd's Sword Ring", 0x200050B4, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Lloyd's Shield Ring", 0x200050BE, DS3ItemCategory.RING), + DS3ItemData("Estus Ring", 0x200050DC, DS3ItemCategory.RING), + DS3ItemData("Ashen Estus Ring", 0x200050E6, DS3ItemCategory.RING), + DS3ItemData("Horsehoof Ring", 0x200050F0, DS3ItemCategory.RING), + DS3ItemData("Carthus Bloodring", 0x200050FA, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Reversal Ring", 0x20005104, DS3ItemCategory.RING), + DS3ItemData("Pontiff's Right Eye", 0x2000510E, DS3ItemCategory.RING), + DS3ItemData("Pontiff's Left Eye", 0x20005136, DS3ItemCategory.RING), + DS3ItemData("Dragonscale Ring", 0x2000515E, DS3ItemCategory.RING), # Items - ("Roster of Knights", 0x4000006C, DS3ItemCategory.SKIP), - ("Cracked Red Eye Orb", 0x4000006F, DS3ItemCategory.SKIP), - ("Divine Blessing", 0x400000F0, DS3ItemCategory.MISC), - ("Hidden Blessing", 0x400000F1, DS3ItemCategory.MISC), - ("Silver Pendant", 0x400000F2, DS3ItemCategory.SKIP), - ("Green Blossom", 0x40000104, DS3ItemCategory.MISC), - ("Budding Green Blossom", 0x40000106, DS3ItemCategory.MISC), - ("Bloodred Moss Clump", 0x4000010E, DS3ItemCategory.SKIP), - ("Purple Moss Clump", 0x4000010F, DS3ItemCategory.MISC), - ("Blooming Purple Moss Clump", 0x40000110, DS3ItemCategory.SKIP), - ("Purging Stone", 0x40000112, DS3ItemCategory.SKIP), - ("Rime-blue Moss Clump", 0x40000114, DS3ItemCategory.SKIP), - ("Repair Powder", 0x40000118, DS3ItemCategory.MISC), - ("Kukri", 0x40000122, DS3ItemCategory.SKIP), - ("Firebomb", 0x40000124, DS3ItemCategory.MISC), - ("Dung Pie", 0x40000125, DS3ItemCategory.SKIP), - ("Alluring Skull", 0x40000126, DS3ItemCategory.MISC), - ("Undead Hunter Charm", 0x40000128, DS3ItemCategory.MISC), - ("Black Firebomb", 0x40000129, DS3ItemCategory.MISC), - ("Rope Firebomb", 0x4000012B, DS3ItemCategory.MISC), - ("Lightning Urn", 0x4000012C, DS3ItemCategory.MISC), - ("Rope Black Firebomb", 0x4000012E, DS3ItemCategory.MISC), - ("Stalk Dung Pie", 0x4000012F, DS3ItemCategory.SKIP), - ("Duel Charm", 0x40000130, DS3ItemCategory.MISC), - ("Throwing Knife", 0x40000136, DS3ItemCategory.MISC), - ("Poison Throwing Knife", 0x40000137, DS3ItemCategory.MISC), - ("Charcoal Pine Resin", 0x4000014A, DS3ItemCategory.MISC), - ("Gold Pine Resin", 0x4000014B, DS3ItemCategory.MISC), - ("Human Pine Resin", 0x4000014E, DS3ItemCategory.MISC), - ("Carthus Rouge", 0x4000014F, DS3ItemCategory.MISC), - ("Pale Pine Resin", 0x40000150, DS3ItemCategory.MISC), - ("Charcoal Pine Bundle", 0x40000154, DS3ItemCategory.MISC), - ("Gold Pine Bundle", 0x40000155, DS3ItemCategory.MISC), - ("Rotten Pine Resin", 0x40000157, DS3ItemCategory.MISC), - ("Homeward Bone", 0x4000015E, DS3ItemCategory.MISC), - ("Coiled Sword Fragment", 0x4000015F, DS3ItemCategory.MISC), - ("Wolf's Blood Swordgrass", 0x4000016E, DS3ItemCategory.MISC), - ("Human Dregs", 0x4000016F, DS3ItemCategory.SKIP), - ("Forked Pale Tongue", 0x40000170, DS3ItemCategory.MISC), - ("Proof of a Concord Well Kept", 0x40000171, DS3ItemCategory.SKIP), - ("Prism Stone", 0x40000172, DS3ItemCategory.SKIP), - ("Binoculars", 0x40000173, DS3ItemCategory.MISC), - ("Proof of a Concord Kept", 0x40000174, DS3ItemCategory.SKIP), - ("Pale Tongue", 0x40000175, DS3ItemCategory.MISC), - ("Vertebra Shackle", 0x40000176, DS3ItemCategory.SKIP), - ("Sunlight Medal", 0x40000177, DS3ItemCategory.SKIP), - ("Dragon Head Stone", 0x40000179, DS3ItemCategory.MISC), - ("Dragon Torso Stone", 0x4000017A, DS3ItemCategory.MISC), - ("Rubbish", 0x4000017C, DS3ItemCategory.SKIP), - ("Dried Finger", 0x40000181, DS3ItemCategory.SKIP), - ("Twinkling Dragon Head Stone", 0x40000183, DS3ItemCategory.MISC), - ("Twinkling Dragon Torso Stone", 0x40000184, DS3ItemCategory.MISC), - ("Fire Keeper Soul", 0x40000186, DS3ItemCategory.MISC), - ("Fading Soul", 0x40000190, DS3ItemCategory.MISC), - ("Soul of a Deserted Corpse", 0x40000191, DS3ItemCategory.MISC), - ("Large Soul of a Deserted Corpse", 0x40000192, DS3ItemCategory.MISC), - ("Soul of an Unknown Traveler", 0x40000193, DS3ItemCategory.MISC), - ("Large Soul of an Unknown Traveler", 0x40000194, DS3ItemCategory.MISC), - ("Soul of a Nameless Soldier", 0x40000195, DS3ItemCategory.MISC), - ("Large Soul of a Nameless Soldier", 0x40000196, DS3ItemCategory.MISC), - ("Soul of a Weary Warrior", 0x40000197, DS3ItemCategory.MISC), - ("Large Soul of a Weary Warrior", 0x40000198, DS3ItemCategory.MISC), - ("Soul of a Crestfallen Knight", 0x40000199, DS3ItemCategory.MISC), - ("Large Soul of a Crestfallen Knight", 0x4000019A, DS3ItemCategory.MISC), - ("Soul of a Proud Paladin", 0x4000019B, DS3ItemCategory.MISC), - ("Large Soul of a Proud Paladin", 0x4000019C, DS3ItemCategory.MISC), - ("Soul of an Intrepid Hero", 0x4000019D, DS3ItemCategory.MISC), - ("Large Soul of an Intrepid Hero", 0x4000019E, DS3ItemCategory.MISC), - ("Soul of a Seasoned Warrior", 0x4000019F, DS3ItemCategory.MISC), - ("Large Soul of a Seasoned Warrior", 0x400001A0, DS3ItemCategory.MISC), - ("Soul of an Old Hand", 0x400001A1, DS3ItemCategory.MISC), - ("Soul of a Venerable Old Hand", 0x400001A2, DS3ItemCategory.MISC), - ("Soul of a Champion", 0x400001A3, DS3ItemCategory.MISC), - ("Soul of a Great Champion", 0x400001A4, DS3ItemCategory.MISC), - ("Seed of a Giant Tree", 0x400001B8, DS3ItemCategory.SKIP), - ("Young White Branch", 0x400001C6, DS3ItemCategory.SKIP), - ("Rusted Coin", 0x400001C7, DS3ItemCategory.MISC), - ("Siegbräu", 0x400001C8, DS3ItemCategory.SKIP), - ("Rusted Gold Coin", 0x400001C9, DS3ItemCategory.MISC), - ("Blue Bug Pellet", 0x400001CA, DS3ItemCategory.SKIP), - ("Red Bug Pellet", 0x400001CB, DS3ItemCategory.SKIP), - ("Yellow Bug Pellet", 0x400001CC, DS3ItemCategory.SKIP), - ("Black Bug Pellet", 0x400001CD, DS3ItemCategory.SKIP), - ("Young White Branch", 0x400001CF, DS3ItemCategory.SKIP), - ("Dark Sigil", 0x400001EA, DS3ItemCategory.SKIP), - ("Ember", 0x400001F4, DS3ItemCategory.MISC), - ("Soul of Champion Gundyr", 0x400002C8, DS3ItemCategory.BOSS), - ("Soul of the Dancer", 0x400002CA, DS3ItemCategory.BOSS), - ("Soul of a Crystal Sage", 0x400002CB, DS3ItemCategory.BOSS), - ("Soul of the Blood of the Wolf", 0x400002CD, DS3ItemCategory.BOSS), - ("Soul of Consumed Oceiros", 0x400002CE, DS3ItemCategory.BOSS), - ("Soul of Boreal Valley Vordt", 0x400002CF, DS3ItemCategory.BOSS), - ("Soul of the Old Demon King", 0x400002D0, DS3ItemCategory.BOSS), - ("Soul of Dragonslayer Armour", 0x400002D1, DS3ItemCategory.BOSS), - ("Soul of the Nameless King", 0x400002D2, DS3ItemCategory.BOSS), - ("Soul of Pontiff Sulyvahn", 0x400002D4, DS3ItemCategory.BOSS), - ("Soul of Aldrich", 0x400002D5, DS3ItemCategory.BOSS), - ("Soul of High Lord Wolnir", 0x400002D6, DS3ItemCategory.BOSS), - ("Soul of the Rotted Greatwood", 0x400002D7, DS3ItemCategory.BOSS), - ("Soul of Rosaria", 0x400002D8, DS3ItemCategory.MISC), - ("Soul of the Deacons of the Deep", 0x400002D9, DS3ItemCategory.BOSS), - ("Soul of the Twin Princes", 0x400002DB, DS3ItemCategory.BOSS), - ("Soul of Yhorm the Giant", 0x400002DC, DS3ItemCategory.BOSS), - ("Soul of the Lords", 0x400002DD, DS3ItemCategory.MISC), - ("Soul of a Demon", 0x400002E3, DS3ItemCategory.BOSS), - ("Soul of a Stray Demon", 0x400002E7, DS3ItemCategory.BOSS), - ("Titanite Shard", 0x400003E8, DS3ItemCategory.MISC), - ("Large Titanite Shard", 0x400003E9, DS3ItemCategory.MISC), - ("Titanite Chunk", 0x400003EA, DS3ItemCategory.MISC), - ("Titanite Slab", 0x400003EB, DS3ItemCategory.MISC), - ("Titanite Scale", 0x400003FC, DS3ItemCategory.MISC), - ("Twinkling Titanite", 0x40000406, DS3ItemCategory.MISC), - ("Heavy Gem", 0x4000044C, DS3ItemCategory.MISC), - ("Sharp Gem", 0x40000456, DS3ItemCategory.MISC), - ("Refined Gem", 0x40000460, DS3ItemCategory.MISC), - ("Crystal Gem", 0x4000046A, DS3ItemCategory.MISC), - ("Simple Gem", 0x40000474, DS3ItemCategory.MISC), - ("Fire Gem", 0x4000047E, DS3ItemCategory.MISC), - ("Chaos Gem", 0x40000488, DS3ItemCategory.MISC), - ("Lightning Gem", 0x40000492, DS3ItemCategory.MISC), - ("Deep Gem", 0x4000049C, DS3ItemCategory.MISC), - ("Dark Gem", 0x400004A6, DS3ItemCategory.MISC), - ("Poison Gem", 0x400004B0, DS3ItemCategory.MISC), - ("Blood Gem", 0x400004BA, DS3ItemCategory.MISC), - ("Raw Gem", 0x400004C4, DS3ItemCategory.MISC), - ("Blessed Gem", 0x400004CE, DS3ItemCategory.MISC), - ("Hollow Gem", 0x400004D8, DS3ItemCategory.MISC), - ("Shriving Stone", 0x400004E2, DS3ItemCategory.MISC), - ("Lift Chamber Key", 0x400007D1, DS3ItemCategory.KEY), - ("Small Doll", 0x400007D5, DS3ItemCategory.KEY), - ("Jailbreaker's Key", 0x400007D7, DS3ItemCategory.KEY), - ("Jailer's Key Ring", 0x400007D8, DS3ItemCategory.KEY), - ("Grave Key", 0x400007D9, DS3ItemCategory.KEY), - ("Cell Key", 0x400007DA, DS3ItemCategory.KEY), - ("Dungeon Ground Floor Key", 0x400007DB, DS3ItemCategory.KEY), - ("Old Cell Key", 0x400007DC, DS3ItemCategory.KEY), - ("Grand Archives Key", 0x400007DE, DS3ItemCategory.KEY), - ("Tower Key", 0x400007DF, DS3ItemCategory.KEY), - ("Small Lothric Banner", 0x40000836, DS3ItemCategory.KEY), - ("Farron Coal", 0x40000837, DS3ItemCategory.MISC), - ("Sage's Coal", 0x40000838, DS3ItemCategory.MISC), - ("Giant's Coal", 0x40000839, DS3ItemCategory.MISC), - ("Profaned Coal", 0x4000083A, DS3ItemCategory.MISC), - ("Mortician's Ashes", 0x4000083B, DS3ItemCategory.MISC), - ("Dreamchaser's Ashes", 0x4000083C, DS3ItemCategory.MISC), - ("Paladin's Ashes", 0x4000083D, DS3ItemCategory.MISC), - ("Grave Warden's Ashes", 0x4000083E, DS3ItemCategory.MISC), - ("Greirat's Ashes", 0x4000083F, DS3ItemCategory.MISC), - ("Orbeck's Ashes", 0x40000840, DS3ItemCategory.MISC), - ("Cornyx's Ashes", 0x40000841, DS3ItemCategory.MISC), - ("Karla's Ashes", 0x40000842, DS3ItemCategory.MISC), - ("Irina's Ashes", 0x40000843, DS3ItemCategory.MISC), - ("Yuria's Ashes", 0x40000844, DS3ItemCategory.MISC), - ("Basin of Vows", 0x40000845, DS3ItemCategory.KEY), - ("Loretta's Bone", 0x40000846, DS3ItemCategory.KEY), - ("Braille Divine Tome of Carim", 0x40000847, DS3ItemCategory.MISC), - ("Braille Divine Tome of Lothric", 0x40000848, DS3ItemCategory.MISC), - ("Cinders of a Lord - Abyss Watcher", 0x4000084B, DS3ItemCategory.KEY), - ("Cinders of a Lord - Aldrich", 0x4000084C, DS3ItemCategory.KEY), - ("Cinders of a Lord - Yhorm the Giant", 0x4000084D, DS3ItemCategory.KEY), - ("Cinders of a Lord - Lothric Prince", 0x4000084E, DS3ItemCategory.KEY), - ("Great Swamp Pyromancy Tome", 0x4000084F, DS3ItemCategory.MISC), - ("Carthus Pyromancy Tome", 0x40000850, DS3ItemCategory.MISC), - ("Izalith Pyromancy Tome", 0x40000851, DS3ItemCategory.MISC), - ("Quelana Pyromancy Tome", 0x40000852, DS3ItemCategory.MISC), - ("Grave Warden Pyromancy Tome", 0x40000853, DS3ItemCategory.MISC), - ("Sage's Scroll", 0x40000854, DS3ItemCategory.MISC), - ("Logan's Scroll", 0x40000855, DS3ItemCategory.MISC), - ("Crystal Scroll", 0x40000856, DS3ItemCategory.MISC), - ("Transposing Kiln", 0x40000857, DS3ItemCategory.MISC), - ("Coiled Sword", 0x40000859, DS3ItemCategory.SKIP), # Useless - ("Eyes of a Fire Keeper", 0x4000085A, DS3ItemCategory.KEY), - ("Sword of Avowal", 0x4000085B, DS3ItemCategory.KEY), - ("Golden Scroll", 0x4000085C, DS3ItemCategory.MISC), - ("Estus Shard", 0x4000085D, DS3ItemCategory.MISC), - ("Hawkwood's Swordgrass", 0x4000085E, DS3ItemCategory.SKIP), - ("Undead Bone Shard", 0x4000085F, DS3ItemCategory.MISC), - ("Deep Braille Divine Tome", 0x40000860, DS3ItemCategory.MISC), - ("Londor Braille Divine Tome", 0x40000861, DS3ItemCategory.MISC), - ("Excrement-covered Ashes", 0x40000862, DS3ItemCategory.MISC), - ("Prisoner Chief's Ashes", 0x40000863, DS3ItemCategory.MISC), - ("Xanthous Ashes", 0x40000864, DS3ItemCategory.MISC), - ("Hollow's Ashes", 0x40000865, DS3ItemCategory.MISC), - ("Patches' Ashes", 0x40000866, DS3ItemCategory.MISC), - ("Dragon Chaser's Ashes", 0x40000867, DS3ItemCategory.MISC), - ("Easterner's Ashes", 0x40000868, DS3ItemCategory.MISC), + DS3ItemData("White Sign Soapstone", 0x40000064, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Red Sign Soapstone", 0x40000066, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Red Eye Orb", 0x40000066, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Roster of Knights", 0x4000006C, DS3ItemCategory.UNIQUE, skip = True), + *DS3ItemData("Cracked Red Eye Orb", 0x4000006F, DS3ItemCategory.MISC, skip = True).counts([5]), + DS3ItemData("Black Eye Orb", 0x40000073, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Divine Blessing", 0x400000F0, DS3ItemCategory.MISC), + DS3ItemData("Hidden Blessing", 0x400000F1, DS3ItemCategory.MISC), + *DS3ItemData("Green Blossom", 0x40000104, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Budding Green Blossom", 0x40000106, DS3ItemCategory.MISC).counts([2, 3]), + *DS3ItemData("Bloodred Moss Clump", 0x4000010E, DS3ItemCategory.MISC, filler = True).counts([3]), + *DS3ItemData("Purple Moss Clump", 0x4000010F, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Blooming Purple Moss Clump", 0x40000110, DS3ItemCategory.MISC).counts([3]), + *DS3ItemData("Purging Stone", 0x40000112, DS3ItemCategory.MISC, skip = True).counts([2, 3]), + *DS3ItemData("Rime-blue Moss Clump", 0x40000114, DS3ItemCategory.MISC, filler = True).counts([2, 4]), + *DS3ItemData("Repair Powder", 0x40000118, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + *DS3ItemData("Kukri", 0x40000122, DS3ItemCategory.MISC).counts([8, 9]), + DS3ItemData("Kukri x5", 0x40000122, DS3ItemCategory.MISC, count = 5, filler = True), + *DS3ItemData("Firebomb", 0x40000124, DS3ItemCategory.MISC).counts([3, 5, 6]), + DS3ItemData("Firebomb x2", 0x40000124, DS3ItemCategory.MISC, count = 2, filler = True), + *DS3ItemData("Dung Pie", 0x40000125, DS3ItemCategory.MISC).counts([2, 4]), + DS3ItemData("Dung Pie x3", 0x40000125, DS3ItemCategory.MISC, count = 3, filler = True), + *DS3ItemData("Alluring Skull", 0x40000126, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Undead Hunter Charm", 0x40000128, DS3ItemCategory.MISC).counts([2, 3]), + *DS3ItemData("Black Firebomb", 0x40000129, DS3ItemCategory.MISC, filler = True).counts([2, 3, 4]), + DS3ItemData("Rope Firebomb", 0x4000012B, DS3ItemCategory.MISC), + *DS3ItemData("Lightning Urn", 0x4000012C, DS3ItemCategory.MISC, filler = True).counts([3, 4, 6]), + DS3ItemData("Rope Black Firebomb", 0x4000012E, DS3ItemCategory.MISC), + *DS3ItemData("Stalk Dung Pie", 0x4000012F, DS3ItemCategory.MISC).counts([6]), + *DS3ItemData("Duel Charm", 0x40000130, DS3ItemCategory.MISC).counts([3]), + *DS3ItemData("Throwing Knife", 0x40000136, DS3ItemCategory.MISC).counts([6, 8]), + DS3ItemData("Throwing Knife x5", 0x40000136, DS3ItemCategory.MISC, count = 5, filler = True), + DS3ItemData("Poison Throwing Knife", 0x40000137, DS3ItemCategory.MISC), + *DS3ItemData("Charcoal Pine Resin", 0x4000014A, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Gold Pine Resin", 0x4000014B, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Human Pine Resin", 0x4000014E, DS3ItemCategory.MISC, filler = True).counts([2, 4]), + *DS3ItemData("Carthus Rouge", 0x4000014F, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Pale Pine Resin", 0x40000150, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Charcoal Pine Bundle", 0x40000154, DS3ItemCategory.MISC).counts([2]), + *DS3ItemData("Gold Pine Bundle", 0x40000155, DS3ItemCategory.MISC).counts([6]), + *DS3ItemData("Rotten Pine Resin", 0x40000157, DS3ItemCategory.MISC).counts([2, 4]), + *DS3ItemData("Homeward Bone", 0x4000015E, DS3ItemCategory.MISC, filler = True).counts([2, 3, 6]), + DS3ItemData("Coiled Sword Fragment", 0x4000015F, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Wolf's Blood Swordgrass", 0x4000016E, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Human Dregs", 0x4000016F, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Forked Pale Tongue", 0x40000170, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Proof of a Concord Well Kept", 0x40000171, DS3ItemCategory.MISC, skip = True), + *DS3ItemData("Prism Stone", 0x40000172, DS3ItemCategory.MISC, skip = True).counts([4, 6, 10]), + DS3ItemData("Binoculars", 0x40000173, DS3ItemCategory.MISC), + DS3ItemData("Proof of a Concord Kept", 0x40000174, DS3ItemCategory.MISC, skip = True), + # One is needed for Leonhard's quest, others are useful for restatting. + DS3ItemData("Pale Tongue", 0x40000175, DS3ItemCategory.MISC, + classification = ItemClassification.progression), + DS3ItemData("Vertebra Shackle", 0x40000176, DS3ItemCategory.MISC, + classification = ItemClassification.progression), # Crow trade + DS3ItemData("Sunlight Medal", 0x40000177, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dragon Head Stone", 0x40000179, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon Torso Stone", 0x4000017A, DS3ItemCategory.UNIQUE), + DS3ItemData("Rubbish", 0x4000017C, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dried Finger", 0x40000181, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Twinkling Dragon Head Stone", 0x40000183, DS3ItemCategory.UNIQUE), + DS3ItemData("Twinkling Dragon Torso Stone", 0x40000184, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Fire Keeper Soul", 0x40000186, DS3ItemCategory.UNIQUE), + # Allow souls up to 2k in value to be used as filler + DS3ItemData("Fading Soul", 0x40000190, DS3ItemCategory.SOUL, souls = 50), + DS3ItemData("Soul of a Deserted Corpse", 0x40000191, DS3ItemCategory.SOUL, souls = 200), + DS3ItemData("Large Soul of a Deserted Corpse", 0x40000192, DS3ItemCategory.SOUL, souls = 400), + DS3ItemData("Soul of an Unknown Traveler", 0x40000193, DS3ItemCategory.SOUL, souls = 800), + DS3ItemData("Large Soul of an Unknown Traveler", 0x40000194, DS3ItemCategory.SOUL, souls = 1000), + DS3ItemData("Soul of a Nameless Soldier", 0x40000195, DS3ItemCategory.SOUL, souls = 2000), + DS3ItemData("Large Soul of a Nameless Soldier", 0x40000196, DS3ItemCategory.SOUL, souls = 3000), + DS3ItemData("Soul of a Weary Warrior", 0x40000197, DS3ItemCategory.SOUL, souls = 5000), + DS3ItemData("Large Soul of a Weary Warrior", 0x40000198, DS3ItemCategory.SOUL, souls = 8000), + DS3ItemData("Soul of a Crestfallen Knight", 0x40000199, DS3ItemCategory.SOUL, souls = 10000), + DS3ItemData("Large Soul of a Crestfallen Knight", 0x4000019A, DS3ItemCategory.SOUL, souls = 20000), + DS3ItemData("Soul of a Proud Paladin", 0x4000019B, DS3ItemCategory.SOUL, souls = 500), + DS3ItemData("Large Soul of a Proud Paladin", 0x4000019C, DS3ItemCategory.SOUL, souls = 1000), + DS3ItemData("Soul of an Intrepid Hero", 0x4000019D, DS3ItemCategory.SOUL, souls = 2000), + DS3ItemData("Large Soul of an Intrepid Hero", 0x4000019E, DS3ItemCategory.SOUL, souls = 2500), + DS3ItemData("Soul of a Seasoned Warrior", 0x4000019F, DS3ItemCategory.SOUL, souls = 5000), + DS3ItemData("Large Soul of a Seasoned Warrior", 0x400001A0, DS3ItemCategory.SOUL, souls = 7500), + DS3ItemData("Soul of an Old Hand", 0x400001A1, DS3ItemCategory.SOUL, souls = 12500), + DS3ItemData("Soul of a Venerable Old Hand", 0x400001A2, DS3ItemCategory.SOUL, souls = 20000), + DS3ItemData("Soul of a Champion", 0x400001A3, DS3ItemCategory.SOUL, souls = 25000), + DS3ItemData("Soul of a Great Champion", 0x400001A4, DS3ItemCategory.SOUL, souls = 50000), + DS3ItemData("Seed of a Giant Tree", 0x400001B8, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression, inject = True), # Crow trade + *DS3ItemData("Mossfruit", 0x400001C4, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Young White Branch", 0x400001C6, DS3ItemCategory.MISC), + *DS3ItemData("Rusted Coin", 0x400001C7, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Siegbräu", 0x400001C8, DS3ItemCategory.MISC, + classification = ItemClassification.progression), # Crow trade + *DS3ItemData("Rusted Gold Coin", 0x400001C9, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Blue Bug Pellet", 0x400001CA, DS3ItemCategory.MISC, filler = True).counts([2]), + *DS3ItemData("Red Bug Pellet", 0x400001CB, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Yellow Bug Pellet", 0x400001CC, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + *DS3ItemData("Black Bug Pellet", 0x400001CD, DS3ItemCategory.MISC, filler = True).counts([2, 3]), + DS3ItemData("Young White Branch", 0x400001CF, DS3ItemCategory.MISC, skip = True), + DS3ItemData("Dark Sigil", 0x400001EA, DS3ItemCategory.MISC, skip = True), + *DS3ItemData("Ember", 0x400001F4, DS3ItemCategory.MISC, filler = True).counts([2]), + DS3ItemData("Hello Carving", 0x40000208, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Thank you Carving", 0x40000209, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Very good! Carving", 0x4000020A, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("I'm sorry Carving", 0x4000020B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Help me! Carving", 0x4000020C, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Soul of Champion Gundyr", 0x400002C8, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Dancer", 0x400002CA, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Crystal Sage", 0x400002CB, DS3ItemCategory.BOSS, souls = 3000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Blood of the Wolf", 0x400002CD, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Consumed Oceiros", 0x400002CE, DS3ItemCategory.BOSS, souls = 12000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Boreal Valley Vordt", 0x400002CF, DS3ItemCategory.BOSS, souls = 2000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Old Demon King", 0x400002D0, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Dragonslayer Armour", 0x400002D1, DS3ItemCategory.BOSS, souls = 15000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Nameless King", 0x400002D2, DS3ItemCategory.BOSS, souls = 16000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Pontiff Sulyvahn", 0x400002D4, DS3ItemCategory.BOSS, souls = 12000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Aldrich", 0x400002D5, DS3ItemCategory.BOSS, souls = 15000, + classification = ItemClassification.progression), + DS3ItemData("Soul of High Lord Wolnir", 0x400002D6, DS3ItemCategory.BOSS, souls = 10000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Rotted Greatwood", 0x400002D7, DS3ItemCategory.BOSS, souls = 3000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Rosaria", 0x400002D8, DS3ItemCategory.BOSS, souls = 5000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Deacons of the Deep", 0x400002D9, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Twin Princes", 0x400002DB, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Yhorm the Giant", 0x400002DC, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Lords", 0x400002DD, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Demon", 0x400002E3, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of a Stray Demon", 0x400002E7, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + *DS3ItemData("Titanite Shard", 0x400003E8, DS3ItemCategory.UPGRADE).counts([2]), + *DS3ItemData("Large Titanite Shard", 0x400003E9, DS3ItemCategory.UPGRADE).counts([2, 3]), + *DS3ItemData("Titanite Chunk", 0x400003EA, DS3ItemCategory.UPGRADE).counts([2, 6]), + DS3ItemData("Titanite Slab", 0x400003EB, DS3ItemCategory.UPGRADE, + classification = ItemClassification.useful), + *DS3ItemData("Titanite Scale", 0x400003FC, DS3ItemCategory.UPGRADE).counts([2, 3]), + *DS3ItemData("Twinkling Titanite", 0x40000406, DS3ItemCategory.UPGRADE).counts([2, 3]), + DS3ItemData("Heavy Gem", 0x4000044C, DS3ItemCategory.UPGRADE), + DS3ItemData("Sharp Gem", 0x40000456, DS3ItemCategory.UPGRADE), + DS3ItemData("Refined Gem", 0x40000460, DS3ItemCategory.UPGRADE), + DS3ItemData("Crystal Gem", 0x4000046A, DS3ItemCategory.UPGRADE), + DS3ItemData("Simple Gem", 0x40000474, DS3ItemCategory.UPGRADE), + DS3ItemData("Fire Gem", 0x4000047E, DS3ItemCategory.UPGRADE), + DS3ItemData("Chaos Gem", 0x40000488, DS3ItemCategory.UPGRADE), + DS3ItemData("Lightning Gem", 0x40000492, DS3ItemCategory.UPGRADE), + DS3ItemData("Deep Gem", 0x4000049C, DS3ItemCategory.UPGRADE), + DS3ItemData("Dark Gem", 0x400004A6, DS3ItemCategory.UPGRADE), + DS3ItemData("Poison Gem", 0x400004B0, DS3ItemCategory.UPGRADE), + DS3ItemData("Blood Gem", 0x400004BA, DS3ItemCategory.UPGRADE), + DS3ItemData("Raw Gem", 0x400004C4, DS3ItemCategory.UPGRADE), + DS3ItemData("Blessed Gem", 0x400004CE, DS3ItemCategory.UPGRADE), + DS3ItemData("Hollow Gem", 0x400004D8, DS3ItemCategory.UPGRADE), + DS3ItemData("Shriving Stone", 0x400004E2, DS3ItemCategory.UPGRADE), + DS3ItemData("Lift Chamber Key", 0x400007D1, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Doll", 0x400007D5, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Jailbreaker's Key", 0x400007D7, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Jailer's Key Ring", 0x400007D8, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Key", 0x400007D9, DS3ItemCategory.UNIQUE), + DS3ItemData("Cell Key", 0x400007DA, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Dungeon Ground Floor Key", 0x400007DB, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Cell Key", 0x400007DC, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grand Archives Key", 0x400007DE, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Tower Key", 0x400007DF, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Lothric Banner", 0x40000836, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Farron Coal", 0x40000837, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Sage's Coal", 0x40000838, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Giant's Coal", 0x40000839, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Profaned Coal", 0x4000083A, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Mortician's Ashes", 0x4000083B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Dreamchaser's Ashes", 0x4000083C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Paladin's Ashes", 0x4000083D, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Warden's Ashes", 0x4000083E, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Greirat's Ashes", 0x4000083F, DS3ItemCategory.UNIQUE), + DS3ItemData("Orbeck's Ashes", 0x40000840, DS3ItemCategory.UNIQUE), + DS3ItemData("Cornyx's Ashes", 0x40000841, DS3ItemCategory.UNIQUE), + DS3ItemData("Karla's Ashes", 0x40000842, DS3ItemCategory.UNIQUE), + DS3ItemData("Irina's Ashes", 0x40000843, DS3ItemCategory.UNIQUE), + DS3ItemData("Yuria's Ashes", 0x40000844, DS3ItemCategory.UNIQUE), + DS3ItemData("Basin of Vows", 0x40000845, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Loretta's Bone", 0x40000846, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Braille Divine Tome of Carim", 0x40000847, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Braille Divine Tome of Lothric", 0x40000848, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Abyss Watcher", 0x4000084B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Aldrich", 0x4000084C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Yhorm the Giant", 0x4000084D, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Cinders of a Lord - Lothric Prince", 0x4000084E, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Great Swamp Pyromancy Tome", 0x4000084F, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Carthus Pyromancy Tome", 0x40000850, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Izalith Pyromancy Tome", 0x40000851, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Quelana Pyromancy Tome", 0x40000852, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Grave Warden Pyromancy Tome", 0x40000853, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Sage's Scroll", 0x40000854, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Logan's Scroll", 0x40000855, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Crystal Scroll", 0x40000856, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Transposing Kiln", 0x40000857, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Coiled Sword", 0x40000859, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Eyes of a Fire Keeper", 0x4000085A, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), # Allow players to do any ending + DS3ItemData("Sword of Avowal", 0x4000085B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Golden Scroll", 0x4000085C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Estus Shard", 0x4000085D, DS3ItemCategory.HEALING, + classification = ItemClassification.useful), + DS3ItemData("Hawkwood's Swordgrass", 0x4000085E, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Undead Bone Shard", 0x4000085F, DS3ItemCategory.HEALING, + classification = ItemClassification.useful), + DS3ItemData("Deep Braille Divine Tome", 0x40000860, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Londor Braille Divine Tome", 0x40000861, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Excrement-covered Ashes", 0x40000862, DS3ItemCategory.UNIQUE, + classification = ItemClassification.useful), + DS3ItemData("Prisoner Chief's Ashes", 0x40000863, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Xanthous Ashes", 0x40000864, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Hollow's Ashes", 0x40000865, DS3ItemCategory.UNIQUE), + DS3ItemData("Patches' Ashes", 0x40000866, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon Chaser's Ashes", 0x40000867, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Easterner's Ashes", 0x40000868, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + + # Fake item for controlling access to Archdragon Peak. The real drop isn't actually an item as + # such, so we have to inject this because there's no slot for it to come from. + DS3ItemData("Path of the Dragon", 0x40002346, DS3ItemCategory.UNIQUE, + inject = True, classification = ItemClassification.progression), # Spells - ("Farron Dart", 0x40124F80, DS3ItemCategory.SPELL), - ("Great Farron Dart", 0x40127690, DS3ItemCategory.SPELL), - ("Soul Arrow", 0x4013D620, DS3ItemCategory.SPELL), - ("Great Soul Arrow", 0x4013DA08, DS3ItemCategory.SPELL), - ("Heavy Soul Arrow", 0x4013DDF0, DS3ItemCategory.SPELL), - ("Great Heavy Soul Arrow", 0x4013E1D8, DS3ItemCategory.SPELL), - ("Homing Soulmass", 0x4013E5C0, DS3ItemCategory.SPELL), - ("Homing Crystal Soulmass", 0x4013E9A8, DS3ItemCategory.SPELL), - ("Soul Spear", 0x4013ED90, DS3ItemCategory.SPELL), - ("Crystal Soul Spear", 0x4013F178, DS3ItemCategory.SPELL), - ("Deep Soul", 0x4013F560, DS3ItemCategory.SPELL), - ("Great Deep Soul", 0x4013F948, DS3ItemCategory.SPELL), - ("Magic Weapon", 0x4013FD30, DS3ItemCategory.SPELL), - ("Great Magic Weapon", 0x40140118, DS3ItemCategory.SPELL), - ("Crystal Magic Weapon", 0x40140500, DS3ItemCategory.SPELL), - ("Magic Shield", 0x40144B50, DS3ItemCategory.SPELL), - ("Great Magic Shield", 0x40144F38, DS3ItemCategory.SPELL), - ("Hidden Weapon", 0x40147260, DS3ItemCategory.SPELL), - ("Hidden Body", 0x40147648, DS3ItemCategory.SPELL), - ("Cast Light", 0x40149970, DS3ItemCategory.SPELL), - ("Repair", 0x4014A528, DS3ItemCategory.SPELL), - ("Spook", 0x4014A910, DS3ItemCategory.SPELL), - ("Chameleon", 0x4014ACF8, DS3ItemCategory.SPELL), - ("Aural Decoy", 0x4014B0E0, DS3ItemCategory.SPELL), - ("White Dragon Breath", 0x4014E790, DS3ItemCategory.SPELL), - ("Farron Hail", 0x4014EF60, DS3ItemCategory.SPELL), - ("Crystal Hail", 0x4014F348, DS3ItemCategory.SPELL), - ("Soul Greatsword", 0x4014F730, DS3ItemCategory.SPELL), - ("Farron Flashsword", 0x4014FB18, DS3ItemCategory.SPELL), - ("Affinity", 0x401875B8, DS3ItemCategory.SPELL), - ("Dark Edge", 0x40189CC8, DS3ItemCategory.SPELL), - ("Soul Stream", 0x4018B820, DS3ItemCategory.SPELL), - ("Twisted Wall of Light", 0x40193138, DS3ItemCategory.SPELL), - ("Pestilent Mist", 0x401A8CE0, DS3ItemCategory.SPELL), # Originally called "Pestilent Mercury" pre 1.15 - ("Fireball", 0x40249F00, DS3ItemCategory.SPELL), - ("Fire Orb", 0x4024A6D0, DS3ItemCategory.SPELL), - ("Firestorm", 0x4024AAB8, DS3ItemCategory.SPELL), - ("Fire Surge", 0x4024B288, DS3ItemCategory.SPELL), - ("Black Serpent", 0x4024BA58, DS3ItemCategory.SPELL), - ("Combustion", 0x4024C610, DS3ItemCategory.SPELL), - ("Great Combustion", 0x4024C9F8, DS3ItemCategory.SPELL), - ("Poison Mist", 0x4024ED20, DS3ItemCategory.SPELL), - ("Toxic Mist", 0x4024F108, DS3ItemCategory.SPELL), - ("Acid Surge", 0x4024F4F0, DS3ItemCategory.SPELL), - ("Iron Flesh", 0x40251430, DS3ItemCategory.SPELL), - ("Flash Sweat", 0x40251818, DS3ItemCategory.SPELL), - ("Carthus Flame Arc", 0x402527B8, DS3ItemCategory.SPELL), - ("Rapport", 0x40252BA0, DS3ItemCategory.SPELL), - ("Power Within", 0x40253B40, DS3ItemCategory.SPELL), - ("Great Chaos Fire Orb", 0x40256250, DS3ItemCategory.SPELL), - ("Chaos Storm", 0x40256638, DS3ItemCategory.SPELL), - ("Fire Whip", 0x40256A20, DS3ItemCategory.SPELL), - ("Black Flame", 0x40256E08, DS3ItemCategory.SPELL), - ("Profaned Flame", 0x402575D8, DS3ItemCategory.SPELL), - ("Chaos Bed Vestiges", 0x402579C0, DS3ItemCategory.SPELL), - ("Warmth", 0x4025B070, DS3ItemCategory.SPELL), - ("Profuse Sweat", 0x402717D0, DS3ItemCategory.SPELL), - ("Black Fire Orb", 0x4027D350, DS3ItemCategory.SPELL), - ("Bursting Fireball", 0x4027FA60, DS3ItemCategory.SPELL), - ("Boulder Heave", 0x40282170, DS3ItemCategory.SPELL), - ("Sacred Flame", 0x40284880, DS3ItemCategory.SPELL), - ("Carthus Beacon", 0x40286F90, DS3ItemCategory.SPELL), - ("Heal Aid", 0x403540D0, DS3ItemCategory.SPELL), - ("Heal", 0x403567E0, DS3ItemCategory.SPELL), - ("Med Heal", 0x40356BC8, DS3ItemCategory.SPELL), - ("Great Heal", 0x40356FB0, DS3ItemCategory.SPELL), - ("Soothing Sunlight", 0x40357398, DS3ItemCategory.SPELL), - ("Replenishment", 0x40357780, DS3ItemCategory.SPELL), - ("Bountiful Sunlight", 0x40357B68, DS3ItemCategory.SPELL), - ("Bountiful Light", 0x40358338, DS3ItemCategory.SPELL), - ("Caressing Tears", 0x40358720, DS3ItemCategory.SPELL), - ("Tears of Denial", 0x4035B600, DS3ItemCategory.SPELL), - ("Homeward", 0x4035B9E8, DS3ItemCategory.SPELL), - ("Force", 0x4035DD10, DS3ItemCategory.SPELL), - ("Wrath of the Gods", 0x4035E0F8, DS3ItemCategory.SPELL), - ("Emit Force", 0x4035E4E0, DS3ItemCategory.SPELL), - ("Seek Guidance", 0x40360420, DS3ItemCategory.SPELL), - ("Lightning Spear", 0x40362B30, DS3ItemCategory.SPELL), - ("Great Lightning Spear", 0x40362F18, DS3ItemCategory.SPELL), - ("Sunlight Spear", 0x40363300, DS3ItemCategory.SPELL), - ("Lightning Storm", 0x403636E8, DS3ItemCategory.SPELL), - ("Gnaw", 0x40363AD0, DS3ItemCategory.SPELL), - ("Dorhys' Gnawing", 0x40363EB8, DS3ItemCategory.SPELL), - ("Magic Barrier", 0x40365240, DS3ItemCategory.SPELL), - ("Great Magic Barrier", 0x40365628, DS3ItemCategory.SPELL), - ("Sacred Oath", 0x40365DF8, DS3ItemCategory.SPELL), - ("Vow of Silence", 0x4036A448, DS3ItemCategory.SPELL), - ("Lightning Blade", 0x4036C770, DS3ItemCategory.SPELL), - ("Darkmoon Blade", 0x4036CB58, DS3ItemCategory.SPELL), - ("Dark Blade", 0x40378AC0, DS3ItemCategory.SPELL), - ("Dead Again", 0x40387520, DS3ItemCategory.SPELL), - ("Lightning Stake", 0x40389C30, DS3ItemCategory.SPELL), - ("Divine Pillars of Light", 0x4038C340, DS3ItemCategory.SPELL), - ("Lifehunt Scythe", 0x4038EA50, DS3ItemCategory.SPELL), - ("Blessed Weapon", 0x40395F80, DS3ItemCategory.SPELL), - ("Deep Protection", 0x40398690, DS3ItemCategory.SPELL), - ("Atonement", 0x4039ADA0, DS3ItemCategory.SPELL), -]] + DS3ItemData("Farron Dart", 0x40124F80, DS3ItemCategory.SPELL), + DS3ItemData("Great Farron Dart", 0x40127690, DS3ItemCategory.SPELL), + DS3ItemData("Soul Arrow", 0x4013D620, DS3ItemCategory.SPELL), + DS3ItemData("Great Soul Arrow", 0x4013DA08, DS3ItemCategory.SPELL), + DS3ItemData("Heavy Soul Arrow", 0x4013DDF0, DS3ItemCategory.SPELL), + DS3ItemData("Great Heavy Soul Arrow", 0x4013E1D8, DS3ItemCategory.SPELL), + DS3ItemData("Homing Soulmass", 0x4013E5C0, DS3ItemCategory.SPELL), + DS3ItemData("Homing Crystal Soulmass", 0x4013E9A8, DS3ItemCategory.SPELL), + DS3ItemData("Soul Spear", 0x4013ED90, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Soul Spear", 0x4013F178, DS3ItemCategory.SPELL), + DS3ItemData("Deep Soul", 0x4013F560, DS3ItemCategory.SPELL), + DS3ItemData("Great Deep Soul", 0x4013F948, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Magic Weapon", 0x4013FD30, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Weapon", 0x40140118, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Magic Weapon", 0x40140500, DS3ItemCategory.SPELL), + DS3ItemData("Magic Shield", 0x40144B50, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Shield", 0x40144F38, DS3ItemCategory.SPELL), + DS3ItemData("Hidden Weapon", 0x40147260, DS3ItemCategory.SPELL), + DS3ItemData("Hidden Body", 0x40147648, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Cast Light", 0x40149970, DS3ItemCategory.SPELL), + DS3ItemData("Repair", 0x4014A528, DS3ItemCategory.SPELL), + DS3ItemData("Spook", 0x4014A910, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Chameleon", 0x4014ACF8, DS3ItemCategory.SPELL, + classification = ItemClassification.progression), + DS3ItemData("Aural Decoy", 0x4014B0E0, DS3ItemCategory.SPELL), + DS3ItemData("White Dragon Breath", 0x4014E790, DS3ItemCategory.SPELL), + DS3ItemData("Farron Hail", 0x4014EF60, DS3ItemCategory.SPELL), + DS3ItemData("Crystal Hail", 0x4014F348, DS3ItemCategory.SPELL), + DS3ItemData("Soul Greatsword", 0x4014F730, DS3ItemCategory.SPELL), + DS3ItemData("Farron Flashsword", 0x4014FB18, DS3ItemCategory.SPELL), + DS3ItemData("Affinity", 0x401875B8, DS3ItemCategory.SPELL), + DS3ItemData("Dark Edge", 0x40189CC8, DS3ItemCategory.SPELL), + DS3ItemData("Soul Stream", 0x4018B820, DS3ItemCategory.SPELL), + DS3ItemData("Twisted Wall of Light", 0x40193138, DS3ItemCategory.SPELL), + DS3ItemData("Pestilent Mist", 0x401A8CE0, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), # Originally called "Pestilent Mercury" pre 1.15 + DS3ItemData("Fireball", 0x40249F00, DS3ItemCategory.SPELL), + DS3ItemData("Fire Orb", 0x4024A6D0, DS3ItemCategory.SPELL), + DS3ItemData("Firestorm", 0x4024AAB8, DS3ItemCategory.SPELL), + DS3ItemData("Fire Surge", 0x4024B288, DS3ItemCategory.SPELL), + DS3ItemData("Black Serpent", 0x4024BA58, DS3ItemCategory.SPELL), + DS3ItemData("Combustion", 0x4024C610, DS3ItemCategory.SPELL), + DS3ItemData("Great Combustion", 0x4024C9F8, DS3ItemCategory.SPELL), + DS3ItemData("Poison Mist", 0x4024ED20, DS3ItemCategory.SPELL), + DS3ItemData("Toxic Mist", 0x4024F108, DS3ItemCategory.SPELL), + DS3ItemData("Acid Surge", 0x4024F4F0, DS3ItemCategory.SPELL), + DS3ItemData("Iron Flesh", 0x40251430, DS3ItemCategory.SPELL), + DS3ItemData("Flash Sweat", 0x40251818, DS3ItemCategory.SPELL), + DS3ItemData("Carthus Flame Arc", 0x402527B8, DS3ItemCategory.SPELL), + DS3ItemData("Rapport", 0x40252BA0, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Power Within", 0x40253B40, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Great Chaos Fire Orb", 0x40256250, DS3ItemCategory.SPELL), + DS3ItemData("Chaos Storm", 0x40256638, DS3ItemCategory.SPELL), + DS3ItemData("Fire Whip", 0x40256A20, DS3ItemCategory.SPELL), + DS3ItemData("Black Flame", 0x40256E08, DS3ItemCategory.SPELL), + DS3ItemData("Profaned Flame", 0x402575D8, DS3ItemCategory.SPELL), + DS3ItemData("Chaos Bed Vestiges", 0x402579C0, DS3ItemCategory.SPELL), + DS3ItemData("Warmth", 0x4025B070, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Profuse Sweat", 0x402717D0, DS3ItemCategory.SPELL), + DS3ItemData("Black Fire Orb", 0x4027D350, DS3ItemCategory.SPELL), + DS3ItemData("Bursting Fireball", 0x4027FA60, DS3ItemCategory.SPELL), + DS3ItemData("Boulder Heave", 0x40282170, DS3ItemCategory.SPELL), + DS3ItemData("Sacred Flame", 0x40284880, DS3ItemCategory.SPELL), + DS3ItemData("Carthus Beacon", 0x40286F90, DS3ItemCategory.SPELL), + DS3ItemData("Heal Aid", 0x403540D0, DS3ItemCategory.SPELL), + DS3ItemData("Heal", 0x403567E0, DS3ItemCategory.SPELL), + DS3ItemData("Med Heal", 0x40356BC8, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Great Heal", 0x40356FB0, DS3ItemCategory.SPELL), + DS3ItemData("Soothing Sunlight", 0x40357398, DS3ItemCategory.SPELL), + DS3ItemData("Replenishment", 0x40357780, DS3ItemCategory.SPELL), + DS3ItemData("Bountiful Sunlight", 0x40357B68, DS3ItemCategory.SPELL), + DS3ItemData("Bountiful Light", 0x40358338, DS3ItemCategory.SPELL), + DS3ItemData("Caressing Tears", 0x40358720, DS3ItemCategory.SPELL), + DS3ItemData("Tears of Denial", 0x4035B600, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Homeward", 0x4035B9E8, DS3ItemCategory.SPELL, + classification = ItemClassification.useful), + DS3ItemData("Force", 0x4035DD10, DS3ItemCategory.SPELL), + DS3ItemData("Wrath of the Gods", 0x4035E0F8, DS3ItemCategory.SPELL), + DS3ItemData("Emit Force", 0x4035E4E0, DS3ItemCategory.SPELL), + DS3ItemData("Seek Guidance", 0x40360420, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Spear", 0x40362B30, DS3ItemCategory.SPELL), + DS3ItemData("Great Lightning Spear", 0x40362F18, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Sunlight Spear", 0x40363300, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Storm", 0x403636E8, DS3ItemCategory.SPELL), + DS3ItemData("Gnaw", 0x40363AD0, DS3ItemCategory.SPELL), + DS3ItemData("Dorhys' Gnawing", 0x40363EB8, DS3ItemCategory.SPELL), + DS3ItemData("Magic Barrier", 0x40365240, DS3ItemCategory.SPELL), + DS3ItemData("Great Magic Barrier", 0x40365628, DS3ItemCategory.SPELL), + DS3ItemData("Sacred Oath", 0x40365DF8, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Vow of Silence", 0x4036A448, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Blade", 0x4036C770, DS3ItemCategory.SPELL), + DS3ItemData("Darkmoon Blade", 0x4036CB58, DS3ItemCategory.SPELL, + inject = True), # Covenant reward + DS3ItemData("Dark Blade", 0x40378AC0, DS3ItemCategory.SPELL), + DS3ItemData("Dead Again", 0x40387520, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Stake", 0x40389C30, DS3ItemCategory.SPELL), + DS3ItemData("Divine Pillars of Light", 0x4038C340, DS3ItemCategory.SPELL), + DS3ItemData("Lifehunt Scythe", 0x4038EA50, DS3ItemCategory.SPELL), + DS3ItemData("Blessed Weapon", 0x40395F80, DS3ItemCategory.SPELL), + DS3ItemData("Deep Protection", 0x40398690, DS3ItemCategory.SPELL), + DS3ItemData("Atonement", 0x4039ADA0, DS3ItemCategory.SPELL), +] -_dlc_items = [DS3ItemData(row[0], row[1], True, row[2]) for row in [ +_dlc_items = [ # Ammunition - ("Millwood Greatarrow", 0x000623E0, DS3ItemCategory.SKIP), + *DS3ItemData("Millwood Greatarrow", 0x000623E0, DS3ItemCategory.MISC).counts([5]), # Weapons - ("Aquamarine Dagger", 0x00116520, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Murky Hand Scythe", 0x00118C30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Onyx Blade", 0x00222E00, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Ringed Knight Straight Sword", 0x00225510, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Gael's Greatsword", 0x00227C20, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Follower Sabre", 0x003EDDC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Demon's Scar", 0x003F04D0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Frayed Blade", 0x004D35A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Herald Curved Greatsword", 0x006159E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Millwood Battle Axe", 0x006D67D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Earth Seeker", 0x006D8EE0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Quakestone Hammer", 0x007ECCF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Ledo's Great Hammer", 0x007EF400, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Follower Javelin", 0x008CD6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ringed Knight Spear", 0x008CFDC0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Lothric War Banner", 0x008D24D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Crucifix of the Mad King", 0x008D4BE0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Splitleaf Greatsword", 0x009B2E90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Friede's Great Scythe", 0x009B55A0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crow Talons", 0x00A89C10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Rose of Ariandel", 0x00B82C70, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Pyromancer's Parting Flame", 0x00CC9ED0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Murky Longstaff", 0x00CCC5E0, DS3ItemCategory.WEAPON_UPGRADE_10), - ("Sacred Chime of Filianore", 0x00CCECF0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Preacher's Right Arm", 0x00CD1400, DS3ItemCategory.WEAPON_UPGRADE_5), - ("White Birch Bow", 0x00D77440, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Millwood Greatbow", 0x00D85EA0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Repeating Crossbow", 0x00D885B0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Giant Door Shield", 0x00F5F8C0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Valorheart", 0x00F646E0, DS3ItemCategory.WEAPON_UPGRADE_5), - ("Crow Quills", 0x00F66DF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), - ("Ringed Knight Paired Greatswords", 0x00F69500, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Aquamarine Dagger", 0x00116520, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Murky Hand Scythe", 0x00118C30, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Onyx Blade", 0x00222E00, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Ringed Knight Straight Sword", 0x00225510, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Gael's Greatsword", 0x00227C20, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Follower Sabre", 0x003EDDC0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Demon's Scar", 0x003F04D0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Frayed Blade", 0x004D35A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Harald Curved Greatsword", 0x006159E0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Millwood Battle Axe", 0x006D67D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Earth Seeker", 0x006D8EE0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Quakestone Hammer", 0x007ECCF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Ledo's Great Hammer", 0x007EF400, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Follower Javelin", 0x008CD6B0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ringed Knight Spear", 0x008CFDC0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Lothric War Banner", 0x008D24D0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Crucifix of the Mad King", 0x008D4BE0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Splitleaf Greatsword", 0x009B2E90, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Friede's Great Scythe", 0x009B55A0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crow Talons", 0x00A89C10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Rose of Ariandel", 0x00B82C70, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Pyromancer's Parting Flame", 0x00CC9ED0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Murky Longstaff", 0x00CCC5E0, DS3ItemCategory.WEAPON_UPGRADE_10), + DS3ItemData("Sacred Chime of Filianore", 0x00CCECF0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Preacher's Right Arm", 0x00CD1400, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("White Birch Bow", 0x00D77440, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Millwood Greatbow", 0x00D85EA0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Repeating Crossbow", 0x00D885B0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Giant Door Shield", 0x00F5F8C0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Valorheart", 0x00F646E0, DS3ItemCategory.WEAPON_UPGRADE_5), + DS3ItemData("Crow Quills", 0x00F66DF0, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE), + DS3ItemData("Ringed Knight Paired Greatswords", 0x00F69500, DS3ItemCategory.WEAPON_UPGRADE_5), # Shields - ("Follower Shield", 0x0135C0E0, DS3ItemCategory.SHIELD_INFUSIBLE), - ("Dragonhead Shield", 0x0135E7F0, DS3ItemCategory.SHIELD), - ("Ethereal Oak Shield", 0x01450320, DS3ItemCategory.SHIELD), - ("Dragonhead Greatshield", 0x01452A30, DS3ItemCategory.SHIELD), - ("Follower Torch", 0x015F1AD0, DS3ItemCategory.SHIELD), + DS3ItemData("Follower Shield", 0x0135C0E0, DS3ItemCategory.SHIELD_INFUSIBLE), + DS3ItemData("Dragonhead Shield", 0x0135E7F0, DS3ItemCategory.SHIELD), + DS3ItemData("Ethereal Oak Shield", 0x01450320, DS3ItemCategory.SHIELD), + DS3ItemData("Dragonhead Greatshield", 0x01452A30, DS3ItemCategory.SHIELD), + DS3ItemData("Follower Torch", 0x015F1AD0, DS3ItemCategory.SHIELD), # Armor - ("Vilhelm's Helm", 0x11312D00, DS3ItemCategory.ARMOR), - ("Vilhelm's Armor", 0x113130E8, DS3ItemCategory.ARMOR), - ("Vilhelm's Gauntlets", 0x113134D0, DS3ItemCategory.ARMOR), - ("Vilhelm's Leggings", 0x113138B8, DS3ItemCategory.ARMOR), - ("Antiquated Plain Garb", 0x11B2E408, DS3ItemCategory.ARMOR), - ("Violet Wrappings", 0x11B2E7F0, DS3ItemCategory.ARMOR), - ("Loincloth 2", 0x11B2EBD8, DS3ItemCategory.ARMOR), - ("Shira's Crown", 0x11C22260, DS3ItemCategory.ARMOR), - ("Shira's Armor", 0x11C22648, DS3ItemCategory.ARMOR), - ("Shira's Gloves", 0x11C22A30, DS3ItemCategory.ARMOR), - ("Shira's Trousers", 0x11C22E18, DS3ItemCategory.ARMOR), - ("Lapp's Helm", 0x11E84800, DS3ItemCategory.ARMOR), - ("Lapp's Armor", 0x11E84BE8, DS3ItemCategory.ARMOR), - ("Lapp's Gauntlets", 0x11E84FD0, DS3ItemCategory.ARMOR), - ("Lapp's Leggings", 0x11E853B8, DS3ItemCategory.ARMOR), - ("Slave Knight Hood", 0x134EDCE0, DS3ItemCategory.ARMOR), - ("Slave Knight Armor", 0x134EE0C8, DS3ItemCategory.ARMOR), - ("Slave Knight Gauntlets", 0x134EE4B0, DS3ItemCategory.ARMOR), - ("Slave Knight Leggings", 0x134EE898, DS3ItemCategory.ARMOR), - ("Ordained Hood", 0x135E1F20, DS3ItemCategory.ARMOR), - ("Ordained Dress", 0x135E2308, DS3ItemCategory.ARMOR), - ("Ordained Trousers", 0x135E2AD8, DS3ItemCategory.ARMOR), - ("Follower Helm", 0x137CA3A0, DS3ItemCategory.ARMOR), - ("Follower Armor", 0x137CA788, DS3ItemCategory.ARMOR), - ("Follower Gloves", 0x137CAB70, DS3ItemCategory.ARMOR), - ("Follower Boots", 0x137CAF58, DS3ItemCategory.ARMOR), - ("Millwood Knight Helm", 0x139B2820, DS3ItemCategory.ARMOR), - ("Millwood Knight Armor", 0x139B2C08, DS3ItemCategory.ARMOR), - ("Millwood Knight Gauntlets", 0x139B2FF0, DS3ItemCategory.ARMOR), - ("Millwood Knight Leggings", 0x139B33D8, DS3ItemCategory.ARMOR), - ("Ringed Knight Hood", 0x13C8EEE0, DS3ItemCategory.ARMOR), - ("Ringed Knight Armor", 0x13C8F2C8, DS3ItemCategory.ARMOR), - ("Ringed Knight Gauntlets", 0x13C8F6B0, DS3ItemCategory.ARMOR), - ("Ringed Knight Leggings", 0x13C8FA98, DS3ItemCategory.ARMOR), - ("Harald Legion Armor", 0x13D83508, DS3ItemCategory.ARMOR), - ("Harald Legion Gauntlets", 0x13D838F0, DS3ItemCategory.ARMOR), - ("Harald Legion Leggings", 0x13D83CD8, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Helm", 0x1405F7E0, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Armor", 0x1405FBC8, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Gauntlets", 0x1405FFB0, DS3ItemCategory.ARMOR), - ("Iron Dragonslayer Leggings", 0x14060398, DS3ItemCategory.ARMOR), - ("White Preacher Head", 0x14153A20, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Helm", 0x14CC5520, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Armor", 0x14CC5908, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Gauntlets", 0x14CC5CF0, DS3ItemCategory.ARMOR), - ("Ruin Sentinel Leggings", 0x14CC60D8, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Hood", 0x14DB9760, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Garb", 0x14DB9B48, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Gloves", 0x14DB9F30, DS3ItemCategory.ARMOR), - ("Desert Pyromancer Skirt", 0x14DBA318, DS3ItemCategory.ARMOR), - ("Black Witch Hat", 0x14EAD9A0, DS3ItemCategory.ARMOR), - ("Black Witch Garb", 0x14EADD88, DS3ItemCategory.ARMOR), - ("Black Witch Wrappings", 0x14EAE170, DS3ItemCategory.ARMOR), - ("Black Witch Trousers", 0x14EAE558, DS3ItemCategory.ARMOR), - ("Black Witch Veil", 0x14FA1BE0, DS3ItemCategory.ARMOR), - ("Blindfold Mask", 0x15095E20, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Helm", 0x11312D00, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Armor", 0x113130E8, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Gauntlets", 0x113134D0, DS3ItemCategory.ARMOR), + DS3ItemData("Vilhelm's Leggings", 0x113138B8, DS3ItemCategory.ARMOR), + DS3ItemData("Antiquated Plain Garb", 0x11B2E408, DS3ItemCategory.ARMOR), + DS3ItemData("Violet Wrappings", 0x11B2E7F0, DS3ItemCategory.ARMOR), + DS3ItemData("Loincloth 2", 0x11B2EBD8, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Crown", 0x11C22260, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Armor", 0x11C22648, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Gloves", 0x11C22A30, DS3ItemCategory.ARMOR), + DS3ItemData("Shira's Trousers", 0x11C22E18, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Helm", 0x11E84800, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Armor", 0x11E84BE8, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Gauntlets", 0x11E84FD0, DS3ItemCategory.ARMOR), + DS3ItemData("Lapp's Leggings", 0x11E853B8, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Hood", 0x134EDCE0, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Armor", 0x134EE0C8, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Gauntlets", 0x134EE4B0, DS3ItemCategory.ARMOR), + DS3ItemData("Slave Knight Leggings", 0x134EE898, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Hood", 0x135E1F20, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Dress", 0x135E2308, DS3ItemCategory.ARMOR), + DS3ItemData("Ordained Trousers", 0x135E2AD8, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Helm", 0x137CA3A0, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Armor", 0x137CA788, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Gloves", 0x137CAB70, DS3ItemCategory.ARMOR), + DS3ItemData("Follower Boots", 0x137CAF58, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Helm", 0x139B2820, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Armor", 0x139B2C08, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Gauntlets", 0x139B2FF0, DS3ItemCategory.ARMOR), + DS3ItemData("Millwood Knight Leggings", 0x139B33D8, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Hood", 0x13C8EEE0, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Armor", 0x13C8F2C8, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Gauntlets", 0x13C8F6B0, DS3ItemCategory.ARMOR), + DS3ItemData("Ringed Knight Leggings", 0x13C8FA98, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Armor", 0x13D83508, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Gauntlets", 0x13D838F0, DS3ItemCategory.ARMOR), + DS3ItemData("Harald Legion Leggings", 0x13D83CD8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Helm", 0x1405F7E0, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Armor", 0x1405FBC8, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Gauntlets", 0x1405FFB0, DS3ItemCategory.ARMOR), + DS3ItemData("Iron Dragonslayer Leggings", 0x14060398, DS3ItemCategory.ARMOR), + DS3ItemData("White Preacher Head", 0x14153A20, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Helm", 0x14CC5520, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Armor", 0x14CC5908, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Gauntlets", 0x14CC5CF0, DS3ItemCategory.ARMOR), + DS3ItemData("Ruin Leggings", 0x14CC60D8, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Hood", 0x14DB9760, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Garb", 0x14DB9B48, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Gloves", 0x14DB9F30, DS3ItemCategory.ARMOR), + DS3ItemData("Desert Pyromancer Skirt", 0x14DBA318, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Hat", 0x14EAD9A0, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Garb", 0x14EADD88, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Wrappings", 0x14EAE170, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Trousers", 0x14EAE558, DS3ItemCategory.ARMOR), + DS3ItemData("Black Witch Veil", 0x14FA1BE0, DS3ItemCategory.ARMOR), + DS3ItemData("Blindfold Mask", 0x15095E20, DS3ItemCategory.ARMOR), # Covenants - ("Spear of the Church", 0x2000276A, DS3ItemCategory.SKIP), + DS3ItemData("Spear of the Church", 0x2000276A, DS3ItemCategory.UNIQUE, skip = True), # Rings - ("Chloranthy Ring+3", 0x20004E2D, DS3ItemCategory.RING), - ("Havel's Ring+3", 0x20004E37, DS3ItemCategory.RING), - ("Ring of Favor+3", 0x20004E41, DS3ItemCategory.RING), - ("Ring of Steel Protection+3", 0x20004E4B, DS3ItemCategory.RING), - ("Wolf Ring+3", 0x20004EE1, DS3ItemCategory.RING), - ("Covetous Gold Serpent Ring+3", 0x20004FA9, DS3ItemCategory.RING), - ("Covetous Silver Serpent Ring+3", 0x20004FB3, DS3ItemCategory.RING), - ("Ring of the Evil Eye+3", 0x20005071, DS3ItemCategory.RING), - ("Chillbite Ring", 0x20005208, DS3ItemCategory.RING), + DS3ItemData("Chloranthy Ring+3", 0x20004E2D, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Havel's Ring+3", 0x20004E37, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of Favor+3", 0x20004E41, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of Steel Protection+3", 0x20004E4B, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Wolf Ring+3", 0x20004EE1, DS3ItemCategory.RING), + DS3ItemData("Covetous Gold Serpent Ring+3", 0x20004FA9, DS3ItemCategory.RING), + DS3ItemData("Covetous Silver Serpent Ring+3", 0x20004FB3, DS3ItemCategory.RING, + classification = ItemClassification.useful), + DS3ItemData("Ring of the Evil Eye+3", 0x20005071, DS3ItemCategory.RING), + DS3ItemData("Chillbite Ring", 0x20005208, DS3ItemCategory.RING), # Items - ("Church Guardian Shiv", 0x4000013B, DS3ItemCategory.MISC), - ("Filianore's Spear Ornament", 0x4000017B, DS3ItemCategory.SKIP), - ("Ritual Spear Fragment", 0x4000028A, DS3ItemCategory.MISC), - ("Divine Spear Fragment", 0x4000028B, DS3ItemCategory.SKIP), - ("Soul of Sister Friede", 0x400002E8, DS3ItemCategory.BOSS), - ("Soul of Slave Knight Gael", 0x400002E9, DS3ItemCategory.BOSS), - ("Soul of the Demon Prince", 0x400002EA, DS3ItemCategory.BOSS), - ("Soul of Darkeater Midir", 0x400002EB, DS3ItemCategory.BOSS), - ("Champion's Bones", 0x40000869, DS3ItemCategory.SKIP), - ("Captain's Ashes", 0x4000086A, DS3ItemCategory.MISC), - ("Contraption Key", 0x4000086B, DS3ItemCategory.KEY), - ("Small Envoy Banner", 0x4000086C, DS3ItemCategory.KEY), - ("Old Woman's Ashes", 0x4000086D, DS3ItemCategory.SKIP), - ("Blood of the Dark Soul", 0x4000086E, DS3ItemCategory.SKIP), + DS3ItemData("Church Guardian Shiv", 0x4000013B, DS3ItemCategory.MISC), + DS3ItemData("Filianore's Spear Ornament", 0x4000017B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Ritual Spear Fragment", 0x4000028A, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Divine Spear Fragment", 0x4000028B, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Soul of Sister Friede", 0x400002E8, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Slave Knight Gael", 0x400002E9, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of the Demon Prince", 0x400002EA, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Soul of Darkeater Midir", 0x400002EB, DS3ItemCategory.BOSS, souls = 20000, + classification = ItemClassification.progression), + DS3ItemData("Champion's Bones", 0x40000869, DS3ItemCategory.UNIQUE, skip = True), + DS3ItemData("Captain's Ashes", 0x4000086A, DS3ItemCategory.MISC, + classification = ItemClassification.progression), + DS3ItemData("Contraption Key", 0x4000086B, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Small Envoy Banner", 0x4000086C, DS3ItemCategory.UNIQUE, + classification = ItemClassification.progression), + DS3ItemData("Old Woman's Ashes", 0x4000086D, DS3ItemCategory.UNIQUE), + DS3ItemData("Blood of the Dark Soul", 0x4000086E, DS3ItemCategory.UNIQUE, skip = True), # Spells - ("Frozen Weapon", 0x401408E8, DS3ItemCategory.SPELL), - ("Old Moonlight", 0x4014FF00, DS3ItemCategory.SPELL), - ("Great Soul Dregs", 0x401879A0, DS3ItemCategory.SPELL), - ("Snap Freeze", 0x401A90C8, DS3ItemCategory.SPELL), - ("Floating Chaos", 0x40257DA8, DS3ItemCategory.SPELL), - ("Flame Fan", 0x40258190, DS3ItemCategory.SPELL), - ("Seething Chaos", 0x402896A0, DS3ItemCategory.SPELL), - ("Lightning Arrow", 0x40358B08, DS3ItemCategory.SPELL), - ("Way of White Corona", 0x403642A0, DS3ItemCategory.SPELL), - ("Projected Heal", 0x40364688, DS3ItemCategory.SPELL), -]] + DS3ItemData("Frozen Weapon", 0x401408E8, DS3ItemCategory.SPELL), + DS3ItemData("Old Moonlight", 0x4014FF00, DS3ItemCategory.SPELL), + DS3ItemData("Great Soul Dregs", 0x401879A0, DS3ItemCategory.SPELL), + DS3ItemData("Snap Freeze", 0x401A90C8, DS3ItemCategory.SPELL), + DS3ItemData("Floating Chaos", 0x40257DA8, DS3ItemCategory.SPELL), + DS3ItemData("Flame Fan", 0x40258190, DS3ItemCategory.SPELL), + DS3ItemData("Seething Chaos", 0x402896A0, DS3ItemCategory.SPELL), + DS3ItemData("Lightning Arrow", 0x40358B08, DS3ItemCategory.SPELL), + DS3ItemData("Way of White Corona", 0x403642A0, DS3ItemCategory.SPELL), + DS3ItemData("Projected Heal", 0x40364688, DS3ItemCategory.SPELL), +] +for item in _dlc_items: + item.is_dlc = True # Unused list for future reference # These items exist to some degree in the code, but aren't accessible # in-game and can't be picked up without modifications -_cut_content_items = [DS3ItemData(row[0], row[1], False, row[2]) for row in [ +_cut_content_items = [ # Weapons - ("Blood-stained Short Sword", 0x00100590, DS3ItemCategory.SKIP), - ("Missionary's Axe", 0x006C2F50, DS3ItemCategory.SKIP), - ("Dragon King Greataxe", 0x006D40C0, DS3ItemCategory.SKIP), - ("Four Knights Hammer", 0x007D4650, DS3ItemCategory.SKIP), - ("Hammer of the Great Tree", 0x007D9470, DS3ItemCategory.SKIP), - ("Lothric's Scythe", 0x009A4430, DS3ItemCategory.SKIP), - ("Ancient Dragon Halberd", 0x009A6B40, DS3ItemCategory.SKIP), - ("Scythe of Want", 0x009A9250, DS3ItemCategory.SKIP), - ("Sacred Beast Catalyst", 0x00C8A730, DS3ItemCategory.SKIP), - ("Deep Pyromancy Flame", 0x00CC9ED0, DS3ItemCategory.SKIP), # Duplicate? - ("Flickering Pyromancy Flame", 0x00CD3B10, DS3ItemCategory.SKIP), - ("Strong Pyromancy Flame", 0x00CD6220, DS3ItemCategory.SKIP), - ("Deep Pyromancy Flame", 0x00CDFE60, DS3ItemCategory.SKIP), # Duplicate? - ("Pitch-Dark Pyromancy Flame", 0x00CE2570, DS3ItemCategory.SKIP), - ("Dancer's Short Bow", 0x00D77440, DS3ItemCategory.SKIP), - ("Shield Crossbow", 0x00D81080, DS3ItemCategory.SKIP), - ("Golden Dual Swords", 0x00F55C80, DS3ItemCategory.SKIP), - ("Channeler's Trident", 0x008C8890, DS3ItemCategory.SKIP), + DS3ItemData("Blood-stained Short Sword", 0x00100590, DS3ItemCategory.UNIQUE), + DS3ItemData("Missionary's Axe", 0x006C2F50, DS3ItemCategory.UNIQUE), + DS3ItemData("Dragon King Greataxe", 0x006D40C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Four Knights Hammer", 0x007D4650, DS3ItemCategory.UNIQUE), + DS3ItemData("Hammer of the Great Tree", 0x007D9470, DS3ItemCategory.UNIQUE), + DS3ItemData("Lothric's Scythe", 0x009A4430, DS3ItemCategory.UNIQUE), + DS3ItemData("Ancient Dragon Halberd", 0x009A6B40, DS3ItemCategory.UNIQUE), + DS3ItemData("Scythe of Want", 0x009A9250, DS3ItemCategory.UNIQUE), + DS3ItemData("Sacred Beast Catalyst", 0x00C8A730, DS3ItemCategory.UNIQUE), + DS3ItemData("Deep Pyromancy Flame", 0x00CC9ED0, DS3ItemCategory.UNIQUE), # Duplicate? + DS3ItemData("Flickering Pyromancy Flame", 0x00CD3B10, DS3ItemCategory.UNIQUE), + DS3ItemData("Strong Pyromancy Flame", 0x00CD6220, DS3ItemCategory.UNIQUE), + DS3ItemData("Deep Pyromancy Flame", 0x00CDFE60, DS3ItemCategory.UNIQUE), # Duplicate? + DS3ItemData("Pitch-Dark Pyromancy Flame", 0x00CE2570, DS3ItemCategory.UNIQUE), + DS3ItemData("Dancer's Short Bow", 0x00D77440, DS3ItemCategory.UNIQUE), + DS3ItemData("Shield Crossbow", 0x00D81080, DS3ItemCategory.UNIQUE), + DS3ItemData("Golden Dual Swords", 0x00F55C80, DS3ItemCategory.UNIQUE), + DS3ItemData("Channeler's Trident", 0x008C8890, DS3ItemCategory.UNIQUE), # Shields - ("Cleric's Parma", 0x013524A0, DS3ItemCategory.SKIP), - ("Prince's Shield", 0x01421CF0, DS3ItemCategory.SKIP), + DS3ItemData("Cleric's Parma", 0x013524A0, DS3ItemCategory.UNIQUE), + DS3ItemData("Prince's Shield", 0x01421CF0, DS3ItemCategory.UNIQUE), # Armor - ("Dingy Maiden's Overcoat", 0x11DA9048, DS3ItemCategory.SKIP), - ("Grotto Hat", 0x11F78A40, DS3ItemCategory.SKIP), - ("Grotto Robe", 0x11F78E28, DS3ItemCategory.SKIP), - ("Grotto Wrap", 0x11F79210, DS3ItemCategory.SKIP), - ("Grotto Trousers", 0x11F795F8, DS3ItemCategory.SKIP), - ("Soldier's Gauntlets", 0x126261D0, DS3ItemCategory.SKIP), - ("Soldier's Hood", 0x1263E0A0, DS3ItemCategory.SKIP), - ("Elder's Robe", 0x129024A8, DS3ItemCategory.SKIP), - ("Saint's Veil", 0x12A70420, DS3ItemCategory.SKIP), - ("Saint's Dress", 0x12A70808, DS3ItemCategory.SKIP), - ("Footman's Hood", 0x12AEA540, DS3ItemCategory.SKIP), - ("Footman's Overcoat", 0x12AEA928, DS3ItemCategory.SKIP), - ("Footman's Bracelets", 0x12AEAD10, DS3ItemCategory.SKIP), - ("Footman's Trousers", 0x12AEB0F8, DS3ItemCategory.SKIP), - ("Scholar's Shed Skin", 0x12E40D20, DS3ItemCategory.SKIP), - ("Man Serpent's Mask", 0x138BE5E0, DS3ItemCategory.SKIP), - ("Man Serpent's Robe", 0x138BE9C8, DS3ItemCategory.SKIP), - ("Old Monarch's Crown", 0x13DFD240, DS3ItemCategory.SKIP), - ("Old Monarch's Robe", 0x13DFD628, DS3ItemCategory.SKIP), - ("Frigid Valley Mask", 0x13FE56C0, DS3ItemCategory.SKIP), - ("Dingy Hood", 0x140D9900, DS3ItemCategory.SKIP), - ("Hexer's Hood", 0x15A995C0, DS3ItemCategory.SKIP), - ("Hexer's Robes", 0x15A999A8, DS3ItemCategory.SKIP), - ("Hexer's Gloves", 0x15A99D90, DS3ItemCategory.SKIP), - ("Hexer's Boots", 0x15A9A178, DS3ItemCategory.SKIP), - ("Varangian Helm", 0x15C81A40, DS3ItemCategory.SKIP), - ("Varangian Armor", 0x15C81E28, DS3ItemCategory.SKIP), - ("Varangian Cuffs", 0x15C82210, DS3ItemCategory.SKIP), - ("Varangian Leggings", 0x15C825F8, DS3ItemCategory.SKIP), + DS3ItemData("Dingy Maiden's Overcoat", 0x11DA9048, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Hat", 0x11F78A40, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Robe", 0x11F78E28, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Wrap", 0x11F79210, DS3ItemCategory.UNIQUE), + DS3ItemData("Grotto Trousers", 0x11F795F8, DS3ItemCategory.UNIQUE), + DS3ItemData("Soldier's Gauntlets", 0x126261D0, DS3ItemCategory.UNIQUE), + DS3ItemData("Soldier's Hood", 0x1263E0A0, DS3ItemCategory.UNIQUE), + DS3ItemData("Elder's Robe", 0x129024A8, DS3ItemCategory.UNIQUE), + DS3ItemData("Saint's Veil", 0x12A70420, DS3ItemCategory.UNIQUE), + DS3ItemData("Saint's Dress", 0x12A70808, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Hood", 0x12AEA540, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Overcoat", 0x12AEA928, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Bracelets", 0x12AEAD10, DS3ItemCategory.UNIQUE), + DS3ItemData("Footman's Trousers", 0x12AEB0F8, DS3ItemCategory.UNIQUE), + DS3ItemData("Scholar's Shed Skin", 0x12E40D20, DS3ItemCategory.UNIQUE), + DS3ItemData("Man Serpent's Mask", 0x138BE5E0, DS3ItemCategory.UNIQUE), + DS3ItemData("Man Serpent's Robe", 0x138BE9C8, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Monarch's Crown", 0x13DFD240, DS3ItemCategory.UNIQUE), + DS3ItemData("Old Monarch's Robe", 0x13DFD628, DS3ItemCategory.UNIQUE), + DS3ItemData("Frigid Valley Mask", 0x13FE56C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Dingy Hood", 0x140D9900, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Hood", 0x15A995C0, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Robes", 0x15A999A8, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Gloves", 0x15A99D90, DS3ItemCategory.UNIQUE), + DS3ItemData("Hexer's Boots", 0x15A9A178, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Helm", 0x15C81A40, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Armor", 0x15C81E28, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Cuffs", 0x15C82210, DS3ItemCategory.UNIQUE), + DS3ItemData("Varangian Leggings", 0x15C825F8, DS3ItemCategory.UNIQUE), # Rings - ("Rare Ring of Sacrifice", 0x20004EFC, DS3ItemCategory.SKIP), - ("Baneful Bird Ring", 0x20005032, DS3ItemCategory.SKIP), - ("Darkmoon Blade Covenant Ring", 0x20004F7E, DS3ItemCategory.SKIP), - ("Yorgh's Ring", 0x2000505A, DS3ItemCategory.SKIP), - ("Ring of Hiding", 0x200050D2, DS3ItemCategory.SKIP), - ("Ring of Sustained Toughness", 0x20005118, DS3ItemCategory.SKIP), - ("Ring of Sustained Energy", 0x20005122, DS3ItemCategory.SKIP), - ("Ring of Sustained Magic", 0x2000512C, DS3ItemCategory.SKIP), - ("Ring of Sustained Essence", 0x20005140, DS3ItemCategory.SKIP), - ("Ring of Sustained Might", 0x2000514A, DS3ItemCategory.SKIP), - ("Ring of Sustained Fortune", 0x20005154, DS3ItemCategory.SKIP), + DS3ItemData("Rare Ring of Sacrifice", 0x20004EFC, DS3ItemCategory.UNIQUE), + DS3ItemData("Baneful Bird Ring", 0x20005032, DS3ItemCategory.UNIQUE), + DS3ItemData("Darkmoon Blade Covenant Ring", 0x20004F7E, DS3ItemCategory.UNIQUE), + DS3ItemData("Yorgh's Ring", 0x2000505A, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Hiding", 0x200050D2, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Toughness", 0x20005118, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Energy", 0x20005122, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Magic", 0x2000512C, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Essence", 0x20005140, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Might", 0x2000514A, DS3ItemCategory.UNIQUE), + DS3ItemData("Ring of Sustained Fortune", 0x20005154, DS3ItemCategory.UNIQUE), # Items - ("Soul of a Wicked Spirit", 0x400002C9, DS3ItemCategory.SKIP), + DS3ItemData("Soul of a Wicked Spirit", 0x400002C9, DS3ItemCategory.UNIQUE), # Spells - ("Dark Orb", 0x4027AC40, DS3ItemCategory.SKIP), - ("Morbid Temptation", 0x40359AA8, DS3ItemCategory.SKIP), - ("Dorris Swarm", 0x40393870, DS3ItemCategory.SKIP), -]] + DS3ItemData("Dark Orb", 0x4027AC40, DS3ItemCategory.UNIQUE), + DS3ItemData("Morbid Temptation", 0x40359AA8, DS3ItemCategory.UNIQUE), + DS3ItemData("Dorris Swarm", 0x40393870, DS3ItemCategory.UNIQUE), +] + + +item_name_groups: Dict[str, Set] = { + "Progression": set(), + "Cinders": set(), + "Weapons": set(), + "Shields": set(), + "Armor": set(), + "Rings": set(), + "Spells": set(), + "Miscellaneous": set(), + "Unique": set(), + "Boss Souls": set(), + "Small Souls": set(), + "Upgrade": set(), + "Healing": set(), +} + item_descriptions = { + "Progression": "Items which unlock locations.", "Cinders": "All four Cinders of a Lord.\n\nOnce you have these four, you can fight Soul of Cinder and win the game.", + "Miscellaneous": "Generic stackable items, such as arrows, firebombs, buffs, and so on.", + "Unique": "Items that are unique per NG cycle, such as scrolls, keys, ashes, and so on. Doesn't include equipment, spells, or souls.", + "Boss Souls": "Souls that can be traded with Ludleth, including Soul of Rosaria.", + "Small Souls": "Soul items, not including boss souls.", + "Upgrade": "Upgrade items, including titanite, gems, and Shriving Stones.", + "Healing": "Undead Bone Shards and Estus Shards.", } + _all_items = _vanilla_items + _dlc_items +for item_data in _all_items: + for group_name in item_data.item_groups(): + item_name_groups[group_name].add(item_data.name) + +filler_item_names = [item_data.name for item_data in _all_items if item_data.filler] item_dictionary = {item_data.name: item_data for item_data in _all_items} diff --git a/worlds/dark_souls_3/Locations.py b/worlds/dark_souls_3/Locations.py index df241a5fd1..08f4b7cd1a 100644 --- a/worlds/dark_souls_3/Locations.py +++ b/worlds/dark_souls_3/Locations.py @@ -1,693 +1,3119 @@ -from enum import IntEnum -from typing import Optional, NamedTuple, Dict +from typing import cast, ClassVar, Optional, Dict, List, Set +from dataclasses import dataclass -from BaseClasses import Location, Region +from BaseClasses import ItemClassification, Location, Region +from .Items import DS3ItemCategory, item_dictionary + +# Regions in approximate order of reward, mostly measured by how high-quality the upgrade items are +# in each region. +region_order = [ + "Cemetery of Ash", + "Firelink Shrine", + "High Wall of Lothric", + "Greirat's Shop", + "Undead Settlement", + "Road of Sacrifices", + "Farron Keep", + "Cathedral of the Deep", + "Catacombs of Carthus", + "Smouldering Lake", + "Irithyll of the Boreal Valley", + "Irithyll Dungeon", + "Karla's Shop", + # The first half of Painted World has one Titanite Slab but mostly Large Titanite Shards, + # much like Irithyll Dungeon. + "Painted World of Ariandel (Before Contraption)", + "Anor Londo", + "Profaned Capital", + # The second half of Painted World has two Titanite Chunks and two Titanite Slabs, which + # puts it on the low end of the post-Lothric Castle areas in terms of rewards. + "Painted World of Ariandel (After Contraption)", + "Lothric Castle", + "Consumed King's Garden", + "Untended Graves", + # List this late because it contains a Titanite Slab in the base game + "Firelink Shrine Bell Tower", + "Grand Archives", + "Archdragon Peak", + "Kiln of the First Flame", + # Both areas of DLC2 have premium rewards. + "Dreg Heap", + "Ringed City", +] -class DS3LocationCategory(IntEnum): - WEAPON = 0 - SHIELD = 1 - ARMOR = 2 - RING = 3 - SPELL = 4 - NPC = 5 - KEY = 6 - BOSS = 7 - MISC = 8 - HEALTH = 9 - PROGRESSIVE_ITEM = 10 - EVENT = 11 +@dataclass +class DS3LocationData: + __location_id: ClassVar[int] = 100000 + """The next location ID to use when creating location data.""" - -class DS3LocationData(NamedTuple): name: str - default_item: str - category: DS3LocationCategory + """The name of this location according to Archipelago. + + This needs to be unique within this world.""" + + default_item_name: Optional[str] + """The name of the item that appears by default in this location. + + If this is None, that indicates that this location is an "event" that's + automatically considered accessed as soon as it's available. Events are used + to indicate major game transitions that aren't otherwise gated by items so + that progression balancing and item smoothing is more accurate for DS3. + """ + + ap_code: Optional[int] = None + """Archipelago's internal ID for this location (also known as its "address").""" + + region_value: int = 0 + """The relative value of items in this location's region. + + This is used to sort locations when placing items like the base game. + """ + + static: Optional[str] = None + """The key in the static randomizer's Slots table that corresponds to this location. + + By default, the static randomizer chooses its location based on the region and the item name. + If the item name is unique across the whole game, it can also look it up based on that alone. If + there are multiple instances of the same item type in the same region, it will assume its order + (in annotations.txt) matches Archipelago's order. + + In cases where this heuristic doesn't work, such as when Archipelago's region categorization or + item name disagrees with the static randomizer's, this field is used to provide an explicit + association instead. + """ + + missable: bool = False + """Whether this item is possible to permanently lose access to. + + This is also used for items that are *technically* possible to get at any time, but are + prohibitively difficult without blocking off other checks (items dropped by NPCs on death + generally fall into this category). + + Missable locations are always marked as excluded, so they will never contain + progression or useful items. + """ + + dlc: bool = False + """Whether this location is only accessible if the DLC is enabled.""" + + ngp: bool = False + """Whether this location only contains an item in NG+ and later. + + By default, these items aren't randomized or included in the randomization pool, but an option + can be set to enable them even for NG runs.""" + + npc: bool = False + """Whether this item is contingent on killing an NPC or following their quest.""" + + prominent: bool = False + """Whether this is one of few particularly prominent places for items to appear. + + This is a small number of locations (boss drops and progression locations) + intended to be set as priority locations for players who don't want a lot of + mandatory checks. + + For bosses with multiple drops, only one should be marked prominent. + """ + + progression: bool = False + """Whether this location normally contains an item that blocks forward progress.""" + + boss: bool = False + """Whether this location is a reward for defeating a full boss.""" + + miniboss: bool = False + """Whether this location is a reward for defeating a miniboss. + + The classification of "miniboss" is a bit fuzzy, but we consider them to be enemies that are + visually distinctive in their locations, usually bigger than normal enemies, with a guaranteed + item drop. NPCs are never considered minibosses, and some normal-looking enemies with guaranteed + drops aren't either (these are instead classified as hidden locations).""" + + drop: bool = False + """Whether this is an item dropped by a (non-boss) enemy. + + This is automatically set to True if miniboss, mimic, lizard, or hostile_npc is True. + """ + + mimic: bool = False + """Whether this location is dropped by a mimic.""" + + hostile_npc: bool = False + """Whether this location is dropped by a hostile NPC. + + An "NPC" is specifically a human (or rather, ash) is built like a player character rather than a + monster. This includes both scripted invaders and NPCs who are always on the overworld. It does + not include initially-friendly NPCs who become hostile as part of a quest or because you attack + them. + """ + + lizard: bool = False + """Whether this location is dropped by a (small) Crystal Lizard.""" + + shop: bool = False + """Whether this location can appear in an NPC's shop. + + Items like Lapp's Set which can appear both in the overworld and in a shop + should still be tagged as shop. + """ + + conditional: bool = False + """Whether this location is conditional on a progression item. + + This is used to track locations that won't become available until an unknown amount of time into + the run, and as such shouldn't have "similar to the base game" items placed in them. + """ + + hidden: bool = False + """Whether this location is particularly tricky to find. + + This is for players without an encyclopedic knowledge of DS3 who don't want to get stuck looking + for an illusory wall or one random mob with a guaranteed drop. + """ + + @property + def is_event(self) -> bool: + """Whether this location represents an event rather than a specific item pickup.""" + return self.default_item_name is None + + def __post_init__(self): + if not self.is_event: + self.ap_code = self.ap_code or DS3LocationData.__location_id + DS3LocationData.__location_id += 1 + if self.miniboss or self.mimic or self.lizard or self.hostile_npc: self.drop = True + + def location_groups(self) -> List[str]: + """The names of location groups this location should appear in. + + This is computed from the properties assigned to this location.""" + names = [] + if self.prominent: names.append("Prominent") + if self.progression: names.append("Progression") + if self.boss: names.append("Boss Rewards") + if self.miniboss: names.append("Miniboss Rewards") + if self.mimic: names.append("Mimic Rewards") + if self.hostile_npc: names.append("Hostile NPC Rewards") + if self.npc: names.append("Friendly NPC Rewards") + if self.lizard: names.append("Small Crystal Lizards") + if self.hidden: names.append("Hidden") + + default_item = item_dictionary[cast(str, self.default_item_name)] + names.append({ + DS3ItemCategory.WEAPON_UPGRADE_5: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10: "Weapons", + DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE: "Weapons", + DS3ItemCategory.SHIELD: "Shields", + DS3ItemCategory.SHIELD_INFUSIBLE: "Shields", + DS3ItemCategory.ARMOR: "Armor", + DS3ItemCategory.RING: "Rings", + DS3ItemCategory.SPELL: "Spells", + DS3ItemCategory.MISC: "Miscellaneous", + DS3ItemCategory.UNIQUE: "Unique", + DS3ItemCategory.BOSS: "Boss Souls", + DS3ItemCategory.SOUL: "Small Souls", + DS3ItemCategory.UPGRADE: "Upgrade", + DS3ItemCategory.HEALING: "Healing", + }[default_item.category]) + if default_item.classification == ItemClassification.progression: + names.append("Progression") + + return names class DarkSouls3Location(Location): game: str = "Dark Souls III" - category: DS3LocationCategory - default_item_name: str + data: DS3LocationData def __init__( self, player: int, - name: str, - category: DS3LocationCategory, - default_item_name: str, - address: Optional[int] = None, - parent: Optional[Region] = None): - super().__init__(player, name, address, parent) - self.default_item_name = default_item_name - self.category = category - - @staticmethod - def get_name_to_id() -> dict: - base_id = 100000 - table_offset = 100 - - table_order = [ - "Firelink Shrine", - "Firelink Shrine Bell Tower", - "High Wall of Lothric", - "Undead Settlement", - "Road of Sacrifices", - "Cathedral of the Deep", - "Farron Keep", - "Catacombs of Carthus", - "Smouldering Lake", - "Irithyll of the Boreal Valley", - "Irithyll Dungeon", - "Profaned Capital", - "Anor Londo", - "Lothric Castle", - "Consumed King's Garden", - "Grand Archives", - "Untended Graves", - "Archdragon Peak", - - "Painted World of Ariandel 1", - "Painted World of Ariandel 2", - "Dreg Heap", - "Ringed City", - - "Progressive Items 1", - "Progressive Items 2", - "Progressive Items 3", - "Progressive Items 4", - "Progressive Items DLC", - "Progressive Items Health", - ] - - output = {} - for i, region_name in enumerate(table_order): - if len(location_tables[region_name]) > table_offset: - raise Exception("A location table has {} entries, that is more than {} entries (table #{})".format(len(location_tables[region_name]), table_offset, i)) - - output.update({location_data.name: id for id, location_data in enumerate(location_tables[region_name], base_id + (table_offset * i))}) - - return output + data: DS3LocationData, + parent: Optional[Region] = None, + event: bool = False): + super().__init__(player, data.name, None if event else data.ap_code, parent) + self.data = data -location_tables = { +# Naming conventions: +# +# * The regions in item names should match the physical region where the item is +# acquired, even if its logical region is different. For example, Irina's +# inventory appears in the "Undead Settlement" region because she's not +# accessible until there, but it begins with "FS:" because that's where her +# items are purchased. +# +# * Avoid using vanilla enemy placements as landmarks, because these are +# randomized by the enemizer by default. Instead, use generic terms like +# "mob", "boss", and "miniboss". +# +# * Location descriptions don't need to direct the player to the precise spot. +# You can assume the player is broadly familiar with Dark Souls III or willing +# to look at a vanilla guide. Just give a general area to look in or an idea +# of what quest a check is connected to. Terseness is valuable: try to keep +# each location description short enough that the whole line doesn't exceed +# 100 characters. +# +# * Use "[name] drop" for items that require killing an NPC who becomes hostile +# as part of their normal quest, "kill [name]" for items that require killing +# them even when they aren't hostile, and just "[name]" for items that are +# naturally available as part of their quest. +location_tables: Dict[str, List[DS3LocationData]] = { + "Cemetery of Ash": [ + DS3LocationData("CA: Soul of a Deserted Corpse - right of spawn", + "Soul of a Deserted Corpse"), + DS3LocationData("CA: Firebomb - down the cliff edge", "Firebomb x5"), + DS3LocationData("CA: Titanite Shard - jump to coffin", "Titanite Shard"), + DS3LocationData("CA: Soul of an Unknown Traveler - by miniboss", + "Soul of an Unknown Traveler"), + DS3LocationData("CA: Speckled Stoneplate Ring+1 - by miniboss", + "Speckled Stoneplate Ring+1", ngp=True), + DS3LocationData("CA: Titanite Scale - miniboss drop", "Titanite Scale", miniboss=True), + DS3LocationData("CA: Coiled Sword - boss drop", "Coiled Sword", prominent=True, + progression=True, boss=True), + ], "Firelink Shrine": [ - DS3LocationData("FS: Broken Straight Sword", "Broken Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("FS: East-West Shield", "East-West Shield", DS3LocationCategory.SHIELD), - DS3LocationData("FS: Uchigatana", "Uchigatana", DS3LocationCategory.WEAPON), - DS3LocationData("FS: Master's Attire", "Master's Attire", DS3LocationCategory.ARMOR), - DS3LocationData("FS: Master's Gloves", "Master's Gloves", DS3LocationCategory.ARMOR), + # Ludleth drop, does not permanently die + DS3LocationData("FS: Skull Ring - kill Ludleth", "Skull Ring", hidden=True, drop=True, + npc=True), + + # Sword Master drops + DS3LocationData("FS: Uchigatana - NPC drop", "Uchigatana", hostile_npc=True), + DS3LocationData("FS: Master's Attire - NPC drop", "Master's Attire", hostile_npc=True), + DS3LocationData("FS: Master's Gloves - NPC drop", "Master's Gloves", hostile_npc=True), + + DS3LocationData("FS: Broken Straight Sword - gravestone after boss", + "Broken Straight Sword"), + DS3LocationData("FS: Homeward Bone - cliff edge after boss", "Homeward Bone"), + DS3LocationData("FS: Ember - path right of Firelink entrance", "Ember"), + DS3LocationData("FS: Soul of a Deserted Corpse - bell tower door", + "Soul of a Deserted Corpse"), + DS3LocationData("FS: East-West Shield - tree by shrine entrance", "East-West Shield"), + DS3LocationData("FS: Homeward Bone - path above shrine entrance", "Homeward Bone"), + DS3LocationData("FS: Ember - above shrine entrance", "Ember"), + DS3LocationData("FS: Wolf Ring+2 - left of boss room exit", "Wolf Ring+2", ngp=True), + # Leonhard (quest) + DS3LocationData("FS: Cracked Red Eye Orb - Leonhard", "Cracked Red Eye Orb x5", + missable=True, npc=True), + # Leonhard (kill or quest), missable because he can disappear sometimes + DS3LocationData("FS: Lift Chamber Key - Leonhard", "Lift Chamber Key", missable=True, + npc=True, drop=True), + + # Shrine Handmaid shop + DS3LocationData("FS: White Sign Soapstone - shop", "White Sign Soapstone", shop=True), + DS3LocationData("FS: Dried Finger - shop", "Dried Finger", shop=True), + DS3LocationData("FS: Tower Key - shop", "Tower Key", progression=True, shop=True), + DS3LocationData("FS: Ember - shop", "Ember", static='99,0:-1:110000:', shop=True), + DS3LocationData("FS: Farron Dart - shop", "Farron Dart", static='99,0:-1:110000:', + shop=True), + DS3LocationData("FS: Soul Arrow - shop", "Soul Arrow", static='99,0:-1:110000:', + shop=True), + DS3LocationData("FS: Heal Aid - shop", "Heal Aid", shop=True), + DS3LocationData("FS: Alluring Skull - Mortician's Ashes", "Alluring Skull", shop=True, + conditional=True), + DS3LocationData("FS: Ember - Mortician's Ashes", "Ember", + static='99,0:-1:110000,70000100:', shop=True, conditional=True), + DS3LocationData("FS: Grave Key - Mortician's Ashes", "Grave Key", shop=True, + conditional=True), + DS3LocationData("FS: Life Ring - Dreamchaser's Ashes", "Life Ring", shop=True, + conditional=True), + # Only if you say where the ashes were found + DS3LocationData("FS: Hidden Blessing - Dreamchaser's Ashes", "Hidden Blessing", + missable=True, shop=True), + DS3LocationData("FS: Lloyd's Shield Ring - Paladin's Ashes", "Lloyd's Shield Ring", + shop=True, conditional=True), + DS3LocationData("FS: Ember - Grave Warden's Ashes", "Ember", + static='99,0:-1:110000,70000103:', shop=True, conditional=True), + # Prisoner Chief's Ashes + DS3LocationData("FS: Karla's Pointed Hat - Prisoner Chief's Ashes", "Karla's Pointed Hat", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Coat - Prisoner Chief's Ashes", "Karla's Coat", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Gloves - Prisoner Chief's Ashes", "Karla's Gloves", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Karla's Trousers - Prisoner Chief's Ashes", "Karla's Trousers", + static='99,0:-1:110000,70000105:', shop=True, conditional=True), + DS3LocationData("FS: Xanthous Overcoat - Xanthous Ashes", "Xanthous Overcoat", shop=True, + conditional=True), + DS3LocationData("FS: Xanthous Gloves - Xanthous Ashes", "Xanthous Gloves", shop=True, + conditional=True), + DS3LocationData("FS: Xanthous Trousers - Xanthous Ashes", "Xanthous Trousers", shop=True, + conditional=True), + DS3LocationData("FS: Ember - Dragon Chaser's Ashes", "Ember", + static='99,0:-1:110000,70000108:', shop=True, conditional=True), + DS3LocationData("FS: Washing Pole - Easterner's Ashes", "Washing Pole", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Helm - Easterner's Ashes", "Eastern Helm", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Armor - Easterner's Ashes", "Eastern Armor", shop=True, + conditional=True), + DS3LocationData("FS: Eastern Gauntlets - Easterner's Ashes", "Eastern Gauntlets", + shop=True, conditional=True), + DS3LocationData("FS: Eastern Leggings - Easterner's Ashes", "Eastern Leggings", shop=True, + conditional=True), + DS3LocationData("FS: Wood Grain Ring - Easterner's Ashes", "Wood Grain Ring", shop=True, + conditional=True), + DS3LocationData("FS: Millwood Knight Helm - Captain's Ashes", "Millwood Knight Helm", + dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Armor - Captain's Ashes", "Millwood Knight Armor", + dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Gauntlets - Captain's Ashes", + "Millwood Knight Gauntlets", dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Millwood Knight Leggings - Captain's Ashes", + "Millwood Knight Leggings", dlc=True, shop=True, conditional=True), + DS3LocationData("FS: Refined Gem - Captain's Ashes", "Refined Gem", dlc=True, shop=True, + conditional=True), + + # Ludleth Shop + DS3LocationData("FS: Vordt's Great Hammer - Ludleth for Vordt", "Vordt's Great Hammer", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Pontiff's Left Eye - Ludleth for Vordt", "Pontiff's Left Eye", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Bountiful Sunlight - Ludleth for Rosaria", "Bountiful Sunlight", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Darkmoon Longbow - Ludleth for Aldrich", "Darkmoon Longbow", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lifehunt Scythe - Ludleth for Aldrich", "Lifehunt Scythe", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Hollowslayer Greatsword - Ludleth for Greatwood", + "Hollowslayer Greatsword", missable=True, boss=True, shop=True), + DS3LocationData("FS: Arstor's Spear - Ludleth for Greatwood", "Arstor's Spear", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Crystal Sage's Rapier - Ludleth for Sage", "Crystal Sage's Rapier", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Crystal Hail - Ludleth for Sage", "Crystal Hail", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Cleric's Candlestick - Ludleth for Deacons", "Cleric's Candlestick", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Deep Soul - Ludleth for Deacons", "Deep Soul", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Havel's Ring - Ludleth for Stray Demon", "Havel's Ring", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Boulder Heave - Ludleth for Stray Demon", "Boulder Heave", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Farron Greatsword - Ludleth for Abyss Watchers", "Farron Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Wolf Knight's Greatsword - Ludleth for Abyss Watchers", + "Wolf Knight's Greatsword", missable=True, boss=True, shop=True), + DS3LocationData("FS: Wolnir's Holy Sword - Ludleth for Wolnir", "Wolnir's Holy Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Black Serpent - Ludleth for Wolnir", "Black Serpent", missable=True, + boss=True, shop=True), + DS3LocationData("FS: Demon's Greataxe - Ludleth for Fire Demon", "Demon's Greataxe", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Demon's Fist - Ludleth for Fire Demon", "Demon's Fist", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Old King's Great Hammer - Ludleth for Old Demon King", + "Old King's Great Hammer", missable=True, boss=True, shop=True), + DS3LocationData("FS: Chaos Bed Vestiges - Ludleth for Old Demon King", "Chaos Bed Vestiges", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Greatsword of Judgment - Ludleth for Pontiff", + "Greatsword of Judgment", missable=True, boss=True, shop=True), + DS3LocationData("FS: Profaned Greatsword - Ludleth for Pontiff", "Profaned Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Yhorm's Great Machete - Ludleth for Yhorm", "Yhorm's Great Machete", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Yhorm's Greatshield - Ludleth for Yhorm", "Yhorm's Greatshield", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dancer's Enchanted Swords - Ludleth for Dancer", + "Dancer's Enchanted Swords", missable=True, boss=True, shop=True), + DS3LocationData("FS: Soothing Sunlight - Ludleth for Dancer", "Soothing Sunlight", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Greataxe - Ludleth for Dragonslayer", + "Dragonslayer Greataxe", missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Greatshield - Ludleth for Dragonslayer", + "Dragonslayer Greatshield", missable=True, boss=True, shop=True), + DS3LocationData("FS: Moonlight Greatsword - Ludleth for Oceiros", "Moonlight Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: White Dragon Breath - Ludleth for Oceiros", "White Dragon Breath", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lorian's Greatsword - Ludleth for Princes", "Lorian's Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Lothric's Holy Sword - Ludleth for Princes", "Lothric's Holy Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Gundyr's Halberd - Ludleth for Champion", "Gundyr's Halberd", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Prisoner's Chain - Ludleth for Champion", "Prisoner's Chain", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Storm Curved Sword - Ludleth for Nameless", "Storm Curved Sword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Dragonslayer Swordspear - Ludleth for Nameless", + "Dragonslayer Swordspear", missable=True, boss=True, shop=True), + DS3LocationData("FS: Lightning Storm - Ludleth for Nameless", "Lightning Storm", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Firelink Greatsword - Ludleth for Cinder", "Firelink Greatsword", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Sunlight Spear - Ludleth for Cinder", "Sunlight Spear", + missable=True, boss=True, shop=True), + DS3LocationData("FS: Friede's Great Scythe - Ludleth for Friede", "Friede's Great Scythe", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Rose of Ariandel - Ludleth for Friede", "Rose of Ariandel", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Demon's Scar - Ludleth for Demon Prince", "Demon's Scar", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Seething Chaos - Ludleth for Demon Prince", "Seething Chaos", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Frayed Blade - Ludleth for Midir", "Frayed Blade", missable=True, + dlc=True, boss=True, shop=True), + DS3LocationData("FS: Old Moonlight - Ludleth for Midir", "Old Moonlight", missable=True, + dlc=True, boss=True, shop=True), + DS3LocationData("FS: Gael's Greatsword - Ludleth for Gael", "Gael's Greatsword", + missable=True, dlc=True, boss=True, shop=True), + DS3LocationData("FS: Repeating Crossbow - Ludleth for Gael", "Repeating Crossbow", + missable=True, dlc=True, boss=True, shop=True), ], "Firelink Shrine Bell Tower": [ - DS3LocationData("FSBT: Covetous Silver Serpent Ring", "Covetous Silver Serpent Ring", DS3LocationCategory.RING), - DS3LocationData("FSBT: Fire Keeper Robe", "Fire Keeper Robe", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Fire Keeper Gloves", "Fire Keeper Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Fire Keeper Skirt", "Fire Keeper Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("FSBT: Estus Ring", "Estus Ring", DS3LocationCategory.RING), - DS3LocationData("FSBT: Fire Keeper Soul", "Fire Keeper Soul", DS3LocationCategory.MISC), + # Guarded by Tower Key + DS3LocationData("FSBT: Homeward Bone - roof", "Homeward Bone x3"), + DS3LocationData("FSBT: Estus Ring - tower base", "Estus Ring"), + DS3LocationData("FSBT: Estus Shard - rafters", "Estus Shard"), + DS3LocationData("FSBT: Fire Keeper Soul - tower top", "Fire Keeper Soul"), + DS3LocationData("FSBT: Fire Keeper Robe - partway down tower", "Fire Keeper Robe"), + DS3LocationData("FSBT: Fire Keeper Gloves - partway down tower", "Fire Keeper Gloves"), + DS3LocationData("FSBT: Fire Keeper Skirt - partway down tower", "Fire Keeper Skirt"), + DS3LocationData("FSBT: Covetous Silver Serpent Ring - illusory wall past rafters", + "Covetous Silver Serpent Ring", hidden=True), + DS3LocationData("FSBT: Twinkling Titanite - lizard behind Firelink", + "Twinkling Titanite", lizard=True), + + # Mark all crow trades as missable since no one wants to have to try trading everything just + # in case it gives a progression item. + DS3LocationData("FSBT: Iron Bracelets - crow for Homeward Bone", "Iron Bracelets", + missable=True), + DS3LocationData("FSBT: Ring of Sacrifice - crow for Loretta's Bone", "Ring of Sacrifice", + missable=True), + DS3LocationData("FSBT: Porcine Shield - crow for Undead Bone Shard", "Porcine Shield", + missable=True), + DS3LocationData("FSBT: Lucatiel's Mask - crow for Vertebra Shackle", "Lucatiel's Mask", + missable=True), + DS3LocationData("FSBT: Very good! Carving - crow for Divine Blessing", + "Very good! Carving", missable=True), + DS3LocationData("FSBT: Thank you Carving - crow for Hidden Blessing", "Thank you Carving", + missable=True), + DS3LocationData("FSBT: I'm sorry Carving - crow for Shriving Stone", "I'm sorry Carving", + missable=True), + DS3LocationData("FSBT: Sunlight Shield - crow for Mendicant's Staff", "Sunlight Shield", + missable=True), + DS3LocationData("FSBT: Hollow Gem - crow for Eleonora", "Hollow Gem", + missable=True), + DS3LocationData("FSBT: Titanite Scale - crow for Blacksmith Hammer", "Titanite Scale x3", + static='99,0:50004330::', missable=True), + DS3LocationData("FSBT: Help me! Carving - crow for any sacred chime", "Help me! Carving", + missable=True), + DS3LocationData("FSBT: Titanite Slab - crow for Coiled Sword Fragment", "Titanite Slab", + missable=True), + DS3LocationData("FSBT: Hello Carving - crow for Alluring Skull", "Hello Carving", + missable=True), + DS3LocationData("FSBT: Armor of the Sun - crow for Siegbräu", "Armor of the Sun", + missable=True), + DS3LocationData("FSBT: Large Titanite Shard - crow for Firebomb", "Large Titanite Shard", + missable=True), + DS3LocationData("FSBT: Titanite Chunk - crow for Black Firebomb", "Titanite Chunk", + missable=True), + DS3LocationData("FSBT: Iron Helm - crow for Lightning Urn", "Iron Helm", missable=True), + DS3LocationData("FSBT: Twinkling Titanite - crow for Prism Stone", "Twinkling Titanite", + missable=True), + DS3LocationData("FSBT: Iron Leggings - crow for Seed of a Giant Tree", "Iron Leggings", + missable=True), + DS3LocationData("FSBT: Lightning Gem - crow for Xanthous Crown", "Lightning Gem", + missable=True), + DS3LocationData("FSBT: Twinkling Titanite - crow for Large Leather Shield", + "Twinkling Titanite", missable=True), + DS3LocationData("FSBT: Blessed Gem - crow for Moaning Shield", "Blessed Gem", + missable=True), ], "High Wall of Lothric": [ - DS3LocationData("HWL: Deep Battle Axe", "Deep Battle Axe", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Club", "Club", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Claymore", "Claymore", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Binoculars", "Binoculars", DS3LocationCategory.MISC), - DS3LocationData("HWL: Longbow", "Longbow", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Mail Breaker", "Mail Breaker", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Broadsword", "Broadsword", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Silver Eagle Kite Shield", "Silver Eagle Kite Shield", DS3LocationCategory.SHIELD), - DS3LocationData("HWL: Astora's Straight Sword", "Astora's Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Cell Key", "Cell Key", DS3LocationCategory.KEY), - DS3LocationData("HWL: Rapier", "Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Lucerne", "Lucerne", DS3LocationCategory.WEAPON), - DS3LocationData("HWL: Small Lothric Banner", "Small Lothric Banner", DS3LocationCategory.KEY), - DS3LocationData("HWL: Basin of Vows", "Basin of Vows", DS3LocationCategory.KEY), - DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", DS3LocationCategory.BOSS), - DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", DS3LocationCategory.BOSS), - DS3LocationData("HWL: Way of Blue", "Way of Blue", DS3LocationCategory.MISC), - DS3LocationData("HWL: Greirat's Ashes", "Greirat's Ashes", DS3LocationCategory.NPC), - DS3LocationData("HWL: Blue Tearstone Ring", "Blue Tearstone Ring", DS3LocationCategory.NPC), + DS3LocationData("HWL: Soul of Boreal Valley Vordt", "Soul of Boreal Valley Vordt", + prominent=True, boss=True), + DS3LocationData("HWL: Soul of the Dancer", "Soul of the Dancer", prominent=True, + boss=True), + DS3LocationData("HWL: Basin of Vows - Emma", "Basin of Vows", prominent=True, + progression=True, conditional=True), + DS3LocationData("HWL: Small Lothric Banner - Emma", "Small Lothric Banner", + prominent=True, progression=True), + DS3LocationData("HWL: Green Blossom - fort walkway, hall behind wheel", "Green Blossom x2", + hidden=True), + DS3LocationData("HWL: Gold Pine Resin - corpse tower, drop", "Gold Pine Resin x2", + hidden=True), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - flame plaza", + "Large Soul of a Deserted Corpse"), + DS3LocationData("HWL: Soul of a Deserted Corpse - by wall tower door", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Standard Arrow - back tower", "Standard Arrow x12"), + DS3LocationData("HWL: Longbow - back tower", "Longbow"), + DS3LocationData("HWL: Firebomb - wall tower, beam", "Firebomb x3"), + DS3LocationData("HWL: Throwing Knife - wall tower, path to Greirat", "Throwing Knife x8"), + DS3LocationData("HWL: Soul of a Deserted Corpse - corpse tower, bottom floor", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Club - flame plaza", "Club"), + DS3LocationData("HWL: Claymore - flame plaza", "Claymore"), + DS3LocationData("HWL: Ember - flame plaza", "Ember"), + DS3LocationData("HWL: Firebomb - corpse tower, under table", "Firebomb x2"), + DS3LocationData("HWL: Titanite Shard - wall tower, corner by bonfire", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Undead Hunter Charm - fort, room off entry, in pot", + "Undead Hunter Charm x2", hidden=True), + DS3LocationData("HWL: Firebomb - top of ladder to fountain", "Firebomb x3"), + DS3LocationData("HWL: Cell Key - fort ground, down stairs", "Cell Key"), + DS3LocationData("HWL: Ember - fountain #1", "Ember"), + DS3LocationData("HWL: Soul of a Deserted Corpse - fort entry, corner", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Lucerne - promenade, side path", "Lucerne"), + DS3LocationData("HWL: Mail Breaker - wall tower, path to Greirat", "Mail Breaker"), + DS3LocationData("HWL: Titanite Shard - fort ground behind crates", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Rapier - fountain, corner", "Rapier"), + DS3LocationData("HWL: Titanite Shard - fort, room off entry", "Titanite Shard"), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - fort roof", + "Large Soul of a Deserted Corpse"), + DS3LocationData("HWL: Black Firebomb - small roof over fountain", "Black Firebomb x3"), + DS3LocationData("HWL: Soul of a Deserted Corpse - path to corpse tower", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Ember - fountain #2", "Ember"), + DS3LocationData("HWL: Large Soul of a Deserted Corpse - platform by fountain", + "Large Soul of a Deserted Corpse", hidden=True), # Easily missed turnoff + DS3LocationData("HWL: Binoculars - corpse tower, upper platform", "Binoculars"), + DS3LocationData("HWL: Ring of Sacrifice - awning by fountain", + "Ring of Sacrifice", hidden=True), # Easily missed turnoff + DS3LocationData("HWL: Throwing Knife - shortcut, lift top", "Throwing Knife x6"), + DS3LocationData("HWL: Soul of a Deserted Corpse - path to back tower, by lift door", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Green Blossom - shortcut, lower courtyard", "Green Blossom x3"), + DS3LocationData("HWL: Broadsword - fort, room off walkway", "Broadsword"), + DS3LocationData("HWL: Soul of a Deserted Corpse - fountain, path to promenade", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Firebomb - fort roof", "Firebomb x3"), + DS3LocationData("HWL: Soul of a Deserted Corpse - wall tower, right of exit", + "Soul of a Deserted Corpse"), + DS3LocationData("HWL: Estus Shard - fort ground, on anvil", "Estus Shard"), + DS3LocationData("HWL: Fleshbite Ring+1 - fort roof, jump to other roof", + "Fleshbite Ring+1", ngp=True, hidden=True), # Hidden jump + DS3LocationData("HWL: Ring of the Evil Eye+2 - fort ground, far wall", + "Ring of the Evil Eye+2", ngp=True, hidden=True), # In barrels + DS3LocationData("HWL: Silver Eagle Kite Shield - fort mezzanine", + "Silver Eagle Kite Shield"), + DS3LocationData("HWL: Astora Straight Sword - fort walkway, drop down", + "Astora Straight Sword", hidden=True), # Hidden fall + DS3LocationData("HWL: Battle Axe - flame tower, mimic", "Battle Axe", + static='01,0:53000960::', mimic=True), + + # Only dropped after transformation + DS3LocationData("HWL: Ember - fort roof, transforming hollow", "Ember", hidden=True), + DS3LocationData("HWL: Titanite Shard - fort roof, transforming hollow", "Titanite Shard", + hidden=True), + DS3LocationData("HWL: Ember - back tower, transforming hollow", "Ember", hidden=True), + DS3LocationData("HWL: Titanite Shard - back tower, transforming hollow", "Titanite Shard", + hidden=True), + + DS3LocationData("HWL: Refined Gem - promenade miniboss", "Refined Gem", miniboss=True), + DS3LocationData("HWL: Way of Blue - Emma", "Way of Blue"), + # Categorize this as an NPC item so that it doesn't get randomized if the Lift Chamber Key + # isn't randomized, since in that case it's missable. + DS3LocationData("HWL: Red Eye Orb - wall tower, miniboss", "Red Eye Orb", + conditional=True, miniboss=True, npc=True), + DS3LocationData("HWL: Raw Gem - fort roof, lizard", "Raw Gem", lizard=True), ], "Undead Settlement": [ - DS3LocationData("US: Small Leather Shield", "Small Leather Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Whip", "Whip", DS3LocationCategory.WEAPON), - DS3LocationData("US: Reinforced Club", "Reinforced Club", DS3LocationCategory.WEAPON), - DS3LocationData("US: Blue Wooden Shield", "Blue Wooden Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Cleric Hat", "Cleric Hat", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Blue Robe", "Cleric Blue Robe", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Gloves", "Cleric Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Cleric Trousers", "Cleric Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mortician's Ashes", "Mortician's Ashes", DS3LocationCategory.KEY), - DS3LocationData("US: Caestus", "Caestus", DS3LocationCategory.WEAPON), - DS3LocationData("US: Plank Shield", "Plank Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Flame Stoneplate Ring", "Flame Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("US: Caduceus Round Shield", "Caduceus Round Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Fire Clutch Ring", "Fire Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("US: Partizan", "Partizan", DS3LocationCategory.WEAPON), - DS3LocationData("US: Bloodbite Ring", "Bloodbite Ring", DS3LocationCategory.RING), - DS3LocationData("US: Red Hilted Halberd", "Red Hilted Halberd", DS3LocationCategory.WEAPON), - DS3LocationData("US: Saint's Talisman", "Saint's Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("US: Irithyll Straight Sword", "Irithyll Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("US: Large Club", "Large Club", DS3LocationCategory.WEAPON), - DS3LocationData("US: Northern Helm", "Northern Helm", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Armor", "Northern Armor", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Gloves", "Northern Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Northern Trousers", "Northern Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Flynn's Ring", "Flynn's Ring", DS3LocationCategory.RING), - DS3LocationData("US: Mirrah Vest", "Mirrah Vest", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mirrah Gloves", "Mirrah Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("US: Mirrah Trousers", "Mirrah Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("US: Chloranthy Ring", "Chloranthy Ring", DS3LocationCategory.RING), - DS3LocationData("US: Loincloth", "Loincloth", DS3LocationCategory.ARMOR), - DS3LocationData("US: Wargod Wooden Shield", "Wargod Wooden Shield", DS3LocationCategory.SHIELD), - DS3LocationData("US: Loretta's Bone", "Loretta's Bone", DS3LocationCategory.KEY), - DS3LocationData("US: Hand Axe", "Hand Axe", DS3LocationCategory.WEAPON), - DS3LocationData("US: Great Scythe", "Great Scythe", DS3LocationCategory.WEAPON), - DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", DS3LocationCategory.BOSS), - DS3LocationData("US: Hawk Ring", "Hawk Ring", DS3LocationCategory.RING), - DS3LocationData("US: Warrior of Sunlight", "Warrior of Sunlight", DS3LocationCategory.MISC), - DS3LocationData("US: Blessed Red and White Shield+1", "Blessed Red and White Shield+1", DS3LocationCategory.SHIELD), - DS3LocationData("US: Irina's Ashes", "Irina's Ashes", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Ashes", "Cornyx's Ashes", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Wrap", "Cornyx's Wrap", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Garb", "Cornyx's Garb", DS3LocationCategory.NPC), - DS3LocationData("US: Cornyx's Skirt", "Cornyx's Skirt", DS3LocationCategory.NPC), - DS3LocationData("US: Pyromancy Flame", "Pyromancy Flame", DS3LocationCategory.NPC), - DS3LocationData("US: Transposing Kiln", "Transposing Kiln", DS3LocationCategory.MISC), - DS3LocationData("US: Tower Key", "Tower Key", DS3LocationCategory.NPC), + DS3LocationData("US: Soul of the Rotted Greatwood", "Soul of the Rotted Greatwood", + prominent=True, boss=True), + DS3LocationData("US: Transposing Kiln - boss drop", "Transposing Kiln", boss=True), + # Missable because it's unavailable if you start as a Pyromancer + DS3LocationData("US: Pyromancy Flame - Cornyx", "Pyromancy Flame", missable=True, + npc=True), + DS3LocationData("US: Old Sage's Blindfold - kill Cornyx", "Old Sage's Blindfold", + npc=True), + DS3LocationData("US: Cornyx's Garb - kill Cornyx", "Cornyx's Garb", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Cornyx's Wrap - kill Cornyx", "Cornyx's Wrap", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Cornyx's Skirt - kill Cornyx", "Cornyx's Skirt", + static='02,0:50006141::', npc=True), + DS3LocationData("US: Tower Key - kill Irina", "Tower Key", missable=True, npc=True), + DS3LocationData("US: Flynn's Ring - tower village, rooftop", "Flynn's Ring"), + DS3LocationData("US: Undead Bone Shard - by white tree", "Undead Bone Shard"), + DS3LocationData("US: Alluring Skull - foot, behind carriage", "Alluring Skull x2"), + DS3LocationData("US: Mortician's Ashes - graveyard by white tree", "Mortician's Ashes", + progression=True), + DS3LocationData("US: Homeward Bone - tower village, jump from roof", "Homeward Bone x2", + static='02,0:53100040::', hidden=True), # Hidden fall + DS3LocationData("US: Caduceus Round Shield - right after stable exit", + "Caduceus Round Shield"), + DS3LocationData("US: Ember - tower basement, miniboss", "Ember"), + DS3LocationData("US: Soul of an Unknown Traveler - chasm crypt", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Repair Powder - first building, balcony", "Repair Powder x2"), + DS3LocationData("US: Homeward Bone - stable roof", "Homeward Bone x2", + static='02,0:53100090::'), + DS3LocationData("US: Titanite Shard - back alley, side path", "Titanite Shard"), + DS3LocationData("US: Wargod Wooden Shield - Pit of Hollows", "Wargod Wooden Shield"), + DS3LocationData("US: Large Soul of a Deserted Corpse - on the way to tower, by well", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Ember - bridge on the way to tower", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - stable", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Titanite Shard - porch after burning tree", "Titanite Shard"), + DS3LocationData("US: Alluring Skull - tower village building, upstairs", + "Alluring Skull x2"), + DS3LocationData("US: Charcoal Pine Bundle - first building, middle floor", + "Charcoal Pine Bundle x2"), + DS3LocationData("US: Blue Wooden Shield - graveyard by white tree", "Blue Wooden Shield"), + DS3LocationData("US: Cleric Hat - graveyard by white tree", "Cleric Hat"), + DS3LocationData("US: Cleric Blue Robe - graveyard by white tree", "Cleric Blue Robe"), + DS3LocationData("US: Cleric Gloves - graveyard by white tree", "Cleric Gloves"), + DS3LocationData("US: Cleric Trousers - graveyard by white tree", "Cleric Trousers"), + DS3LocationData("US: Soul of an Unknown Traveler - portcullis by burning tree", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Charcoal Pine Resin - hanging corpse room", "Charcoal Pine Resin x2"), + DS3LocationData("US: Loincloth - by Velka statue", "Loincloth"), + DS3LocationData("US: Bloodbite Ring - miniboss in sewer", "Bloodbite Ring", + miniboss=True), # Giant Rat drop + DS3LocationData("US: Charcoal Pine Bundle - first building, bottom floor", + "Charcoal Pine Bundle x2"), + DS3LocationData("US: Soul of an Unknown Traveler - back alley, past crates", + "Soul of an Unknown Traveler", hidden=True), + DS3LocationData("US: Titanite Shard - back alley, up ladder", "Titanite Shard"), + DS3LocationData("US: Red Hilted Halberd - chasm crypt", "Red Hilted Halberd"), + DS3LocationData("US: Rusted Coin - awning above Dilapidated Bridge", "Rusted Coin x2"), + DS3LocationData("US: Caestus - sewer", "Caestus"), + DS3LocationData("US: Saint's Talisman - chasm, by ladder", "Saint's Talisman"), + DS3LocationData("US: Alluring Skull - on the way to tower, behind building", + "Alluring Skull x3"), + DS3LocationData("US: Large Club - tower village, by miniboss", "Large Club"), + DS3LocationData("US: Titanite Shard - chasm #1", "Titanite Shard"), + DS3LocationData("US: Titanite Shard - chasm #2", "Titanite Shard"), + DS3LocationData("US: Fading Soul - outside stable", "Fading Soul"), + DS3LocationData("US: Titanite Shard - lower path to Cliff Underside", "Titanite Shard", + hidden=True), # hidden fall + DS3LocationData("US: Hand Axe - by Cornyx", "Hand Axe"), + DS3LocationData("US: Soul of an Unknown Traveler - pillory past stable", + "Soul of an Unknown Traveler"), + DS3LocationData("US: Ember - by stairs to boss", "Ember"), + DS3LocationData("US: Mirrah Vest - tower village, jump from roof", "Mirrah Vest", + hidden=True), # Hidden fall + DS3LocationData("US: Mirrah Gloves - tower village, jump from roof", "Mirrah Gloves", + hidden=True), # Hidden fall + DS3LocationData("US: Mirrah Trousers - tower village, jump from roof", "Mirrah Trousers", + hidden=True), # Hidden fall + DS3LocationData("US: Plank Shield - outside stable, by NPC", "Plank Shield"), + DS3LocationData("US: Red Bug Pellet - tower village building, basement", + "Red Bug Pellet x2"), + DS3LocationData("US: Chloranthy Ring - tower village, jump from roof", "Chloranthy Ring", + hidden=True), # Hidden fall + DS3LocationData("US: Fire Clutch Ring - wooden walkway past stable", "Fire Clutch Ring"), + DS3LocationData("US: Estus Shard - under burning tree", "Estus Shard"), + DS3LocationData("US: Firebomb - stable roof", "Firebomb x6"), + # In enemy rando, the enemy may not burst through the wall and make this room obvious + DS3LocationData("US: Whip - back alley, behind wooden wall", "Whip", hidden=True), + DS3LocationData("US: Great Scythe - building by white tree, balcony", "Great Scythe"), + DS3LocationData("US: Homeward Bone - foot, drop overlook", "Homeward Bone", + static='02,0:53100540::'), + DS3LocationData("US: Large Soul of a Deserted Corpse - around corner by Cliff Underside", + "Large Soul of a Deserted Corpse", hidden=True), # Hidden corner + DS3LocationData("US: Ember - behind burning tree", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - across from Foot of the High Wall", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Fading Soul - by white tree", "Fading Soul"), + DS3LocationData("US: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("US: Ember - by white tree", "Ember"), + DS3LocationData("US: Large Soul of a Deserted Corpse - by white tree", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("US: Reinforced Club - by white tree", "Reinforced Club"), + DS3LocationData("US: Soul of a Nameless Soldier - top of tower", + "Soul of a Nameless Soldier"), + DS3LocationData("US: Loretta's Bone - first building, hanging corpse on balcony", + "Loretta's Bone"), + DS3LocationData("US: Northern Helm - tower village, hanging corpse", "Northern Helm"), + DS3LocationData("US: Northern Armor - tower village, hanging corpse", "Northern Armor"), + DS3LocationData("US: Northern Gloves - tower village, hanging corpse", "Northern Gloves"), + DS3LocationData("US: Northern Trousers - tower village, hanging corpse", + "Northern Trousers"), + DS3LocationData("US: Partizan - hanging corpse above Cliff Underside", "Partizan", + missable=True), # requires projectile + DS3LocationData("US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transport", + "Flame Stoneplate Ring"), + DS3LocationData("US: Red and White Shield - chasm, hanging corpse", "Red and White Shield", + static="02,0:53100740::", missable=True), # requires projectile + DS3LocationData("US: Small Leather Shield - first building, hanging corpse by entrance", + "Small Leather Shield"), + DS3LocationData("US: Pale Tongue - tower village, hanging corpse", "Pale Tongue"), + DS3LocationData("US: Large Soul of a Deserted Corpse - hanging corpse room, over stairs", + "Large Soul of a Deserted Corpse"), + DS3LocationData("US: Kukri - hanging corpse above burning tree", "Kukri x9", + missable=True), # requires projectile + DS3LocationData("US: Life Ring+1 - tower on the way to village", "Life Ring+1", ngp=True), + DS3LocationData("US: Poisonbite Ring+1 - graveyard by white tree, near well", + "Poisonbite Ring+1", ngp=True), + DS3LocationData("US: Covetous Silver Serpent Ring+2 - tower village, drop down from roof", + "Covetous Silver Serpent Ring+2", ngp=True, hidden=True), # Hidden fall + DS3LocationData("US: Human Pine Resin - tower village building, chest upstairs", + "Human Pine Resin x4"), + DS3LocationData("US: Homeward Bone - tower village, right at start", "Homeward Bone", + static='02,0:53100540::'), + DS3LocationData("US: Irithyll Straight Sword - miniboss drop, by Road of Sacrifices", + "Irithyll Straight Sword", miniboss=True), + DS3LocationData("US: Fire Gem - tower village, miniboss drop", "Fire Gem", miniboss=True), + DS3LocationData("US: Warrior of Sunlight - hanging corpse room, drop through hole", + "Warrior of Sunlight", hidden=True), # hidden fall + DS3LocationData("US: Mound-makers - Hodrick", "Mound-makers", missable=True), + DS3LocationData("US: Sharp Gem - lizard by Dilapidated Bridge", "Sharp Gem", lizard=True), + DS3LocationData("US: Heavy Gem - chasm, lizard", "Heavy Gem", lizard=True), + DS3LocationData("US: Siegbräu - Siegward", "Siegbräu", missable=True, npc=True), + DS3LocationData("US: Heavy Gem - Hawkwood", "Heavy Gem", static='00,0:50006070::', + missable=True, npc=True), # Hawkwood (quest, after Greatwood or Sage) + DS3LocationData("US -> RS", None), + + # Yoel/Yuria of Londor + DS3LocationData("FS: Soul Arrow - Yoel/Yuria", "Soul Arrow", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Heavy Soul Arrow - Yoel/Yuria", "Heavy Soul Arrow", + static='99,0:-1:50000,110000,70000116:', + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Weapon - Yoel/Yuria", "Magic Weapon", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Magic Shield - Yoel/Yuria", "Magic Shield", + static='99,0:-1:50000,110000,70000116:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Greatsword - Yoel/Yuria", "Soul Greatsword", + static='99,0:-1:50000,110000,70000450,70000475:', missable=True, + npc=True, shop=True), + DS3LocationData("FS: Dark Hand - Yoel/Yuria", "Dark Hand", missable=True, npc=True), + DS3LocationData("FS: Untrue White Ring - Yoel/Yuria", "Untrue White Ring", missable=True, + npc=True), + DS3LocationData("FS: Untrue Dark Ring - Yoel/Yuria", "Untrue Dark Ring", missable=True, + npc=True), + DS3LocationData("FS: Londor Braille Divine Tome - Yoel/Yuria", "Londor Braille Divine Tome", + static='99,0:-1:40000,110000,70000116:', missable=True, npc=True), + DS3LocationData("FS: Darkdrift - Yoel/Yuria", "Darkdrift", missable=True, drop=True, + npc=True), # kill her or kill Soul of Cinder + + # Cornyx of the Great Swamp + # These aren't missable because the Shrine Handmaid will carry them if you kill Cornyx. + DS3LocationData("FS: Fireball - Cornyx", "Fireball", npc=True, shop=True), + DS3LocationData("FS: Fire Surge - Cornyx", "Fire Surge", npc=True, shop=True), + DS3LocationData("FS: Great Combustion - Cornyx", "Great Combustion", npc=True, + shop=True), + DS3LocationData("FS: Flash Sweat - Cornyx", "Flash Sweat", npc=True, shop=True), + # These are missable if you kill Cornyx before giving him the right tomes. + DS3LocationData("FS: Poison Mist - Cornyx for Great Swamp Tome", "Poison Mist", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Fire Orb - Cornyx for Great Swamp Tome", "Fire Orb", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Profuse Sweat - Cornyx for Great Swamp Tome", "Profuse Sweat", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Bursting Fireball - Cornyx for Great Swamp Tome", "Bursting Fireball", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Acid Surge - Cornyx for Carthus Tome", "Acid Surge", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Carthus Flame Arc - Cornyx for Carthus Tome", "Carthus Flame Arc", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Carthus Beacon - Cornyx for Carthus Tome", "Carthus Beacon", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Great Chaos Fire Orb - Cornyx for Izalith Tome", + "Great Chaos Fire Orb", missable=True, npc=True, shop=True), + DS3LocationData("FS: Chaos Storm - Cornyx for Izalith Tome", "Chaos Storm", missable=True, + npc=True, shop=True), + + # Irina of Carim + # These aren't in their own location because you don't actually need the Grave Key to access + # Irena—you can just fall down the cliff near Eygon. + DS3LocationData("FS: Saint's Ring - Irina", "Saint's Ring", npc=True, shop=True), + DS3LocationData("FS: Heal - Irina", "Heal", npc=True, shop=True), + DS3LocationData("FS: Replenishment - Irina", "Replenishment", npc=True, shop=True), + DS3LocationData("FS: Caressing Tears - Irina", "Caressing Tears", npc=True, shop=True), + DS3LocationData("FS: Homeward - Irina", "Homeward", npc=True, shop=True), + DS3LocationData("FS: Med Heal - Irina for Tome of Carim", "Med Heal", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Tears of Denial - Irina for Tome of Carim", "Tears of Denial", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Force - Irina for Tome of Carim", "Force", missable=True, npc=True, + shop=True), + DS3LocationData("FS: Bountiful Light - Irina for Tome of Lothric", "Bountiful Light", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Barrier - Irina for Tome of Lothric", "Magic Barrier", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Blessed Weapon - Irina for Tome of Lothric", "Blessed Weapon", + missable=True, npc=True, shop=True), ], "Road of Sacrifices": [ - DS3LocationData("RS: Brigand Twindaggers", "Brigand Twindaggers", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Brigand Hood", "Brigand Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Armor", "Brigand Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Gauntlets", "Brigand Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Brigand Trousers", "Brigand Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Butcher Knife", "Butcher Knife", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Brigand Axe", "Brigand Axe", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Braille Divine Tome of Carim", "Braille Divine Tome of Carim", DS3LocationCategory.MISC), - DS3LocationData("RS: Morne's Ring", "Morne's Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Twin Dragon Greatshield", "Twin Dragon Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Heretic's Staff", "Heretic's Staff", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Sorcerer Hood", "Sorcerer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Robe", "Sorcerer Robe", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Gloves", "Sorcerer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sorcerer Trousers", "Sorcerer Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sage Ring", "Sage Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Fallen Knight Helm", "Fallen Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Armor", "Fallen Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Gauntlets", "Fallen Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Fallen Knight Trousers", "Fallen Knight Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Hood", "Conjurator Hood", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Robe", "Conjurator Robe", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Manchettes", "Conjurator Manchettes", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Conjurator Boots", "Conjurator Boots", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Great Swamp Pyromancy Tome", "Great Swamp Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("RS: Great Club", "Great Club", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Exile Greatsword", "Exile Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Farron Coal", "Farron Coal", DS3LocationCategory.MISC), - DS3LocationData("RS: Sellsword Twinblades", "Sellsword Twinblades", DS3LocationCategory.WEAPON), - DS3LocationData("RS: Sellsword Helm", "Sellsword Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Armor", "Sellsword Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Gauntlet", "Sellsword Gauntlet", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Sellsword Trousers", "Sellsword Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Golden Falcon Shield", "Golden Falcon Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Herald Helm", "Herald Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Armor", "Herald Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Gloves", "Herald Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Herald Trousers", "Herald Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RS: Grass Crest Shield", "Grass Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", DS3LocationCategory.BOSS), - DS3LocationData("RS: Great Swamp Ring", "Great Swamp Ring", DS3LocationCategory.RING), - DS3LocationData("RS: Orbeck's Ashes", "Orbeck's Ashes", DS3LocationCategory.NPC), + DS3LocationData("RS: Soul of a Crystal Sage", "Soul of a Crystal Sage", prominent=True, + boss=True), + DS3LocationData("RS: Exile Greatsword - NPC drop by Farron Keep", "Exile Greatsword", + hostile_npc=True), # Exile Knight #2 drop + DS3LocationData("RS: Great Club - NPC drop by Farron Keep", "Great Club", + hostile_npc=True), # Exile Knight #1 drop + DS3LocationData("RS: Heysel Pick - Heysel drop", "Heysel Pick", missable=True, + hostile_npc=True), + DS3LocationData("RS: Xanthous Crown - Heysel drop", "Xanthous Crown", missable=True, + hostile_npc=True), + DS3LocationData("RS: Butcher Knife - NPC drop beneath road", "Butcher Knife", + hostile_npc=True), # Madwoman + DS3LocationData("RS: Titanite Shard - water by Halfway Fortress", "Titanite Shard"), + DS3LocationData("RS: Titanite Shard - woods, left of path from Halfway Fortress", + "Titanite Shard"), + DS3LocationData("RS: Green Blossom - by deep water", "Green Blossom x4"), + DS3LocationData("RS: Estus Shard - left of fire behind stronghold left room", + "Estus Shard"), + DS3LocationData("RS: Ring of Sacrifice - stronghold, drop from right room balcony", + "Ring of Sacrifice", hidden=True), # hidden fall + DS3LocationData("RS: Soul of an Unknown Traveler - drop along wall from Halfway Fortress", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Fallen Knight Helm - water's edge by Farron Keep", + "Fallen Knight Helm"), + DS3LocationData("RS: Fallen Knight Armor - water's edge by Farron Keep", + "Fallen Knight Armor"), + DS3LocationData("RS: Fallen Knight Gauntlets - water's edge by Farron Keep", + "Fallen Knight Gauntlets"), + DS3LocationData("RS: Fallen Knight Trousers - water's edge by Farron Keep", + "Fallen Knight Trousers"), + DS3LocationData("RS: Heretic's Staff - stronghold left room", "Heretic's Staff"), + DS3LocationData("RS: Large Soul of an Unknown Traveler - left of stairs to Farron Keep", + "Large Soul of an Unknown Traveler"), + DS3LocationData("RS: Conjurator Hood - deep water", "Conjurator Hood"), + DS3LocationData("RS: Conjurator Robe - deep water", "Conjurator Robe"), + DS3LocationData("RS: Conjurator Manchettes - deep water", "Conjurator Manchettes"), + DS3LocationData("RS: Conjurator Boots - deep water", "Conjurator Boots"), + DS3LocationData("RS: Soul of an Unknown Traveler - right of door to stronghold left", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Green Blossom - water beneath stronghold", "Green Blossom x2"), + DS3LocationData("RS: Great Swamp Pyromancy Tome - deep water", + "Great Swamp Pyromancy Tome"), + DS3LocationData("RS: Homeward Bone - balcony by Farron Keep", "Homeward Bone x2"), + DS3LocationData("RS: Titanite Shard - woods, surrounded by enemies", "Titanite Shard"), + DS3LocationData("RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfire", + "Twin Dragon Greatshield"), + DS3LocationData("RS: Sorcerer Hood - water beneath stronghold", "Sorcerer Hood", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Robe - water beneath stronghold", "Sorcerer Robe", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Gloves - water beneath stronghold", "Sorcerer Gloves", + hidden=True), # Hidden fall + DS3LocationData("RS: Sorcerer Trousers - water beneath stronghold", "Sorcerer Trousers", + hidden=True), # Hidden fall + DS3LocationData("RS: Sage Ring - water beneath stronghold", "Sage Ring", + hidden=True), # Hidden fall + DS3LocationData("RS: Grass Crest Shield - water by Crucifixion Woods bonfire", + "Grass Crest Shield"), + DS3LocationData("RS: Ember - right of fire behind stronghold left room", "Ember"), + DS3LocationData("RS: Blue Bug Pellet - broken stairs by Orbeck", "Blue Bug Pellet x2"), + DS3LocationData("RS: Soul of an Unknown Traveler - road, by wagon", + "Soul of an Unknown Traveler"), + DS3LocationData("RS: Shriving Stone - road, by start", "Shriving Stone"), + DS3LocationData("RS: Titanite Shard - road, on bridge after you go under", + "Titanite Shard"), + DS3LocationData("RS: Brigand Twindaggers - beneath road", "Brigand Twindaggers"), + DS3LocationData("RS: Braille Divine Tome of Carim - drop from bridge to Halfway Fortress", + "Braille Divine Tome of Carim", hidden=True), # Hidden fall + DS3LocationData("RS: Ember - right of Halfway Fortress entrance", "Ember"), + DS3LocationData("RS: Sellsword Twinblades - keep perimeter", "Sellsword Twinblades"), + DS3LocationData("RS: Golden Falcon Shield - path from stronghold right room to Farron Keep", + "Golden Falcon Shield"), + DS3LocationData("RS: Brigand Axe - beneath road", "Brigand Axe"), + DS3LocationData("RS: Brigand Hood - beneath road", "Brigand Hood"), + DS3LocationData("RS: Brigand Armor - beneath road", "Brigand Armor"), + DS3LocationData("RS: Brigand Gauntlets - beneath road", "Brigand Gauntlets"), + DS3LocationData("RS: Brigand Trousers - beneath road", "Brigand Trousers"), + DS3LocationData("RS: Morne's Ring - drop from bridge to Halfway Fortress", "Morne's Ring", + hidden=True), # Hidden fall + DS3LocationData("RS: Sellsword Helm - keep perimeter balcony", "Sellsword Helm"), + DS3LocationData("RS: Sellsword Armor - keep perimeter balcony", "Sellsword Armor"), + DS3LocationData("RS: Sellsword Gauntlet - keep perimeter balcony", "Sellsword Gauntlet"), + DS3LocationData("RS: Sellsword Trousers - keep perimeter balcony", "Sellsword Trousers"), + DS3LocationData("RS: Farron Coal - keep perimeter", "Farron Coal"), + DS3LocationData("RS: Chloranthy Ring+2 - road, drop across from carriage", + "Chloranthy Ring+2", hidden=True, ngp=True), # Hidden fall + DS3LocationData("RS: Lingering Dragoncrest Ring+1 - water", "Lingering Dragoncrest Ring+1", + ngp=True), + DS3LocationData("RS: Great Swamp Ring - miniboss drop, by Farron Keep", + "Great Swamp Ring", miniboss=True), # Giant Crab drop + DS3LocationData("RS: Blue Sentinels - Horace", "Blue Sentinels", + missable=True, npc=True), # Horace quest + DS3LocationData("RS: Crystal Gem - stronghold, lizard", "Crystal Gem"), + DS3LocationData("RS: Fading Soul - woods by Crucifixion Woods bonfire", "Fading Soul", + static='03,0:53300210::'), + + # Orbeck shop, all missable because he'll disappear if you don't talk to him for too long or + # if you don't give him a scroll. + DS3LocationData("FS: Farron Dart - Orbeck", "Farron Dart", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Arrow - Orbeck", "Soul Arrow", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Great Soul Arrow - Orbeck", "Great Soul Arrow", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Heavy Soul Arrow - Orbeck", "Heavy Soul Arrow", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Great Heavy Soul Arrow - Orbeck", "Great Heavy Soul Arrow", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Magic Weapon - Orbeck", "Magic Weapon", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Magic Shield - Orbeck", "Magic Shield", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True, + shop=True), + DS3LocationData("FS: Spook - Orbeck", "Spook", missable=True, npc=True, shop=True), + DS3LocationData("FS: Aural Decoy - Orbeck", "Aural Decoy", missable=True, npc=True, + shop=True), + DS3LocationData("FS: Soul Greatsword - Orbeck", "Soul Greatsword", + static='99,0:-1:110000,130100,70000111:', missable=True, npc=True), + DS3LocationData("FS: Farron Flashsword - Orbeck", "Farron Flashsword", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Pestilent Mist - Orbeck for any scroll", "Pestilent Mist", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Great Farron Dart - Orbeck for Sage's Scroll", "Great Farron Dart", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Farron Hail - Orbeck for Sage's Scroll", "Farron Hail", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Homing Soulmass - Orbeck for Logan's Scroll", "Homing Soulmass", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Soul Spear - Orbeck for Logan's Scroll", "Soul Spear", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Homing Crystal Soulmass - Orbeck for Crystal Scroll", + "Homing Crystal Soulmass", missable=True, npc=True, shop=True), + DS3LocationData("FS: Crystal Soul Spear - Orbeck for Crystal Scroll", "Crystal Soul Spear", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Crystal Magic Weapon - Orbeck for Crystal Scroll", + "Crystal Magic Weapon", missable=True, npc=True, shop=True), + DS3LocationData("FS: Cast Light - Orbeck for Golden Scroll", "Cast Light", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Twisted Wall of Light - Orbeck for Golden Scroll", + "Twisted Wall of Light", missable=True, npc=True, shop=True), + DS3LocationData("FS: Hidden Weapon - Orbeck for Golden Scroll", "Hidden Weapon", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Hidden Body - Orbeck for Golden Scroll", "Hidden Body", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Repair - Orbeck for Golden Scroll", "Repair", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Clandestine Coat - shop with Orbeck's Ashes", "Clandestine Coat", + missable=True, npc=True, + shop=True), # Shrine Handmaid with Orbeck's Ashes + reload + DS3LocationData("FS: Young Dragon Ring - Orbeck for one scroll and buying three spells", + "Young Dragon Ring", missable=True, npc=True), + DS3LocationData("FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spells", + "Slumbering Dragoncrest Ring", missable=True, npc=True), + DS3LocationData("RS -> FK", None), + + # Shrine Handmaid after killing exiles + DS3LocationData("FS: Exile Mask - shop after killing NPCs in RS", "Exile Mask", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Armor - shop after killing NPCs in RS", "Exile Armor", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Gauntlets - shop after killing NPCs in RS", "Exile Gauntlets", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Exile Leggings - shop after killing NPCs in RS", "Exile Leggings", + hostile_npc=True, shop=True, hidden=True), + + # Shrine Handmaid after killing Crystal Sage + DS3LocationData("FS: Sage's Big Hat - shop after killing RS boss", "Sage's Big Hat", + boss=True, shop=True), + + # Yuria of Londor for Orbeck's Ashes + DS3LocationData("FS: Morion Blade - Yuria for Orbeck's Ashes", "Morion Blade", + missable=True, npc=True), ], "Cathedral of the Deep": [ - DS3LocationData("CD: Paladin's Ashes", "Paladin's Ashes", DS3LocationCategory.MISC), - DS3LocationData("CD: Spider Shield", "Spider Shield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Crest Shield", "Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Notched Whip", "Notched Whip", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Astora Greatsword", "Astora Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Executioner's Greatsword", "Executioner's Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Curse Ward Greatshield", "Curse Ward Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("CD: Saint-tree Bellvine", "Saint-tree Bellvine", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Poisonbite Ring", "Poisonbite Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Lloyd's Sword Ring", "Lloyd's Sword Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Seek Guidance", "Seek Guidance", DS3LocationCategory.SPELL), - DS3LocationData("CD: Aldrich's Sapphire", "Aldrich's Sapphire", DS3LocationCategory.RING), - DS3LocationData("CD: Deep Braille Divine Tome", "Deep Braille Divine Tome", DS3LocationCategory.MISC), - DS3LocationData("CD: Saint Bident", "Saint Bident", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Maiden Hood", "Maiden Hood", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Robe", "Maiden Robe", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Gloves", "Maiden Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Maiden Skirt", "Maiden Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Armor", "Drang Armor", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Gauntlets", "Drang Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Shoes", "Drang Shoes", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Drang Hammers", "Drang Hammers", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Deep Ring", "Deep Ring", DS3LocationCategory.RING), - DS3LocationData("CD: Archdeacon White Crown", "Archdeacon White Crown", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Archdeacon Holy Garb", "Archdeacon Holy Garb", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Archdeacon Skirt", "Archdeacon Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("CD: Arbalest", "Arbalest", DS3LocationCategory.WEAPON), - DS3LocationData("CD: Small Doll", "Small Doll", DS3LocationCategory.KEY), - DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", DS3LocationCategory.BOSS), - DS3LocationData("CD: Rosaria's Fingers", "Rosaria's Fingers", DS3LocationCategory.MISC) + DS3LocationData("CD: Herald Helm - path, by fire", "Herald Helm"), + DS3LocationData("CD: Herald Armor - path, by fire", "Herald Armor"), + DS3LocationData("CD: Herald Gloves - path, by fire", "Herald Gloves"), + DS3LocationData("CD: Herald Trousers - path, by fire", "Herald Trousers"), + DS3LocationData("CD: Twinkling Titanite - path, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Twinkling Titanite - path, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Small Doll - boss drop", "Small Doll", prominent=True, + progression=True, boss=True), + DS3LocationData("CD: Soul of the Deacons of the Deep", "Soul of the Deacons of the Deep", + boss=True), + DS3LocationData("CD: Black Eye Orb - Rosaria from Leonhard's quest", "Black Eye Orb", + missable=True, npc=True), + DS3LocationData("CD: Winged Spear - kill Patches", "Winged Spear", drop=True, + missable=True), # Patches (kill) + DS3LocationData("CD: Spider Shield - NPC drop on path", "Spider Shield", + hostile_npc=True), # Brigand + DS3LocationData("CD: Notched Whip - Cleansing Chapel", "Notched Whip"), + DS3LocationData("CD: Titanite Shard - Cleansing Chapel windowsill, by miniboss", + "Titanite Shard"), + DS3LocationData("CD: Astora Greatsword - graveyard, left of entrance", "Astora Greatsword"), + DS3LocationData("CD: Executioner's Greatsword - graveyard, far end", + "Executioner's Greatsword"), + DS3LocationData("CD: Undead Bone Shard - gravestone by white tree", "Undead Bone Shard"), + DS3LocationData("CD: Curse Ward Greatshield - by ladder from white tree to moat", + "Curse Ward Greatshield"), + DS3LocationData("CD: Titanite Shard - moat, far end", "Titanite Shard"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balcony", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Paladin's Ashes - path, guarded by lower NPC", "Paladin's Ashes", + progression=True), + DS3LocationData("CD: Arbalest - upper roofs, end of furthest buttress", "Arbalest"), + DS3LocationData("CD: Ember - by back door", "Ember"), + DS3LocationData("CD: Ember - side chapel upstairs, up ladder", "Ember"), + DS3LocationData("CD: Poisonbite Ring - moat, hall past miniboss", "Poisonbite Ring"), + DS3LocationData("CD: Drang Armor - main hall, east", "Drang Armor"), + DS3LocationData("CD: Ember - edge of platform before boss", "Ember"), + DS3LocationData("CD: Duel Charm - next to Patches in onion armor", "Duel Charm x3"), + DS3LocationData("CD: Seek Guidance - side chapel upstairs", "Seek Guidance"), + DS3LocationData("CD: Estus Shard - monument outside Cleansing Chapel", "Estus Shard"), + DS3LocationData("CD: Maiden Hood - main hall south", "Maiden Hood"), + DS3LocationData("CD: Maiden Robe - main hall south", "Maiden Robe"), + DS3LocationData("CD: Maiden Gloves - main hall south", "Maiden Gloves"), + DS3LocationData("CD: Maiden Skirt - main hall south", "Maiden Skirt"), + DS3LocationData("CD: Pale Tongue - upper roofs, outdoors far end", "Pale Tongue"), + DS3LocationData("CD: Fading Soul - graveyard, far end", "Fading Soul"), + DS3LocationData("CD: Blessed Gem - upper roofs, rafters", "Blessed Gem"), + DS3LocationData("CD: Red Bug Pellet - right of cathedral front doors", "Red Bug Pellet"), + DS3LocationData("CD: Soul of a Nameless Soldier - main hall south", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Duel Charm - by first elevator", "Duel Charm"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - main hall south, side path", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Ember - side chapel, miniboss room", "Ember"), + DS3LocationData("CD: Repair Powder - by white tree", "Repair Powder x3"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - by white tree #1", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - by white tree #2", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Undead Hunter Charm - lower roofs, up stairs between buttresses", + "Undead Hunter Charm x3"), + DS3LocationData("CD: Red Bug Pellet - lower roofs, up stairs between buttresses", + "Red Bug Pellet x3"), + DS3LocationData("CD: Titanite Shard - outside building by white tree", "Titanite Shard", + hidden=True), # Easily missable side path + DS3LocationData("CD: Titanite Shard - moat, up a slope", "Titanite Shard"), + DS3LocationData("CD: Rusted Coin - left of cathedral front doors, behind crates", + "Rusted Coin x2", hidden=True), + DS3LocationData("CD: Drang Hammers - main hall east", "Drang Hammers"), + DS3LocationData("CD: Drang Shoes - main hall east", "Drang Shoes"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - main hall east", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CD: Pale Tongue - main hall east", "Pale Tongue"), + DS3LocationData("CD: Drang Gauntlets - main hall east", "Drang Gauntlets"), + DS3LocationData("CD: Soul of a Nameless Soldier - lower roofs, side room", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Exploding Bolt - ledge above main hall south", "Exploding Bolt x6"), + DS3LocationData("CD: Lloyd's Sword Ring - ledge above main hall south", + "Lloyd's Sword Ring"), + DS3LocationData("CD: Soul of a Nameless Soldier - ledge above main hall south", + "Soul of a Nameless Soldier"), + DS3LocationData("CD: Homeward Bone - outside main hall south door", "Homeward Bone x2"), + DS3LocationData("CD: Deep Gem - down stairs by first elevator", "Deep Gem"), + DS3LocationData("CD: Titanite Shard - path, side path by Cathedral of the Deep bonfire", + "Titanite Shard"), + DS3LocationData("CD: Large Soul of an Unknown Traveler - path, against outer wall", + "Large Soul of an Unknown Traveler"), + # Before the stairs leading down into the Deacons fight + DS3LocationData("CD: Ring of the Evil Eye+1 - by stairs to boss", "Ring of the Evil Eye+1", + ngp=True), + DS3LocationData("CD: Ring of Favor+2 - upper roofs, on buttress", "Ring of Favor+2", + hidden=True, ngp=True), # Hidden fall + DS3LocationData("CD: Crest Shield - path, drop down by Cathedral of the Deep bonfire", + "Crest Shield", hidden=True), # Hidden fall + DS3LocationData("CD: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("CD: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("CD: Saint-tree Bellvine - moat, by water", "Saint-tree Bellvine"), + DS3LocationData("CD: Saint Bident - outside main hall south door", "Saint Bident"), + # Archdeacon set is hidden because you have to return to a cleared area + DS3LocationData("CD: Archdeacon White Crown - boss room after killing boss", + "Archdeacon White Crown", boss=True, hidden=True), + DS3LocationData("CD: Archdeacon Holy Garb - boss room after killing boss", + "Archdeacon Holy Garb", boss=True, hidden=True), + DS3LocationData("CD: Archdeacon Skirt - boss room after killing boss", "Archdeacon Skirt", + boss=True, hidden=True), + # Heysel items may not be missable, but it's not clear what causes them to trigger + DS3LocationData("CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed Chamber", + "Heysel Pick", missable=True), + DS3LocationData("CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed Chamber", + "Xanthous Crown", missable=True), + DS3LocationData("CD: Deep Ring - upper roofs, passive mob drop in first tower", "Deep Ring", + drop=True, hidden=True), + DS3LocationData("CD: Deep Braille Divine Tome - mimic by side chapel", + "Deep Braille Divine Tome", mimic=True), + DS3LocationData("CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed Chamber", + "Red Sign Soapstone", drop=True, hidden=True), + DS3LocationData("CD: Aldrich's Sapphire - side chapel, miniboss drop", "Aldrich's Sapphire", + miniboss=True), # Deep Accursed Drop + DS3LocationData("CD: Titanite Scale - moat, miniboss drop", "Titanite Scale", + miniboss=True), # Ravenous Crystal Lizard drop + DS3LocationData("CD: Twinkling Titanite - moat, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Twinkling Titanite - moat, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("CD: Rosaria's Fingers - Rosaria", "Rosaria's Fingers", + hidden=True), # Hidden fall + DS3LocationData("CD -> PW1", None), + + # Longfinger Kirk drops + DS3LocationData("CD: Barbed Straight Sword - Kirk drop", "Barbed Straight Sword", + missable=True, hostile_npc=True), + DS3LocationData("CD: Spiked Shield - Kirk drop", "Spiked Shield", missable=True, + hostile_npc=True), + # In Rosaria's Bed Chamber + DS3LocationData("CD: Helm of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Helm of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Armor of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Armor of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Gauntlets of Thorns", missable=True, hostile_npc=True), + DS3LocationData("CD: Leggings of Thorns - Rosaria's Bed Chamber after killing Kirk", + "Leggings of Thorns", missable=True, hostile_npc=True), + + # Unbreakable Patches + DS3LocationData("CD: Rusted Coin - don't forgive Patches", "Rusted Coin", + missable=True, npc=True), + DS3LocationData("FS: Rusted Gold Coin - don't forgive Patches", "Rusted Gold Coin", + static='99,0:50006201::', missable=True, + npc=True), # Don't forgive Patches + DS3LocationData("CD: Shotel - Patches", "Shotel", missable=True, npc=True, shop=True), + DS3LocationData("CD: Ember - Patches", "Ember", missable=True, npc=True, shop=True), + DS3LocationData("CD: Horsehoof Ring - Patches", "Horsehoof Ring", missable=True, + npc=True, drop=True, shop=True), # (kill or buy) ], "Farron Keep": [ - DS3LocationData("FK: Ragged Mask", "Ragged Mask", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Iron Flesh", "Iron Flesh", DS3LocationCategory.SPELL), - DS3LocationData("FK: Golden Scroll", "Golden Scroll", DS3LocationCategory.MISC), - DS3LocationData("FK: Antiquated Dress", "Antiquated Dress", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Antiquated Gloves", "Antiquated Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Antiquated Skirt", "Antiquated Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Helm", "Nameless Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Armor", "Nameless Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Gauntlets", "Nameless Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Nameless Knight Leggings", "Nameless Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Sunlight Talisman", "Sunlight Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Wolf's Blood Swordgrass", "Wolf's Blood Swordgrass", DS3LocationCategory.MISC), - DS3LocationData("FK: Greatsword", "Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Sage's Coal", "Sage's Coal", DS3LocationCategory.MISC), - DS3LocationData("FK: Stone Parma", "Stone Parma", DS3LocationCategory.SHIELD), - DS3LocationData("FK: Sage's Scroll", "Sage's Scroll", DS3LocationCategory.MISC), - DS3LocationData("FK: Crown of Dusk", "Crown of Dusk", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Lingering Dragoncrest Ring", "Lingering Dragoncrest Ring", DS3LocationCategory.RING), - DS3LocationData("FK: Pharis's Hat", "Pharis's Hat", DS3LocationCategory.ARMOR), - DS3LocationData("FK: Black Bow of Pharis", "Black Bow of Pharis", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Dreamchaser's Ashes", "Dreamchaser's Ashes", DS3LocationCategory.MISC), - DS3LocationData("FK: Great Axe", "Great Axe", DS3LocationCategory.WEAPON), - DS3LocationData("FK: Dragon Crest Shield", "Dragon Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("FK: Lightning Spear", "Lightning Spear", DS3LocationCategory.SPELL), - DS3LocationData("FK: Atonement", "Atonement", DS3LocationCategory.SPELL), - DS3LocationData("FK: Great Magic Weapon", "Great Magic Weapon", DS3LocationCategory.SPELL), - DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", "Cinders of a Lord - Abyss Watcher", DS3LocationCategory.KEY), - DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", DS3LocationCategory.BOSS), - DS3LocationData("FK: Soul of a Stray Demon", "Soul of a Stray Demon", DS3LocationCategory.BOSS), - DS3LocationData("FK: Watchdogs of Farron", "Watchdogs of Farron", DS3LocationCategory.MISC), + DS3LocationData("FK: Lightning Spear - upper keep, far side of the wall", + "Lightning Spear"), + DS3LocationData("FK: Dragon Crest Shield - upper keep, far side of the wall", + "Dragon Crest Shield"), + DS3LocationData("FK: Soul of the Blood of the Wolf", "Soul of the Blood of the Wolf", + boss=True), + DS3LocationData("FK: Cinders of a Lord - Abyss Watcher", + "Cinders of a Lord - Abyss Watcher", + static="03,0:50002100::", prominent=True, progression=True, + boss=True), + DS3LocationData("FK: Manikin Claws - Londor Pale Shade drop", "Manikin Claws", + missable=True, hostile_npc=True, + npc=True), # Londor Pale Shade (if Yoel/Yuria hostile) + DS3LocationData("FK: Purple Moss Clump - keep ruins, ritual island", + "Purple Moss Clump x2"), + DS3LocationData("FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfire", + "Purple Moss Clump x4"), + DS3LocationData("FK: Greatsword - ramp by keep ruins ritual island", "Greatsword"), + DS3LocationData("FK: Hollow Gem - perimeter, drop down into swamp", "Hollow Gem", + hidden=True), + DS3LocationData("FK: Purple Moss Clump - Farron Keep bonfire, around right corner", + "Purple Moss Clump x3"), + DS3LocationData("FK: Undead Bone Shard - pavilion by keep ruins bonfire island", + "Undead Bone Shard"), + DS3LocationData("FK: Atonement - perimeter, drop down into swamp", "Atonement", + hidden=True), + DS3LocationData("FK: Titanite Shard - by ladder to keep proper", "Titanite Shard"), + DS3LocationData("FK: Iron Flesh - Farron Keep bonfire, right after exit", "Iron Flesh"), + DS3LocationData("FK: Stone Parma - near wall by left island", "Stone Parma"), + DS3LocationData("FK: Rotten Pine Resin - left island, behind fire", "Rotten Pine Resin x2"), + DS3LocationData("FK: Titanite Shard - between left island and keep ruins", "Titanite Shard"), + DS3LocationData("FK: Rusted Gold Coin - right island, behind wall", "Rusted Gold Coin", + hidden=True), + DS3LocationData("FK: Nameless Knight Helm - corner of keep and right island", + "Nameless Knight Helm"), + DS3LocationData("FK: Nameless Knight Armor - corner of keep and right island", + "Nameless Knight Armor"), + DS3LocationData("FK: Nameless Knight Gauntlets - corner of keep and right island", + "Nameless Knight Gauntlets"), + DS3LocationData("FK: Nameless Knight Leggings - corner of keep and right island", + "Nameless Knight Leggings"), + DS3LocationData("FK: Shriving Stone - perimeter, just past stone doors", "Shriving Stone"), + DS3LocationData("FK: Repair Powder - outside hidden cave", "Repair Powder x4", + hidden=True), + DS3LocationData("FK: Golden Scroll - hidden cave", "Golden Scroll", hidden=True), + DS3LocationData("FK: Sage's Scroll - near wall by keep ruins bonfire island", + "Sage's Scroll"), + DS3LocationData("FK: Dreamchaser's Ashes - keep proper, illusory wall", + "Dreamchaser's Ashes", progression=True, hidden=True), + DS3LocationData("FK: Titanite Shard - keep ruins bonfire island, under ramp", + "Titanite Shard"), + DS3LocationData("FK: Wolf's Blood Swordgrass - by ladder to keep proper", + "Wolf's Blood Swordgrass"), + DS3LocationData("FK: Great Magic Weapon - perimeter, by door to Road of Sacrifices", + "Great Magic Weapon"), + DS3LocationData("FK: Ember - perimeter, path to boss", "Ember"), + DS3LocationData("FK: Titanite Shard - swamp by right island", "Titanite Shard x2"), + DS3LocationData("FK: Titanite Shard - by left island stairs", "Titanite Shard"), + DS3LocationData("FK: Titanite Shard - by keep ruins ritual island stairs", "Titanite Shard"), + DS3LocationData("FK: Black Bug Pellet - perimeter, hill by boss door", + "Black Bug Pellet x3"), + DS3LocationData("FK: Rotten Pine Resin - outside pavilion by left island", + "Rotten Pine Resin x4"), + DS3LocationData("FK: Poison Gem - near wall by keep ruins bridge", "Poison Gem"), + DS3LocationData("FK: Ragged Mask - Farron Keep bonfire, around left corner", "Ragged Mask"), + DS3LocationData("FK: Estus Shard - between Farron Keep bonfire and left island", + "Estus Shard"), + DS3LocationData("FK: Homeward Bone - right island, behind fire", "Homeward Bone x2"), + DS3LocationData("FK: Titanite Shard - Farron Keep bonfire, left after exit", + "Titanite Shard"), + DS3LocationData("FK: Large Soul of a Nameless Soldier - corner of keep and right island", + "Large Soul of a Nameless Soldier", hidden=True), # Tricky corner to spot + DS3LocationData("FK: Prism Stone - by left island stairs", "Prism Stone x10"), + DS3LocationData("FK: Large Soul of a Nameless Soldier - near wall by right island", + "Large Soul of a Nameless Soldier"), + DS3LocationData("FK: Sage's Coal - pavilion by left island", "Sage's Coal"), + DS3LocationData("FK: Gold Pine Bundle - by white tree", "Gold Pine Bundle x6"), + DS3LocationData("FK: Ember - by white tree", "Ember"), + DS3LocationData("FK: Soul of a Nameless Soldier - by white tree", "Soul of a Nameless Soldier"), + DS3LocationData("FK: Large Soul of an Unknown Traveler - by white tree", + "Large Soul of an Unknown Traveler"), + DS3LocationData("FK: Greataxe - upper keep, by miniboss", "Greataxe"), + DS3LocationData("FK: Ember - upper keep, by miniboss #1", "Ember"), + DS3LocationData("FK: Ember - upper keep, by miniboss #2", "Ember"), + DS3LocationData("FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wall", + "Dark Stoneplate Ring+2", ngp=True, hidden=True), + DS3LocationData("FK: Magic Stoneplate Ring+1 - between right island and wall", + "Magic Stoneplate Ring+1", ngp=True), + DS3LocationData("FK: Wolf Ring+1 - keep ruins bonfire island, outside building", + "Wolf Ring+1", ngp=True), + DS3LocationData("FK: Antiquated Dress - hidden cave", "Antiquated Dress", hidden=True), + DS3LocationData("FK: Antiquated Gloves - hidden cave", "Antiquated Gloves", hidden=True), + DS3LocationData("FK: Antiquated Skirt - hidden cave", "Antiquated Skirt", hidden=True), + DS3LocationData("FK: Sunlight Talisman - estus soup island, by ladder to keep proper", + "Sunlight Talisman"), + DS3LocationData("FK: Young White Branch - by white tree #1", "Young White Branch"), + DS3LocationData("FK: Young White Branch - by white tree #2", "Young White Branch"), + DS3LocationData("FK: Crown of Dusk - by white tree", "Crown of Dusk"), + DS3LocationData("FK: Lingering Dragoncrest Ring - by white tree, miniboss drop", + "Lingering Dragoncrest Ring", miniboss=True), # Great Crab drop + DS3LocationData("FK: Pharis's Hat - miniboss drop, by keep ruins near wall", + "Pharis's Hat", miniboss=True), # Elder Ghru drop + DS3LocationData("FK: Black Bow of Pharis - miniboss drop, by keep ruins near wall", + "Black Bow of Pharis", miniboss=True), # Elder Ghru drop + DS3LocationData("FK: Titanite Scale - perimeter, miniboss drop", "Titanite Scale x2", + miniboss=True), # Ravenous Crystal Lizard drop + DS3LocationData("FK: Large Titanite Shard - upper keep, lizard in open", + "Large Titanite Shard", lizard=True), + DS3LocationData("FK: Large Titanite Shard - upper keep, lizard by wyvern", + "Large Titanite Shard", lizard=True), + DS3LocationData("FK: Heavy Gem - upper keep, lizard on stairs", "Heavy Gem", lizard=True), + DS3LocationData("FK: Twinkling Titanite - keep proper, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("FK: Soul of a Stray Demon - upper keep, miniboss drop", + "Soul of a Stray Demon", miniboss=True), + DS3LocationData("FK: Watchdogs of Farron - Old Wolf", "Watchdogs of Farron"), + DS3LocationData("FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "Hawkwood's Shield", missable=True, + npc=True), # Hawkwood (quest, after Greatwood, Sage, Watchers, and Deacons) + DS3LocationData("US: Hawk Ring - Giant Archer", "Hawk Ring", drop=True, + npc=True), # Giant archer (kill or quest), here because you need to + # collect all seven White Branch locations to get it peacefully + + # Hawkwood after killing Abyss Watchers + DS3LocationData("FS: Farron Ring - Hawkwood", "Farron Ring", + missable=True, npc=True), + + # Shrine Handmaid after killing Abyss Watchers + DS3LocationData("FS: Undead Legion Helm - shop after killing FK boss", "Undead Legion Helm", + boss=True, shop=True), + DS3LocationData("FS: Undead Legion Armor - shop after killing FK boss", + "Undead Legion Armor", boss=True, shop=True), + DS3LocationData("FS: Undead Legion Gauntlet - shop after killing FK boss", + "Undead Legion Gauntlet", boss=True, shop=True), + DS3LocationData("FS: Undead Legion Leggings - shop after killing FK boss", + "Undead Legion Leggings", boss=True, shop=True), + + # Appears after killing Havel Knight in Archdragon Peak + DS3LocationData("FK: Havel's Helm - upper keep, after killing AP belfry roof NPC", + "Havel's Helm", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Armor - upper keep, after killing AP belfry roof NPC", + "Havel's Armor", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPC", + "Havel's Gauntlets", hidden=True, hostile_npc=True), + DS3LocationData("FK: Havel's Leggings - upper keep, after killing AP belfry roof NPC", + "Havel's Leggings", hidden=True, hostile_npc=True), ], "Catacombs of Carthus": [ - DS3LocationData("CC: Carthus Pyromancy Tome", "Carthus Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("CC: Carthus Milkring", "Carthus Milkring", DS3LocationCategory.RING), - DS3LocationData("CC: Grave Warden's Ashes", "Grave Warden's Ashes", DS3LocationCategory.MISC), - DS3LocationData("CC: Carthus Bloodring", "Carthus Bloodring", DS3LocationCategory.RING), - DS3LocationData("CC: Grave Warden Pyromancy Tome", "Grave Warden Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("CC: Old Sage's Blindfold", "Old Sage's Blindfold", DS3LocationCategory.ARMOR), - DS3LocationData("CC: Witch's Ring", "Witch's Ring", DS3LocationCategory.RING), - DS3LocationData("CC: Black Blade", "Black Blade", DS3LocationCategory.WEAPON), - DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", DS3LocationCategory.BOSS), - DS3LocationData("CC: Soul of a Demon", "Soul of a Demon", DS3LocationCategory.BOSS), + DS3LocationData("CC: Soul of High Lord Wolnir", "Soul of High Lord Wolnir", + prominent=True, boss=True), + DS3LocationData("CC: Carthus Rouge - atrium upper, left after entrance", + "Carthus Rouge x2"), + DS3LocationData("CC: Sharp Gem - atrium lower, right before exit", "Sharp Gem"), + DS3LocationData("CC: Soul of a Nameless Soldier - atrium lower, down hall", + "Soul of a Nameless Soldier"), + DS3LocationData("CC: Titanite Shard - atrium lower, corner by stairs", "Titanite Shard x2"), + DS3LocationData("CC: Bloodred Moss Clump - atrium lower, down more stairs", + "Bloodred Moss Clump x3"), + DS3LocationData("CC: Carthus Milkring - crypt upper, among pots", "Carthus Milkring"), + DS3LocationData("CC: Ember - atrium, on long stairway", "Ember"), + DS3LocationData("CC: Carthus Rouge - crypt across, corner", "Carthus Rouge x3"), + DS3LocationData("CC: Ember - crypt upper, end of hall past hole", "Ember"), + DS3LocationData("CC: Carthus Bloodring - crypt lower, end of side hall", "Carthus Bloodring"), + DS3LocationData("CC: Titanite Shard - crypt lower, left of entrance", "Titanite Shard x2"), + DS3LocationData("CC: Titanite Shard - crypt lower, start of side hall", "Titanite Shard x2"), + DS3LocationData("CC: Ember - crypt lower, shortcut to cavern", "Ember"), + DS3LocationData("CC: Carthus Pyromancy Tome - atrium lower, jump from bridge", + "Carthus Pyromancy Tome", + hidden=True), # Behind illusory wall or hidden drop + DS3LocationData("CC: Large Titanite Shard - crypt upper, skeleton ball hall", + "Large Titanite Shard"), + DS3LocationData("CC: Large Titanite Shard - crypt across, middle hall", + "Large Titanite Shard"), + DS3LocationData("CC: Yellow Bug Pellet - cavern, on overlook", "Yellow Bug Pellet x3"), + DS3LocationData("CC: Large Soul of a Nameless Soldier - cavern, before bridge", + "Large Soul of a Nameless Soldier"), + DS3LocationData("CC: Black Bug Pellet - cavern, before bridge", "Black Bug Pellet x2"), + DS3LocationData("CC: Grave Warden's Ashes - crypt across, corner", "Grave Warden's Ashes", + progression=True), + DS3LocationData("CC: Large Titanite Shard - tomb lower", "Large Titanite Shard"), + DS3LocationData("CC: Large Soul of a Nameless Soldier - tomb lower", + "Large Soul of a Nameless Soldier"), + DS3LocationData("CC: Old Sage's Blindfold - tomb, hall before bonfire", + "Old Sage's Blindfold"), + DS3LocationData("CC: Witch's Ring - tomb, hall before bonfire", "Witch's Ring"), + DS3LocationData("CC: Soul of a Nameless Soldier - atrium upper, up more stairs", + "Soul of a Nameless Soldier"), + DS3LocationData("CC: Grave Warden Pyromancy Tome - boss arena", + "Grave Warden Pyromancy Tome"), + DS3LocationData("CC: Large Soul of an Unknown Traveler - crypt upper, hall middle", + "Large Soul of an Unknown Traveler"), + DS3LocationData("CC: Ring of Steel Protection+2 - atrium upper, drop onto pillar", + "Ring of Steel Protection+2", ngp=True), + DS3LocationData("CC: Thunder Stoneplate Ring+1 - crypt upper, among pots", + "Thunder Stoneplate Ring+1", ngp=True), + DS3LocationData("CC: Undead Bone Shard - crypt upper, skeleton ball drop", + "Undead Bone Shard", hidden=True), # Skeleton Ball puzzle + DS3LocationData("CC: Dark Gem - crypt lower, skeleton ball drop", "Dark Gem", + hidden=True), # Skeleton Ball puzzle + DS3LocationData("CC: Black Blade - tomb, mimic", "Black Blade", mimic=True), + DS3LocationData("CC: Soul of a Demon - tomb, miniboss drop", "Soul of a Demon", + miniboss=True), + DS3LocationData("CC: Twinkling Titanite - atrium lower, lizard down more stairs", + "Twinkling Titanite", lizard=True), + DS3LocationData("CC: Fire Gem - cavern, lizard", "Fire Gem", lizard=True), + DS3LocationData("CC: Homeward Bone - Irithyll bridge", "Homeward Bone"), + DS3LocationData("CC: Pontiff's Right Eye - Irithyll bridge, miniboss drop", + "Pontiff's Right Eye", miniboss=True), # Sulyvahn's Beast drop + + # Shrine Handmaid after killing High Lord Wolnir + DS3LocationData("FS: Wolnir's Crown - shop after killing CC boss", "Wolnir's Crown", + boss=True, shop=True), ], "Smouldering Lake": [ - DS3LocationData("SL: Shield of Want", "Shield of Want", DS3LocationCategory.SHIELD), - DS3LocationData("SL: Speckled Stoneplate Ring", "Speckled Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("SL: Dragonrider Bow", "Dragonrider Bow", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Lightning Stake", "Lightning Stake", DS3LocationCategory.SPELL), - DS3LocationData("SL: Izalith Pyromancy Tome", "Izalith Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("SL: Black Knight Sword", "Black Knight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Quelana Pyromancy Tome", "Quelana Pyromancy Tome", DS3LocationCategory.MISC), - DS3LocationData("SL: Toxic Mist", "Toxic Mist", DS3LocationCategory.SPELL), - DS3LocationData("SL: White Hair Talisman", "White Hair Talisman", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Izalith Staff", "Izalith Staff", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Sacred Flame", "Sacred Flame", DS3LocationCategory.SPELL), - DS3LocationData("SL: Fume Ultra Greatsword", "Fume Ultra Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("SL: Black Iron Greatshield", "Black Iron Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", DS3LocationCategory.BOSS), - DS3LocationData("SL: Knight Slayer's Ring", "Knight Slayer's Ring", DS3LocationCategory.RING), + DS3LocationData("SL: Soul of the Old Demon King", "Soul of the Old Demon King", + prominent=True, boss=True), + DS3LocationData("SL: Fume Ultra Greatsword - ruins basement, NPC drop", + "Fume Ultra Greatsword", hostile_npc=True), # Knight Slayer Tsorig drop + DS3LocationData("SL: Black Iron Greatshield - ruins basement, NPC drop", + "Black Iron Greatshield", hostile_npc=True), # Knight Slayer Tsorig drop + DS3LocationData("SL: Large Titanite Shard - ledge by Demon Ruins bonfire", + "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by entrance", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, straight from entrance", + "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by tree #1", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by miniboss", "Large Titanite Shard"), + DS3LocationData("SL: Yellow Bug Pellet - side lake", "Yellow Bug Pellet x2"), + DS3LocationData("SL: Large Titanite Shard - side lake #1", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - side lake #2", "Large Titanite Shard"), + DS3LocationData("SL: Large Titanite Shard - lake, by tree #2", "Large Titanite Shard"), + DS3LocationData("SL: Speckled Stoneplate Ring - lake, ballista breaks bricks", + "Speckled Stoneplate Ring", hidden=True), # Requires careful ballista shot + DS3LocationData("SL: Homeward Bone - path to ballista", "Homeward Bone x2"), + DS3LocationData("SL: Ember - ruins main upper, hall end by hole", "Ember"), + DS3LocationData("SL: Chaos Gem - lake, far end by mob", "Chaos Gem"), + DS3LocationData("SL: Ember - ruins main lower, path to antechamber", "Ember"), + DS3LocationData("SL: Izalith Pyromancy Tome - antechamber, room near bonfire", + "Izalith Pyromancy Tome"), + DS3LocationData("SL: Black Knight Sword - ruins main lower, illusory wall in far hall", + "Black Knight Sword", hidden=True), + DS3LocationData("SL: Ember - ruins main upper, just after entrance", "Ember"), + DS3LocationData("SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey room", + "Quelana Pyromancy Tome", hidden=True), + DS3LocationData("SL: Izalith Staff - ruins basement, second illusory wall behind chest", + "Izalith Staff", hidden=True), + DS3LocationData("SL: White Hair Talisman - ruins main lower, in lava", + "White Hair Talisman", + missable=True), # This may not even be possible to get without enough fire + # protection gear which the player may not have + DS3LocationData("SL: Toxic Mist - ruins main lower, in lava", "Toxic Mist", + missable=True), # This is _probably_ reachable with normal gear, but it + # still sucks and will probably force a death. + DS3LocationData("SL: Undead Bone Shard - ruins main lower, left after stairs", + "Undead Bone Shard"), + DS3LocationData("SL: Titanite Scale - ruins basement, path to lava", "Titanite Scale"), + DS3LocationData("SL: Shield of Want - lake, by miniboss", "Shield of Want"), + DS3LocationData("SL: Soul of a Crestfallen Knight - ruins basement, above lava", + "Soul of a Crestfallen Knight"), + + # Lava items are missable because they require a complex set of armor, rings, spells, and + # undead bone shards to reliably access without dying. + DS3LocationData("SL: Ember - ruins basement, in lava", "Ember", missable=True), # In lava + DS3LocationData("SL: Sacred Flame - ruins basement, in lava", "Sacred Flame", + missable=True), # In lava + + DS3LocationData("SL: Dragonrider Bow - by ladder from ruins basement to ballista", + "Dragonrider Bow", hidden=True), # Hidden fall + DS3LocationData("SL: Estus Shard - antechamber, illusory wall", "Estus Shard", + hidden=True), + DS3LocationData("SL: Bloodbite Ring+1 - behind ballista", "Bloodbite Ring+1", ngp=True), + DS3LocationData("SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hall", + "Flame Stoneplate Ring+2", ngp=True, hidden=True), + DS3LocationData("SL: Large Titanite Shard - ruins basement, illusory wall in upper hall", + "Large Titanite Shard x3", hidden=True), + DS3LocationData("SL: Undead Bone Shard - lake, miniboss drop", "Undead Bone Shard", + miniboss=True), # Sand Worm drop + DS3LocationData("SL: Lightning Stake - lake, miniboss drop", "Lightning Stake", + miniboss=True), # Sand Worm drop + DS3LocationData("SL: Twinkling Titanite - path to side lake, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("SL: Titanite Chunk - path to side lake, lizard", "Titanite Chunk", + lizard=True), + DS3LocationData("SL: Chaos Gem - antechamber, lizard at end of long hall", "Chaos Gem", + lizard=True), + DS3LocationData("SL: Knight Slayer's Ring - ruins basement, NPC drop", + "Knight Slayer's Ring", hostile_npc=True), # Knight Slayer Tsorig drop + + # Horace the Hushed + # These are listed here even though you can kill Horace in the Road of Sacrifices because + # the player may want to complete his and Anri's quest first. + DS3LocationData("SL: Llewellyn Shield - Horace drop", "Llewellyn Shield", npc=True, + hostile_npc=True), + DS3LocationData("FS: Executioner Helm - shop after killing Horace", "Executioner Helm", + npc=True, hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Executioner Armor - shop after killing Horace", "Executioner Armor", + npc=True, hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Executioner Gauntlets - shop after killing Horace", + "Executioner Gauntlets", hostile_npc=True, npc=True, shop=True, + hidden=True), + DS3LocationData("FS: Executioner Leggings - shop after killing Horace", + "Executioner Leggings", hostile_npc=True, npc=True, shop=True, + hidden=True), + + # Shrine Handmaid after killing Knight Slayer Tsorig + DS3LocationData("FS: Black Iron Helm - shop after killing Tsorig", "Black Iron Helm", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Armor - shop after killing Tsorig", "Black Iron Armor", + hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Gauntlets - shop after killing Tsorig", + "Black Iron Gauntlets", hostile_npc=True, shop=True, hidden=True), + DS3LocationData("FS: Black Iron Leggings - shop after killing Tsorig", + "Black Iron Leggings", hostile_npc=True, shop=True, hidden=True), + + # Near Cornyx's cage after killing Old Demon King with Cuculus + DS3LocationData("US: Spotted Whip - by Cornyx's cage after Cuculus quest", "Spotted Whip", + missable=True, boss=True, npc=True), + DS3LocationData("US: Cornyx's Garb - by Cornyx's cage after Cuculus quest", + "Cornyx's Garb", static='02,0:53100100::', missable=True, boss=True, + npc=True), + DS3LocationData("US: Cornyx's Wrap - by Cornyx's cage after Cuculus quest", "Cornyx's Wrap", + static='02,0:53100100::', missable=True, boss=True, npc=True), + DS3LocationData("US: Cornyx's Skirt - by Cornyx's cage after Cuculus quest", + "Cornyx's Skirt", static='02,0:53100100::', missable=True, boss=True, + npc=True), ], "Irithyll of the Boreal Valley": [ - DS3LocationData("IBV: Dorhys' Gnawing", "Dorhys' Gnawing", DS3LocationCategory.SPELL), - DS3LocationData("IBV: Witchtree Branch", "Witchtree Branch", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Magic Clutch Ring", "Magic Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Ring of the Sun's First Born", "Ring of the Sun's First Born", DS3LocationCategory.RING), - DS3LocationData("IBV: Roster of Knights", "Roster of Knights", DS3LocationCategory.MISC), - DS3LocationData("IBV: Pontiff's Right Eye", "Pontiff's Right Eye", DS3LocationCategory.RING), - DS3LocationData("IBV: Yorshka's Spear", "Yorshka's Spear", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Great Heal", "Great Heal", DS3LocationCategory.SPELL), - DS3LocationData("IBV: Smough's Great Hammer", "Smough's Great Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Leo Ring", "Leo Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Excrement-covered Ashes", "Excrement-covered Ashes", DS3LocationCategory.MISC), - DS3LocationData("IBV: Dark Stoneplate Ring", "Dark Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Easterner's Ashes", "Easterner's Ashes", DS3LocationCategory.MISC), - DS3LocationData("IBV: Painting Guardian's Curved Sword", "Painting Guardian's Curved Sword", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Painting Guardian Hood", "Painting Guardian Hood", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Gown", "Painting Guardian Gown", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Gloves", "Painting Guardian Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Painting Guardian Waistcloth", "Painting Guardian Waistcloth", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Dragonslayer Greatbow", "Dragonslayer Greatbow", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Reversal Ring", "Reversal Ring", DS3LocationCategory.RING), - DS3LocationData("IBV: Brass Helm", "Brass Helm", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Armor", "Brass Armor", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Gauntlets", "Brass Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Brass Leggings", "Brass Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("IBV: Ring of Favor", "Ring of Favor", DS3LocationCategory.RING), - DS3LocationData("IBV: Golden Ritual Spear", "Golden Ritual Spear", DS3LocationCategory.WEAPON), - DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", DS3LocationCategory.BOSS), - DS3LocationData("IBV: Aldrich Faithful", "Aldrich Faithful", DS3LocationCategory.MISC), - DS3LocationData("IBV: Drang Twinspears", "Drang Twinspears", DS3LocationCategory.WEAPON), + DS3LocationData("IBV: Soul of Pontiff Sulyvahn", "Soul of Pontiff Sulyvahn", + prominent=True, boss=True), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - central, by bonfire", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Large Titanite Shard - ascent, down ladder in last building", + "Large Titanite Shard"), + DS3LocationData("IBV: Soul of a Weary Warrior - central, by first fountain", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Soul of a Weary Warrior - central, railing by first fountain", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Rime-blue Moss Clump - central, by bonfire", "Rime-blue Moss Clump"), + DS3LocationData("IBV: Witchtree Branch - by Dorhys", "Witchtree Branch", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Large Titanite Shard - central, side path after first fountain", + "Large Titanite Shard"), + DS3LocationData("IBV: Budding Green Blossom - central, by second fountain", + "Budding Green Blossom"), + DS3LocationData("IBV: Rime-blue Moss Clump - central, past second fountain", + "Rime-blue Moss Clump x2"), + DS3LocationData("IBV: Large Titanite Shard - central, balcony just before plaza", + "Large Titanite Shard"), + DS3LocationData("IBV: Large Titanite Shard - path to Dorhys", "Large Titanite Shard", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Ring of the Sun's First Born - fall from in front of cathedral", + "Ring of the Sun's First Born", + hidden=True), # Hidden fall + DS3LocationData("IBV: Large Soul of a Nameless Soldier - path to plaza", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Large Titanite Shard - plaza, balcony overlooking ascent", + "Large Titanite Shard"), + DS3LocationData("IBV: Large Titanite Shard - plaza, by stairs to church", + "Large Titanite Shard"), + DS3LocationData("IBV: Soul of a Weary Warrior - plaza, side room lower", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Magic Clutch Ring - plaza, illusory wall", "Magic Clutch Ring", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Fading Soul - descent, cliff edge #1", "Fading Soul"), + DS3LocationData("IBV: Fading Soul - descent, cliff edge #2", "Fading Soul"), + DS3LocationData("IBV: Homeward Bone - descent, before gravestone", "Homeward Bone x3"), + DS3LocationData("IBV: Undead Bone Shard - descent, behind gravestone", "Undead Bone Shard", + hidden=True), # Hidden behind gravestone + DS3LocationData("IBV: Kukri - descent, side path", "Kukri x8"), + DS3LocationData("IBV: Rusted Gold Coin - descent, side path", "Rusted Gold Coin"), + DS3LocationData("IBV: Blue Bug Pellet - descent, dark room", "Blue Bug Pellet x2"), + DS3LocationData("IBV: Shriving Stone - descent, dark room rafters", "Shriving Stone"), + DS3LocationData("IBV: Blood Gem - descent, platform before lake", "Blood Gem"), + DS3LocationData("IBV: Green Blossom - lake, by stairs from descent", "Green Blossom x3"), + DS3LocationData("IBV: Ring of Sacrifice - lake, right of stairs from descent", + "Ring of Sacrifice"), + DS3LocationData("IBV: Great Heal - lake, dead Corpse-Grub", "Great Heal"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - lake island", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Green Blossom - lake wall", "Green Blossom x3"), + DS3LocationData("IBV: Dung Pie - sewer #1", "Dung Pie x3"), + DS3LocationData("IBV: Dung Pie - sewer #2", "Dung Pie x3"), + # These don't actually guard any single item sales. Maybe we can inject one manually? + DS3LocationData("IBV: Excrement-covered Ashes - sewer, by stairs", + "Excrement-covered Ashes"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - ascent, after great hall", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Soul of a Weary Warrior - ascent, by final staircase", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Large Titanite Shard - ascent, by elevator door", + "Large Titanite Shard"), + DS3LocationData("IBV: Blue Bug Pellet - ascent, in last building", "Blue Bug Pellet x2"), + DS3LocationData("IBV: Ember - shortcut from church to cathedral", "Ember"), + DS3LocationData("IBV: Green Blossom - lake, by Distant Manor", "Green Blossom"), + DS3LocationData("IBV: Lightning Gem - plaza center", "Lightning Gem"), + DS3LocationData("IBV: Large Soul of a Nameless Soldier - central, by second fountain", + "Large Soul of a Nameless Soldier"), + DS3LocationData("IBV: Soul of a Weary Warrior - plaza, side room upper", + "Soul of a Weary Warrior"), + DS3LocationData("IBV: Proof of a Concord Kept - Church of Yorshka altar", + "Proof of a Concord Kept"), + DS3LocationData("IBV: Rusted Gold Coin - Distant Manor, drop after stairs", + "Rusted Gold Coin"), + DS3LocationData("IBV: Chloranthy Ring+1 - plaza, behind altar", "Chloranthy Ring+1", + ngp=True), + DS3LocationData("IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark room", + "Covetous Gold Serpent Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("IBV: Wood Grain Ring+2 - ascent, right after great hall", "Wood Grain Ring+2", + ngp=True), + DS3LocationData("IBV: Divine Blessing - great hall, chest", "Divine Blessing"), + DS3LocationData("IBV: Smough's Great Hammer - great hall, chest", + "Smough's Great Hammer"), + DS3LocationData("IBV: Yorshka's Spear - descent, dark room rafters chest", "Yorshka's Spear"), + DS3LocationData("IBV: Leo Ring - great hall, chest", "Leo Ring"), + DS3LocationData("IBV: Dorhys' Gnawing - Dorhys drop", "Dorhys' Gnawing", + hidden=True), # Behind illusory wall + DS3LocationData("IBV: Divine Blessing - great hall, mob drop", + "Divine Blessing", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, main floor mob drop", + "Large Titanite Shard", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, upstairs mob drop #1", + "Large Titanite Shard x2", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Large Titanite Shard - great hall, upstairs mob drop #2", + "Large Titanite Shard x2", drop=True, + hidden=True), # Guaranteed drop from normal-looking Silver Knight + DS3LocationData("IBV: Roster of Knights - descent, first landing", "Roster of Knights"), + DS3LocationData("IBV: Twinkling Titanite - descent, lizard behind illusory wall", + "Twinkling Titanite", lizard=True, hidden=True), # Behind illusory wall + DS3LocationData("IBV: Twinkling Titanite - central, lizard before plaza", + "Twinkling Titanite", lizard=True), + DS3LocationData("IBV: Large Titanite Shard - Distant Manor, under overhang", + "Large Titanite Shard"), + DS3LocationData("IBV: Siegbräu - Siegward", "Siegbräu", missable=True, npc=True), + DS3LocationData("IBV: Emit Force - Siegward", "Emit Force", missable=True, npc=True), + DS3LocationData("IBV -> ID", None), + + # After winning both Londor Pale Shade invasions + DS3LocationData("FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twice", + "Sneering Mask", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Robe", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Gloves", missable=True, hostile_npc=True), + DS3LocationData("FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twice", + "Pale Shade Trousers", missable=True, hostile_npc=True), + + # Anri of Astora + DS3LocationData("IBV: Ring of the Evil Eye - Anri", "Ring of the Evil Eye", missable=True, + npc=True), + + # Sirris quest after killing Creighton + DS3LocationData("FS: Mail Breaker - Sirris for killing Creighton", "Mail Breaker", + static='99,0:50006080::', missable=True, hostile_npc=True, + npc=True), + DS3LocationData("FS: Silvercat Ring - Sirris for killing Creighton", "Silvercat Ring", + missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Dragonslayer's Axe - Creighton drop", "Dragonslayer's Axe", + missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Creighton's Steel Mask - bridge after killing Creighton", + "Creighton's Steel Mask", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Mail - bridge after killing Creighton", + "Mirrah Chain Mail", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Gloves - bridge after killing Creighton", + "Mirrah Chain Gloves", missable=True, hostile_npc=True, npc=True), + DS3LocationData("IBV: Mirrah Chain Leggings - bridge after killing Creighton", + "Mirrah Chain Leggings", missable=True, hostile_npc=True, npc=True), ], "Irithyll Dungeon": [ - DS3LocationData("ID: Bellowing Dragoncrest Ring", "Bellowing Dragoncrest Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Jailbreaker's Key", "Jailbreaker's Key", DS3LocationCategory.KEY), - DS3LocationData("ID: Prisoner Chief's Ashes", "Prisoner Chief's Ashes", DS3LocationCategory.KEY), - DS3LocationData("ID: Old Sorcerer Hat", "Old Sorcerer Hat", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Coat", "Old Sorcerer Coat", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Gauntlets", "Old Sorcerer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Old Sorcerer Boots", "Old Sorcerer Boots", DS3LocationCategory.ARMOR), - DS3LocationData("ID: Great Magic Shield", "Great Magic Shield", DS3LocationCategory.SPELL), - DS3LocationData("ID: Dragon Torso Stone", "Dragon Torso Stone", DS3LocationCategory.MISC), - DS3LocationData("ID: Lightning Blade", "Lightning Blade", DS3LocationCategory.SPELL), - DS3LocationData("ID: Profaned Coal", "Profaned Coal", DS3LocationCategory.MISC), - DS3LocationData("ID: Xanthous Ashes", "Xanthous Ashes", DS3LocationCategory.MISC), - DS3LocationData("ID: Old Cell Key", "Old Cell Key", DS3LocationCategory.KEY), - DS3LocationData("ID: Pickaxe", "Pickaxe", DS3LocationCategory.WEAPON), - DS3LocationData("ID: Profaned Flame", "Profaned Flame", DS3LocationCategory.SPELL), - DS3LocationData("ID: Covetous Gold Serpent Ring", "Covetous Gold Serpent Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Jailer's Key Ring", "Jailer's Key Ring", DS3LocationCategory.KEY), - DS3LocationData("ID: Dusk Crown Ring", "Dusk Crown Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Dark Clutch Ring", "Dark Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("ID: Karla's Ashes", "Karla's Ashes", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Pointed Hat", "Karla's Pointed Hat", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Coat", "Karla's Coat", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Gloves", "Karla's Gloves", DS3LocationCategory.NPC), - DS3LocationData("ID: Karla's Trousers", "Karla's Trousers", DS3LocationCategory.NPC), + DS3LocationData("ID: Titanite Slab - Siegward", "Titanite Slab", missable=True, + npc=True), + DS3LocationData("ID: Murakumo - Alva drop", "Murakumo", missable=True, + hostile_npc=True), + DS3LocationData("ID: Large Titanite Shard - after bonfire, second cell on left", + "Large Titanite Shard"), + DS3LocationData("ID: Fading Soul - B1 near, main hall", "Fading Soul"), + DS3LocationData("ID: Large Soul of a Nameless Soldier - B2, hall by stairs", + "Large Soul of a Nameless Soldier"), + DS3LocationData("ID: Jailbreaker's Key - B1 far, cell after gate", "Jailbreaker's Key"), + DS3LocationData("ID: Pale Pine Resin - B1 far, cell with broken wall", + "Pale Pine Resin x2"), + DS3LocationData("ID: Simple Gem - B2 far, cell by stairs", "Simple Gem"), + DS3LocationData("ID: Large Soul of a Nameless Soldier - B2 far, by lift", + "Large Soul of a Nameless Soldier"), + DS3LocationData("ID: Large Titanite Shard - B1 far, rightmost cell", + "Large Titanite Shard"), + DS3LocationData("ID: Homeward Bone - path from B2 to pit", "Homeward Bone x2"), + DS3LocationData("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", + "Bellowing Dragoncrest Ring", conditional=True), + DS3LocationData("ID: Soul of a Weary Warrior - by drop to pit", "Soul of a Weary Warrior"), + DS3LocationData("ID: Soul of a Crestfallen Knight - balcony above pit", + "Soul of a Crestfallen Knight"), + DS3LocationData("ID: Lightning Bolt - awning over pit", "Lightning Bolt x9"), + DS3LocationData("ID: Large Titanite Shard - pit #1", "Large Titanite Shard"), + DS3LocationData("ID: Profaned Flame - pit", "Profaned Flame"), + DS3LocationData("ID: Large Titanite Shard - pit #2", "Large Titanite Shard"), + DS3LocationData("ID: Soul of a Weary Warrior - stairs between pit and B3", + "Soul of a Weary Warrior"), + DS3LocationData("ID: Dung Pie - B3, by path from pit", "Dung Pie x4"), + DS3LocationData("ID: Ember - B3 center", "Ember"), + DS3LocationData("ID: Ember - B3 far right", "Ember"), + DS3LocationData("ID: Profaned Coal - B3 far, left cell", "Profaned Coal"), + DS3LocationData("ID: Large Titanite Shard - B3 near, right corner", "Large Titanite Shard"), + DS3LocationData("ID: Old Sorcerer Hat - B2 near, middle cell", "Old Sorcerer Hat"), + DS3LocationData("ID: Old Sorcerer Coat - B2 near, middle cell", "Old Sorcerer Coat"), + DS3LocationData("ID: Old Sorcerer Gauntlets - B2 near, middle cell", + "Old Sorcerer Gauntlets"), + DS3LocationData("ID: Old Sorcerer Boots - B2 near, middle cell", "Old Sorcerer Boots"), + DS3LocationData("ID: Large Soul of a Weary Warrior - just before Profaned Capital", + "Large Soul of a Weary Warrior"), + DS3LocationData("ID: Covetous Gold Serpent Ring - Siegward's cell", + "Covetous Gold Serpent Ring", conditional=True), + DS3LocationData("ID: Lightning Blade - B3 lift, middle platform", "Lightning Blade"), + DS3LocationData("ID: Rusted Coin - after bonfire, first cell on left", "Rusted Coin"), + DS3LocationData("ID: Dusk Crown Ring - B3 far, right cell", "Dusk Crown Ring"), + DS3LocationData("ID: Pickaxe - path from pit to B3", "Pickaxe"), + DS3LocationData("ID: Xanthous Ashes - B3 far, right cell", "Xanthous Ashes", + progression=True), + DS3LocationData("ID: Large Titanite Shard - B1 near, by door", "Large Titanite Shard"), + DS3LocationData("ID: Rusted Gold Coin - after bonfire, last cell on right", + "Rusted Gold Coin"), + DS3LocationData("ID: Old Cell Key - stairs between pit and B3", "Old Cell Key"), + DS3LocationData("ID: Covetous Silver Serpent Ring+1 - pit lift, middle platform", + "Covetous Silver Serpent Ring+1", ngp=True), + DS3LocationData("ID: Dragon Torso Stone - B3, outside lift", "Dragon Torso Stone"), + DS3LocationData("ID: Prisoner Chief's Ashes - B2 near, locked cell by stairs", + "Prisoner Chief's Ashes", progression=True), + DS3LocationData("ID: Great Magic Shield - B2 near, mob drop in far left cell", + "Great Magic Shield", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Corpse-Grub + DS3LocationData("ID: Dragonslayer Lightning Arrow - pit, mimic in hall", + "Dragonslayer Lightning Arrow x10", mimic=True), + DS3LocationData("ID: Titanite Scale - B3 far, mimic in hall", "Titanite Scale x2", + mimic=True), + DS3LocationData("ID: Dark Clutch Ring - stairs between pit and B3, mimic", + "Dark Clutch Ring", mimic=True), + DS3LocationData("ID: Estus Shard - mimic on path from B2 to pit", "Estus Shard", + mimic=True), + DS3LocationData("ID: Titanite Chunk - balcony above pit, lizard", "Titanite Chunk", + lizard=True), + DS3LocationData("ID: Titanite Scale - B2 far, lizard", "Titanite Scale", lizard=True), + + # These are missable because of a bug that causes them to be dropped wherever the giant is + # randomized to, instead of where the miniboss is in vanilla. + DS3LocationData("ID: Dung Pie - pit, miniboss drop", "Dung Pie x4", + miniboss=True, missable=True), # Giant slave drop + DS3LocationData("ID: Titanite Chunk - pit, miniboss drop", "Titanite Chunk", + miniboss=True, missable=True), # Giant Slave Drop + + # Alva (requires ember) + DS3LocationData("ID: Alva Helm - B3 near, by Karla's cell, after killing Alva", "Alva Helm", + missable=True, npc=True), + DS3LocationData("ID: Alva Armor - B3 near, by Karla's cell, after killing Alva", + "Alva Armor", missable=True, npc=True), + DS3LocationData("ID: Alva Gauntlets - B3 near, by Karla's cell, after killing Alva", + "Alva Gauntlets", missable=True, npc=True), + DS3LocationData("ID: Alva Leggings - B3 near, by Karla's cell, after killing Alva", + "Alva Leggings", missable=True, npc=True), ], "Profaned Capital": [ - DS3LocationData("PC: Cursebite Ring", "Cursebite Ring", DS3LocationCategory.RING), - DS3LocationData("PC: Court Sorcerer Hood", "Court Sorcerer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Robe", "Court Sorcerer Robe", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Gloves", "Court Sorcerer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Court Sorcerer Trousers", "Court Sorcerer Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("PC: Wrath of the Gods", "Wrath of the Gods", DS3LocationCategory.SPELL), - DS3LocationData("PC: Logan's Scroll", "Logan's Scroll", DS3LocationCategory.MISC), - DS3LocationData("PC: Eleonora", "Eleonora", DS3LocationCategory.WEAPON), - DS3LocationData("PC: Court Sorcerer's Staff", "Court Sorcerer's Staff", DS3LocationCategory.WEAPON), - DS3LocationData("PC: Greatshield of Glory", "Greatshield of Glory", DS3LocationCategory.SHIELD), - DS3LocationData("PC: Storm Ruler", "Storm Ruler", DS3LocationCategory.KEY), - DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", "Cinders of a Lord - Yhorm the Giant", DS3LocationCategory.KEY), - DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", DS3LocationCategory.BOSS), + DS3LocationData("PC: Soul of Yhorm the Giant", "Soul of Yhorm the Giant", boss=True), + DS3LocationData("PC: Cinders of a Lord - Yhorm the Giant", + "Cinders of a Lord - Yhorm the Giant", static="07,0:50002170::", + prominent=True, progression=True, boss=True), + DS3LocationData("PC: Logan's Scroll - chapel roof, NPC drop", "Logan's Scroll", + hostile_npc=True), # Sorcerer + DS3LocationData("PC: Purging Stone - chapel ground floor", "Purging Stone x3"), + DS3LocationData("PC: Rusted Coin - tower exterior", "Rusted Coin x2"), + DS3LocationData("PC: Rusted Gold Coin - halls above swamp", "Rusted Gold Coin"), + DS3LocationData("PC: Purging Stone - swamp, by chapel ladder", "Purging Stone"), + DS3LocationData("PC: Cursebite Ring - swamp, below halls", "Cursebite Ring"), + DS3LocationData("PC: Poison Gem - swamp, below halls", "Poison Gem"), + DS3LocationData("PC: Shriving Stone - swamp, by chapel door", "Shriving Stone"), + DS3LocationData("PC: Poison Arrow - chapel roof", "Poison Arrow x18"), + DS3LocationData("PC: Rubbish - chapel, down stairs from second floor", "Rubbish"), + DS3LocationData("PC: Onislayer Greatarrow - bridge", "Onislayer Greatarrow x8"), + DS3LocationData("PC: Large Soul of a Weary Warrior - bridge, far end", + "Large Soul of a Weary Warrior"), + DS3LocationData("PC: Rusted Coin - below bridge #1", "Rusted Coin"), + DS3LocationData("PC: Rusted Coin - below bridge #2", "Rusted Coin"), + DS3LocationData("PC: Blooming Purple Moss Clump - walkway above swamp", + "Blooming Purple Moss Clump x3"), + DS3LocationData("PC: Wrath of the Gods - chapel, drop from roof", "Wrath of the Gods"), + DS3LocationData("PC: Onislayer Greatbow - drop from bridge", "Onislayer Greatbow", + hidden=True), # Hidden fall + DS3LocationData("PC: Jailer's Key Ring - hall past chapel", "Jailer's Key Ring", + progression=True), + DS3LocationData("PC: Ember - palace, far room", "Ember"), + DS3LocationData("PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entrance", + "Flame Stoneplate Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("PC: Magic Stoneplate Ring+2 - tower base", "Magic Stoneplate Ring+2", + ngp=True), + DS3LocationData("PC: Court Sorcerer Hood - chapel, second floor", "Court Sorcerer Hood"), + DS3LocationData("PC: Court Sorcerer Robe - chapel, second floor", "Court Sorcerer Robe"), + DS3LocationData("PC: Court Sorcerer Gloves - chapel, second floor", "Court Sorcerer Gloves"), + DS3LocationData("PC: Court Sorcerer Trousers - chapel, second floor", + "Court Sorcerer Trousers"), + DS3LocationData("PC: Storm Ruler - boss room", "Storm Ruler"), + DS3LocationData("PC: Undead Bone Shard - by bonfire", "Undead Bone Shard"), + DS3LocationData("PC: Eleonora - chapel ground floor, kill mob", "Eleonora", + drop=True, + hidden=True), # Guaranteed drop from a normal-looking Monstrosity of Sin + DS3LocationData("PC: Rusted Gold Coin - palace, mimic in far room", "Rusted Gold Coin x2", + mimic=True), + DS3LocationData("PC: Court Sorcerer's Staff - chapel, mimic on second floor", + "Court Sorcerer's Staff", mimic=True), + DS3LocationData("PC: Greatshield of Glory - palace, mimic in far room", + "Greatshield of Glory", mimic=True), + DS3LocationData("PC: Twinkling Titanite - halls above swamp, lizard #1", + "Twinkling Titanite", lizard=True), + DS3LocationData("PC: Twinkling Titanite - halls above swamp, lizard #2", + "Twinkling Titanite", lizard=True), + DS3LocationData("PC: Siegbräu - Siegward after killing boss", "Siegbräu", + missable=True, npc=True), + + # Siegward drops (kill or quest) + DS3LocationData("PC: Storm Ruler - Siegward", "Storm Ruler", static='02,0:50006218::', + missable=True, drop=True, npc=True), + DS3LocationData("PC: Pierce Shield - Siegward", "Pierce Shield", missable=True, + drop=True, npc=True), ], + # We consider "Anor Londo" to be everything accessible only after killing Pontiff. This doesn't + # match up one-to-one with where the game pops up the region name, but it balances items better + # and covers the region that's full of DS1 Anor Londo references. "Anor Londo": [ - DS3LocationData("AL: Giant's Coal", "Giant's Coal", DS3LocationCategory.MISC), - DS3LocationData("AL: Sun Princess Ring", "Sun Princess Ring", DS3LocationCategory.RING), - DS3LocationData("AL: Aldrich's Ruby", "Aldrich's Ruby", DS3LocationCategory.RING), - DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", DS3LocationCategory.KEY), - DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", DS3LocationCategory.BOSS), + DS3LocationData("AL: Soul of Aldrich", "Soul of Aldrich", boss=True), + DS3LocationData("AL: Cinders of a Lord - Aldrich", "Cinders of a Lord - Aldrich", + static='06,0:50002130::', prominent=True, progression=True, + boss=True), + DS3LocationData("AL: Yorshka's Chime - kill Yorshka", "Yorshka's Chime", missable=True, + drop=True, + npc=True), # Hidden walkway, missable because it will break Sirris's quest + DS3LocationData("AL: Drang Twinspears - plaza, NPC drop", "Drang Twinspears", drop=True, + hidden=True), + DS3LocationData("AL: Estus Shard - dark cathedral, by left stairs", "Estus Shard"), + DS3LocationData("AL: Painting Guardian's Curved Sword - prison tower rafters", + "Painting Guardian's Curved Sword", hidden=True), # Invisible walkway + DS3LocationData("AL: Brass Helm - tomb", "Brass Helm", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Armor - tomb", "Brass Armor", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Gauntlets - tomb", "Brass Gauntlets", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Brass Leggings - tomb", "Brass Leggings", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Human Dregs - water reserves", "Human Dregs", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Ember - spiral staircase, bottom", "Ember"), + DS3LocationData("AL: Large Titanite Shard - bottom of the furthest buttress", + "Large Titanite Shard"), + DS3LocationData("AL: Large Titanite Shard - right after light cathedral", + "Large Titanite Shard"), + DS3LocationData("AL: Large Titanite Shard - walkway, side path by cathedral", + "Large Titanite Shard"), + DS3LocationData("AL: Soul of a Weary Warrior - plaza, nearer", "Soul of a Weary Warrior"), + DS3LocationData("AL: Ember - plaza, right side", "Ember"), + DS3LocationData("AL: Ember - plaza, further", "Ember"), + DS3LocationData("AL: Large Titanite Shard - balcony by dead giants", + "Large Titanite Shard"), + DS3LocationData("AL: Dark Stoneplate Ring - by dark stairs up from plaza", + "Dark Stoneplate Ring"), + DS3LocationData("AL: Large Titanite Shard - bottom of the nearest buttress", + "Large Titanite Shard"), + DS3LocationData("AL: Deep Gem - water reserves", "Deep Gem"), + DS3LocationData("AL: Titanite Scale - top of ladder up to buttresses", "Titanite Scale"), + DS3LocationData("AL: Dragonslayer Greatarrow - drop from nearest buttress", + "Dragonslayer Greatarrow x5", static='06,0:53700620::', + hidden=True), # Hidden fall + DS3LocationData("AL: Dragonslayer Greatbow - drop from nearest buttress", + "Dragonslayer Greatbow", static='06,0:53700620::', + hidden=True), # Hidden fall + DS3LocationData("AL: Easterner's Ashes - below top of furthest buttress", + "Easterner's Ashes", progression=True), + DS3LocationData("AL: Painting Guardian Hood - prison tower, rafters", + "Painting Guardian Hood", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Gown - prison tower, rafters", + "Painting Guardian Gown", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Gloves - prison tower, rafters", + "Painting Guardian Gloves", hidden=True), # Invisible walkway + DS3LocationData("AL: Painting Guardian Waistcloth - prison tower, rafters", + "Painting Guardian Waistcloth", hidden=True), # Invisible walkway + DS3LocationData("AL: Soul of a Crestfallen Knight - right of dark cathedral entrance", + "Soul of a Crestfallen Knight"), + DS3LocationData("AL: Moonlight Arrow - dark cathedral, up right stairs", + "Moonlight Arrow x6"), + DS3LocationData("AL: Proof of a Concord Kept - dark cathedral, up left stairs", + "Proof of a Concord Kept"), + DS3LocationData("AL: Large Soul of a Weary Warrior - left of dark cathedral entrance", + "Large Soul of a Weary Warrior"), + DS3LocationData("AL: Giant's Coal - by giant near dark cathedral", "Giant's Coal"), + DS3LocationData("AL: Havel's Ring+2 - prison tower, rafters", "Havel's Ring+2", ngp=True, + hidden=True), # Invisible walkway + DS3LocationData("AL: Ring of Favor+1 - light cathedral, upstairs", "Ring of Favor+1", + ngp=True), + DS3LocationData("AL: Sun Princess Ring - dark cathedral, after boss", "Sun Princess Ring"), + DS3LocationData("AL: Reversal Ring - tomb, chest in corner", "Reversal Ring", + hidden=True), # Behind illusory wall + DS3LocationData("AL: Golden Ritual Spear - light cathedral, mimic upstairs", + "Golden Ritual Spear", mimic=True), + DS3LocationData("AL: Ring of Favor - water reserves, both minibosses", "Ring of Favor", + miniboss=True, + hidden=True), # Sulyvahn's Beast Duo drop, behind illusory wall + DS3LocationData("AL: Blade of the Darkmoon - Yorshka with Darkmoon Loyalty", + "Blade of the Darkmoon", missable=True, drop=True, + npc=True), # Hidden walkway, missable because it will break Sirris's quest + DS3LocationData("AL: Simple Gem - light cathedral, lizard upstairs", "Simple Gem", + lizard=True), + DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #1", + "Twinkling Titanite", lizard=True), + DS3LocationData("AL: Twinkling Titanite - lizard after light cathedral #2", + "Twinkling Titanite", lizard=True), + DS3LocationData("AL: Aldrich's Ruby - dark cathedral, miniboss", "Aldrich's Ruby", + miniboss=True), # Deep Accursed drop + DS3LocationData("AL: Aldrich Faithful - water reserves, talk to McDonnel", "Aldrich Faithful", + hidden=True), # Behind illusory wall + + DS3LocationData("FS: Budding Green Blossom - shop after killing Creighton and AL boss", + "Budding Green Blossom", static='99,0:-1:110000,70000118:', + missable=True, npc=True, + shop=True), # sold by Shrine Maiden after killing Aldrich and helping + # Sirris defeat Creighton + + # Sirris (quest completion) + DS3LocationData("FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "Sunset Shield", missable=True, hostile_npc=True, npc=True), + # In Pit of Hollows after killing Hodrick + DS3LocationData("US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "Sunset Helm", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "Sunset Armor", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "Sunset Gauntlets", missable=True, hostile_npc=True, npc=True), + DS3LocationData("US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + "Sunset Leggings", missable=True, hostile_npc=True, npc=True), + + # Shrine Handmaid after killing Sulyvahn's Beast Duo + DS3LocationData("FS: Helm of Favor - shop after killing water reserve minibosses", + "Helm of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Embraced Armor of Favor - shop after killing water reserve minibosses", + "Embraced Armor of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Gauntlets of Favor - shop after killing water reserve minibosses", + "Gauntlets of Favor", hidden=True, miniboss=True, shop=True), + DS3LocationData("FS: Leggings of Favor - shop after killing water reserve minibosses", + "Leggings of Favor", hidden=True, miniboss=True, shop=True), + + # Anri of Astora + DS3LocationData("AL: Chameleon - tomb after marrying Anri", "Chameleon", missable=True, + npc=True), + DS3LocationData("AL: Anri's Straight Sword - Anri quest", "Anri's Straight Sword", + missable=True, npc=True), + + # Shrine Handmaid after killing Ringfinger Leonhard + # This is listed here even though you can kill Leonhard immediately because we want the + # logic to assume people will do his full quest. Missable because he can disappear forever + # if you use up all your Pale Tongues. + DS3LocationData("FS: Leonhard's Garb - shop after killing Leonhard", + "Leonhard's Garb", hidden=True, npc=True, shop=True, missable=True), + DS3LocationData("FS: Leonhard's Gauntlets - shop after killing Leonhard", + "Leonhard's Gauntlets", hidden=True, npc=True, shop=True, + missable=True), + DS3LocationData("FS: Leonhard's Trousers - shop after killing Leonhard", + "Leonhard's Trousers", hidden=True, npc=True, shop=True, + missable=True), + + # Shrine Handmaid after killing Alrich, Devourer of Gods + DS3LocationData("FS: Smough's Helm - shop after killing AL boss", "Smough's Helm", + boss=True, shop=True), + DS3LocationData("FS: Smough's Armor - shop after killing AL boss", "Smough's Armor", + boss=True, shop=True), + DS3LocationData("FS: Smough's Gauntlets - shop after killing AL boss", "Smough's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Smough's Leggings - shop after killing AL boss", "Smough's Leggings", + boss=True, shop=True), + + # Ringfinger Leonhard (quest or kill) + DS3LocationData("AL: Crescent Moon Sword - Leonhard drop", "Crescent Moon Sword", + missable=True, npc=True), + DS3LocationData("AL: Silver Mask - Leonhard drop", "Silver Mask", missable=True, + npc=True), + DS3LocationData("AL: Soul of Rosaria - Leonhard drop", "Soul of Rosaria", missable=True, + npc=True), + + # Shrine Handmaid after killing Anri or completing their quest + DS3LocationData("FS: Elite Knight Helm - shop after Anri quest", "Elite Knight Helm", + npc=True, shop=True), + DS3LocationData("FS: Elite Knight Armor - shop after Anri quest", "Elite Knight Armor", + npc=True, shop=True), + DS3LocationData("FS: Elite Knight Gauntlets - shop after Anri quest", + "Elite Knight Gauntlets", npc=True, shop=True), + DS3LocationData("FS: Elite Knight Leggings - shop after Anri quest", + "Elite Knight Leggings", npc=True, shop=True), ], "Lothric Castle": [ - DS3LocationData("LC: Hood of Prayer", "Hood of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Robe of Prayer", "Robe of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Skirt of Prayer", "Skirt of Prayer", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Sacred Bloom Shield", "Sacred Bloom Shield", DS3LocationCategory.SHIELD), - DS3LocationData("LC: Winged Knight Helm", "Winged Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Armor", "Winged Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Gauntlets", "Winged Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Winged Knight Leggings", "Winged Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("LC: Greatlance", "Greatlance", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Sniper Crossbow", "Sniper Crossbow", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Spirit Tree Crest Shield", "Spirit Tree Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("LC: Red Tearstone Ring", "Red Tearstone Ring", DS3LocationCategory.RING), - DS3LocationData("LC: Caitha's Chime", "Caitha's Chime", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Braille Divine Tome of Lothric", "Braille Divine Tome of Lothric", DS3LocationCategory.MISC), - DS3LocationData("LC: Knight's Ring", "Knight's Ring", DS3LocationCategory.RING), - DS3LocationData("LC: Irithyll Rapier", "Irithyll Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Sunlight Straight Sword", "Sunlight Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", DS3LocationCategory.BOSS), - DS3LocationData("LC: Grand Archives Key", "Grand Archives Key", DS3LocationCategory.KEY), - DS3LocationData("LC: Gotthard Twinswords", "Gotthard Twinswords", DS3LocationCategory.WEAPON), + DS3LocationData("LC: Soul of Dragonslayer Armour", "Soul of Dragonslayer Armour", + prominent=True, boss=True), + DS3LocationData("LC: Sniper Bolt - moat, right path end", "Sniper Bolt x11"), + DS3LocationData("LC: Sniper Crossbow - moat, right path end", "Sniper Crossbow"), + DS3LocationData("LC: Titanite Scale - dark room, upper balcony", "Titanite Scale"), + DS3LocationData("LC: Titanite Chunk - dark room mid, out door opposite wyvern", + "Titanite Chunk"), + DS3LocationData("LC: Greatlance - overlooking Dragon Barracks bonfire", "Greatlance"), + DS3LocationData("LC: Titanite Chunk - ascent, first balcony", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - ascent, turret before barricades", "Titanite Chunk"), + DS3LocationData("LC: Sacred Bloom Shield - ascent, behind illusory wall", + "Sacred Bloom Shield", hidden=True), # Behind illusory wall + DS3LocationData("LC: Titanite Chunk - ascent, final turret", "Titanite Chunk x2"), + DS3LocationData("LC: Refined Gem - plaza", "Refined Gem"), + DS3LocationData("LC: Soul of a Crestfallen Knight - by lift bottom", + "Soul of a Crestfallen Knight"), + DS3LocationData("LC: Undead Bone Shard - moat, far ledge", "Undead Bone Shard"), + DS3LocationData("LC: Lightning Urn - moat, right path, first room", "Lightning Urn x3"), + DS3LocationData("LC: Titanite Chunk - moat #1", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - moat #2", "Titanite Chunk"), + DS3LocationData("LC: Titanite Chunk - moat, near ledge", "Titanite Chunk"), + DS3LocationData("LC: Caitha's Chime - chapel, drop onto roof", "Caitha's Chime"), + DS3LocationData("LC: Lightning Urn - plaza", "Lightning Urn x6"), + DS3LocationData("LC: Ember - plaza, by gate", "Ember"), + DS3LocationData("LC: Raw Gem - plaza left", "Raw Gem"), + DS3LocationData("LC: Black Firebomb - dark room lower", "Black Firebomb x3"), + DS3LocationData("LC: Pale Pine Resin - dark room upper, by mimic", "Pale Pine Resin"), + DS3LocationData("LC: Large Soul of a Weary Warrior - main hall, by lever", + "Large Soul of a Weary Warrior"), + DS3LocationData("LC: Sunlight Medal - by lift top", "Sunlight Medal"), + DS3LocationData("LC: Soul of a Crestfallen Knight - wyvern room, balcony", + "Soul of a Crestfallen Knight", hidden=True), # Hidden fall + DS3LocationData("LC: Titanite Chunk - altar roof", "Titanite Chunk"), + DS3LocationData("LC: Titanite Scale - dark room mid, out door opposite wyvern", + "Titanite Scale"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - moat, right path", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Knight's Ring - altar", "Knight's Ring"), + DS3LocationData("LC: Ember - main hall, left of stairs", "Ember"), + DS3LocationData("LC: Large Soul of a Weary Warrior - ascent, last turret", + "Large Soul of a Weary Warrior"), + DS3LocationData("LC: Ember - by Dragon Barracks bonfire", "Ember"), + DS3LocationData("LC: Twinkling Titanite - ascent, side room", "Twinkling Titanite"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - dark room mid", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Ember - plaza center", "Ember"), + DS3LocationData("LC: Winged Knight Helm - ascent, behind illusory wall", + "Winged Knight Helm", hidden=True), + DS3LocationData("LC: Winged Knight Armor - ascent, behind illusory wall", + "Winged Knight Armor", hidden=True), + DS3LocationData("LC: Winged Knight Gauntlets - ascent, behind illusory wall", + "Winged Knight Gauntlets", hidden=True), + DS3LocationData("LC: Winged Knight Leggings - ascent, behind illusory wall", + "Winged Knight Leggings", hidden=True), + DS3LocationData("LC: Rusted Coin - chapel", "Rusted Coin x2"), + DS3LocationData("LC: Braille Divine Tome of Lothric - wyvern room", + "Braille Divine Tome of Lothric", hidden=True), # Hidden fall + DS3LocationData("LC: Red Tearstone Ring - chapel, drop onto roof", "Red Tearstone Ring"), + DS3LocationData("LC: Twinkling Titanite - moat, left side", "Twinkling Titanite x2"), + DS3LocationData("LC: Large Soul of a Nameless Soldier - plaza left, by pillar", + "Large Soul of a Nameless Soldier"), + DS3LocationData("LC: Titanite Scale - altar", "Titanite Scale x3"), + DS3LocationData("LC: Titanite Scale - chapel, chest", "Titanite Scale"), + DS3LocationData("LC: Hood of Prayer", "Hood of Prayer"), + DS3LocationData("LC: Robe of Prayer - ascent, chest at beginning", "Robe of Prayer"), + DS3LocationData("LC: Skirt of Prayer - ascent, chest at beginning", "Skirt of Prayer"), + DS3LocationData("LC: Spirit Tree Crest Shield - basement, chest", + "Spirit Tree Crest Shield"), + DS3LocationData("LC: Titanite Scale - basement, chest", "Titanite Scale"), + DS3LocationData("LC: Twinkling Titanite - basement, chest #1", "Twinkling Titanite"), + DS3LocationData("LC: Twinkling Titanite - basement, chest #2", "Twinkling Titanite x2"), + DS3LocationData("LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop down", + "Life Ring+2", ngp=True, hidden=True), # Hidden fall + DS3LocationData("LC: Dark Stoneplate Ring+1 - wyvern room, balcony", + "Dark Stoneplate Ring+1", ngp=True, hidden=True), # Hidden fall + DS3LocationData("LC: Thunder Stoneplate Ring+2 - chapel, drop onto roof", + "Thunder Stoneplate Ring+2", ngp=True), + DS3LocationData("LC: Sunlight Straight Sword - wyvern room, mimic", + "Sunlight Straight Sword", mimic=True, hidden=True), # Hidden fall + DS3LocationData("LC: Titanite Scale - dark room, upper, mimic", "Titanite Scale x3", + mimic=True), + DS3LocationData("LC: Ember - wyvern room, wyvern foot mob drop", "Ember x2", + drop=True, hidden=True), # Hidden fall, Pus of Man Wyvern drop + DS3LocationData("LC: Titanite Chunk - wyvern room, wyvern foot mob drop", "Titanite Chunk x2", + drop=True, hidden=True), # Hidden fall, Pus of Man Wyvern drop + DS3LocationData("LC: Ember - dark room mid, pus of man mob drop", "Ember x2", + drop=True), # Pus of Man Wyvern drop + DS3LocationData("LC: Titanite Chunk - dark room mid, pus of man mob drop", + "Titanite Chunk x2"), + DS3LocationData("LC: Irithyll Rapier - basement, miniboss drop", "Irithyll Rapier", + miniboss=True), # Boreal Outrider drop + DS3LocationData("LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizard", + "Twinkling Titanite x2", lizard=True, missable=True), + DS3LocationData("LC: Twinkling Titanite - moat, right path, lizard", + "Twinkling Titanite x2", lizard=True, missable=True), + DS3LocationData("LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bosses", + "Gotthard Twinswords", conditional=True), + DS3LocationData("LC: Grand Archives Key - by Grand Archives door, after PC and AL bosses", + "Grand Archives Key", prominent=True, progression=True, + conditional=True), + DS3LocationData("LC: Titanite Chunk - down stairs after boss", "Titanite Chunk"), + + # Eygon of Carim (kill or quest) + DS3LocationData("FS: Morne's Great Hammer - Eygon", "Morne's Great Hammer", npc=True), + DS3LocationData("FS: Moaning Shield - Eygon", "Moaning Shield", npc=True), + + # Shrine Handmaid after killing Dragonslayer Armour (or Eygon of Carim) + DS3LocationData("FS: Dancer's Crown - shop after killing LC entry boss", "Dancer's Crown", + boss=True, shop=True), + DS3LocationData("FS: Dancer's Armor - shop after killing LC entry boss", "Dancer's Armor", + boss=True, shop=True), + DS3LocationData("FS: Dancer's Gauntlets - shop after killing LC entry boss", + "Dancer's Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Dancer's Leggings - shop after killing LC entry boss", + "Dancer's Leggings", boss=True, shop=True), + + # Shrine Handmaid after killing Dragonslayer Armour (or Eygon of Carim) + DS3LocationData("FS: Morne's Helm - shop after killing Eygon or LC boss", "Morne's Helm", + boss=True, shop=True), + DS3LocationData("FS: Morne's Armor - shop after killing Eygon or LC boss", "Morne's Armor", + boss=True, shop=True), + DS3LocationData("FS: Morne's Gauntlets - shop after killing Eygon or LC boss", + "Morne's Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Morne's Leggings - shop after killing Eygon or LC boss", + "Morne's Leggings", boss=True, shop=True), ], "Consumed King's Garden": [ - DS3LocationData("CKG: Dragonscale Ring", "Dragonscale Ring", DS3LocationCategory.RING), - DS3LocationData("CKG: Shadow Mask", "Shadow Mask", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Garb", "Shadow Garb", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Gauntlets", "Shadow Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Shadow Leggings", "Shadow Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("CKG: Claw", "Claw", DS3LocationCategory.WEAPON), - DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", DS3LocationCategory.BOSS), - DS3LocationData("CKG: Magic Stoneplate Ring", "Magic Stoneplate Ring", DS3LocationCategory.RING), + DS3LocationData("CKG: Soul of Consumed Oceiros", "Soul of Consumed Oceiros", + prominent=True, boss=True), + # Could classify this as "hidden" because it's midway down an elevator, but the elevator is + # so slow and the midway point is so obvious that it's not actually hard to find. + DS3LocationData("CKG: Estus Shard - balcony", "Estus Shard"), + DS3LocationData("CKG: Shadow Mask - under center platform", "Shadow Mask"), + DS3LocationData("CKG: Shadow Garb - under rotunda", "Shadow Garb"), + DS3LocationData("CKG: Shadow Gauntlets - under rotunda", "Shadow Gauntlets"), + DS3LocationData("CKG: Shadow Leggings - under rotunda", "Shadow Leggings"), + DS3LocationData("CKG: Black Firebomb - under rotunda", "Black Firebomb x2"), + DS3LocationData("CKG: Claw - under rotunda", "Claw"), + DS3LocationData("CKG: Titanite Chunk - up lone stairway", "Titanite Chunk"), + DS3LocationData("CKG: Dragonscale Ring - shortcut, leave halfway down lift", + "Dragonscale Ring"), + DS3LocationData("CKG: Human Pine Resin - toxic pool, past rotunda", "Human Pine Resin"), + DS3LocationData("CKG: Titanite Chunk - shortcut", "Titanite Chunk"), + DS3LocationData("CKG: Titanite Chunk - balcony, drop onto rubble", "Titanite Chunk"), + DS3LocationData("CKG: Soul of a Weary Warrior - before first lift", + "Soul of a Weary Warrior"), + DS3LocationData("CKG: Dark Gem - under lone stairway", "Dark Gem"), + DS3LocationData("CKG: Titanite Scale - shortcut", "Titanite Scale"), + DS3LocationData("CKG: Human Pine Resin - pool by lift", "Human Pine Resin x2"), + DS3LocationData("CKG: Titanite Chunk - right of shortcut lift bottom", "Titanite Chunk"), + DS3LocationData("CKG: Ring of Sacrifice - under balcony", "Ring of Sacrifice"), + DS3LocationData("CKG: Wood Grain Ring+1 - by first elevator bottom", "Wood Grain Ring+1", + ngp=True), + DS3LocationData("CKG: Sage Ring+2 - balcony, drop onto rubble, jump back", "Sage Ring+2", + ngp=True, hidden=True), + DS3LocationData("CKG: Titanite Scale - tomb, chest #1", "Titanite Scale"), + DS3LocationData("CKG: Titanite Scale - tomb, chest #2", "Titanite Scale"), + DS3LocationData("CKG: Magic Stoneplate Ring - mob drop before boss", + "Magic Stoneplate Ring", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Cathedral Knight + + # After Oceiros's boss room, only once the Drakeblood summon in AP has been killed + DS3LocationData("CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "Drakeblood Helm", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "Drakeblood Armor", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "Drakeblood Gauntlets", hostile_npc=True, hidden=True), + DS3LocationData("CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + "Drakeblood Leggings", hostile_npc=True, hidden=True), ], "Grand Archives": [ - DS3LocationData("GA: Avelyn", "Avelyn", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Witch's Locks", "Witch's Locks", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Power Within", "Power Within", DS3LocationCategory.SPELL), - DS3LocationData("GA: Scholar Ring", "Scholar Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Soul Stream", "Soul Stream", DS3LocationCategory.SPELL), - DS3LocationData("GA: Fleshbite Ring", "Fleshbite Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Crystal Chime", "Crystal Chime", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Golden Wing Crest Shield", "Golden Wing Crest Shield", DS3LocationCategory.SHIELD), - DS3LocationData("GA: Onikiri and Ubadachi", "Onikiri and Ubadachi", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Hunter's Ring", "Hunter's Ring", DS3LocationCategory.RING), - DS3LocationData("GA: Divine Pillars of Light", "Divine Pillars of Light", DS3LocationCategory.SPELL), - DS3LocationData("GA: Cinders of a Lord - Lothric Prince", "Cinders of a Lord - Lothric Prince", DS3LocationCategory.KEY), - DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", DS3LocationCategory.BOSS), - DS3LocationData("GA: Sage's Crystal Staff", "Sage's Crystal Staff", DS3LocationCategory.WEAPON), - DS3LocationData("GA: Outrider Knight Helm", "Outrider Knight Helm", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Armor", "Outrider Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Gauntlets", "Outrider Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Outrider Knight Leggings", "Outrider Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("GA: Crystal Scroll", "Crystal Scroll", DS3LocationCategory.MISC), + DS3LocationData("GA: Titanite Slab - final elevator secret", "Titanite Slab", + hidden=True), + DS3LocationData("GA: Soul of the Twin Princes", "Soul of the Twin Princes", boss=True), + DS3LocationData("GA: Cinders of a Lord - Lothric Prince", + "Cinders of a Lord - Lothric Prince", + static="09,0:50002040::", prominent=True, progression=True, + boss=True), + DS3LocationData("GA: Onikiri and Ubadachi - outside 5F, NPC drop", "Onikiri and Ubadachi", + hostile_npc=True, # Black Hand Kamui drop + missable=True), # This is placed at the location the NPC gets randomized + # to, which makes it hard to include in logic. + DS3LocationData("GA: Golden Wing Crest Shield - outside 5F, NPC drop", + "Golden Wing Crest Shield", + hostile_npc=True), # Lion Knight Albert drop + DS3LocationData("GA: Sage's Crystal Staff - outside 5F, NPC drop", + "Sage's Crystal Staff", + hostile_npc=True), # Daughter of Crystal Kriemhild drop + DS3LocationData("GA: Titanite Chunk - 1F, up right stairs", "Titanite Chunk"), + DS3LocationData("GA: Titanite Chunk - 1F, path from wax pool", "Titanite Chunk"), + DS3LocationData("GA: Soul of a Crestfallen Knight - 1F, loop left after drop", + "Soul of a Crestfallen Knight"), + DS3LocationData("GA: Titanite Chunk - 1F, balcony", "Titanite Chunk"), + DS3LocationData("GA: Fleshbite Ring - up stairs from 4F", "Fleshbite Ring"), + DS3LocationData("GA: Soul of a Crestfallen Knight - path to dome", + "Soul of a Crestfallen Knight"), + DS3LocationData("GA: Soul of a Nameless Soldier - dark room", "Soul of a Nameless Soldier"), + DS3LocationData("GA: Crystal Chime - 1F, path from wax pool", "Crystal Chime"), + DS3LocationData("GA: Titanite Scale - dark room, upstairs", "Titanite Scale"), + DS3LocationData("GA: Estus Shard - dome, far balcony", "Estus Shard"), + DS3LocationData("GA: Homeward Bone - 2F early balcony", "Homeward Bone x3"), + DS3LocationData("GA: Titanite Scale - 2F, titanite scale atop bookshelf", "Titanite Scale", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - 2F, by wax pool", "Titanite Chunk"), + DS3LocationData("GA: Hollow Gem - rooftops lower, in hall", "Hollow Gem", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Scale - 3F, corner up stairs", "Titanite Scale"), + DS3LocationData("GA: Titanite Scale - 1F, up stairs on bookshelf", "Titanite Scale"), + DS3LocationData("GA: Titanite Scale - 3F, by ladder to 2F late", "Titanite Scale", + hidden=True), # Hidden by a table + DS3LocationData("GA: Shriving Stone - 2F late, by ladder from 3F", "Shriving Stone"), + DS3LocationData("GA: Large Soul of a Crestfallen Knight - 4F, back", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("GA: Titanite Chunk - rooftops, balcony", "Titanite Chunk"), + DS3LocationData("GA: Titanite Scale - rooftops lower, path to 2F", "Titanite Scale x3", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - rooftops lower, ledge by buttress", "Titanite Chunk", + hidden=True), # Hidden fall + DS3LocationData("GA: Soul of a Weary Warrior - rooftops, by lizards", + "Soul of a Weary Warrior"), + DS3LocationData("GA: Titanite Chunk - rooftops, just before 5F", "Titanite Chunk"), + DS3LocationData("GA: Ember - 5F, by entrance", "Ember"), + DS3LocationData("GA: Blessed Gem - rafters", "Blessed Gem"), + DS3LocationData("GA: Titanite Chunk - 5F, far balcony", "Titanite Chunk x2"), + DS3LocationData("GA: Large Soul of a Crestfallen Knight - outside 5F", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("GA: Avelyn - 1F, drop from 3F onto bookshelves", "Avelyn", + hidden=True), # Hidden fall + DS3LocationData("GA: Titanite Chunk - 2F, right after dark room", "Titanite Chunk"), + DS3LocationData("GA: Hunter's Ring - dome, very top", "Hunter's Ring"), + DS3LocationData("GA: Divine Pillars of Light - cage above rafters", + "Divine Pillars of Light"), + DS3LocationData("GA: Power Within - dark room, behind retractable bookshelf", + "Power Within", hidden=True), # Switch in darkened room + DS3LocationData("GA: Sage Ring+1 - rafters, second level down", "Sage Ring+1", ngp=True), + DS3LocationData("GA: Lingering Dragoncrest Ring+2 - dome, room behind spire", + "Lingering Dragoncrest Ring+2", ngp=True), + DS3LocationData("GA: Divine Blessing - rafters, down lower level ladder", + "Divine Blessing"), + DS3LocationData("GA: Twinkling Titanite - rafters, down lower level ladder", + "Twinkling Titanite x3"), + DS3LocationData("GA: Witch's Locks - dark room, behind retractable bookshelf", + "Witch's Locks", hidden=True), # Switch in darkened room + DS3LocationData("GA: Titanite Slab - 1F, after pulling 2F switch", "Titanite Slab", + hidden=True), + DS3LocationData("GA: Titanite Scale - 4F, chest by exit", "Titanite Scale x3"), + DS3LocationData("GA: Soul Stream - 3F, behind illusory wall", "Soul Stream", + hidden=True), # Behind illusory wall + DS3LocationData("GA: Scholar Ring - 2F, between late and early", "Scholar Ring"), + DS3LocationData("GA: Undead Bone Shard - 5F, by entrance", "Undead Bone Shard"), + DS3LocationData("GA: Titanite Slab - dome, kill all mobs", "Titanite Slab", + drop=True, + hidden=True), # Guaranteed drop from killing all Winged Knights + DS3LocationData("GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Helm", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Armor", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Gauntlets", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss drop", + "Outrider Knight Leggings", miniboss=True, + hidden=True), # Behind illusory wall, Outrider Knight drop + DS3LocationData("GA: Crystal Scroll - 2F late, miniboss drop", "Crystal Scroll", + miniboss=True), # Crystal Sage drop + DS3LocationData("GA: Twinkling Titanite - dark room, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Chaos Gem - dark room, lizard", "Chaos Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - 1F, lizard by drop", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Crystal Gem - 1F, lizard by drop", "Crystal Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - 2F, lizard by entrance", "Twinkling Titanite x2", + lizard=True), + DS3LocationData("GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizard", + "Titanite Scale x2", lizard=True, hidden=True), # Hidden fall + DS3LocationData("GA: Twinkling Titanite - rooftops, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Heavy Gem - rooftops, lizard", "Heavy Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - rooftops, lizard #2", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Sharp Gem - rooftops, lizard", "Sharp Gem", lizard=True), + DS3LocationData("GA: Twinkling Titanite - up stairs from 4F, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("GA: Refined Gem - up stairs from 4F, lizard", "Refined Gem", + lizard=True), + DS3LocationData("GA: Twinkling Titanite - dark room, lizard #2", "Twinkling Titanite x2", + lizard=True), + + # Shrine Handmaid after killing NPCs + DS3LocationData("FS: Faraam Helm - shop after killing GA NPC", "Faraam Helm", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Armor - shop after killing GA NPC", "Faraam Armor", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Gauntlets - shop after killing GA NPC", "Faraam Gauntlets", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Faraam Boots - shop after killing GA NPC", "Faraam Boots", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Black Hand Hat - shop after killing GA NPC", "Black Hand Hat", + hidden=True, hostile_npc=True, shop=True), + DS3LocationData("FS: Black Hand Armor - shop after killing GA NPC", "Black Hand Armor", + hidden=True, hostile_npc=True, shop=True), + + # Shrine Handmaid after killing Lothric, Younger Prince + DS3LocationData("FS: Lorian's Helm - shop after killing GA boss", "Lorian's Helm", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Armor - shop after killing GA boss", "Lorian's Armor", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Gauntlets - shop after killing GA boss", "Lorian's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Lorian's Leggings - shop after killing GA boss", "Lorian's Leggings", + boss=True, shop=True), + + # Sirris quest completion + beat Twin Princes + DS3LocationData("FS: Sunless Talisman - Sirris, kill GA boss", "Sunless Talisman", + missable=True, npc=True), + DS3LocationData("FS: Sunless Veil - shop, Sirris quest, kill GA boss", "Sunless Veil", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Armor - shop, Sirris quest, kill GA boss", "Sunless Armor", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "Sunless Gauntlets", missable=True, npc=True, shop=True), + DS3LocationData("FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + "Sunless Leggings", missable=True, npc=True, shop=True), + + # Unbreakable Patches + DS3LocationData("FS: Hidden Blessing - Patches after searching GA", "Hidden Blessing", + missable=True, npc=True, shop=True), ], "Untended Graves": [ - DS3LocationData("UG: Ashen Estus Ring", "Ashen Estus Ring", DS3LocationCategory.RING), - DS3LocationData("UG: Black Knight Glaive", "Black Knight Glaive", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Hornet Ring", "Hornet Ring", DS3LocationCategory.RING), - DS3LocationData("UG: Chaos Blade", "Chaos Blade", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Blacksmith Hammer", "Blacksmith Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("UG: Eyes of a Fire Keeper", "Eyes of a Fire Keeper", DS3LocationCategory.KEY), - DS3LocationData("UG: Coiled Sword Fragment", "Coiled Sword Fragment", DS3LocationCategory.MISC), - DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", DS3LocationCategory.BOSS), + DS3LocationData("UG: Soul of Champion Gundyr", "Soul of Champion Gundyr", prominent=True, + boss=True), + DS3LocationData("UG: Priestess Ring - shop", "Priestess Ring", shop=True), + DS3LocationData("UG: Shriving Stone - swamp, by bonfire", "Shriving Stone"), + DS3LocationData("UG: Titanite Chunk - swamp, left path by fountain", "Titanite Chunk"), + DS3LocationData("UG: Soul of a Crestfallen Knight - swamp, center", + "Soul of a Crestfallen Knight"), + DS3LocationData("UG: Titanite Chunk - swamp, right path by fountain", "Titanite Chunk"), + DS3LocationData("UG: Ashen Estus Ring - swamp, path opposite bonfire", "Ashen Estus Ring"), + DS3LocationData("UG: Black Knight Glaive - boss arena", "Black Knight Glaive"), + DS3LocationData("UG: Hidden Blessing - cemetery, behind coffin", "Hidden Blessing"), + DS3LocationData("UG: Eyes of a Fire Keeper - shrine, Irina's room", "Eyes of a Fire Keeper", + hidden=True), # Illusory wall + DS3LocationData("UG: Soul of a Crestfallen Knight - environs, above shrine entrance", + "Soul of a Crestfallen Knight"), + DS3LocationData("UG: Blacksmith Hammer - shrine, Andre's room", "Blacksmith Hammer"), + DS3LocationData("UG: Chaos Blade - environs, left of shrine", "Chaos Blade"), + DS3LocationData("UG: Hornet Ring - environs, right of main path after killing FK boss", + "Hornet Ring", conditional=True), + DS3LocationData("UG: Coiled Sword Fragment - shrine, dead bonfire", "Coiled Sword Fragment", + boss=True), + DS3LocationData("UG: Life Ring+3 - shrine, behind big throne", "Life Ring+3", ngp=True), + DS3LocationData("UG: Ring of Steel Protection+1 - environs, behind bell tower", + "Ring of Steel Protection+1", ngp=True), + + # Yuria shop, or Shrine Handmaiden with Hollow's Ashes + # This is here because this is where the ashes end up if you kill Yoel or Yuria + DS3LocationData("FS: Ring of Sacrifice - Yuria shop", "Ring of Sacrifice", + static='99,0:-1:40000,110000,70000107,70000116:', npc=True, + shop=True), + + # Untended Graves Handmaid + # All shop items are missable because she can be killed, except Priestess ring because she + # drops it on death anyway. + DS3LocationData("UG: Ember - shop", "Ember", shop=True, missable=True), + # Untended Graves Handmaid after killing Abyss Watchers + DS3LocationData("UG: Wolf Knight Helm - shop after killing FK boss", "Wolf Knight Helm", + boss=True, shop=True, conditional=True, + missable=True), + DS3LocationData("UG: Wolf Knight Armor - shop after killing FK boss", + "Wolf Knight Armor", boss=True, shop=True, missable=True), + DS3LocationData("UG: Wolf Knight Gauntlets - shop after killing FK boss", + "Wolf Knight Gauntlets", boss=True, shop=True, missable=True), + DS3LocationData("UG: Wolf Knight Leggings - shop after killing FK boss", + "Wolf Knight Leggings", boss=True, shop=True, missable=True), + + # Shrine Handmaid after killing Champion Gundyr + DS3LocationData("FS: Gundyr's Helm - shop after killing UG boss", "Gundyr's Helm", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Armor - shop after killing UG boss", "Gundyr's Armor", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Gauntlets - shop after killing UG boss", "Gundyr's Gauntlets", + boss=True, shop=True), + DS3LocationData("FS: Gundyr's Leggings - shop after killing UG boss", "Gundyr's Leggings", + boss=True, shop=True), ], "Archdragon Peak": [ - DS3LocationData("AP: Lightning Clutch Ring", "Lightning Clutch Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Ancient Dragon Greatshield", "Ancient Dragon Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("AP: Ring of Steel Protection", "Ring of Steel Protection", DS3LocationCategory.RING), - DS3LocationData("AP: Calamity Ring", "Calamity Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Drakeblood Greatsword", "Drakeblood Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Dragonslayer Spear", "Dragonslayer Spear", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Thunder Stoneplate Ring", "Thunder Stoneplate Ring", DS3LocationCategory.RING), - DS3LocationData("AP: Great Magic Barrier", "Great Magic Barrier", DS3LocationCategory.SPELL), - DS3LocationData("AP: Dragon Chaser's Ashes", "Dragon Chaser's Ashes", DS3LocationCategory.MISC), - DS3LocationData("AP: Twinkling Dragon Torso Stone", "Twinkling Dragon Torso Stone", DS3LocationCategory.MISC), - DS3LocationData("AP: Dragonslayer Helm", "Dragonslayer Helm", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Armor", "Dragonslayer Armor", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Gauntlets", "Dragonslayer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Dragonslayer Leggings", "Dragonslayer Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("AP: Ricard's Rapier", "Ricard's Rapier", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", DS3LocationCategory.BOSS), - DS3LocationData("AP: Dragon Tooth", "Dragon Tooth", DS3LocationCategory.WEAPON), - DS3LocationData("AP: Havel's Greatshield", "Havel's Greatshield", DS3LocationCategory.SHIELD), + DS3LocationData("AP: Dragon Head Stone - fort, boss drop", "Dragon Head Stone", + prominent=True, boss=True), + DS3LocationData("AP: Soul of the Nameless King", "Soul of the Nameless King", + prominent=True, boss=True), + DS3LocationData("AP: Dragon Tooth - belfry roof, NPC drop", "Dragon Tooth", + hostile_npc=True), # Havel Knight drop + DS3LocationData("AP: Havel's Greatshield - belfry roof, NPC drop", "Havel's Greatshield", + hostile_npc=True), # Havel Knight drop + DS3LocationData("AP: Drakeblood Greatsword - mausoleum, NPC drop", "Drakeblood Greatsword", + hostile_npc=True), + DS3LocationData("AP: Ricard's Rapier - belfry, NPC drop", "Ricard's Rapier", + hostile_npc=True), + DS3LocationData("AP: Lightning Clutch Ring - intro, left of boss door", + "Lightning Clutch Ring"), + DS3LocationData("AP: Stalk Dung Pie - fort overlook", "Stalk Dung Pie x6"), + DS3LocationData("AP: Titanite Chunk - fort, second room balcony", "Titanite Chunk"), + DS3LocationData("AP: Titanite Scale - mausoleum, downstairs balcony #1", + "Titanite Scale"), + DS3LocationData("AP: Soul of a Weary Warrior - intro, first cliff edge", + "Soul of a Weary Warrior"), + DS3LocationData("AP: Titanite Chunk - intro, left before archway", "Titanite Chunk"), + DS3LocationData("AP: Lightning Gem - intro, side rise", "Lightning Gem"), + DS3LocationData("AP: Homeward Bone - intro, path to bonfire", "Homeward Bone x2"), + DS3LocationData("AP: Soul of a Nameless Soldier - intro, right before archway", + "Soul of a Nameless Soldier"), + DS3LocationData("AP: Titanite Chunk - intro, archway corner", "Titanite Chunk"), + DS3LocationData("AP: Ember - fort overlook #1", "Ember"), + DS3LocationData("AP: Large Soul of a Weary Warrior - fort, center", + "Large Soul of a Weary Warrior"), + DS3LocationData("AP: Large Soul of a Nameless Soldier - fort, by stairs to first room", + "Large Soul of a Nameless Soldier"), + DS3LocationData("AP: Lightning Urn - fort, left of first room entrance", + "Lightning Urn x4"), + DS3LocationData("AP: Lightning Bolt - rotunda", "Lightning Bolt x12"), + DS3LocationData("AP: Titanite Chunk - rotunda", "Titanite Chunk x2"), + # Not 100% sure about this location name, can't find this on any maps + DS3LocationData("AP: Dung Pie - fort, landing after second room", "Dung Pie x3"), + DS3LocationData("AP: Titanite Scale - mausoleum, downstairs balcony #2", "Titanite Scale"), + DS3LocationData("AP: Soul of a Weary Warrior - walkway, building window", + "Soul of a Weary Warrior"), + DS3LocationData("AP: Soul of a Crestfallen Knight - mausoleum, upstairs", + "Soul of a Crestfallen Knight"), + DS3LocationData("AP: Titanite Chunk - intro, behind rock", "Titanite Chunk"), + DS3LocationData("AP: Ember - fort overlook #2", "Ember"), + DS3LocationData("AP: Thunder Stoneplate Ring - walkway, up ladder", + "Thunder Stoneplate Ring"), + DS3LocationData("AP: Titanite Scale - mausoleum, upstairs balcony", "Titanite Scale"), + DS3LocationData("AP: Ember - belfry, below bell", "Ember"), + DS3LocationData("AP: Ancient Dragon Greatshield - intro, on archway", + "Ancient Dragon Greatshield"), + DS3LocationData("AP: Large Soul of a Crestfallen Knight - summit, by fountain", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("AP: Dragon Chaser's Ashes - summit, side path", "Dragon Chaser's Ashes", + progression=True), + DS3LocationData("AP: Ember - intro, by bonfire", "Ember"), + DS3LocationData("AP: Dragonslayer Spear - gate after mausoleum", "Dragonslayer Spear"), + DS3LocationData("AP: Dragonslayer Helm - plaza", "Dragonslayer Helm"), + DS3LocationData("AP: Dragonslayer Armor - plaza", "Dragonslayer Armor"), + DS3LocationData("AP: Dragonslayer Gauntlets - plaza", "Dragonslayer Gauntlets"), + DS3LocationData("AP: Dragonslayer Leggings - plaza", "Dragonslayer Leggings"), + DS3LocationData("AP: Twinkling Titanite - fort, end of rafters", "Twinkling Titanite x2"), + DS3LocationData("AP: Twinkling Titanite - fort, down second room balcony ladder", + "Twinkling Titanite x2"), + DS3LocationData("AP: Titanite Slab - belfry roof", "Titanite Slab"), + DS3LocationData("AP: Great Magic Barrier - drop off belfry roof", "Great Magic Barrier", + hidden=True), # Hidden fall + DS3LocationData("AP: Titanite Slab - plaza", "Titanite Slab"), + DS3LocationData("AP: Ring of Steel Protection - fort overlook, beside stairs", + "Ring of Steel Protection"), + DS3LocationData("AP: Havel's Ring+1 - summit, after building", "Havel's Ring+1", + ngp=True), + DS3LocationData("AP: Covetous Gold Serpent Ring+2 - plaza", "Covetous Gold Serpent Ring+2", + ngp=True), + DS3LocationData("AP: Titanite Scale - walkway building", "Titanite Scale x3"), + DS3LocationData("AP: Twinkling Titanite - belfry, by ladder to roof", + "Twinkling Titanite x3"), + DS3LocationData("AP: Twinkling Dragon Torso Stone - summit, gesture at altar", + "Twinkling Dragon Torso Stone", hidden=True), # Requires gesture + DS3LocationData("AP: Calamity Ring - mausoleum, gesture at altar", "Calamity Ring", + hidden=True), # Requires gesture + DS3LocationData("AP: Twinkling Titanite - walkway building, lizard", + "Twinkling Titanite x3", lizard=True), + DS3LocationData("AP: Titanite Chunk - walkway, miniboss drop", "Titanite Chunk x6", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("AP: Titanite Scale - walkway, miniboss drop", "Titanite Scale x3", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("AP: Twinkling Titanite - walkway, miniboss drop", "Twinkling Titanite x3", + miniboss=True), # Wyvern miniboss drop + DS3LocationData("FS: Hawkwood's Swordgrass - Andre after gesture in AP summit", + "Hawkwood's Swordgrass", conditional=True, hidden=True), + + # Shrine Handmaid after killing Nameless King + DS3LocationData("FS: Golden Crown - shop after killing AP boss", "Golden Crown", + boss=True, shop=True), + DS3LocationData("FS: Dragonscale Armor - shop after killing AP boss", "Dragonscale Armor", + boss=True, shop=True), + DS3LocationData("FS: Golden Bracelets - shop after killing AP boss", "Golden Bracelets", + boss=True, shop=True), + DS3LocationData("FS: Dragonscale Waistcloth - shop after killing AP boss", + "Dragonscale Waistcloth", boss=True, shop=True), + DS3LocationData("FK: Twinkling Dragon Head Stone - Hawkwood drop", + "Twinkling Dragon Head Stone", missable=True, + npc=True), # Hawkwood (quest) + ], + "Kiln of the First Flame": [ + DS3LocationData("KFF: Soul of the Lords", "Soul of the Lords", boss=True), + + # Shrine Handmaid after placing all Cinders of a Lord + DS3LocationData("FS: Titanite Slab - shop after placing all Cinders", "Titanite Slab", + static='99,0:-1:9210,110000:', hidden=True), + DS3LocationData("FS: Firelink Helm - shop after placing all Cinders", "Firelink Helm", + boss=True, shop=True), + DS3LocationData("FS: Firelink Armor - shop after placing all Cinders", "Firelink Armor", + boss=True, shop=True), + DS3LocationData("FS: Firelink Gauntlets - shop after placing all Cinders", + "Firelink Gauntlets", boss=True, shop=True), + DS3LocationData("FS: Firelink Leggings - shop after placing all Cinders", + "Firelink Leggings", boss=True, shop=True), + + # Yuria (quest, after Soul of Cinder) + DS3LocationData("FS: Billed Mask - Yuria after killing KFF boss", "Billed Mask", + missable=True, npc=True), + DS3LocationData("FS: Black Dress - Yuria after killing KFF boss", "Black Dress", + missable=True, npc=True), + DS3LocationData("FS: Black Gauntlets - Yuria after killing KFF boss", "Black Gauntlets", + missable=True, npc=True), + DS3LocationData("FS: Black Leggings - Yuria after killing KFF boss", "Black Leggings", + missable=True, npc=True), ], - "Kiln of the First Flame": [], # DLC - "Painted World of Ariandel 1": [ - DS3LocationData("PW: Follower Javelin", "Follower Javelin", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Frozen Weapon", "Frozen Weapon", DS3LocationCategory.SPELL), - DS3LocationData("PW: Millwood Greatbow", "Millwood Greatbow", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Captain's Ashes", "Captain's Ashes", DS3LocationCategory.MISC), - DS3LocationData("PW: Millwood Battle Axe", "Millwood Battle Axe", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Ethereal Oak Shield", "Ethereal Oak Shield", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Crow Quills", "Crow Quills", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Slave Knight Hood", "Slave Knight Hood", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Armor", "Slave Knight Armor", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Gauntlets", "Slave Knight Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Slave Knight Leggings", "Slave Knight Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Way of White Corona", "Way of White Corona", DS3LocationCategory.SPELL), - DS3LocationData("PW: Crow Talons", "Crow Talons", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Onyx Blade", "Onyx Blade", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Contraption Key", "Contraption Key", DS3LocationCategory.KEY), + "Painted World of Ariandel (Before Contraption)": [ + DS3LocationData("PW1: Valorheart - boss drop", "Valorheart", prominent=True, boss=True), + DS3LocationData("PW1: Contraption Key - library, NPC drop", "Contraption Key", + prominent=True, progression=True, + hostile_npc=True), # Sir Vilhelm drop + DS3LocationData("PW1: Onyx Blade - library, NPC drop", "Onyx Blade", + hostile_npc=True), # Sir Vilhelm drop + DS3LocationData("PW1: Chillbite Ring - Friede", "Chillbite Ring", + npc=True), # Friede conversation + DS3LocationData("PW1: Rime-blue Moss Clump - snowfield upper, starting cave", + "Rime-blue Moss Clump x2"), + DS3LocationData("PW1: Poison Gem - snowfield upper, forward from bonfire", "Poison Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, path back up", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Follower Javelin - snowfield lower, path back up", "Follower Javelin"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, path to village", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Homeward Bone - snowfield village, outcropping", "Homeward Bone x6"), + DS3LocationData("PW1: Blessed Gem - snowfield, behind tower", "Blessed Gem", + hidden=True), # Hidden behind a tower + DS3LocationData("PW1: Captain's Ashes - snowfield tower, 6F", "Captain's Ashes", + progression=True), + DS3LocationData("PW1: Black Firebomb - snowfield lower, path to bonfire", + "Black Firebomb x2"), + DS3LocationData("PW1: Shriving Stone - below bridge near", "Shriving Stone"), + DS3LocationData("PW1: Millwood Greatarrow - snowfield village, loop back to lower", + "Millwood Greatarrow x5"), + DS3LocationData("PW1: Millwood Greatbow - snowfield village, loop back to lower", + "Millwood Greatbow"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield upper", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Rusted Coin - snowfield lower, straight from fall", "Rusted Coin"), + DS3LocationData("PW1: Large Titanite Shard - snowfield lower, left from fall", + "Large Titanite Shard"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliff", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Crow Quills - settlement loop, jump into courtyard", "Crow Quills", + hidden=True), # Hidden fall + DS3LocationData("PW1: Simple Gem - settlement, lowest level, behind gate", "Simple Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfire", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Slave Knight Hood - settlement roofs, drop by ladder", + "Slave Knight Hood"), + DS3LocationData("PW1: Slave Knight Armor - settlement roofs, drop by ladder", + "Slave Knight Armor"), + DS3LocationData("PW1: Slave Knight Gauntlets - settlement roofs, drop by ladder", + "Slave Knight Gauntlets"), + DS3LocationData("PW1: Slave Knight Leggings - settlement roofs, drop by ladder", + "Slave Knight Leggings"), + DS3LocationData("PW1: Ember - settlement main, left building after bridge", "Ember"), + DS3LocationData("PW1: Dark Gem - settlement back, egg building", "Dark Gem"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement roofs, balcony", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfire", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Rusted Gold Coin - settlement roofs, roof near second ladder", + "Rusted Gold Coin x3"), + DS3LocationData("PW1: Soul of a Crestfallen Knight - settlement hall, rafters", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW1: Way of White Corona - settlement hall, by altar", + "Way of White Corona"), + DS3LocationData("PW1: Rusted Coin - right of library", "Rusted Coin x2"), + DS3LocationData("PW1: Young White Branch - right of library", "Young White Branch"), + DS3LocationData("PW1: Budding Green Blossom - settlement courtyard, ledge", + "Budding Green Blossom x3"), + DS3LocationData("PW1: Crow Talons - settlement roofs, near bonfire", "Crow Talons"), + DS3LocationData("PW1: Hollow Gem - beside chapel", "Hollow Gem"), + DS3LocationData("PW1: Rime-blue Moss Clump - below bridge far", "Rime-blue Moss Clump x4"), + DS3LocationData("PW1: Follower Sabre - roots above depths", "Follower Sabre"), + DS3LocationData("PW1: Ember - roots above depths", "Ember"), + DS3LocationData("PW1: Snap Freeze - depths, far end, mob drop", "Snap Freeze", drop=True, + hidden=True), # Guaranteed drop from normal-looking Tree Woman + DS3LocationData("PW1: Rime-blue Moss Clump - snowfield upper, overhang", + "Rime-blue Moss Clump"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliff", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Ember - settlement, building near bonfire", "Ember"), + DS3LocationData("PW1: Frozen Weapon - snowfield lower, egg zone", "Frozen Weapon"), + DS3LocationData("PW1: Titanite Slab - depths, up secret ladder", "Titanite Slab", + static='11,0:54500640::', + hidden=True), # Must kill normal-looking Tree Woman + DS3LocationData("PW1: Homeward Bone - depths, up hill", "Homeward Bone x2"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - below snowfield village overhang", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Large Soul of a Weary Warrior - settlement hall roof", + "Large Soul of a Weary Warrior"), + DS3LocationData("PW1: Large Soul of an Unknown Traveler - settlement back", + "Large Soul of an Unknown Traveler"), + DS3LocationData("PW1: Heavy Gem - snowfield village", "Heavy Gem"), + DS3LocationData("PW1: Large Soul of a Weary Warrior - snowfield tower, 6F", + "Large Soul of a Weary Warrior"), + DS3LocationData("PW1: Millwood Battle Axe - snowfield tower, 5F", "Millwood Battle Axe"), + DS3LocationData("PW1: Ethereal Oak Shield - snowfield tower, 3F", "Ethereal Oak Shield"), + DS3LocationData("PW1: Soul of a Weary Warrior - snowfield tower, 1F", + "Soul of a Weary Warrior"), + DS3LocationData("PW1: Twinkling Titanite - snowfield tower, 3F lizard", + "Twinkling Titanite", lizard=True), + DS3LocationData("PW1: Large Titanite Shard - lizard under bridge near", + "Large Titanite Shard", lizard=True), + DS3LocationData("PW1: Twinkling Titanite - roots, lizard", "Twinkling Titanite", + lizard=True), + DS3LocationData("PW1: Twinkling Titanite - settlement roofs, lizard before hall", + "Twinkling Titanite", lizard=True), + DS3LocationData("PW1: Large Titanite Shard - settlement loop, lizard", + "Large Titanite Shard x2", lizard=True), ], - "Painted World of Ariandel 2": [ - DS3LocationData("PW: Quakestone Hammer", "Quakestone Hammer", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Earth Seeker", "Earth Seeker", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Follower Torch", "Follower Torch", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Follower Shield", "Follower Shield", DS3LocationCategory.SHIELD), - DS3LocationData("PW: Follower Sabre", "Follower Sabre", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Snap Freeze", "Snap Freeze", DS3LocationCategory.SPELL), - DS3LocationData("PW: Floating Chaos", "Floating Chaos", DS3LocationCategory.SPELL), - DS3LocationData("PW: Pyromancer's Parting Flame", "Pyromancer's Parting Flame", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Vilhelm's Helm", "Vilhelm's Helm", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Armor", "Vilhelm's Armor", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Gauntlets", "Vilhelm's Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Vilhelm's Leggings", "Vilhelm's Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("PW: Valorheart", "Valorheart", DS3LocationCategory.WEAPON), - DS3LocationData("PW: Champion's Bones", "Champion's Bones", DS3LocationCategory.MISC), - DS3LocationData("PW: Soul of Sister Friede", "Soul of Sister Friede", DS3LocationCategory.BOSS), - DS3LocationData("PW: Chillbite Ring", "Chillbite Ring", DS3LocationCategory.RING), + "Painted World of Ariandel (After Contraption)": [ + DS3LocationData("PW2: Soul of Sister Friede", "Soul of Sister Friede", prominent=True, + boss=True), + DS3LocationData("PW2: Titanite Slab - boss drop", "Titanite Slab", + static='11,0:50004700::', + boss=True), # One-time drop after Friede Phase 2 + DS3LocationData("PW2: Floating Chaos - NPC drop", "Floating Chaos", hostile_npc=True, + hidden=True), # Livid Pyromancer Dunnel drop (requires ember) + DS3LocationData("PW2: Prism Stone - pass, tree by beginning", "Prism Stone x10"), + DS3LocationData("PW2: Titanite Chunk - pass, cliff overlooking bonfire", "Titanite Chunk"), + DS3LocationData("PW2: Titanite Chunk - pass, by kickable tree", "Titanite Chunk"), + DS3LocationData("PW2: Follower Shield - pass, far cliffside", "Follower Shield"), + DS3LocationData("PW2: Large Titanite Shard - pass, just before B1", + "Large Titanite Shard x2"), + DS3LocationData("PW2: Quakestone Hammer - pass, side path near B1", "Quakestone Hammer"), + DS3LocationData("PW2: Ember - pass, central alcove", "Ember"), + DS3LocationData("PW2: Large Titanite Shard - pass, far side path", + "Large Titanite Shard x2"), + DS3LocationData("PW2: Soul of a Crestfallen Knight - pit edge #1", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Soul of a Crestfallen Knight - pit edge #2", + "Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Large Soul of a Crestfallen Knight - pit, by tree", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("PW2: Earth Seeker - pit cave", "Earth Seeker"), + DS3LocationData("PW2: Follower Torch - pass, far side path", "Follower Torch"), + DS3LocationData("PW2: Dung Pie - B1", "Dung Pie x2"), + DS3LocationData("PW2: Vilhelm's Helm", "Vilhelm's Helm"), + DS3LocationData("PW2: Vilhelm's Armor - B2, along wall", "Vilhelm's Armor"), + DS3LocationData("PW2: Vilhelm's Gauntlets - B2, along wall", "Vilhelm's Gauntlets"), + DS3LocationData("PW2: Vilhelm's Leggings - B2, along wall", "Vilhelm's Leggings"), + DS3LocationData("PW2: Blood Gem - B2, center", "Blood Gem"), + DS3LocationData("PW2: Pyromancer's Parting Flame - rotunda", + "Pyromancer's Parting Flame", hidden=True), # Behind illusory wall + DS3LocationData("PW2: Homeward Bone - rotunda", "Homeward Bone x2", + hidden=True), # Behind illusory wall + DS3LocationData("PW2: Twinkling Titanite - B3, lizard #1", "Twinkling Titanite", + lizard=True), + DS3LocationData("PW2: Twinkling Titanite - B3, lizard #2", "Twinkling Titanite", + lizard=True), + + # Corvian Settler after killing Friede + DS3LocationData("PW1: Titanite Slab - Corvian", "Titanite Slab", npc=True), + + # Shrine Handmaid after killing Sister Friede + DS3LocationData("FS: Ordained Hood - shop after killing PW2 boss", "Ordained Hood", + boss=True, shop=True), + DS3LocationData("FS: Ordained Dress - shop after killing PW2 boss", "Ordained Dress", + boss=True, shop=True), + DS3LocationData("FS: Ordained Trousers - shop after killing PW2 boss", "Ordained Trousers", + boss=True, shop=True), ], "Dreg Heap": [ - DS3LocationData("DH: Loincloth", "Loincloth", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Aquamarine Dagger", "Aquamarine Dagger", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Murky Hand Scythe", "Murky Hand Scythe", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Murky Longstaff", "Murky Longstaff", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Great Soul Dregs", "Great Soul Dregs", DS3LocationCategory.SPELL), - DS3LocationData("DH: Lothric War Banner", "Lothric War Banner", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Projected Heal", "Projected Heal", DS3LocationCategory.SPELL), - DS3LocationData("DH: Desert Pyromancer Hood", "Desert Pyromancer Hood", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Garb", "Desert Pyromancer Garb", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Gloves", "Desert Pyromancer Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Desert Pyromancer Skirt", "Desert Pyromancer Skirt", DS3LocationCategory.ARMOR), - DS3LocationData("DH: Giant Door Shield", "Giant Door Shield", DS3LocationCategory.SHIELD), - DS3LocationData("DH: Herald Curved Greatsword", "Herald Curved Greatsword", DS3LocationCategory.WEAPON), - DS3LocationData("DH: Flame Fan", "Flame Fan", DS3LocationCategory.SPELL), - DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", DS3LocationCategory.BOSS), - DS3LocationData("DH: Small Envoy Banner", "Small Envoy Banner", DS3LocationCategory.KEY), - DS3LocationData("DH: Ring of Favor+3", "Ring of Favor+3", DS3LocationCategory.RING), - DS3LocationData("DH: Covetous Silver Serpent Ring+3", "Covetous Silver Serpent Ring+3", DS3LocationCategory.RING), - DS3LocationData("DH: Ring of Steel Protection+3", "Ring of Steel Protection+3", DS3LocationCategory.RING), + DS3LocationData("DH: Soul of the Demon Prince", "Soul of the Demon Prince", + prominent=True, boss=True), + DS3LocationData("DH: Siegbräu - Lapp", "Siegbräu", missable=True, drop=True, + npc=True), # Lapp (quest or kill) + DS3LocationData("DH: Flame Fan - swamp upper, NPC drop", "Flame Fan", + hostile_npc=True), # Desert Pyromancer Zoey drop + DS3LocationData("DH: Ember - castle, behind spire", "Ember"), + DS3LocationData("DH: Soul of a Weary Warrior - castle overhang", "Soul of a Weary Warrior"), + DS3LocationData("DH: Titanite Chunk - castle, up stairs", "Titanite Chunk"), + DS3LocationData("DH: Aquamarine Dagger - castle, up stairs", "Aquamarine Dagger"), + DS3LocationData("DH: Twinkling Titanite - library, chandelier", "Twinkling Titanite"), + DS3LocationData("DH: Murky Hand Scythe - library, behind bookshelves", "Murky Hand Scythe"), + DS3LocationData("DH: Divine Blessing - library, after drop", "Divine Blessing"), + DS3LocationData("DH: Ring of Steel Protection+3 - ledge before church", + "Ring of Steel Protection+3"), + DS3LocationData("DH: Soul of a Crestfallen Knight - church, altar", + "Soul of a Crestfallen Knight"), + DS3LocationData("DH: Rusted Coin - behind fountain after church", "Rusted Coin x2"), + DS3LocationData("DH: Titanite Chunk - pantry, first room", "Titanite Chunk"), + DS3LocationData("DH: Murky Longstaff - pantry, last room", "Murky Longstaff"), + DS3LocationData("DH: Ember - pantry, behind crates just before upstairs", "Ember", + hidden=True), # Behind illusory wall + DS3LocationData("DH: Great Soul Dregs - pantry upstairs", "Great Soul Dregs", + hidden=True), # Behind illusory wall + DS3LocationData("DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop down", + "Covetous Silver Serpent Ring+3", hidden=True), # Behind illusory wall + DS3LocationData("DH: Titanite Chunk - path from church, by pillar", "Titanite Chunk"), + DS3LocationData("DH: Homeward Bone - end of path from church", "Homeward Bone x3"), + DS3LocationData("DH: Lightning Urn - wall outside church", "Lightning Urn x4"), + DS3LocationData("DH: Projected Heal - parapets balcony", "Projected Heal"), + DS3LocationData("DH: Large Soul of a Weary Warrior - parapets, hall", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Lothric War Banner - parapets, end of hall", "Lothric War Banner"), + DS3LocationData("DH: Titanite Scale - library, back of room", "Titanite Scale"), + DS3LocationData("DH: Black Firebomb - ruins, up windmill from bonfire", "Black Firebomb x4"), + DS3LocationData("DH: Titanite Chunk - ruins, path from bonfire", "Titanite Chunk"), + DS3LocationData("DH: Twinkling Titanite - ruins, root near bonfire", "Twinkling Titanite"), + DS3LocationData("DH: Desert Pyromancer Garb - ruins, by shack near cliff", + "Desert Pyromancer Garb"), + DS3LocationData("DH: Titanite Chunk - ruins, by far shack", "Titanite Chunk x2"), + DS3LocationData("DH: Giant Door Shield - ruins, path below far shack", "Giant Door Shield"), + DS3LocationData("DH: Ember - ruins, alcove before swamp", "Ember"), + DS3LocationData("DH: Desert Pyromancer Gloves - swamp, far right", + "Desert Pyromancer Gloves"), + DS3LocationData("DH: Desert Pyromancer Skirt - swamp right, by roots", + "Desert Pyromancer Skirt"), + DS3LocationData("DH: Titanite Scale - swamp upper, drop and jump into tower", + "Titanite Scale"), + DS3LocationData("DH: Purple Moss Clump - swamp shack", "Purple Moss Clump x4"), + DS3LocationData("DH: Ring of Favor+3 - swamp right, up root", "Ring of Favor+3"), + DS3LocationData("DH: Titanite Chunk - swamp right, drop partway up root", "Titanite Chunk"), + DS3LocationData("DH: Large Soul of a Weary Warrior - swamp, under overhang", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Titanite Slab - swamp, path under overhang", "Titanite Slab"), + DS3LocationData("DH: Titanite Chunk - swamp, along buildings", "Titanite Chunk"), + DS3LocationData("DH: Loincloth - swamp, left edge", "Loincloth"), + DS3LocationData("DH: Titanite Chunk - swamp, path to upper", "Titanite Chunk"), + DS3LocationData("DH: Large Soul of a Weary Warrior - swamp center", + "Large Soul of a Weary Warrior"), + DS3LocationData("DH: Harald Curved Greatsword - swamp left, under root", + "Harald Curved Greatsword"), + DS3LocationData("DH: Homeward Bone - swamp left, on root", "Homeward Bone"), + DS3LocationData("DH: Prism Stone - swamp upper, tunnel start", "Prism Stone x6"), + DS3LocationData("DH: Desert Pyromancer Hood - swamp upper, tunnel end", + "Desert Pyromancer Hood"), + DS3LocationData("DH: Twinkling Titanite - swamp upper, drop onto root", + "Twinkling Titanite", hidden=True), # Hidden fall + DS3LocationData("DH: Divine Blessing - swamp upper, building roof", "Divine Blessing"), + DS3LocationData("DH: Ember - ruins, alcove on cliff", "Ember", hidden=True), # Hidden fall + DS3LocationData("DH: Small Envoy Banner - boss drop", "Small Envoy Banner", + progression=True, boss=True), + DS3LocationData("DH: Twinkling Titanite - ruins, alcove on cliff, mob drop", + "Twinkling Titanite x2", drop=True, + hidden=True), # Hidden fall, also guaranteed drop from killing normal-looking pilgrim + DS3LocationData("DH: Twinkling Titanite - swamp upper, mob drop on roof", + "Twinkling Titanite x2", drop=True, + hidden=True), # Hidden fall, also guaranteed drop from killing normal-looking pilgrim + DS3LocationData("DH: Twinkling Titanite - path after church, mob drop", + "Twinkling Titanite x2", drop=True, + hidden=True), # Guaranteed drop from killing normal-looking pilgrim + + # Stone-humped Hag's shop + DS3LocationData("DH: Splitleaf Greatsword - shop", "Splitleaf Greatsword", shop=True), + DS3LocationData("DH: Divine Blessing - shop", "Divine Blessing", shop=True), + DS3LocationData("DH: Hidden Blessing - shop", "Hidden Blessing", shop=True), + DS3LocationData("DH: Rusted Gold Coin - shop", "Rusted Gold Coin", shop=True), + DS3LocationData("DH: Ember - shop", "Ember", shop=True), ], "Ringed City": [ - DS3LocationData("RC: Ruin Sentinel Helm", "Ruin Sentinel Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Armor", "Ruin Sentinel Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Gauntlets", "Ruin Sentinel Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Ruin Sentinel Leggings", "Ruin Sentinel Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Veil", "Black Witch Veil", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Hat", "Black Witch Hat", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Garb", "Black Witch Garb", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Wrappings", "Black Witch Wrappings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Black Witch Trousers", "Black Witch Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RC: White Preacher Head", "White Preacher Head", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Havel's Ring+3", "Havel's Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Ringed Knight Spear", "Ringed Knight Spear", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Dragonhead Shield", "Dragonhead Shield", DS3LocationCategory.SHIELD), - DS3LocationData("RC: Ringed Knight Straight Sword", "Ringed Knight Straight Sword", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Preacher's Right Arm", "Preacher's Right Arm", DS3LocationCategory.WEAPON), - DS3LocationData("RC: White Birch Bow", "White Birch Bow", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Church Guardian Shiv", "Church Guardian Shiv", DS3LocationCategory.MISC), - DS3LocationData("RC: Dragonhead Greatshield", "Dragonhead Greatshield", DS3LocationCategory.SHIELD), - DS3LocationData("RC: Ringed Knight Paired Greatswords", "Ringed Knight Paired Greatswords", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Shira's Crown", "Shira's Crown", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Armor", "Shira's Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Gloves", "Shira's Gloves", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Shira's Trousers", "Shira's Trousers", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Crucifix of the Mad King", "Crucifix of the Mad King", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Sacred Chime of Filianore", "Sacred Chime of Filianore", DS3LocationCategory.WEAPON), - DS3LocationData("RC: Iron Dragonslayer Helm", "Iron Dragonslayer Helm", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Armor", "Iron Dragonslayer Armor", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Gauntlets", "Iron Dragonslayer Gauntlets", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Iron Dragonslayer Leggings", "Iron Dragonslayer Leggings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Lightning Arrow", "Lightning Arrow", DS3LocationCategory.SPELL), - DS3LocationData("RC: Ritual Spear Fragment", "Ritual Spear Fragment", DS3LocationCategory.MISC), - DS3LocationData("RC: Antiquated Plain Garb", "Antiquated Plain Garb", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Violet Wrappings", "Violet Wrappings", DS3LocationCategory.ARMOR), - DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", DS3LocationCategory.BOSS), - DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", DS3LocationCategory.BOSS), - DS3LocationData("RC: Blood of the Dark Soul", "Blood of the Dark Soul", DS3LocationCategory.KEY), - DS3LocationData("RC: Chloranthy Ring+3", "Chloranthy Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Covetous Gold Serpent Ring+3", "Covetous Gold Serpent Ring+3", DS3LocationCategory.RING), - DS3LocationData("RC: Ring of the Evil Eye+3", "Ring of the Evil Eye+3", DS3LocationCategory.RING), - DS3LocationData("RC: Wolf Ring+3", "Wolf Ring+3", DS3LocationCategory.RING), + DS3LocationData("RC: Titanite Slab - mid boss drop", "Titanite Slab", + prominent=True, boss=True), # Halflight drop, only once + DS3LocationData("RC: Filianore's Spear Ornament - mid boss drop", + "Filianore's Spear Ornament"), + DS3LocationData("RC: Soul of Darkeater Midir", "Soul of Darkeater Midir", prominent=True, + boss=True), + DS3LocationData("RC: Sacred Chime of Filianore - ashes, NPC drop", + "Sacred Chime of Filianore", + hostile_npc=True), # Shira (kill or quest) + DS3LocationData("RC: Titanite Slab - ashes, NPC drop", "Titanite Slab", + hostile_npc=True), # Shira (kill or quest) + DS3LocationData("RC: Crucifix of the Mad King - ashes, NPC drop", + "Crucifix of the Mad King", hostile_npc=True), # Shira drop + DS3LocationData("RC: Ledo's Great Hammer - streets high, opposite building, NPC drop", + "Ledo's Great Hammer", hostile_npc=True, + missable=True), # Silver Knight Ledo drop, doesn't invade once Halflight + # is defeated + DS3LocationData("RC: Wolf Ring+3 - street gardens, NPC drop", "Wolf Ring+3", + hostile_npc=True, + missable=True), # Alva drop, doesn't invade once Halflight is defeated + DS3LocationData("RC: Blindfold Mask - grave, NPC drop", "Blindfold Mask", + hostile_npc=True), # Moaning Knight drop + DS3LocationData("RC: Titanite Scale - wall top, behind spawn", "Titanite Scale"), # wrong + DS3LocationData("RC: Ruin Helm - wall top, under stairs to bonfire", "Ruin Helm"), + DS3LocationData("RC: Ruin Armor - wall top, under stairs to bonfire", "Ruin Armor"), + DS3LocationData("RC: Ruin Gauntlets - wall top, under stairs to bonfire", "Ruin Gauntlets"), + DS3LocationData("RC: Ruin Leggings - wall top, under stairs to bonfire", "Ruin Leggings"), + DS3LocationData("RC: Budding Green Blossom - wall top, in flower cluster", + "Budding Green Blossom x2"), + DS3LocationData("RC: Titanite Chunk - wall top, among graves", "Titanite Chunk x2"), + DS3LocationData("RC: Ember - wall top, by statue", "Ember"), + DS3LocationData("RC: Budding Green Blossom - wall top, flowers by stairs", + "Budding Green Blossom x2"), + DS3LocationData("RC: Hidden Blessing - wall top, tomb under platform", "Hidden Blessing", + hidden=True), # hidden fall + DS3LocationData("RC: Soul of a Crestfallen Knight - wall top, under drop", + "Soul of a Crestfallen Knight", hidden=True), # hidden fall + DS3LocationData("RC: Large Soul of a Weary Warrior - wall top, right of small tomb", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Ember - wall upper, balcony", "Ember"), + DS3LocationData("RC: Purging Stone - wall top, by door to upper", "Purging Stone x2"), + DS3LocationData("RC: Hollow Gem - wall upper, path to tower", "Hollow Gem"), + DS3LocationData("RC: Titanite Chunk - wall upper, courtyard alcove", "Titanite Chunk"), + DS3LocationData("RC: Twinkling Titanite - wall tower, jump from chandelier", + "Twinkling Titanite", hidden=True), # Hidden fall + DS3LocationData("RC: Shriving Stone - wall tower, bottom floor center", "Shriving Stone"), + DS3LocationData("RC: Shira's Crown - Shira's room after killing ashes NPC", "Shira's Crown", + hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Armor - Shira's room after killing ashes NPC", "Shira's Armor", + hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Gloves - Shira's room after killing ashes NPC", + "Shira's Gloves", hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Shira's Trousers - Shira's room after killing ashes NPC", + "Shira's Trousers", hidden=True), # Have to return to a cleared area + DS3LocationData("RC: Mossfruit - streets near left, path to garden", "Mossfruit x2"), + DS3LocationData("RC: Large Soul of a Crestfallen Knight - streets, far stairs", + "Large Soul of a Crestfallen Knight"), + DS3LocationData("RC: Ringed Knight Spear - streets, down far right hall", + "Ringed Knight Spear"), + DS3LocationData("RC: Black Witch Hat - streets garden", "Black Witch Hat", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Garb - streets garden", "Black Witch Garb", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Wrappings - streets garden", "Black Witch Wrappings", + hostile_npc=True), # Alva + DS3LocationData("RC: Black Witch Trousers - streets garden", "Black Witch Trousers", + hostile_npc=True), # Alva + DS3LocationData("RC: Dragonhead Shield - streets monument, across bridge", + "Dragonhead Shield", hidden=True), # "Show Your Humanity" puzzle + DS3LocationData("RC: Titanite Chunk - streets, near left drop", "Titanite Chunk", + hidden=True), # Hidden fall + DS3LocationData("RC: Mossfruit - streets, far left alcove", "Mossfruit x2"), + DS3LocationData("RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "Large Soul of a Crestfallen Knight", + hidden=True), # "Show Your Humanity" puzzle + DS3LocationData("RC: Covetous Gold Serpent Ring+3 - streets, by Lapp", + "Covetous Gold Serpent Ring+3"), + DS3LocationData("RC: Titanite Chunk - streets high, building opposite", "Titanite Chunk x2"), + DS3LocationData("RC: Dark Gem - swamp near, by stairs", "Dark Gem"), + DS3LocationData("RC: Prism Stone - swamp near, railing by bonfire", "Prism Stone x4"), + DS3LocationData("RC: Ringed Knight Straight Sword - swamp near, tower on peninsula", + "Ringed Knight Straight Sword"), + DS3LocationData("RC: Havel's Ring+3 - streets high, drop from building opposite", + "Havel's Ring+3", hidden=True), # Hidden fall + DS3LocationData("RC: Titanite Chunk - swamp near left, by spire top", "Titanite Chunk"), + DS3LocationData("RC: Twinkling Titanite - swamp near left", "Twinkling Titanite"), + DS3LocationData("RC: Soul of a Weary Warrior - swamp center", "Soul of a Weary Warrior"), + DS3LocationData("RC: Preacher's Right Arm - swamp near right, by tower", + "Preacher's Right Arm"), + DS3LocationData("RC: Rubbish - swamp far, by crystal", "Rubbish"), + DS3LocationData("RC: Titanite Chunk - swamp near right, behind rock", + "Titanite Chunk"), + DS3LocationData("RC: Black Witch Veil - swamp near right, by sunken church", + "Black Witch Veil"), + DS3LocationData("RC: Twinkling Titanite - swamp near right, on sunken church", + "Twinkling Titanite"), + DS3LocationData("RC: Soul of a Crestfallen Knight - swamp near left, nook", + "Soul of a Crestfallen Knight"), + DS3LocationData("RC: White Preacher Head - swamp near, nook right of stairs", + "White Preacher Head"), + DS3LocationData("RC: Titanite Scale - swamp far, by miniboss", "Titanite Scale"), + DS3LocationData("RC: Dragonhead Greatshield - lower cliff, under bridge", + "Dragonhead Greatshield"), + DS3LocationData("RC: Titanite Scale - lower cliff, path under bridge", "Titanite Scale x2"), + DS3LocationData("RC: Rubbish - lower cliff, middle", "Rubbish"), + DS3LocationData("RC: Large Soul of a Weary Warrior - lower cliff, end", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Titanite Scale - lower cliff, first alcove", "Titanite Scale x2"), + DS3LocationData("RC: Titanite Scale - lower cliff, lower path", "Titanite Scale"), + DS3LocationData("RC: Lightning Gem - grave, room after first drop", "Lightning Gem"), + DS3LocationData("RC: Blessed Gem - grave, down lowest stairs", "Blessed Gem"), + DS3LocationData("RC: Simple Gem - grave, up stairs after first drop", "Simple Gem"), + DS3LocationData("RC: Large Soul of a Weary Warrior - wall lower, past two illusory walls", + "Large Soul of a Weary Warrior", hidden=True), + DS3LocationData("RC: Lightning Arrow - wall lower, past three illusory walls", + "Lightning Arrow"), + DS3LocationData("RC: Chloranthy Ring+3 - wall hidden, drop onto statue", + "Chloranthy Ring+3", hidden=True), # Hidden fall + DS3LocationData("RC: Ember - wall hidden, statue room", "Ember"), + DS3LocationData("RC: Filianore's Spear Ornament - wall hidden, by ladder", + "Filianore's Spear Ornament"), + DS3LocationData("RC: Antiquated Plain Garb - wall hidden, before boss", + "Antiquated Plain Garb"), + DS3LocationData("RC: Violet Wrappings - wall hidden, before boss", "Violet Wrappings"), + DS3LocationData("RC: Soul of a Weary Warrior - lower cliff, by first alcove", + "Soul of a Weary Warrior"), + DS3LocationData("RC: Twinkling Titanite - church path, left of boss door", + "Twinkling Titanite x2"), + DS3LocationData("RC: Budding Green Blossom - church path", "Budding Green Blossom x3"), + DS3LocationData("RC: Titanite Chunk - swamp center, peninsula edge", "Titanite Chunk"), + DS3LocationData("RC: Large Soul of a Weary Warrior - swamp center, by peninsula", + "Large Soul of a Weary Warrior"), + DS3LocationData("RC: Soul of a Weary Warrior - swamp right, by sunken church", + "Soul of a Weary Warrior"), + DS3LocationData("RC: Titanite Scale - upper cliff, bridge", "Titanite Scale"), + DS3LocationData("RC: Soul of a Crestfallen Knight - swamp far, behind crystal", + "Soul of a Crestfallen Knight"), + DS3LocationData("RC: White Birch Bow - swamp far left, up hill", "White Birch Bow"), + DS3LocationData("RC: Titanite Chunk - swamp far left, up hill", "Titanite Chunk"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #1", + "Young White Branch"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #2", + "Young White Branch"), + DS3LocationData("RC: Young White Branch - swamp far left, by white tree #3", + "Young White Branch"), + DS3LocationData("RC: Ringed Knight Paired Greatswords - church path, mob drop", + "Ringed Knight Paired Greatswords", drop=True, + hidden=True), # Guaranteed drop from a normal-looking Ringed Knight + DS3LocationData("RC: Hidden Blessing - swamp center, mob drop", "Hidden Blessing", + drop=True, hidden=True), # Guaranteed drop from Judicator + DS3LocationData("RC: Divine Blessing - wall top, mob drop", "Divine Blessing", + drop=True, hidden=True), # Guaranteed drop from Judicator + DS3LocationData("RC: Divine Blessing - streets monument, mob drop", "Divine Blessing", + drop=True, + hidden=True), # Guaranteed drop from Judicator, "Show Your Humanity" puzzle + DS3LocationData("RC: Ring of the Evil Eye+3 - grave, mimic", "Ring of the Evil Eye+3", + mimic=True), + DS3LocationData("RC: Iron Dragonslayer Helm - swamp far, miniboss drop", + "Iron Dragonslayer Helm", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Armor - swamp far, miniboss drop", + "Iron Dragonslayer Armor", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Gauntlets - swamp far, miniboss drop", + "Iron Dragonslayer Gauntlets", miniboss=True), + DS3LocationData("RC: Iron Dragonslayer Leggings - swamp far, miniboss drop", + "Iron Dragonslayer Leggings", miniboss=True), + DS3LocationData("RC: Church Guardian Shiv - swamp far left, in building", + "Church Guardian Shiv"), + DS3LocationData("RC: Spears of the Church - hidden boss drop", "Spears of the Church", + boss=True), # Midir drop + DS3LocationData("RC: Ritual Spear Fragment - church path", "Ritual Spear Fragment"), + DS3LocationData("RC: Titanite Scale - swamp far, lagoon entrance", "Titanite Scale"), + DS3LocationData("RC: Twinkling Titanite - grave, lizard past first drop", + "Twinkling Titanite", lizard=True), + DS3LocationData("RC: Titanite Scale - grave, lizard past first drop", "Titanite Scale", + lizard=True), + DS3LocationData("RC: Twinkling Titanite - streets high, lizard", "Twinkling Titanite x2", + lizard=True), + DS3LocationData("RC: Titanite Scale - wall lower, lizard", "Titanite Scale", lizard=True), + DS3LocationData("RC: Twinkling Titanite - wall top, lizard on side path", + "Twinkling Titanite", lizard=True), + DS3LocationData("RC: Soul of Slave Knight Gael", "Soul of Slave Knight Gael", + prominent=True, boss=True), + DS3LocationData("RC: Blood of the Dark Soul - end boss drop", "Blood of the Dark Soul"), + DS3LocationData("RC: Titanite Slab - ashes, mob drop", "Titanite Slab", + drop=True, + hidden=True), # Guaranteed drop from normal-looking Ringed Knight + + # Lapp + DS3LocationData("RC: Siegbräu - Lapp", "Siegbräu", missable=True, + npc=True), # Lapp (quest) + # Quest or Shrine Handmaiden after death + DS3LocationData("RC: Lapp's Helm - Lapp", "Lapp's Helm", npc=True, shop=True), + DS3LocationData("RC: Lapp's Armor - Lapp", "Lapp's Armor", npc=True, shop=True), + DS3LocationData("RC: Lapp's Gauntlets - Lapp", "Lapp's Gauntlets", npc=True, shop=True), + DS3LocationData("RC: Lapp's Leggings - Lapp", "Lapp's Leggings", npc=True, shop=True), ], - # Progressive - "Progressive Items 1": [] + - # Upgrade materials - [DS3LocationData(f"Titanite Shard #{i + 1}", "Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(26)] + - [DS3LocationData(f"Large Titanite Shard #{i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(28)] + - [DS3LocationData(f"Titanite Slab #{i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Twinkling Titanite #{i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)], + # Unlockable shops. We only bother creating a "region" for these for shops that are locked + # behind keys and always have items available either through the shop or through the NPC's + # ashes. + "Greirat's Shop": [ + DS3LocationData("FS: Blue Tearstone Ring - Greirat", "Blue Tearstone Ring", + static='01,0:50006120::', npc=True), + DS3LocationData("FS: Ember - Greirat", "Ember", static="99,0:-1:110000,120000,70000110:", + shop=True, npc=True), - "Progressive Items 2": [] + - # Items - [DS3LocationData(f"Green Blossom #{i + 1}", "Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Firebomb #{i + 1}", "Firebomb", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Alluring Skull #{i + 1}", "Alluring Skull", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Undead Hunter Charm #{i + 1}", "Undead Hunter Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Duel Charm #{i + 1}", "Duel Charm", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Throwing Knife #{i + 1}", "Throwing Knife", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Gold Pine Resin #{i + 1}", "Gold Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Charcoal Pine Resin #{i + 1}", "Charcoal Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Human Pine Resin #{i + 1}", "Human Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Carthus Rouge #{i + 1}", "Carthus Rouge", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Pale Pine Resin #{i + 1}", "Pale Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Charcoal Pine Bundle #{i + 1}", "Charcoal Pine Bundle", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rotten Pine Resin #{i + 1}", "Rotten Pine Resin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Homeward Bone #{i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(16)] + - [DS3LocationData(f"Pale Tongue #{i + 1}", "Pale Tongue", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Rusted Coin #{i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rusted Gold Coin #{i + 1}", "Rusted Gold Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Ember #{i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(45)], + # Undead Settlement rewards + DS3LocationData("FS: Divine Blessing - Greirat from US", "Divine Blessing", + static='99,0:-1:110000,120000,70000150,70000175:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Ember - Greirat from US", "Ember", + static='99,0:-1:110000,120000,70000150,70000175:', missable=True, + shop=True, npc=True), - "Progressive Items 3": [] + - # Souls & Bulk Upgrade Materials - [DS3LocationData(f"Fading Soul #{i + 1}", "Fading Soul", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of a Deserted Corpse #{i + 1}", "Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of a Deserted Corpse #{i + 1}", "Large Soul of a Deserted Corpse", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of an Unknown Traveler #{i + 1}", "Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of an Unknown Traveler #{i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Soul of a Nameless Soldier #{i + 1}", "Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Large Soul of a Nameless Soldier #{i + 1}", "Large Soul of a Nameless Soldier", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Soul of a Weary Warrior #{i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Soul of a Crestfallen Knight #{i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Titanite Chunk #{i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(22)] + - [DS3LocationData(f"Titanite Scale #{i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(29)], + # Irityhll rewards + DS3LocationData("FS: Divine Blessing - Greirat from IBV", "Divine Blessing", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Hidden Blessing - Greirat from IBV", "Hidden Blessing", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Titanite Scale - Greirat from IBV", "Titanite Scale", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), + DS3LocationData("FS: Twinkling Titanite - Greirat from IBV", "Twinkling Titanite", + static='99,0:-1:110000,120000,70000151,70000176:', missable=True, + shop=True, npc=True), - "Progressive Items 4": [] + - # Gems & Random Consumables - [DS3LocationData(f"Ring of Sacrifice #{i + 1}", "Ring of Sacrifice", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(4)] + - [DS3LocationData(f"Divine Blessing #{i + 1}", "Divine Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Hidden Blessing #{i + 1}", "Hidden Blessing", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Budding Green Blossom #{i + 1}", "Budding Green Blossom", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Bloodred Moss Clump #{i + 1}", "Bloodred Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Purple Moss Clump #{i + 1}", "Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Blooming Purple Moss Clump #{i + 1}", "Blooming Purple Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Purging Stone #{i + 1}", "Purging Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Rime-blue Moss Clump #{i + 1}", "Rime-blue Moss Clump", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Repair Powder #{i + 1}", "Repair Powder", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Kukri #{i + 1}", "Kukri", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Lightning Urn #{i + 1}", "Lightning Urn", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Rubbish #{i + 1}", "Rubbish", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Blue Bug Pellet #{i + 1}", "Blue Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Red Bug Pellet #{i + 1}", "Red Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Yellow Bug Pellet #{i + 1}", "Yellow Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Black Bug Pellet #{i + 1}", "Black Bug Pellet", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Heavy Gem #{i + 1}", "Heavy Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Sharp Gem #{i + 1}", "Sharp Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Refined Gem #{i + 1}", "Refined Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Crystal Gem #{i + 1}", "Crystal Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Simple Gem #{i + 1}", "Simple Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Fire Gem #{i + 1}", "Fire Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Chaos Gem #{i + 1}", "Chaos Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Lightning Gem #{i + 1}", "Lightning Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Deep Gem #{i + 1}", "Deep Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Dark Gem #{i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Poison Gem #{i + 1}", "Poison Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blood Gem #{i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Raw Gem #{i + 1}", "Raw Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blessed Gem #{i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Hollow Gem #{i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Shriving Stone #{i + 1}", "Shriving Stone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)], + # Lothric rewards (from Shrine Handmaid) + DS3LocationData("FS: Ember - shop for Greirat's Ashes", "Twinkling Titanite", + static='99,0:-1:110000,120000,70000152,70000177:', missable=True, + shop=True, npc=True), + ], + "Karla's Shop": [ + DS3LocationData("FS: Affinity - Karla", "Affinity", shop=True, npc=True), + DS3LocationData("FS: Dark Edge - Karla", "Dark Edge", shop=True, npc=True), - "Progressive Items DLC": [] + - # Upgrade materials - [DS3LocationData(f"Large Titanite Shard ${i + 1}", "Large Titanite Shard", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Titanite Chunk ${i + 1}", "Titanite Chunk", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(15)] + - [DS3LocationData(f"Titanite Slab ${i + 1}", "Titanite Slab", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Twinkling Titanite ${i + 1}", "Twinkling Titanite", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Titanite Scale ${i + 1}", "Titanite Scale", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(11)] + + # Quelana Pyromancy Tome + DS3LocationData("FS: Firestorm - Karla for Quelana Tome", "Firestorm", missable=True, + shop=True, npc=True), + DS3LocationData("FS: Rapport - Karla for Quelana Tome", "Rapport", missable=True, + shop=True, npc=True), + DS3LocationData("FS: Fire Whip - Karla for Quelana Tome", "Fire Whip", missable=True, + shop=True, npc=True), + # Grave Warden Pyromancy Tome + DS3LocationData("FS: Black Flame - Karla for Grave Warden Tome", "Black Flame", + missable=True, shop=True, npc=True), + DS3LocationData("FS: Black Fire Orb - Karla for Grave Warden Tome", "Black Fire Orb", + missable=True, shop=True, npc=True), - # Items - [DS3LocationData(f"Homeward Bone ${i + 1}", "Homeward Bone", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Rusted Coin ${i + 1}", "Rusted Coin", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + - [DS3LocationData(f"Ember ${i + 1}", "Ember", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(10)] + + # Deep Braille Divine Tome. This can also be given to Irina, but it'll fail her quest + DS3LocationData("FS: Gnaw - Karla for Deep Braille Tome", "Gnaw", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Deep Protection - Karla for Deep Braille Tome", "Deep Protection", + missable=True, npc=True, shop=True), - # Souls - [DS3LocationData(f"Large Soul of an Unknown Traveler ${i + 1}", "Large Soul of an Unknown Traveler", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(9)] + - [DS3LocationData(f"Soul of a Weary Warrior ${i + 1}", "Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(5)] + - [DS3LocationData(f"Large Soul of a Weary Warrior ${i + 1}", "Large Soul of a Weary Warrior", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Soul of a Crestfallen Knight ${i + 1}", "Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(6)] + - [DS3LocationData(f"Large Soul of a Crestfallen Knight ${i + 1}", "Large Soul of a Crestfallen Knight", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(3)] + + # Londor Braille Divine Tome. This can also be given to Irina, but it'll fail her quest + DS3LocationData("FS: Vow of Silence - Karla for Londor Tome", "Vow of Silence", + missable=True, npc=True, shop=True), + DS3LocationData("FS: Dark Blade - Karla for Londor Tome", "Dark Blade", missable=True, + npc=True, shop=True), + DS3LocationData("FS: Dead Again - Karla for Londor Tome", "Dead Again", missable=True, + npc=True, shop=True), - # Gems - [DS3LocationData(f"Dark Gem ${i + 1}", "Dark Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Blood Gem ${i + 1}", "Blood Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(1)] + - [DS3LocationData(f"Blessed Gem ${i + 1}", "Blessed Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)] + - [DS3LocationData(f"Hollow Gem ${i + 1}", "Hollow Gem", DS3LocationCategory.PROGRESSIVE_ITEM) for i in range(2)], + # Drops on death. Missable because the player would have to decide between killing her or + # seeing everything she sells. + DS3LocationData("FS: Karla's Pointed Hat - kill Karla", "Karla's Pointed Hat", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Coat - kill Karla", "Karla's Coat", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Gloves - kill Karla", "Karla's Gloves", + static='07,0:50006150::', missable=True, drop=True, npc=True), + DS3LocationData("FS: Karla's Trousers - kill Karla", "Karla's Trousers", + static='07,0:50006150::', missable=True, drop=True, npc=True), + ], +} - "Progressive Items Health": [] + - # Healing - [DS3LocationData(f"Estus Shard #{i + 1}", "Estus Shard", DS3LocationCategory.HEALTH) for i in range(11)] + - [DS3LocationData(f"Undead Bone Shard #{i + 1}", "Undead Bone Shard", DS3LocationCategory.HEALTH) for i in range(10)], +for i, region in enumerate(region_order): + for location in location_tables[region]: location.region_value = i + +for region in [ + "Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)", + "Dreg Heap", + "Ringed City", +]: + for location in location_tables[region]: + location.dlc = True + +for region in [ + "Firelink Shrine Bell Tower", + "Greirat's Shop", + "Karla's Shop" +]: + for location in location_tables[region]: + location.conditional = True + +location_name_groups: Dict[str, Set[str]] = { + # We could insert these locations automatically with setdefault(), but we set them up explicitly + # instead so we can choose the ordering. + "Prominent": set(), + "Progression": set(), + "Boss Rewards": set(), + "Miniboss Rewards": set(), + "Mimic Rewards": set(), + "Hostile NPC Rewards": set(), + "Friendly NPC Rewards": set(), + "Small Crystal Lizards": set(), + "Upgrade": set(), + "Small Souls": set(), + "Boss Souls": set(), + "Unique": set(), + "Healing": set(), + "Miscellaneous": set(), + "Hidden": set(), + "Weapons": set(), + "Shields": set(), + "Armor": set(), + "Rings": set(), + "Spells": set(), +} + +location_descriptions = { + "Prominent": "A small number of locations that are in very obvious locations. Mostly boss " + \ + "drops. Ideal for setting as priority locations.", + "Progression": "Locations that contain items in vanilla which unlock other locations.", + "Boss Rewards": "Boss drops. Does not include soul transfusions or shop items.", + "Miniboss Rewards": "Miniboss drops. Only includes enemies considered minibosses by the " + \ + "enemy randomizer.", + "Mimic Rewards": "Drops from enemies that are mimics in vanilla.", + "Hostile NPC Rewards": "Drops from NPCs that are hostile to you. This includes scripted " + \ + "invaders and initially-friendly NPCs that must be fought as part of their quest.", + "Friendly NPC Rewards": "Items given by friendly NPCs as part of their quests or from " + \ + "non-violent interaction.", + "Upgrade": "Locations that contain upgrade items in vanilla, including titanite, gems, and " + \ + "Shriving Stones.", + "Small Souls": "Locations that contain soul items in vanilla, not including boss souls.", + "Boss Souls": "Locations that contain boss souls in vanilla, as well as Soul of Rosaria.", + "Unique": "Locations that contain items in vanilla that are unique per NG cycle, such as " + \ + "scrolls, keys, ashes, and so on. Doesn't cover equipment, spells, or souls.", + "Healing": "Locations that contain Undead Bone Shards and Estus Shards in vanilla.", + "Miscellaneous": "Locations that contain generic stackable items in vanilla, such as arrows, " + + "firebombs, buffs, and so on.", + "Hidden": "Locations that are particularly difficult to find, such as behind illusory " + \ + "walls, down hidden drops, and so on. Does not include large locations like Untended " + \ + "Graves or Archdragon Peak.", + "Weapons": "Locations that contain weapons in vanilla.", + "Shields": "Locations that contain shields in vanilla.", + "Armor": "Locations that contain armor in vanilla.", + "Rings": "Locations that contain rings in vanilla.", + "Spells": "Locations that contain spells in vanilla.", } location_dictionary: Dict[str, DS3LocationData] = {} -for location_table in location_tables.values(): +for location_name, location_table in location_tables.items(): location_dictionary.update({location_data.name: location_data for location_data in location_table}) + + for location_data in location_table: + if not location_data.is_event: + for group_name in location_data.location_groups(): + location_name_groups[group_name].add(location_data.name) + + # Allow entire locations to be added to location sets. + if not location_name.endswith(" Shop"): + location_name_groups[location_name] = set([ + location_data.name for location_data in location_table + if not location_data.is_event + ]) + +location_name_groups["Painted World of Ariandel"] = ( + location_name_groups["Painted World of Ariandel (Before Contraption)"] + .union(location_name_groups["Painted World of Ariandel (After Contraption)"]) +) +del location_name_groups["Painted World of Ariandel (Before Contraption)"] +del location_name_groups["Painted World of Ariandel (After Contraption)"] + +location_name_groups["DLC"] = ( + location_name_groups["Painted World of Ariandel"] + .union(location_name_groups["Dreg Heap"]) + .union(location_name_groups["Ringed City"]) +) diff --git a/worlds/dark_souls_3/Options.py b/worlds/dark_souls_3/Options.py index df0bb953b8..ad81dd9f7b 100644 --- a/worlds/dark_souls_3/Options.py +++ b/worlds/dark_souls_3/Options.py @@ -1,80 +1,78 @@ -import typing +from dataclasses import dataclass +import json +from typing import Any, Dict -from Options import Toggle, DefaultOnToggle, Option, Range, Choice, ItemDict, DeathLink +from Options import Choice, DeathLink, DefaultOnToggle, ExcludeLocations, NamedRange, OptionDict, \ + OptionGroup, PerGameCommonOptions, Range, Removed, Toggle + +## Game Options -class RandomizeWeaponLocations(DefaultOnToggle): - """Randomizes weapons (+76 locations)""" - display_name = "Randomize Weapon Locations" +class EarlySmallLothricBanner(Choice): + """Force Small Lothric Banner into an early sphere in your world or across all worlds.""" + display_name = "Early Small Lothric Banner" + option_off = 0 + option_early_global = 1 + option_early_local = 2 + default = option_off -class RandomizeShieldLocations(DefaultOnToggle): - """Randomizes shields (+24 locations)""" - display_name = "Randomize Shield Locations" +class LateBasinOfVowsOption(Choice): + """Guarantee that you don't need to enter Lothric Castle until later in the run. + + - **Off:** You may have to enter Lothric Castle and the areas beyond it immediately after High + Wall of Lothric. + - **After Small Lothric Banner:** You may have to enter Lothric Castle after Catacombs of + Carthus. + - **After Small Doll:** You won't have to enter Lothric Castle until after Irithyll of the + Boreal Valley. + """ + display_name = "Late Basin of Vows" + option_off = 0 + alias_false = 0 + option_after_small_lothric_banner = 1 + alias_true = 1 + option_after_small_doll = 2 -class RandomizeArmorLocations(DefaultOnToggle): - """Randomizes armor pieces (+97 locations)""" - display_name = "Randomize Armor Locations" +class LateDLCOption(Choice): + """Guarantee that you don't need to enter the DLC until later in the run. + + - **Off:** You may have to enter the DLC after Catacombs of Carthus. + - **After Small Doll:** You may have to enter the DLC after Irithyll of the Boreal Valley. + - **After Basin:** You won't have to enter the DLC until after Lothric Castle. + """ + display_name = "Late DLC" + option_off = 0 + alias_false = 0 + option_after_small_doll = 1 + alias_true = 1 + option_after_basin = 2 -class RandomizeRingLocations(DefaultOnToggle): - """Randomizes rings (+49 locations)""" - display_name = "Randomize Ring Locations" +class EnableDLCOption(Toggle): + """Include DLC locations, items, and enemies in the randomized pools. + + To use this option, you must own both the "Ashes of Ariandel" and the "Ringed City" DLCs. + """ + display_name = "Enable DLC" -class RandomizeSpellLocations(DefaultOnToggle): - """Randomizes spells (+18 locations)""" - display_name = "Randomize Spell Locations" +class EnableNGPOption(Toggle): + """Include items and locations exclusive to NG+ cycles.""" + display_name = "Enable NG+" -class RandomizeKeyLocations(DefaultOnToggle): - """Randomizes items which unlock doors or bypass barriers""" - display_name = "Randomize Key Locations" +## Equipment + +class RandomizeStartingLoadout(DefaultOnToggle): + """Randomizes the equipment characters begin with.""" + display_name = "Randomize Starting Loadout" -class RandomizeBossSoulLocations(DefaultOnToggle): - """Randomizes Boss Souls (+18 Locations)""" - display_name = "Randomize Boss Soul Locations" - - -class RandomizeNPCLocations(Toggle): - """Randomizes friendly NPC drops (meaning you will probably have to kill them) (+14 locations)""" - display_name = "Randomize NPC Locations" - - -class RandomizeMiscLocations(Toggle): - """Randomizes miscellaneous items (ashes, tomes, scrolls, etc.) to the pool. (+36 locations)""" - display_name = "Randomize Miscellaneous Locations" - - -class RandomizeHealthLocations(Toggle): - """Randomizes health upgrade items. (+21 locations)""" - display_name = "Randomize Health Upgrade Locations" - - -class RandomizeProgressiveLocationsOption(Toggle): - """Randomizes upgrade materials and consumables such as the titanite shards, firebombs, resin, etc... - - Instead of specific locations, these are progressive, so Titanite Shard #1 is the first titanite shard - you pick up, regardless of whether it's from an enemy drop late in the game or an item on the ground in the - first 5 minutes.""" - display_name = "Randomize Progressive Locations" - - -class PoolTypeOption(Choice): - """Changes which non-progression items you add to the pool - - Shuffle: Items are picked from the locations being randomized - Various: Items are picked from a list of all items in the game, but are the same type of item they replace""" - display_name = "Pool Type" - option_shuffle = 0 - option_various = 1 - - -class GuaranteedItemsOption(ItemDict): - """Guarantees that the specified items will be in the item pool""" - display_name = "Guaranteed Items" +class RequireOneHandedStartingWeapons(DefaultOnToggle): + """Require starting equipment to be usable one-handed.""" + display_name = "Require One-Handed Starting Weapons" class AutoEquipOption(Toggle): @@ -83,47 +81,56 @@ class AutoEquipOption(Toggle): class LockEquipOption(Toggle): - """Lock the equipment slots so you cannot change your armor or your left/right weapons. Works great with the - Auto-equip option.""" + """Lock the equipment slots so you cannot change your armor or your left/right weapons. + + Works great with the Auto-equip option. + """ display_name = "Lock Equipment Slots" +class NoEquipLoadOption(Toggle): + """Disable the equip load constraint from the game.""" + display_name = "No Equip Load" + + class NoWeaponRequirementsOption(Toggle): - """Disable the weapon requirements by removing any movement or damage penalties. - Permitting you to use any weapon early""" + """Disable the weapon requirements by removing any movement or damage penalties, permitting you + to use any weapon early. + """ display_name = "No Weapon Requirements" class NoSpellRequirementsOption(Toggle): - """Disable the spell requirements permitting you to use any spell""" + """Disable the spell requirements permitting you to use any spell.""" display_name = "No Spell Requirements" -class NoEquipLoadOption(Toggle): - """Disable the equip load constraint from the game""" - display_name = "No Equip Load" - +## Weapons class RandomizeInfusionOption(Toggle): """Enable this option to infuse a percentage of the pool of weapons and shields.""" display_name = "Randomize Infusion" -class RandomizeInfusionPercentageOption(Range): - """The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled""" +class RandomizeInfusionPercentageOption(NamedRange): + """The percentage of weapons/shields in the pool to be infused if Randomize Infusion is toggled. + """ display_name = "Percentage of Infused Weapons" range_start = 0 range_end = 100 default = 33 + # 3/155 weapons are infused in the base game, or about 2% + special_range_names = {"similar to base game": 2} class RandomizeWeaponLevelOption(Choice): - """Enable this option to upgrade a percentage of the pool of weapons to a random value between the minimum and - maximum levels defined. + """Enable this option to upgrade a percentage of the pool of weapons to a random value between + the minimum and maximum levels defined. - All: All weapons are eligible, both basic and epic - Basic: Only weapons that can be upgraded to +10 - Epic: Only weapons that can be upgraded to +5""" + - **All:** All weapons are eligible, both basic and epic + - **Basic:** Only weapons that can be upgraded to +10 + - **Epic:** Only weapons that can be upgraded to +5 + """ display_name = "Randomize Weapon Level" option_none = 0 option_all = 1 @@ -132,7 +139,7 @@ class RandomizeWeaponLevelOption(Choice): class RandomizeWeaponLevelPercentageOption(Range): - """The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled""" + """The percentage of weapons in the pool to be upgraded if randomize weapons level is toggled.""" display_name = "Percentage of Randomized Weapons" range_start = 0 range_end = 100 @@ -140,7 +147,7 @@ class RandomizeWeaponLevelPercentageOption(Range): class MinLevelsIn5WeaponPoolOption(Range): - """The minimum upgraded value of a weapon in the pool of weapons that can only reach +5""" + """The minimum upgraded value of a weapon in the pool of weapons that can only reach +5.""" display_name = "Minimum Level of +5 Weapons" range_start = 0 range_end = 5 @@ -148,7 +155,7 @@ class MinLevelsIn5WeaponPoolOption(Range): class MaxLevelsIn5WeaponPoolOption(Range): - """The maximum upgraded value of a weapon in the pool of weapons that can only reach +5""" + """The maximum upgraded value of a weapon in the pool of weapons that can only reach +5.""" display_name = "Maximum Level of +5 Weapons" range_start = 0 range_end = 5 @@ -156,7 +163,7 @@ class MaxLevelsIn5WeaponPoolOption(Range): class MinLevelsIn10WeaponPoolOption(Range): - """The minimum upgraded value of a weapon in the pool of weapons that can reach +10""" + """The minimum upgraded value of a weapon in the pool of weapons that can reach +10.""" display_name = "Minimum Level of +10 Weapons" range_start = 0 range_end = 10 @@ -164,72 +171,308 @@ class MinLevelsIn10WeaponPoolOption(Range): class MaxLevelsIn10WeaponPoolOption(Range): - """The maximum upgraded value of a weapon in the pool of weapons that can reach +10""" + """The maximum upgraded value of a weapon in the pool of weapons that can reach +10.""" display_name = "Maximum Level of +10 Weapons" range_start = 0 range_end = 10 default = 10 -class EarlySmallLothricBanner(Choice): - """This option makes it so the user can choose to force the Small Lothric Banner into an early sphere in their world or - into an early sphere across all worlds.""" - display_name = "Early Small Lothric Banner" - option_off = 0 - option_early_global = 1 - option_early_local = 2 - default = option_off +## Item Smoothing + +class SmoothSoulItemsOption(DefaultOnToggle): + """Distribute soul items in a similar order as the base game. + + By default, soul items will be distributed totally randomly. If this is set, less valuable soul + items will generally appear in earlier spheres and more valuable ones will generally appear + later. + """ + display_name = "Smooth Soul Items" -class LateBasinOfVowsOption(Toggle): - """This option makes it so the Basin of Vows is still randomized, but guarantees you that you wont have to venture into - Lothric Castle to find your Small Lothric Banner to get out of High Wall of Lothric. So you may find Basin of Vows early, - but you wont have to fight Dancer to find your Small Lothric Banner.""" - display_name = "Late Basin of Vows" +class SmoothUpgradeItemsOption(DefaultOnToggle): + """Distribute upgrade items in a similar order as the base game. + + By default, upgrade items will be distributed totally randomly. If this is set, lower-level + upgrade items will generally appear in earlier spheres and higher-level ones will generally + appear later. + """ + display_name = "Smooth Upgrade Items" -class LateDLCOption(Toggle): - """This option makes it so you are guaranteed to find your Small Doll without having to venture off into the DLC, - effectively putting anything in the DLC in logic after finding both Contraption Key and Small Doll, - and being able to get into Irithyll of the Boreal Valley.""" - display_name = "Late DLC" +class SmoothUpgradedWeaponsOption(DefaultOnToggle): + """Distribute upgraded weapons in a similar order as the base game. + + By default, upgraded weapons will be distributed totally randomly. If this is set, lower-level + weapons will generally appear in earlier spheres and higher-level ones will generally appear + later. + """ + display_name = "Smooth Upgraded Weapons" -class EnableDLCOption(Toggle): - """To use this option, you must own both the ASHES OF ARIANDEL and the RINGED CITY DLC""" - display_name = "Enable DLC" +### Enemies + +class RandomizeEnemiesOption(DefaultOnToggle): + """Randomize enemy and boss placements.""" + display_name = "Randomize Enemies" -dark_souls_options: typing.Dict[str, Option] = { - "enable_weapon_locations": RandomizeWeaponLocations, - "enable_shield_locations": RandomizeShieldLocations, - "enable_armor_locations": RandomizeArmorLocations, - "enable_ring_locations": RandomizeRingLocations, - "enable_spell_locations": RandomizeSpellLocations, - "enable_key_locations": RandomizeKeyLocations, - "enable_boss_locations": RandomizeBossSoulLocations, - "enable_npc_locations": RandomizeNPCLocations, - "enable_misc_locations": RandomizeMiscLocations, - "enable_health_upgrade_locations": RandomizeHealthLocations, - "enable_progressive_locations": RandomizeProgressiveLocationsOption, - "pool_type": PoolTypeOption, - "guaranteed_items": GuaranteedItemsOption, - "auto_equip": AutoEquipOption, - "lock_equip": LockEquipOption, - "no_weapon_requirements": NoWeaponRequirementsOption, - "randomize_infusion": RandomizeInfusionOption, - "randomize_infusion_percentage": RandomizeInfusionPercentageOption, - "randomize_weapon_level": RandomizeWeaponLevelOption, - "randomize_weapon_level_percentage": RandomizeWeaponLevelPercentageOption, - "min_levels_in_5": MinLevelsIn5WeaponPoolOption, - "max_levels_in_5": MaxLevelsIn5WeaponPoolOption, - "min_levels_in_10": MinLevelsIn10WeaponPoolOption, - "max_levels_in_10": MaxLevelsIn10WeaponPoolOption, - "early_banner": EarlySmallLothricBanner, - "late_basin_of_vows": LateBasinOfVowsOption, - "late_dlc": LateDLCOption, - "no_spell_requirements": NoSpellRequirementsOption, - "no_equip_load": NoEquipLoadOption, - "death_link": DeathLink, - "enable_dlc": EnableDLCOption, -} +class SimpleEarlyBossesOption(DefaultOnToggle): + """Avoid replacing Iudex Gundyr and Vordt with late bosses. + + This excludes all bosses after Dancer of the Boreal Valley from these two boss fights. Disable + it for a chance at a much harder early game. + + This is ignored unless enemies are randomized. + """ + display_name = "Simple Early Bosses" + + +class ScaleEnemiesOption(DefaultOnToggle): + """Scale randomized enemy stats to match the areas in which they appear. + + Disabling this will tend to make the early game much more difficult and the late game much + easier. + + This is ignored unless enemies are randomized. + """ + display_name = "Scale Enemies" + + +class RandomizeMimicsWithEnemiesOption(Toggle): + """Mix Mimics into the main enemy pool. + + If this is enabled, Mimics will be replaced by normal enemies who drop the Mimic rewards on + death, and Mimics will be placed randomly in place of normal enemies. It's recommended to enable + Impatient Mimics as well if you enable this. + + This is ignored unless enemies are randomized. + """ + display_name = "Randomize Mimics With Enemies" + + +class RandomizeSmallCrystalLizardsWithEnemiesOption(Toggle): + """Mix small Crystal Lizards into the main enemy pool. + + If this is enabled, Crystal Lizards will be replaced by normal enemies who drop the Crystal + Lizard rewards on death, and Crystal Lizards will be placed randomly in place of normal enemies. + + This is ignored unless enemies are randomized. + """ + display_name = "Randomize Small Crystal Lizards With Enemies" + + +class ReduceHarmlessEnemiesOption(Toggle): + """Reduce the frequency that "harmless" enemies appear. + + Enable this to add a bit of extra challenge. This severely limits the number of enemies that are + slow to aggro, slow to attack, and do very little damage that appear in the enemy pool. + + This is ignored unless enemies are randomized. + """ + display_name = "Reduce Harmless Enemies" + + +class AllChestsAreMimicsOption(Toggle): + """Replace all chests with mimics that drop the same items. + + If "Randomize Mimics With Enemies" is set, these chests will instead be replaced with random + enemies that drop the same items. + + This is ignored unless enemies are randomized. + """ + display_name = "All Chests Are Mimics" + + +class ImpatientMimicsOption(Toggle): + """Mimics attack as soon as you get close instead of waiting for you to open them. + + This is ignored unless enemies are randomized. + """ + display_name = "Impatient Mimics" + + +class RandomEnemyPresetOption(OptionDict): + """The YAML preset for the static enemy randomizer. + + See the static randomizer documentation in `randomizer\\presets\\README.txt` for details. + Include this as nested YAML. For example: + + .. code-block:: YAML + + random_enemy_preset: + RemoveSource: Ancient Wyvern; Darkeater Midir + DontRandomize: Iudex Gundyr + """ + display_name = "Random Enemy Preset" + supports_weighting = False + default = {} + + valid_keys = ["Description", "RecommendFullRandomization", "RecommendNoEnemyProgression", + "OopsAll", "Boss", "Miniboss", "Basic", "BuffBasicEnemiesAsBosses", + "DontRandomize", "RemoveSource", "Enemies"] + + @classmethod + def get_option_name(cls, value: Dict[str, Any]) -> str: + return json.dumps(value) + + +## Item & Location + +class DS3ExcludeLocations(ExcludeLocations): + """Prevent these locations from having an important item.""" + default = frozenset({"Hidden", "Small Crystal Lizards", "Upgrade", "Small Souls", "Miscellaneous"}) + + +class ExcludedLocationBehaviorOption(Choice): + """How to choose items for excluded locations in DS3. + + - **Allow Useful:** Excluded locations can't have progression items, but they can have useful + items. + - **Forbid Useful:** Neither progression items nor useful items can be placed in excluded + locations. + - **Do Not Randomize:** Excluded locations always contain the same item as in vanilla Dark Souls + III. + + A "progression item" is anything that's required to unlock another location in some game. A + "useful item" is something each game defines individually, usually items that are quite + desirable but not strictly necessary. + """ + display_name = "Excluded Locations Behavior" + option_allow_useful = 1 + option_forbid_useful = 2 + option_do_not_randomize = 3 + default = 2 + + +class MissableLocationBehaviorOption(Choice): + """Which items can be placed in locations that can be permanently missed. + + - **Allow Useful:** Missable locations can't have progression items, but they can have useful + items. + - **Forbid Useful:** Neither progression items nor useful items can be placed in missable + locations. + - **Do Not Randomize:** Missable locations always contain the same item as in vanilla Dark Souls + III. + + A "progression item" is anything that's required to unlock another location in some game. A + "useful item" is something each game defines individually, usually items that are quite + desirable but not strictly necessary. + """ + display_name = "Missable Locations Behavior" + option_allow_useful = 1 + option_forbid_useful = 2 + option_do_not_randomize = 3 + default = 2 + + +@dataclass +class DarkSouls3Options(PerGameCommonOptions): + # Game Options + early_banner: EarlySmallLothricBanner + late_basin_of_vows: LateBasinOfVowsOption + late_dlc: LateDLCOption + death_link: DeathLink + enable_dlc: EnableDLCOption + enable_ngp: EnableNGPOption + + # Equipment + random_starting_loadout: RandomizeStartingLoadout + require_one_handed_starting_weapons: RequireOneHandedStartingWeapons + auto_equip: AutoEquipOption + lock_equip: LockEquipOption + no_equip_load: NoEquipLoadOption + no_weapon_requirements: NoWeaponRequirementsOption + no_spell_requirements: NoSpellRequirementsOption + + # Weapons + randomize_infusion: RandomizeInfusionOption + randomize_infusion_percentage: RandomizeInfusionPercentageOption + randomize_weapon_level: RandomizeWeaponLevelOption + randomize_weapon_level_percentage: RandomizeWeaponLevelPercentageOption + min_levels_in_5: MinLevelsIn5WeaponPoolOption + max_levels_in_5: MaxLevelsIn5WeaponPoolOption + min_levels_in_10: MinLevelsIn10WeaponPoolOption + max_levels_in_10: MaxLevelsIn10WeaponPoolOption + + # Item Smoothing + smooth_soul_items: SmoothSoulItemsOption + smooth_upgrade_items: SmoothUpgradeItemsOption + smooth_upgraded_weapons: SmoothUpgradedWeaponsOption + + # Enemies + randomize_enemies: RandomizeEnemiesOption + simple_early_bosses: SimpleEarlyBossesOption + scale_enemies: ScaleEnemiesOption + randomize_mimics_with_enemies: RandomizeMimicsWithEnemiesOption + randomize_small_crystal_lizards_with_enemies: RandomizeSmallCrystalLizardsWithEnemiesOption + reduce_harmless_enemies: ReduceHarmlessEnemiesOption + all_chests_are_mimics: AllChestsAreMimicsOption + impatient_mimics: ImpatientMimicsOption + random_enemy_preset: RandomEnemyPresetOption + + # Item & Location + exclude_locations: DS3ExcludeLocations + excluded_location_behavior: ExcludedLocationBehaviorOption + missable_location_behavior: MissableLocationBehaviorOption + + # Removed + pool_type: Removed + enable_weapon_locations: Removed + enable_shield_locations: Removed + enable_armor_locations: Removed + enable_ring_locations: Removed + enable_spell_locations: Removed + enable_key_locations: Removed + enable_boss_locations: Removed + enable_npc_locations: Removed + enable_misc_locations: Removed + enable_health_upgrade_locations: Removed + enable_progressive_locations: Removed + guaranteed_items: Removed + excluded_locations: Removed + missable_locations: Removed + + +option_groups = [ + OptionGroup("Equipment", [ + RandomizeStartingLoadout, + RequireOneHandedStartingWeapons, + AutoEquipOption, + LockEquipOption, + NoEquipLoadOption, + NoWeaponRequirementsOption, + NoSpellRequirementsOption, + ]), + OptionGroup("Weapons", [ + RandomizeInfusionOption, + RandomizeInfusionPercentageOption, + RandomizeWeaponLevelOption, + RandomizeWeaponLevelPercentageOption, + MinLevelsIn5WeaponPoolOption, + MaxLevelsIn5WeaponPoolOption, + MinLevelsIn10WeaponPoolOption, + MaxLevelsIn10WeaponPoolOption, + ]), + OptionGroup("Item Smoothing", [ + SmoothSoulItemsOption, + SmoothUpgradeItemsOption, + SmoothUpgradedWeaponsOption, + ]), + OptionGroup("Enemies", [ + RandomizeEnemiesOption, + SimpleEarlyBossesOption, + ScaleEnemiesOption, + RandomizeMimicsWithEnemiesOption, + RandomizeSmallCrystalLizardsWithEnemiesOption, + ReduceHarmlessEnemiesOption, + AllChestsAreMimicsOption, + ImpatientMimicsOption, + RandomEnemyPresetOption, + ]), + OptionGroup("Item & Location Options", [ + DS3ExcludeLocations, + ExcludedLocationBehaviorOption, + MissableLocationBehaviorOption, + ]) +] diff --git a/worlds/dark_souls_3/__init__.py b/worlds/dark_souls_3/__init__.py index 0200109811..159a870c76 100644 --- a/worlds/dark_souls_3/__init__.py +++ b/worlds/dark_souls_3/__init__.py @@ -1,15 +1,19 @@ # world/dark_souls_3/__init__.py -from typing import Dict, Set, List +from collections.abc import Sequence +from collections import defaultdict +import json +from logging import warning +from typing import cast, Any, Callable, Dict, Set, List, Optional, TextIO, Union -from BaseClasses import MultiWorld, Region, Item, Entrance, Tutorial, ItemClassification -from Options import Toggle +from BaseClasses import CollectionState, MultiWorld, Region, Location, LocationProgressType, Entrance, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld -from worlds.generic.Rules import set_rule, add_rule, add_item_rule +from worlds.generic.Rules import CollectionRule, ItemRule, add_rule, add_item_rule -from .Items import DarkSouls3Item, DS3ItemCategory, item_dictionary, key_item_names, item_descriptions -from .Locations import DarkSouls3Location, DS3LocationCategory, location_tables, location_dictionary -from .Options import RandomizeWeaponLevelOption, PoolTypeOption, EarlySmallLothricBanner, dark_souls_options +from .Bosses import DS3BossInfo, all_bosses, default_yhorm_location +from .Items import DarkSouls3Item, DS3ItemData, Infusion, UsefulIf, filler_item_names, item_descriptions, item_dictionary, item_name_groups +from .Locations import DarkSouls3Location, DS3LocationData, location_tables, location_descriptions, location_dictionary, location_name_groups, region_order +from .Options import DarkSouls3Options, option_groups class DarkSouls3Web(WebWorld): @@ -34,91 +38,117 @@ class DarkSouls3Web(WebWorld): ) tutorials = [setup_en, setup_fr] - + option_groups = option_groups item_descriptions = item_descriptions + rich_text_options_doc = True class DarkSouls3World(World): """ Dark souls III is an Action role-playing game and is part of the Souls series developed by FromSoftware. - Played in a third-person perspective, players have access to various weapons, armour, magic, and consumables that + Played from a third-person perspective, players have access to various weapons, armour, magic, and consumables that they can use to fight their enemies. """ - game: str = "Dark Souls III" - option_definitions = dark_souls_options - topology_present: bool = True + game = "Dark Souls III" + options: DarkSouls3Options + options_dataclass = DarkSouls3Options web = DarkSouls3Web() base_id = 100000 - enabled_location_categories: Set[DS3LocationCategory] required_client_version = (0, 4, 2) - item_name_to_id = DarkSouls3Item.get_name_to_id() - location_name_to_id = DarkSouls3Location.get_name_to_id() - item_name_groups = { - "Cinders": { - "Cinders of a Lord - Abyss Watcher", - "Cinders of a Lord - Aldrich", - "Cinders of a Lord - Yhorm the Giant", - "Cinders of a Lord - Lothric Prince" - } + item_name_to_id = {data.name: data.ap_code for data in item_dictionary.values() if data.ap_code is not None} + location_name_to_id = { + location.name: location.ap_code + for locations in location_tables.values() + for location in locations + if location.ap_code is not None } + location_name_groups = location_name_groups + item_name_groups = item_name_groups + location_descriptions = location_descriptions + item_descriptions = item_descriptions + + yhorm_location: DS3BossInfo = default_yhorm_location + """If enemy randomization is enabled, this is the boss who Yhorm the Giant should replace. + + This is used to determine where the Storm Ruler can be placed. + """ + + all_excluded_locations: Set[str] = set() + """This is the same value as `self.options.exclude_locations.value` initially, but if + `options.exclude_locations` gets cleared due to `excluded_locations: allow_useful` this still + holds the old locations so we can ensure they don't get necessary items. + """ + + local_itempool: List[DarkSouls3Item] = [] + """The pool of all items within this particular world. This is a subset of + `self.multiworld.itempool`.""" def __init__(self, multiworld: MultiWorld, player: int): super().__init__(multiworld, player) - self.locked_items = [] - self.locked_locations = [] - self.main_path_locations = [] - self.enabled_location_categories = set() + self.all_excluded_locations = set() + def generate_early(self) -> None: + self.all_excluded_locations.update(self.options.exclude_locations.value) - def generate_early(self): - if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.WEAPON) - if self.multiworld.enable_shield_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.SHIELD) - if self.multiworld.enable_armor_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.ARMOR) - if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.RING) - if self.multiworld.enable_spell_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.SPELL) - if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.NPC) - if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.KEY) - if self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_global: - self.multiworld.early_items[self.player]['Small Lothric Banner'] = 1 - elif self.multiworld.early_banner[self.player] == EarlySmallLothricBanner.option_early_local: - self.multiworld.local_early_items[self.player]['Small Lothric Banner'] = 1 - if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.BOSS) - if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.MISC) - if self.multiworld.enable_health_upgrade_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.HEALTH) - if self.multiworld.enable_progressive_locations[self.player] == Toggle.option_true: - self.enabled_location_categories.add(DS3LocationCategory.PROGRESSIVE_ITEM) + # Inform Universal Tracker where Yhorm is being randomized to. + if hasattr(self.multiworld, "re_gen_passthrough"): + if "Dark Souls III" in self.multiworld.re_gen_passthrough: + if self.multiworld.re_gen_passthrough["Dark Souls III"]["options"]["randomize_enemies"]: + yhorm_data = self.multiworld.re_gen_passthrough["Dark Souls III"]["yhorm"] + for boss in all_bosses: + if yhorm_data.startswith(boss.name): + self.yhorm_location = boss + # Randomize Yhorm manually so that we know where to place the Storm Ruler. + elif self.options.randomize_enemies: + self.yhorm_location = self.random.choice( + [boss for boss in all_bosses if self._allow_boss_for_yhorm(boss)]) - def create_regions(self): - progressive_location_table = [] - if self.multiworld.enable_progressive_locations[self.player]: - progressive_location_table = [] + \ - location_tables["Progressive Items 1"] + \ - location_tables["Progressive Items 2"] + \ - location_tables["Progressive Items 3"] + \ - location_tables["Progressive Items 4"] + # If Yhorm is early, make sure the Storm Ruler is easily available to avoid BK + # Iudex Gundyr is handled separately in _fill_local_items + if ( + self.yhorm_location.name == "Vordt of the Boreal Valley" or ( + self.yhorm_location.name == "Dancer of the Boreal Valley" and + not self.options.late_basin_of_vows + ) + ): + self.multiworld.local_early_items[self.player]["Storm Ruler"] = 1 - if self.multiworld.enable_dlc[self.player].value: - progressive_location_table += location_tables["Progressive Items DLC"] + def _allow_boss_for_yhorm(self, boss: DS3BossInfo) -> bool: + """Returns whether boss is a valid location for Yhorm in this seed.""" - if self.multiworld.enable_health_upgrade_locations[self.player]: - progressive_location_table += location_tables["Progressive Items Health"] + if not self.options.enable_dlc and boss.dlc: return False + if not self._is_location_available("PC: Storm Ruler - boss room"): + # If the Storm Ruler isn't randomized, make sure the player can get to the normal Storm + # Ruler location before they need to get through Yhorm. + if boss.before_storm_ruler: return False + + # If the Small Doll also wasn't randomized, make sure Yhorm isn't blocking access to it + # or it won't be possible to get into Profaned Capital before beating him. + if ( + not self._is_location_available("CD: Small Doll - boss drop") + and boss.name in {"Crystal Sage", "Deacons of the Deep"} + ): + return False + + if boss.name != "Iudex Gundyr": return True + + # Cemetery of Ash has very few locations and all of them are excluded by default, so only + # allow Yhorm as Iudex Gundyr if there's at least one available location. + return any( + self._is_location_available(location) + and location.name not in self.all_excluded_locations + and location.name != "CA: Coiled Sword - boss drop" + for location in location_tables["Cemetery of Ash"] + ) + + def create_regions(self) -> None: # Create Vanilla Regions - regions: Dict[str, Region] = {} - regions["Menu"] = self.create_region("Menu", progressive_location_table) + regions: Dict[str, Region] = {"Menu": self.create_region("Menu", {})} regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [ + "Cemetery of Ash", "Firelink Shrine", "Firelink Shrine Bell Tower", "High Wall of Lothric", @@ -138,18 +168,15 @@ class DarkSouls3World(World): "Untended Graves", "Archdragon Peak", "Kiln of the First Flame", + "Greirat's Shop", + "Karla's Shop", ]}) - # Adds Path of the Dragon as an event item for Archdragon Peak access - potd_location = DarkSouls3Location(self.player, "CKG: Path of the Dragon", DS3LocationCategory.EVENT, "Path of the Dragon", None, regions["Consumed King's Garden"]) - potd_location.place_locked_item(Item("Path of the Dragon", ItemClassification.progression, None, self.player)) - regions["Consumed King's Garden"].locations.append(potd_location) - # Create DLC Regions - if self.multiworld.enable_dlc[self.player]: + if self.options.enable_dlc: regions.update({region_name: self.create_region(region_name, location_tables[region_name]) for region_name in [ - "Painted World of Ariandel 1", - "Painted World of Ariandel 2", + "Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)", "Dreg Heap", "Ringed City", ]}) @@ -161,7 +188,9 @@ class DarkSouls3World(World): connection.connect(regions[to_region]) regions["Menu"].exits.append(Entrance(self.player, "New Game", regions["Menu"])) - self.multiworld.get_entrance("New Game", self.player).connect(regions["Firelink Shrine"]) + self.multiworld.get_entrance("New Game", self.player).connect(regions["Cemetery of Ash"]) + + create_connection("Cemetery of Ash", "Firelink Shrine") create_connection("Firelink Shrine", "High Wall of Lothric") create_connection("Firelink Shrine", "Firelink Shrine Bell Tower") @@ -169,6 +198,7 @@ class DarkSouls3World(World): create_connection("High Wall of Lothric", "Undead Settlement") create_connection("High Wall of Lothric", "Lothric Castle") + create_connection("High Wall of Lothric", "Greirat's Shop") create_connection("Undead Settlement", "Road of Sacrifices") @@ -185,6 +215,7 @@ class DarkSouls3World(World): create_connection("Irithyll Dungeon", "Archdragon Peak") create_connection("Irithyll Dungeon", "Profaned Capital") + create_connection("Irithyll Dungeon", "Karla's Shop") create_connection("Lothric Castle", "Consumed King's Garden") create_connection("Lothric Castle", "Grand Archives") @@ -192,357 +223,1349 @@ class DarkSouls3World(World): create_connection("Consumed King's Garden", "Untended Graves") # Connect DLC Regions - if self.multiworld.enable_dlc[self.player]: - create_connection("Cathedral of the Deep", "Painted World of Ariandel 1") - create_connection("Painted World of Ariandel 1", "Painted World of Ariandel 2") - create_connection("Painted World of Ariandel 2", "Dreg Heap") + if self.options.enable_dlc: + create_connection("Cathedral of the Deep", "Painted World of Ariandel (Before Contraption)") + create_connection("Painted World of Ariandel (Before Contraption)", + "Painted World of Ariandel (After Contraption)") + create_connection("Painted World of Ariandel (After Contraption)", "Dreg Heap") create_connection("Dreg Heap", "Ringed City") - # For each region, add the associated locations retrieved from the corresponding location_table def create_region(self, region_name, location_table) -> Region: new_region = Region(region_name, self.player, self.multiworld) + # Use this to un-exclude event locations so the fill doesn't complain about items behind + # them being unreachable. + excluded = self.options.exclude_locations.value + for location in location_table: - if location.category in self.enabled_location_categories: - new_location = DarkSouls3Location( - self.player, - location.name, - location.category, - location.default_item, - self.location_name_to_id[location.name], - new_region - ) + if self._is_location_available(location): + new_location = DarkSouls3Location(self.player, location, new_region) + if ( + # Exclude missable locations that don't allow useful items + location.missable and self.options.missable_location_behavior == "forbid_useful" + and not ( + # Unless they are excluded to a higher degree already + location.name in self.all_excluded_locations + and self.options.missable_location_behavior < self.options.excluded_location_behavior + ) + ) or ( + # Lift Chamber Key is missable. Exclude Lift-Chamber-Key-Locked locations if it isn't randomized + not self._is_location_available("FS: Lift Chamber Key - Leonhard") + and location.name == "HWL: Red Eye Orb - wall tower, miniboss" + ) or ( + # Chameleon is missable. Exclude Chameleon-locked locations if it isn't randomized + not self._is_location_available("AL: Chameleon - tomb after marrying Anri") + and location.name in {"RC: Dragonhead Shield - streets monument, across bridge", + "RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "RC: Divine Blessing - streets monument, mob drop", "RC: Lapp's Helm - Lapp", + "RC: Lapp's Armor - Lapp", + "RC: Lapp's Gauntlets - Lapp", + "RC: Lapp's Leggings - Lapp"} + ): + new_location.progress_type = LocationProgressType.EXCLUDED else: - # Replace non-randomized progression items with events - event_item = self.create_item(location.default_item) - if event_item.classification != ItemClassification.progression: + # Don't allow missable duplicates of progression items to be expected progression. + if location.name in {"PC: Storm Ruler - Siegward", + "US: Pyromancy Flame - Cornyx", + "US: Tower Key - kill Irina"}: continue + # Replace non-randomized items with events that give the default item + event_item = ( + self.create_item(location.default_item_name) if location.default_item_name + else DarkSouls3Item.event(location.name, self.player) + ) + new_location = DarkSouls3Location( self.player, - location.name, - location.category, - location.default_item, - None, - new_region + location, + parent = new_region, + event = True, ) event_item.code = None new_location.place_locked_item(event_item) - - if region_name == "Menu": - add_item_rule(new_location, lambda item: not item.advancement) + if location.name in excluded: + excluded.remove(location.name) + # Only remove from all_excluded if excluded does not have priority over missable + if not (self.options.missable_location_behavior < self.options.excluded_location_behavior): + self.all_excluded_locations.remove(location.name) new_region.locations.append(new_location) self.multiworld.regions.append(new_region) return new_region - - def create_items(self): - dlc_enabled = self.multiworld.enable_dlc[self.player] == Toggle.option_true - - itempool_by_category = {category: [] for category in self.enabled_location_categories} + def create_items(self) -> None: + # Just used to efficiently deduplicate items + item_set: Set[str] = set() # Gather all default items on randomized locations + self.local_itempool = [] num_required_extra_items = 0 - for location in self.multiworld.get_locations(self.player): - if location.category in itempool_by_category: - if item_dictionary[location.default_item_name].category == DS3ItemCategory.SKIP: + for location in cast(List[DarkSouls3Location], self.multiworld.get_unfilled_locations(self.player)): + if not self._is_location_available(location.name): + raise Exception("DS3 generation bug: Added an unavailable location.") + + default_item_name = cast(str, location.data.default_item_name) + item = item_dictionary[default_item_name] + if item.skip: + num_required_extra_items += 1 + elif not item.unique: + self.local_itempool.append(self.create_item(default_item_name)) + else: + # For unique items, make sure there aren't duplicates in the item set even if there + # are multiple in-game locations that provide them. + if default_item_name in item_set: num_required_extra_items += 1 else: - itempool_by_category[location.category].append(location.default_item_name) + item_set.add(default_item_name) + self.local_itempool.append(self.create_item(default_item_name)) - # Replace each item category with a random sample of items of those types - if self.multiworld.pool_type[self.player] == PoolTypeOption.option_various: - def create_random_replacement_list(item_categories: Set[DS3ItemCategory], num_items: int): - candidates = [ - item.name for item - in item_dictionary.values() - if (item.category in item_categories and (not item.is_dlc or dlc_enabled)) - ] - return self.multiworld.random.sample(candidates, num_items) + injectables = self._create_injectable_items(num_required_extra_items) + num_required_extra_items -= len(injectables) + self.local_itempool.extend(injectables) - if DS3LocationCategory.WEAPON in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.WEAPON] = create_random_replacement_list( - { - DS3ItemCategory.WEAPON_UPGRADE_5, - DS3ItemCategory.WEAPON_UPGRADE_10, - DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE - }, - len(itempool_by_category[DS3LocationCategory.WEAPON]) - ) - if DS3LocationCategory.SHIELD in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.SHIELD] = create_random_replacement_list( - {DS3ItemCategory.SHIELD, DS3ItemCategory.SHIELD_INFUSIBLE}, - len(itempool_by_category[DS3LocationCategory.SHIELD]) - ) - if DS3LocationCategory.ARMOR in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.ARMOR] = create_random_replacement_list( - {DS3ItemCategory.ARMOR}, - len(itempool_by_category[DS3LocationCategory.ARMOR]) - ) - if DS3LocationCategory.RING in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.RING] = create_random_replacement_list( - {DS3ItemCategory.RING}, - len(itempool_by_category[DS3LocationCategory.RING]) - ) - if DS3LocationCategory.SPELL in self.enabled_location_categories: - itempool_by_category[DS3LocationCategory.SPELL] = create_random_replacement_list( - {DS3ItemCategory.SPELL}, - len(itempool_by_category[DS3LocationCategory.SPELL]) - ) + # Extra filler items for locations containing skip items + self.local_itempool.extend(self.create_item(self.get_filler_item_name()) for _ in range(num_required_extra_items)) - itempool: List[DarkSouls3Item] = [] - for category in self.enabled_location_categories: - itempool += [self.create_item(name) for name in itempool_by_category[category]] - - # A list of items we can replace - removable_items = [item for item in itempool if item.classification != ItemClassification.progression] - - guaranteed_items = self.multiworld.guaranteed_items[self.player].value - for item_name in guaranteed_items: - # Break early just in case nothing is removable (if user is trying to guarantee more - # items than the pool can hold, for example) - if len(removable_items) == 0: - break - - num_existing_copies = len([item for item in itempool if item.name == item_name]) - for _ in range(guaranteed_items[item_name]): - if num_existing_copies > 0: - num_existing_copies -= 1 - continue - - if num_required_extra_items > 0: - # We can just add them instead of using "Soul of an Intrepid Hero" later - num_required_extra_items -= 1 - else: - if len(removable_items) == 0: - break - - # Try to construct a list of items with the same category that can be removed - # If none exist, just remove something at random - removable_shortlist = [ - item for item - in removable_items - if item_dictionary[item.name].category == item_dictionary[item_name].category - ] - if len(removable_shortlist) == 0: - removable_shortlist = removable_items - - removed_item = self.multiworld.random.choice(removable_shortlist) - removable_items.remove(removed_item) # To avoid trying to replace the same item twice - itempool.remove(removed_item) - - itempool.append(self.create_item(item_name)) - - # Extra filler items for locations containing SKIP items - itempool += [self.create_filler() for _ in range(num_required_extra_items)] + # Potentially fill some items locally and remove them from the itempool + self._fill_local_items() # Add items to itempool - self.multiworld.itempool += itempool + self.multiworld.itempool += self.local_itempool + def _create_injectable_items(self, num_required_extra_items: int) -> List[DarkSouls3Item]: + """Returns a list of items to inject into the multiworld instead of skipped items. - def create_item(self, name: str) -> Item: - useful_categories = { - DS3ItemCategory.WEAPON_UPGRADE_5, - DS3ItemCategory.WEAPON_UPGRADE_10, - DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, - DS3ItemCategory.SPELL, - } - data = self.item_name_to_id[name] + If there isn't enough room to inject all the necessary progression items + that are in missable locations by default, this adds them to the + player's starting inventory. + """ - if name in key_item_names: - item_classification = ItemClassification.progression - elif item_dictionary[name].category in useful_categories or name in {"Estus Shard", "Undead Bone Shard"}: - item_classification = ItemClassification.useful - else: - item_classification = ItemClassification.filler + all_injectable_items = [ + item for item + in item_dictionary.values() + if item.inject and (not item.is_dlc or self.options.enable_dlc) + ] + injectable_mandatory = [ + item for item in all_injectable_items + if item.classification == ItemClassification.progression + ] + injectable_optional = [ + item for item in all_injectable_items + if item.classification != ItemClassification.progression + ] - return DarkSouls3Item(name, item_classification, data, self.player) + number_to_inject = min(num_required_extra_items, len(all_injectable_items)) + items = ( + self.random.sample( + injectable_mandatory, + k=min(len(injectable_mandatory), number_to_inject) + ) + + self.random.sample( + injectable_optional, + k=max(0, number_to_inject - len(injectable_mandatory)) + ) + ) + if number_to_inject < len(injectable_mandatory): + # It's worth considering the possibility of _removing_ unimportant + # items from the pool to inject these instead rather than just + # making them part of the starting health back + for item in injectable_mandatory: + if item in items: continue + self.multiworld.push_precollected(self.create_item(item)) + warning( + f"Couldn't add \"{item.name}\" to the item pool for " + + f"{self.player_name}. Adding it to the starting " + + f"inventory instead." + ) + + return [self.create_item(item) for item in items] + + def create_item(self, item: Union[str, DS3ItemData]) -> DarkSouls3Item: + data = item if isinstance(item, DS3ItemData) else item_dictionary[item] + classification = None + if self.multiworld and data.useful_if != UsefulIf.DEFAULT and ( + ( + data.useful_if == UsefulIf.BASE and + not self.options.enable_dlc and + not self.options.enable_ngp + ) + or (data.useful_if == UsefulIf.NO_DLC and not self.options.enable_dlc) + or (data.useful_if == UsefulIf.NO_NGP and not self.options.enable_ngp) + ): + classification = ItemClassification.useful + + if ( + self.options.randomize_weapon_level != "none" + and data.category.upgrade_level + # Because we require the Pyromancy Flame to be available early, don't upgrade it so it + # doesn't get shuffled around by weapon smoothing. + and data.name != "Pyromancy Flame" + ): + # if the user made an error and set a min higher than the max we default to the max + max_5 = self.options.max_levels_in_5.value + min_5 = min(self.options.min_levels_in_5.value, max_5) + max_10 = self.options.max_levels_in_10.value + min_10 = min(self.options.min_levels_in_10.value, max_10) + weapon_level_percentage = self.options.randomize_weapon_level_percentage + + if self.random.randint(0, 99) < weapon_level_percentage: + if data.category.upgrade_level == 5: + data = data.upgrade(self.random.randint(min_5, max_5)) + elif data.category.upgrade_level == 10: + data = data.upgrade(self.random.randint(min_10, max_10)) + + if self.options.randomize_infusion and data.category.is_infusible: + infusion_percentage = self.options.randomize_infusion_percentage + if self.random.randint(0, 99) < infusion_percentage: + data = data.infuse(self.random.choice(list(Infusion))) + + return DarkSouls3Item(self.player, data, classification=classification) + + def _fill_local_items(self) -> None: + """Removes certain items from the item pool and manually places them in the local world. + + We can't do this in pre_fill because the itempool may not be modified after create_items. + """ + # If Yhorm is at Iudex Gundyr, Storm Ruler must be randomized, so it can always be moved. + # Fill this manually so that, if very few slots are available in Cemetery of Ash, this + # doesn't get locked out by bad rolls on the next two fills. + if self.yhorm_location.name == "Iudex Gundyr": + self._fill_local_item("Storm Ruler", ["Cemetery of Ash"], + lambda location: location.name != "CA: Coiled Sword - boss drop") + + # If the Coiled Sword is vanilla, it is early enough and doesn't need to be placed. + # Don't place this in the multiworld because it's necessary almost immediately, and don't + # mark it as a blocker for HWL because having a miniscule Sphere 1 screws with progression balancing. + if self._is_location_available("CA: Coiled Sword - boss drop"): + self._fill_local_item("Coiled Sword", ["Cemetery of Ash", "Firelink Shrine"]) + + # If the HWL Raw Gem is vanilla, it is early enough and doesn't need to be removed. If + # upgrade smoothing is enabled, make sure one raw gem is available early for SL1 players + if ( + self._is_location_available("HWL: Raw Gem - fort roof, lizard") + and self.options.smooth_upgrade_items + ): + self._fill_local_item("Raw Gem", [ + "Cemetery of Ash", + "Firelink Shrine", + "High Wall of Lothric" + ]) + + def _fill_local_item( + self, name: str, + regions: List[str], + additional_condition: Optional[Callable[[DS3LocationData], bool]] = None, + ) -> None: + """Chooses a valid location for the item with the given name and places it there. + + This always chooses a local location among the given regions. If additional_condition is + passed, only locations meeting that condition will be considered. + + If the item could not be placed, it will be added to starting inventory. + """ + item = next((item for item in self.local_itempool if item.name == name), None) + if not item: return + + candidate_locations = [ + location for location in ( + self.multiworld.get_location(location.name, self.player) + for region in regions + for location in location_tables[region] + if self._is_location_available(location) + and not location.missable + and not location.conditional + and (not additional_condition or additional_condition(location)) + ) + # We can't use location.progress_type here because it's not set + # until after `set_rules()` runs. + if not location.item and location.name not in self.all_excluded_locations + and location.item_rule(item) + ] + + self.local_itempool.remove(item) + + if not candidate_locations: + warning(f"Couldn't place \"{name}\" in a valid location for {self.player_name}. Adding it to starting inventory instead.") + location = next( + (location for location in self._get_our_locations() if location.data.default_item_name == item.name), + None + ) + if location: self._replace_with_filler(location) + self.multiworld.push_precollected(self.create_item(name)) + return + + location = self.random.choice(candidate_locations) + location.place_locked_item(item) + + def _replace_with_filler(self, location: DarkSouls3Location) -> None: + """If possible, choose a filler item to replace location's current contents with.""" + if location.locked: return + + # Try 10 filler items. If none of them work, give up and leave it as-is. + for _ in range(0, 10): + candidate = self.create_filler() + if location.item_rule(candidate): + location.item = candidate + return def get_filler_item_name(self) -> str: - return "Soul of an Intrepid Hero" - + return self.random.choice(filler_item_names) def set_rules(self) -> None: - # Define the access rules to the entrances - set_rule(self.multiworld.get_entrance("Go To Undead Settlement", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) - set_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player), - lambda state: state.has("Basin of Vows", self.player)) - set_rule(self.multiworld.get_entrance("Go To Irithyll of the Boreal Valley", self.player), - lambda state: state.has("Small Doll", self.player)) - set_rule(self.multiworld.get_entrance("Go To Archdragon Peak", self.player), - lambda state: state.has("Path of the Dragon", self.player)) - set_rule(self.multiworld.get_entrance("Go To Grand Archives", self.player), - lambda state: state.has("Grand Archives Key", self.player)) - set_rule(self.multiworld.get_entrance("Go To Kiln of the First Flame", self.player), - lambda state: state.has("Cinders of a Lord - Abyss Watcher", self.player) and - state.has("Cinders of a Lord - Yhorm the Giant", self.player) and - state.has("Cinders of a Lord - Aldrich", self.player) and - state.has("Cinders of a Lord - Lothric Prince", self.player)) + randomized_items = {item.name for item in self.local_itempool} - if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Lothric Castle", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) + self._add_shop_rules() + self._add_npc_rules() + self._add_transposition_rules() + self._add_crow_rules() + self._add_allow_useful_location_rules() + self._add_early_item_rules(randomized_items) + + self._add_entrance_rule("Firelink Shrine Bell Tower", "Tower Key") + self._add_entrance_rule("Undead Settlement", lambda state: ( + state.has("Small Lothric Banner", self.player) + and self._can_get(state, "HWL: Soul of Boreal Valley Vordt") + )) + self._add_entrance_rule("Road of Sacrifices", "US -> RS") + self._add_entrance_rule( + "Cathedral of the Deep", + lambda state: self._can_get(state, "RS: Soul of a Crystal Sage") + ) + self._add_entrance_rule("Farron Keep", "RS -> FK") + self._add_entrance_rule( + "Catacombs of Carthus", + lambda state: self._can_get(state, "FK: Soul of the Blood of the Wolf") + ) + self._add_entrance_rule("Irithyll Dungeon", "IBV -> ID") + self._add_entrance_rule( + "Lothric Castle", + lambda state: self._can_get(state, "HWL: Soul of the Dancer") + ) + self._add_entrance_rule( + "Untended Graves", + lambda state: self._can_get(state, "CKG: Soul of Consumed Oceiros") + ) + self._add_entrance_rule("Irithyll of the Boreal Valley", lambda state: ( + state.has("Small Doll", self.player) + and self._can_get(state, "CC: Soul of High Lord Wolnir") + )) + self._add_entrance_rule( + "Anor Londo", + lambda state: self._can_get(state, "IBV: Soul of Pontiff Sulyvahn") + ) + self._add_entrance_rule("Archdragon Peak", "Path of the Dragon") + self._add_entrance_rule("Grand Archives", lambda state: ( + state.has("Grand Archives Key", self.player) + and self._can_get(state, "LC: Soul of Dragonslayer Armour") + )) + self._add_entrance_rule("Kiln of the First Flame", lambda state: ( + state.has("Cinders of a Lord - Abyss Watcher", self.player) + and state.has("Cinders of a Lord - Yhorm the Giant", self.player) + and state.has("Cinders of a Lord - Aldrich", self.player) + and state.has("Cinders of a Lord - Lothric Prince", self.player) + and state.has("Transposing Kiln", self.player) + )) + + if self.options.late_basin_of_vows: + self._add_entrance_rule("Lothric Castle", lambda state: ( + state.has("Small Lothric Banner", self.player) + # Make sure these are actually available early. + and ( + "Transposing Kiln" not in randomized_items + or state.has("Transposing Kiln", self.player) + ) and ( + "Pyromancy Flame" not in randomized_items + or state.has("Pyromancy Flame", self.player) + ) + # This isn't really necessary, but it ensures that the game logic knows players will + # want to do Lothric Castle after at least being _able_ to access Catacombs. This is + # useful for smooth item placement. + and self._has_any_scroll(state) + )) + + if self.options.late_basin_of_vows > 1: # After Small Doll + self._add_entrance_rule("Lothric Castle", "Small Doll") # DLC Access Rules Below - if self.multiworld.enable_dlc[self.player]: - set_rule(self.multiworld.get_entrance("Go To Ringed City", self.player), - lambda state: state.has("Small Envoy Banner", self.player)) + if self.options.enable_dlc: + self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "CD -> PW1") + self._add_entrance_rule("Painted World of Ariandel (After Contraption)", "Contraption Key") + self._add_entrance_rule( + "Dreg Heap", + lambda state: self._can_get(state, "PW2: Soul of Sister Friede") + ) + self._add_entrance_rule("Ringed City", lambda state: ( + state.has("Small Envoy Banner", self.player) + and self._can_get(state, "DH: Soul of the Demon Prince") + )) - # If key items are randomized, must have contraption key to enter second half of Ashes DLC - # If key items are not randomized, Contraption Key is guaranteed to be accessible before it is needed - if self.multiworld.enable_key_locations[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 2", self.player), - lambda state: state.has("Contraption Key", self.player)) + if self.options.late_dlc: + self._add_entrance_rule( + "Painted World of Ariandel (Before Contraption)", + lambda state: state.has("Small Doll", self.player) and self._has_any_scroll(state)) - if self.multiworld.late_dlc[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_entrance("Go To Painted World of Ariandel 1", self.player), - lambda state: state.has("Small Doll", self.player)) + if self.options.late_dlc > 1: # After Basin + self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") # Define the access rules to some specific locations - set_rule(self.multiworld.get_location("PC: Cinders of a Lord - Yhorm the Giant", self.player), - lambda state: state.has("Storm Ruler", self.player)) + if self._is_location_available("FS: Lift Chamber Key - Leonhard"): + self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", + "Lift Chamber Key") + self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", + "Jailbreaker's Key") + self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") + self._add_location_rule([ + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss" + ], lambda state: self._can_get(state, "FK: Cinders of a Lord - Abyss Watcher")) + self._add_location_rule( + "ID: Prisoner Chief's Ashes - B2 near, locked cell by stairs", + "Jailer's Key Ring" + ) + self._add_entrance_rule("Karla's Shop", "Jailer's Key Ring") - if self.multiworld.enable_ring_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("ID: Bellowing Dragoncrest Ring", self.player), - lambda state: state.has("Jailbreaker's Key", self.player)) - set_rule(self.multiworld.get_location("ID: Covetous Gold Serpent Ring", self.player), - lambda state: state.has("Old Cell Key", self.player)) - set_rule(self.multiworld.get_location("UG: Hornet Ring", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) + # The static randomizer edits events to guarantee that Greirat won't go to Lothric until + # Grand Archives is available, so his shop will always be available one way or another. + self._add_entrance_rule("Greirat's Shop", "Cell Key") - if self.multiworld.enable_npc_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("HWL: Greirat's Ashes", self.player), - lambda state: state.has("Cell Key", self.player)) - set_rule(self.multiworld.get_location("HWL: Blue Tearstone Ring", self.player), - lambda state: state.has("Cell Key", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Ashes", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Pointed Hat", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Coat", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Gloves", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) - set_rule(self.multiworld.get_location("ID: Karla's Trousers", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) + self._add_location_rule("HWL: Soul of the Dancer", "Basin of Vows") - if self.multiworld.enable_misc_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("ID: Prisoner Chief's Ashes", self.player), - lambda state: state.has("Jailer's Key Ring", self.player)) + # Lump Soul of the Dancer in with LC for locations that should not be reachable + # before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US) + if self.options.late_basin_of_vows: + self._add_location_rule("HWL: Soul of the Dancer", lambda state: ( + state.has("Small Lothric Banner", self.player) + # Make sure these are actually available early. + and ( + "Transposing Kiln" not in randomized_items + or state.has("Transposing Kiln", self.player) + ) and ( + "Pyromancy Flame" not in randomized_items + or state.has("Pyromancy Flame", self.player) + ) + # This isn't really necessary, but it ensures that the game logic knows players will + # want to do Lothric Castle after at least being _able_ to access Catacombs. This is + # useful for smooth item placement. + and self._has_any_scroll(state) + )) - if self.multiworld.enable_boss_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("PC: Soul of Yhorm the Giant", self.player), - lambda state: state.has("Storm Ruler", self.player)) - set_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), - lambda state: state.has("Basin of Vows", self.player)) + if self.options.late_basin_of_vows > 1: # After Small Doll + self._add_location_rule("HWL: Soul of the Dancer", "Small Doll") - # Lump Soul of the Dancer in with LC for locations that should not be reachable - # before having access to US. (Prevents requiring getting Basin to fight Dancer to get SLB to go to US) - if self.multiworld.late_basin_of_vows[self.player] == Toggle.option_true: - add_rule(self.multiworld.get_location("HWL: Soul of the Dancer", self.player), - lambda state: state.has("Small Lothric Banner", self.player)) + self._add_location_rule([ + "LC: Grand Archives Key - by Grand Archives door, after PC and AL bosses", + "LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bosses" + ], lambda state: ( + self._can_get(state, "AL: Cinders of a Lord - Aldrich") and + self._can_get(state, "PC: Cinders of a Lord - Yhorm the Giant") + )) - gotthard_corpse_rule = lambda state: \ - (state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and - state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player)) + self._add_location_rule([ + "FS: Morne's Great Hammer - Eygon", + "FS: Moaning Shield - Eygon" + ], lambda state: ( + self._can_get(state, "LC: Soul of Dragonslayer Armour") and + self._can_get(state, "FK: Soul of the Blood of the Wolf") + )) - set_rule(self.multiworld.get_location("LC: Grand Archives Key", self.player), gotthard_corpse_rule) + self._add_location_rule([ + "CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPC", + "CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPC", + ], lambda state: self._can_go_to(state, "Archdragon Peak")) - if self.multiworld.enable_weapon_locations[self.player] == Toggle.option_true: - set_rule(self.multiworld.get_location("LC: Gotthard Twinswords", self.player), gotthard_corpse_rule) + self._add_location_rule([ + "FK: Havel's Helm - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Armor - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPC", + "FK: Havel's Leggings - upper keep, after killing AP belfry roof NPC", + ], lambda state: self._can_go_to(state, "Archdragon Peak")) - self.multiworld.completion_condition[self.player] = lambda state: \ - state.has("Cinders of a Lord - Abyss Watcher", self.player) and \ - state.has("Cinders of a Lord - Yhorm the Giant", self.player) and \ - state.has("Cinders of a Lord - Aldrich", self.player) and \ - state.has("Cinders of a Lord - Lothric Prince", self.player) + self._add_location_rule([ + "RC: Dragonhead Shield - streets monument, across bridge", + "RC: Large Soul of a Crestfallen Knight - streets monument, across bridge", + "RC: Divine Blessing - streets monument, mob drop", + "RC: Lapp's Helm - Lapp", + "RC: Lapp's Armor - Lapp", + "RC: Lapp's Gauntlets - Lapp", + "RC: Lapp's Leggings - Lapp", + ], "Chameleon") + # Forbid shops from carrying items with multiple counts (the static randomizer has its own + # logic for choosing how many shop items to sell), and from carrying soul items. + for location in location_dictionary.values(): + if location.shop: + self._add_item_rule( + location.name, + lambda item: ( + item.player != self.player or + (item.data.count == 1 and not item.data.souls) + ) + ) + + # This particular location is bugged, and will drop two copies of whatever item is placed + # there. + if self._is_location_available("US: Young White Branch - by white tree #2"): + self._add_item_rule( + "US: Young White Branch - by white tree #2", + lambda item: item.player == self.player and not item.data.unique + ) + + # Make sure the Storm Ruler is available BEFORE Yhorm the Giant + if self.yhorm_location.name == "Ancient Wyvern": + # This is a white lie, you can get to a bunch of items in AP before you beat the Wyvern, + # but this saves us from having to split the entire region in two just to mark which + # specific items are before and after. + self._add_entrance_rule("Archdragon Peak", "Storm Ruler") + for location in self.yhorm_location.locations: + self._add_location_rule(location, "Storm Ruler") + + self.multiworld.completion_condition[self.player] = lambda state: self._can_get(state, "KFF: Soul of the Lords") + + def _add_shop_rules(self) -> None: + """Adds rules for items unlocked in shops.""" + + # Ashes + ashes = { + "Mortician's Ashes": ["Alluring Skull", "Ember", "Grave Key"], + "Dreamchaser's Ashes": ["Life Ring", "Hidden Blessing"], + "Paladin's Ashes": ["Lloyd's Shield Ring"], + "Grave Warden's Ashes": ["Ember"], + "Prisoner Chief's Ashes": [ + "Karla's Pointed Hat", "Karla's Coat", "Karla's Gloves", "Karla's Trousers" + ], + "Xanthous Ashes": ["Xanthous Overcoat", "Xanthous Gloves", "Xanthous Trousers"], + "Dragon Chaser's Ashes": ["Ember"], + "Easterner's Ashes": [ + "Washing Pole", "Eastern Helm", "Eastern Armor", "Eastern Gauntlets", + "Eastern Leggings", "Wood Grain Ring", + ], + "Captain's Ashes": [ + "Millwood Knight Helm", "Millwood Knight Armor", "Millwood Knight Gauntlets", + "Millwood Knight Leggings", "Refined Gem", + ] + } + for (ash, items) in ashes.items(): + self._add_location_rule([f"FS: {item} - {ash}" for item in items], ash) + + # Shop unlocks + shop_unlocks = { + "Cornyx": [ + ( + "Great Swamp Pyromancy Tome", "Great Swamp Tome", + ["Poison Mist", "Fire Orb", "Profuse Sweat", "Bursting Fireball"] + ), + ( + "Carthus Pyromancy Tome", "Carthus Tome", + ["Acid Surge", "Carthus Flame Arc", "Carthus Beacon"] + ), + ("Izalith Pyromancy Tome", "Izalith Tome", ["Great Chaos Fire Orb", "Chaos Storm"]), + ], + "Irina": [ + ( + "Braille Divine Tome of Carim", "Tome of Carim", + ["Med Heal", "Tears of Denial", "Force"] + ), + ( + "Braille Divine Tome of Lothric", "Tome of Lothric", + ["Bountiful Light", "Magic Barrier", "Blessed Weapon"] + ), + ], + "Orbeck": [ + ("Sage's Scroll", "Sage's Scroll", ["Great Farron Dart", "Farron Hail"]), + ( + "Golden Scroll", "Golden Scroll", + [ + "Cast Light", "Repair", "Hidden Weapon", "Hidden Body", + "Twisted Wall of Light" + ], + ), + ("Logan's Scroll", "Logan's Scroll", ["Homing Soulmass", "Soul Spear"]), + ( + "Crystal Scroll", "Crystal Scroll", + ["Homing Crystal Soulmass", "Crystal Soul Spear", "Crystal Magic Weapon"] + ), + ], + "Karla": [ + ("Quelana Pyromancy Tome", "Quelana Tome", ["Firestorm", "Rapport", "Fire Whip"]), + ( + "Grave Warden Pyromancy Tome", "Grave Warden Tome", + ["Black Flame", "Black Fire Orb"] + ), + ("Deep Braille Divine Tome", "Deep Braille Tome", ["Gnaw", "Deep Protection"]), + ( + "Londor Braille Divine Tome", "Londor Tome", + ["Vow of Silence", "Dark Blade", "Dead Again"] + ), + ], + } + for (shop, unlocks) in shop_unlocks.items(): + for (key, key_name, items) in unlocks: + self._add_location_rule( + [f"FS: {item} - {shop} for {key_name}" for item in items], key) + + def _add_npc_rules(self) -> None: + """Adds rules for items accessible via NPC quests. + + We list missable locations here even though they never contain progression items so that the + game knows what sphere they're in. This is especially useful for item smoothing. (We could + add rules for boss transposition items as well, but then we couldn't freely reorder boss + soul locations for smoothing.) + + Generally, for locations that can be accessed early by killing NPCs, we set up requirements + assuming the player _doesn't_ so they aren't forced to start killing allies to advance the + quest. + """ + + ## Greirat + + self._add_location_rule([ + "FS: Divine Blessing - Greirat from US", + "FS: Ember - Greirat from US", + ], lambda state: ( + self._can_go_to(state, "Undead Settlement") + and state.has("Loretta's Bone", self.player) + )) + self._add_location_rule([ + "FS: Divine Blessing - Greirat from IBV", + "FS: Hidden Blessing - Greirat from IBV", + "FS: Titanite Scale - Greirat from IBV", + "FS: Twinkling Titanite - Greirat from IBV", + "FS: Ember - shop for Greirat's Ashes" + ], lambda state: ( + self._can_go_to(state, "Irithyll of the Boreal Valley") + and self._can_get(state, "FS: Divine Blessing - Greirat from US") + # Either Patches or Siegward can save Greirat, but we assume the player will want to use + # Patches because it's harder to screw up + and self._can_get(state, "CD: Shotel - Patches") + )) + self._add_location_rule([ + "FS: Ember - shop for Greirat's Ashes", + ], lambda state: ( + self._can_go_to(state, "Grand Archives") + and self._can_get(state, "FS: Divine Blessing - Greirat from IBV") + )) + + ## Patches + + # Patches will only set up shop in Firelink once he's tricked you in the bell tower. He'll + # only do _that_ once you've spoken to Siegward after killing the Fire Demon and lit the + # Rosaria's Bed Chamber bonfire. He _won't_ set up shop in the Cathedral if you light the + # Rosaria's Bed Chamber bonfire before getting tricked by him, so we assume these locations + # require the bell tower. + self._add_location_rule([ + "CD: Shotel - Patches", + "CD: Ember - Patches", + "FS: Rusted Gold Coin - don't forgive Patches" + ], lambda state: ( + self._can_go_to(state, "Firelink Shrine Bell Tower") + and self._can_go_to(state, "Cathedral of the Deep") + )) + + # Patches sells this after you tell him to search for Greirat in Grand Archives + self._add_location_rule([ + "FS: Hidden Blessing - Patches after searching GA" + ], lambda state: ( + self._can_get(state, "CD: Shotel - Patches") + and self._can_get(state, "FS: Ember - shop for Greirat's Ashes") + )) + + # Only make the player kill Patches once all his other items are available + self._add_location_rule([ + "CD: Winged Spear - kill Patches", + # You don't _have_ to kill him for this, but he has to be in Firelink at the same time + # as Greirat to get it in the shop and that may not be feasible if the player progresses + # Greirat's quest much faster. + "CD: Horsehoof Ring - Patches", + ], lambda state: ( + self._can_get(state, "FS: Hidden Blessing - Patches after searching GA") + and self._can_get(state, "FS: Rusted Gold Coin - don't forgive Patches") + )) + + ## Leonhard + + self._add_location_rule([ + # Talk to Leonhard in Firelink with a Pale Tongue after lighting Cliff Underside or + # killing Greatwood. This doesn't consume the Pale Tongue, it just has to be in + # inventory + "FS: Lift Chamber Key - Leonhard", + # Progress Leonhard's quest and then return to Rosaria after lighting Profaned Capital + "CD: Black Eye Orb - Rosaria from Leonhard's quest", + ], "Pale Tongue") + + self._add_location_rule([ + "CD: Black Eye Orb - Rosaria from Leonhard's quest", + ], lambda state: ( + # The Black Eye Orb location won't spawn until you kill the HWL miniboss and resting at + # the Profaned Capital bonfire. + self._can_get(state, "HWL: Red Eye Orb - wall tower, miniboss") + and self._can_go_to(state, "Profaned Capital") + )) + + # Perhaps counterintuitively, you CAN fight Leonhard before you access the location that + # would normally give you the Black Eye Orb. + self._add_location_rule([ + "AL: Crescent Moon Sword - Leonhard drop", + "AL: Silver Mask - Leonhard drop", + "AL: Soul of Rosaria - Leonhard drop", + ] + [ + f"FS: {item} - shop after killing Leonhard" + for item in ["Leonhard's Garb", "Leonhard's Gauntlets", "Leonhard's Trousers"] + ], "Black Eye Orb") + + ## Hawkwood + + # After Hawkwood leaves and once you have the Torso Stone, you can fight him for dragon + # stones. Andre will give Swordgrass as a hint as well + self._add_location_rule([ + "FK: Twinkling Dragon Head Stone - Hawkwood drop", + "FS: Hawkwood's Swordgrass - Andre after gesture in AP summit" + ], lambda state: ( + self._can_get(state, "FS: Hawkwood's Shield - gravestone after Hawkwood leaves") + and state.has("Twinkling Dragon Torso Stone", self.player) + )) + + ## Siegward + + # Unlock Siegward's cell after progressing his quest + self._add_location_rule([ + "ID: Titanite Slab - Siegward", + ], lambda state: ( + state.has("Old Cell Key", self.player) + # Progressing Siegward's quest requires buying his armor from Patches. + and self._can_get(state, "CD: Shotel - Patches") + )) + + # These drop after completing Siegward's quest and talking to him in Yhorm's arena + self._add_location_rule([ + "PC: Siegbräu - Siegward after killing boss", + "PC: Storm Ruler - Siegward", + "PC: Pierce Shield - Siegward", + ], lambda state: ( + self._can_get(state, "ID: Titanite Slab - Siegward") + and self._can_get(state, "PC: Soul of Yhorm the Giant") + )) + + ## Sirris + + # Kill Greatwood and turn in Dreamchaser's Ashes to trigger this opportunity for invasion + self._add_location_rule([ + "FS: Mail Breaker - Sirris for killing Creighton", + "FS: Silvercat Ring - Sirris for killing Creighton", + "IBV: Creighton's Steel Mask - bridge after killing Creighton", + "IBV: Mirrah Chain Gloves - bridge after killing Creighton", + "IBV: Mirrah Chain Leggings - bridge after killing Creighton", + "IBV: Mirrah Chain Mail - bridge after killing Creighton", + "IBV: Dragonslayer's Axe - Creighton drop", + # Killing Pontiff without progressing Sirris's quest will break it. + "IBV: Soul of Pontiff Sulyvahn" + ], lambda state: ( + self._can_get(state, "US: Soul of the Rotted Greatwood") + and state.has("Dreamchaser's Ashes", self.player) + )) + # Add indirect condition since reaching AL requires defeating Pontiff which requires defeating Greatwood in US + self.multiworld.register_indirect_condition( + self.get_region("Undead Settlement"), + self.get_entrance("Go To Anor Londo") + ) + + # Kill Creighton and Aldrich to trigger this opportunity for invasion + self._add_location_rule([ + "FS: Budding Green Blossom - shop after killing Creighton and AL boss", + "FS: Sunset Shield - by grave after killing Hodrick w/Sirris", + "US: Sunset Helm - Pit of Hollows after killing Hodrick w/Sirris", + "US: Sunset Armor - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Gauntlets - pit of hollows after killing Hodrick w/Sirris", + "US: Sunset Leggings - pit of hollows after killing Hodrick w/Sirris", + ], lambda state: ( + self._can_get(state, "FS: Mail Breaker - Sirris for killing Creighton") + and self._can_get(state, "AL: Soul of Aldrich") + )) + + # Kill Hodrick and Twin Princes to trigger the end of the quest + self._add_location_rule([ + "FS: Sunless Talisman - Sirris, kill GA boss", + "FS: Sunless Veil - shop, Sirris quest, kill GA boss", + "FS: Sunless Armor - shop, Sirris quest, kill GA boss", + "FS: Sunless Gauntlets - shop, Sirris quest, kill GA boss", + "FS: Sunless Leggings - shop, Sirris quest, kill GA boss", + # Killing Yorshka will anger Sirris and stop her quest, so don't expect it until the + # quest is done + "AL: Yorshka's Chime - kill Yorshka", + ], lambda state: ( + self._can_get(state, "US: Soul of the Rotted Greatwood") + and state.has("Dreamchaser's Ashes", self.player) + )) + + ## Cornyx + + self._add_location_rule([ + "US: Old Sage's Blindfold - kill Cornyx", + "US: Cornyx's Garb - kill Cornyx", + "US: Cornyx's Wrap - kill Cornyx", + "US: Cornyx's Skirt - kill Cornyx", + ], lambda state: ( + state.has("Great Swamp Pyromancy Tome", self.player) + and state.has("Carthus Pyromancy Tome", self.player) + and state.has("Izalith Pyromancy Tome", self.player) + )) + + self._add_location_rule([ + "US: Old Sage's Blindfold - kill Cornyx", "US: Cornyx's Garb - kill Cornyx", + "US: Cornyx's Wrap - kill Cornyx", "US: Cornyx's Skirt - kill Cornyx" + ], lambda state: ( + state.has("Great Swamp Pyromancy Tome", self.player) + and state.has("Carthus Pyromancy Tome", self.player) + and state.has("Izalith Pyromancy Tome", self.player) + )) + + ## Irina + + self._add_location_rule([ + "US: Tower Key - kill Irina", + ], lambda state: ( + state.has("Braille Divine Tome of Carim", self.player) + and state.has("Braille Divine Tome of Lothric", self.player) + )) + + ## Karla + + self._add_location_rule([ + "FS: Karla's Pointed Hat - kill Karla", + "FS: Karla's Coat - kill Karla", + "FS: Karla's Gloves - kill Karla", + "FS: Karla's Trousers - kill Karla", + ], lambda state: ( + state.has("Quelana Pyromancy Tome", self.player) + and state.has("Grave Warden Pyromancy Tome", self.player) + and state.has("Deep Braille Divine Tome", self.player) + and state.has("Londor Braille Divine Tome", self.player) + )) + + ## Emma + + self._add_location_rule("HWL: Basin of Vows - Emma", "Small Doll") + + ## Orbeck + + self._add_location_rule([ + "FS: Morion Blade - Yuria for Orbeck's Ashes", + "FS: Clandestine Coat - shop with Orbeck's Ashes" + ], lambda state: ( + state.has("Golden Scroll", self.player) + and state.has("Logan's Scroll", self.player) + and state.has("Crystal Scroll", self.player) + and state.has("Sage's Scroll", self.player) + )) + + self._add_location_rule([ + "FS: Pestilent Mist - Orbeck for any scroll", + "FS: Young Dragon Ring - Orbeck for one scroll and buying three spells", + # Make sure that the player can keep Orbeck around by giving him at least one scroll + # before killing Abyss Watchers. + "FK: Soul of the Blood of the Wolf", + "FK: Cinders of a Lord - Abyss Watcher", + "FS: Undead Legion Helm - shop after killing FK boss", + "FS: Undead Legion Armor - shop after killing FK boss", + "FS: Undead Legion Gauntlet - shop after killing FK boss", + "FS: Undead Legion Leggings - shop after killing FK boss", + "FS: Farron Ring - Hawkwood", + "FS: Hawkwood's Shield - gravestone after Hawkwood leaves", + "UG: Hornet Ring - environs, right of main path after killing FK boss", + "UG: Wolf Knight Helm - shop after killing FK boss", + "UG: Wolf Knight Armor - shop after killing FK boss", + "UG: Wolf Knight Gauntlets - shop after killing FK boss", + "UG: Wolf Knight Leggings - shop after killing FK boss", + ], self._has_any_scroll) + + # Not really necessary but ensures players can decide which way to go + if self.options.enable_dlc: + self._add_entrance_rule( + "Painted World of Ariandel (After Contraption)", + self._has_any_scroll + ) + + ## Anri + + # Anri only leaves Road of Sacrifices once Deacons is defeated + self._add_location_rule([ + "IBV: Ring of the Evil Eye - Anri", + "AL: Chameleon - tomb after marrying Anri", + ], lambda state: self._can_get(state, "CD: Soul of the Deacons of the Deep")) + + # If the player does Anri's non-marriage quest, they'll need to defeat the AL boss as well + # before it's complete. + self._add_location_rule([ + "AL: Anri's Straight Sword - Anri quest", + "FS: Elite Knight Helm - shop after Anri quest", + "FS: Elite Knight Armor - shop after Anri quest", + "FS: Elite Knight Gauntlets - shop after Anri quest", + "FS: Elite Knight Leggings - shop after Anri quest", + ], lambda state: ( + self._can_get(state, "IBV: Ring of the Evil Eye - Anri") and + self._can_get(state, "AL: Soul of Aldrich") + )) + + def _add_transposition_rules(self) -> None: + """Adds rules for items obtainable from Ludleth by soul transposition.""" + + transpositions = [ + ( + "Soul of Boreal Valley Vordt", "Vordt", + ["Vordt's Great Hammer", "Pontiff's Left Eye"] + ), + ("Soul of Rosaria", "Rosaria", ["Bountiful Sunlight"]), + ("Soul of Aldrich", "Aldrich", ["Darkmoon Longbow", "Lifehunt Scythe"]), + ( + "Soul of the Rotted Greatwood", "Greatwood", + ["Hollowslayer Greatsword", "Arstor's Spear"] + ), + ("Soul of a Crystal Sage", "Sage", ["Crystal Sage's Rapier", "Crystal Hail"]), + ("Soul of the Deacons of the Deep", "Deacons", ["Cleric's Candlestick", "Deep Soul"]), + ("Soul of a Stray Demon", "Stray Demon", ["Havel's Ring", "Boulder Heave"]), + ( + "Soul of the Blood of the Wolf", "Abyss Watchers", + ["Farron Greatsword", "Wolf Knight's Greatsword"] + ), + ("Soul of High Lord Wolnir", "Wolnir", ["Wolnir's Holy Sword", "Black Serpent"]), + ("Soul of a Demon", "Fire Demon", ["Demon's Greataxe", "Demon's Fist"]), + ( + "Soul of the Old Demon King", "Old Demon King", + ["Old King's Great Hammer", "Chaos Bed Vestiges"] + ), + ( + "Soul of Pontiff Sulyvahn", "Pontiff", + ["Greatsword of Judgment", "Profaned Greatsword"] + ), + ("Soul of Yhorm the Giant", "Yhorm", ["Yhorm's Great Machete", "Yhorm's Greatshield"]), + ("Soul of the Dancer", "Dancer", ["Dancer's Enchanted Swords", "Soothing Sunlight"]), + ( + "Soul of Dragonslayer Armour", "Dragonslayer", + ["Dragonslayer Greataxe", "Dragonslayer Greatshield"] + ), + ( + "Soul of Consumed Oceiros", "Oceiros", + ["Moonlight Greatsword", "White Dragon Breath"] + ), + ( + "Soul of the Twin Princes", "Princes", + ["Lorian's Greatsword", "Lothric's Holy Sword"] + ), + ("Soul of Champion Gundyr", "Champion", ["Gundyr's Halberd", "Prisoner's Chain"]), + ( + "Soul of the Nameless King", "Nameless", + ["Storm Curved Sword", "Dragonslayer Swordspear", "Lightning Storm"] + ), + ("Soul of the Lords", "Cinder", ["Firelink Greatsword", "Sunlight Spear"]), + ("Soul of Sister Friede", "Friede", ["Friede's Great Scythe", "Rose of Ariandel"]), + ("Soul of the Demon Prince", "Demon Prince", ["Demon's Scar", "Seething Chaos"]), + ("Soul of Darkeater Midir", "Midir", ["Frayed Blade", "Old Moonlight"]), + ("Soul of Slave Knight Gael", "Gael", ["Gael's Greatsword", "Repeating Crossbow"]), + ] + for (soul, soul_name, items) in transpositions: + self._add_location_rule([ + f"FS: {item} - Ludleth for {soul_name}" for item in items + ], lambda state, s=soul: ( + state.has(s, self.player) and state.has("Transposing Kiln", self.player) + )) + + def _add_crow_rules(self) -> None: + """Adds rules for items obtainable by trading items to the crow on Firelink roof.""" + + crow = { + "Loretta's Bone": "Ring of Sacrifice", + # "Avelyn": "Titanite Scale", # Missing from static randomizer + "Coiled Sword Fragment": "Titanite Slab", + "Seed of a Giant Tree": "Iron Leggings", + "Siegbräu": "Armor of the Sun", + # Static randomizer can't randomize Hodrick's drop yet + # "Vertebra Shackle": "Lucatiel's Mask", + "Xanthous Crown": "Lightning Gem", + "Mendicant's Staff": "Sunlight Shield", + "Blacksmith Hammer": "Titanite Scale", + "Large Leather Shield": "Twinkling Titanite", + "Moaning Shield": "Blessed Gem", + "Eleonora": "Hollow Gem", + } + for (given, received) in crow.items(): + name = f"FSBT: {received} - crow for {given}" + self._add_location_rule(name, given) + + # Don't let crow items have foreign items because they're picked up in a way that's + # missed by the hook we use to send location items + self._add_item_rule(name, lambda item: ( + item.player == self.player + # Because of the weird way they're delivered, crow items don't seem to support + # infused or upgraded weapons. + and not item.data.is_infused + and not item.data.is_upgraded + )) + + def _add_allow_useful_location_rules(self) -> None: + """Adds rules for locations that can contain useful but not necessary items. + + If we allow useful items in the excluded locations, we don't want Archipelago's fill + algorithm to consider them excluded because it never allows useful items there. Instead, we + manually add item rules to exclude important items. + """ + + all_locations = self._get_our_locations() + + allow_useful_locations = ( + ( + { + location.name + for location in all_locations + if location.name in self.all_excluded_locations + and not location.data.missable + } + if self.options.excluded_location_behavior < self.options.missable_location_behavior + else self.all_excluded_locations + ) + if self.options.excluded_location_behavior == "allow_useful" + else set() + ).union( + { + location.name + for location in all_locations + if location.data.missable + and not ( + location.name in self.all_excluded_locations + and self.options.missable_location_behavior < + self.options.excluded_location_behavior + ) + } + if self.options.missable_location_behavior == "allow_useful" + else set() + ) + for location in allow_useful_locations: + self._add_item_rule( + location, + lambda item: not item.advancement + ) + + if self.options.excluded_location_behavior == "allow_useful": + self.options.exclude_locations.value.clear() + + def _add_early_item_rules(self, randomized_items: Set[str]) -> None: + """Adds rules to make sure specific items are available early.""" + + if "Pyromancy Flame" in randomized_items: + # Make this available early because so many items are useless without it. + self._add_entrance_rule("Road of Sacrifices", "Pyromancy Flame") + self._add_entrance_rule("Consumed King's Garden", "Pyromancy Flame") + self._add_entrance_rule("Grand Archives", "Pyromancy Flame") + if "Transposing Kiln" in randomized_items: + # Make this available early so players can make use of their boss souls. + self._add_entrance_rule("Road of Sacrifices", "Transposing Kiln") + self._add_entrance_rule("Consumed King's Garden", "Transposing Kiln") + self._add_entrance_rule("Grand Archives", "Transposing Kiln") + # Make this available pretty early + if "Small Lothric Banner" in randomized_items: + if self.options.early_banner == "early_global": + self.multiworld.early_items[self.player]["Small Lothric Banner"] = 1 + elif self.options.early_banner == "early_local": + self.multiworld.local_early_items[self.player]["Small Lothric Banner"] = 1 + + def _has_any_scroll(self, state: CollectionState) -> bool: + """Returns whether the given state has any scroll item.""" + return ( + state.has("Sage's Scroll", self.player) + or state.has("Golden Scroll", self.player) + or state.has("Logan's Scroll", self.player) + or state.has("Crystal Scroll", self.player) + ) + + def _add_location_rule(self, location: Union[str, List[str]], rule: Union[CollectionRule, str]) -> None: + """Sets a rule for the given location if it that location is randomized. + + The rule can just be a single item/event name as well as an explicit rule lambda. + """ + locations = location if isinstance(location, list) else [location] + for location in locations: + data = location_dictionary[location] + if data.dlc and not self.options.enable_dlc: return + if data.ngp and not self.options.enable_ngp: return + + if not self._is_location_available(location): return + if isinstance(rule, str): + assert item_dictionary[rule].classification == ItemClassification.progression + rule = lambda state, item=rule: state.has(item, self.player) + add_rule(self.multiworld.get_location(location, self.player), rule) + + def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: + """Sets a rule for the entrance to the given region.""" + assert region in location_tables + if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return + if isinstance(rule, str): + if " -> " not in rule: + assert item_dictionary[rule].classification == ItemClassification.progression + rule = lambda state, item=rule: state.has(item, self.player) + add_rule(self.multiworld.get_entrance("Go To " + region, self.player), rule) + + def _add_item_rule(self, location: str, rule: ItemRule) -> None: + """Sets a rule for what items are allowed in a given location.""" + if not self._is_location_available(location): return + add_item_rule(self.multiworld.get_location(location, self.player), rule) + + def _can_go_to(self, state, region) -> bool: + """Returns whether state can access the given region name.""" + return state.can_reach_entrance(f"Go To {region}", self.player) + + def _can_get(self, state, location) -> bool: + """Returns whether state can access the given location name.""" + return state.can_reach_location(location, self.player) + + def _is_location_available( + self, + location: Union[str, DS3LocationData, DarkSouls3Location] + ) -> bool: + """Returns whether the given location is being randomized.""" + if isinstance(location, DS3LocationData): + data = location + elif isinstance(location, DarkSouls3Location): + data = location.data + else: + data = location_dictionary[location] + + return ( + not data.is_event + and (not data.dlc or bool(self.options.enable_dlc)) + and (not data.ngp or bool(self.options.enable_ngp)) + and not ( + self.options.excluded_location_behavior == "do_not_randomize" + and data.name in self.all_excluded_locations + ) + and not ( + self.options.missable_location_behavior == "do_not_randomize" + and data.missable + ) + ) + + def write_spoiler(self, spoiler_handle: TextIO) -> None: + text = "" + + if self.yhorm_location != default_yhorm_location: + text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n" + + if self.options.excluded_location_behavior == "allow_useful": + text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n" + + if text: + text = "\n" + text + "\n" + spoiler_handle.write(text) + + def post_fill(self): + """If item smoothing is enabled, rearrange items so they scale up smoothly through the run. + + This determines the approximate order a given silo of items (say, soul items) show up in the + main game, then rearranges their shuffled placements to match that order. It determines what + should come "earlier" or "later" based on sphere order: earlier spheres get lower-level + items, later spheres get higher-level ones. Within a sphere, items in DS3 are distributed in + region order, and then the best items in a sphere go into the multiworld. + """ + + locations_by_sphere = [ + sorted(loc for loc in sphere if loc.item.player == self.player and not loc.locked) + for sphere in self.multiworld.get_spheres() + ] + + # All items in the base game in approximately the order they appear + all_item_order: List[DS3ItemData] = [ + item_dictionary[location.default_item_name] + for region in region_order + # Shuffle locations within each region. + for location in self._shuffle(location_tables[region]) + if self._is_location_available(location) + ] + + # All DarkSouls3Items for this world that have been assigned anywhere, grouped by name + full_items_by_name: Dict[str, List[DarkSouls3Item]] = defaultdict(list) + for location in self.multiworld.get_filled_locations(): + if location.item.player == self.player and ( + location.player != self.player or self._is_location_available(location) + ): + full_items_by_name[location.item.name].append(location.item) + + def smooth_items(item_order: List[Union[DS3ItemData, DarkSouls3Item]]) -> None: + """Rearrange all items in item_order to match that order. + + Note: this requires that item_order exactly matches the number of placed items from this + world matching the given names. + """ + + # Convert items to full DarkSouls3Items. + converted_item_order: List[DarkSouls3Item] = [ + item for item in ( + ( + # full_items_by_name won't contain DLC items if the DLC is disabled. + (full_items_by_name[item.name] or [None]).pop(0) + if isinstance(item, DS3ItemData) else item + ) + for item in item_order + ) + # Never re-order event items, because they weren't randomized in the first place. + if item and item.code is not None + ] + + names = {item.name for item in converted_item_order} + + all_matching_locations = [ + loc + for sphere in locations_by_sphere + for loc in sphere + if loc.item.name in names + ] + + # It's expected that there may be more total items than there are matching locations if + # the player has chosen a more limited accessibility option, since the matching + # locations *only* include items in the spheres of accessibility. + if len(converted_item_order) < len(all_matching_locations): + raise Exception( + f"DS3 bug: there are {len(all_matching_locations)} locations that can " + + f"contain smoothed items, but only {len(converted_item_order)} items to smooth." + ) + + for sphere in locations_by_sphere: + locations = [loc for loc in sphere if loc.item.name in names] + + # Check the game, not the player, because we know how to sort within regions for DS3 + offworld = self._shuffle([loc for loc in locations if loc.game != "Dark Souls III"]) + onworld = sorted((loc for loc in locations if loc.game == "Dark Souls III"), + key=lambda loc: loc.data.region_value) + + # Give offworld regions the last (best) items within a given sphere + for location in onworld + offworld: + new_item = self._pop_item(location, converted_item_order) + location.item = new_item + new_item.location = location + + if self.options.smooth_upgrade_items: + base_names = { + "Titanite Shard", "Large Titanite Shard", "Titanite Chunk", "Titanite Slab", + "Titanite Scale", "Twinkling Titanite", "Farron Coal", "Sage's Coal", "Giant's Coal", + "Profaned Coal" + } + smooth_items([item for item in all_item_order if item.base_name in base_names]) + + if self.options.smooth_soul_items: + smooth_items([ + item for item in all_item_order + if item.souls and item.classification != ItemClassification.progression + ]) + + if self.options.smooth_upgraded_weapons: + upgraded_weapons = [ + location.item + for location in self.multiworld.get_filled_locations() + if location.item.player == self.player + and location.item.level and location.item.level > 0 + and location.item.classification != ItemClassification.progression + ] + upgraded_weapons.sort(key=lambda item: item.level) + smooth_items(upgraded_weapons) + + def _shuffle(self, seq: Sequence) -> List: + """Returns a shuffled copy of a sequence.""" + copy = list(seq) + self.random.shuffle(copy) + return copy + + def _pop_item( + self, + location: Location, + items: List[DarkSouls3Item] + ) -> DarkSouls3Item: + """Returns the next item in items that can be assigned to location.""" + for i, item in enumerate(items): + if location.can_fill(self.multiworld.state, item, False): + return items.pop(i) + + # If we can't find a suitable item, give up and assign an unsuitable one. + return items.pop(0) + + def _get_our_locations(self) -> List[DarkSouls3Location]: + return cast(List[DarkSouls3Location], self.multiworld.get_locations(self.player)) def fill_slot_data(self) -> Dict[str, object]: slot_data: Dict[str, object] = {} - # Depending on the specified option, modify items hexadecimal value to add an upgrade level or infusion - name_to_ds3_code = {item.name: item.ds3_code for item in item_dictionary.values()} + # Once all clients support overlapping item IDs, adjust the DS3 AP item IDs to encode the + # in-game ID as well as the count so that we don't need to send this information at all. + # + # We include all the items the game knows about so that users can manually request items + # that aren't randomized, and then we _also_ include all the items that are placed in + # practice `item_dictionary.values()` doesn't include upgraded or infused weapons. + all_items = { + cast(DarkSouls3Item, location.item).data + for location in self.multiworld.get_filled_locations() + # item.code None is used for events, which we want to skip + if location.item.code is not None and location.item.player == self.player + }.union(item_dictionary.values()) - # Randomize some weapon upgrades - if self.multiworld.randomize_weapon_level[self.player] != RandomizeWeaponLevelOption.option_none: - # if the user made an error and set a min higher than the max we default to the max - max_5 = self.multiworld.max_levels_in_5[self.player] - min_5 = min(self.multiworld.min_levels_in_5[self.player], max_5) - max_10 = self.multiworld.max_levels_in_10[self.player] - min_10 = min(self.multiworld.min_levels_in_10[self.player], max_10) - weapon_level_percentage = self.multiworld.randomize_weapon_level_percentage[self.player] + ap_ids_to_ds3_ids: Dict[str, int] = {} + item_counts: Dict[str, int] = {} + for item in all_items: + if item.ap_code is None: continue + if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code + if item.count != 1: item_counts[str(item.ap_code)] = item.count - for item in item_dictionary.values(): - if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < weapon_level_percentage: - if item.category == DS3ItemCategory.WEAPON_UPGRADE_5: - name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_5, max_5) - elif item.category in {DS3ItemCategory.WEAPON_UPGRADE_10, DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE}: - name_to_ds3_code[item.name] += self.multiworld.per_slot_randoms[self.player].randint(min_10, max_10) - - # Randomize some weapon infusions - if self.multiworld.randomize_infusion[self.player] == Toggle.option_true: - infusion_percentage = self.multiworld.randomize_infusion_percentage[self.player] - for item in item_dictionary.values(): - if item.category in {DS3ItemCategory.WEAPON_UPGRADE_10_INFUSIBLE, DS3ItemCategory.SHIELD_INFUSIBLE}: - if self.multiworld.per_slot_randoms[self.player].randint(0, 99) < infusion_percentage: - name_to_ds3_code[item.name] += 100 * self.multiworld.per_slot_randoms[self.player].randint(0, 15) - - # Create the mandatory lists to generate the player's output file - items_id = [] - items_address = [] - locations_id = [] - locations_address = [] - locations_target = [] - for location in self.multiworld.get_filled_locations(): - # Skip events - if location.item.code is None: - continue - - if location.item.player == self.player: - items_id.append(location.item.code) - items_address.append(name_to_ds3_code[location.item.name]) - - if location.player == self.player: - locations_address.append(item_dictionary[location_dictionary[location.name].default_item].ds3_code) - locations_id.append(location.address) - if location.item.player == self.player: - locations_target.append(name_to_ds3_code[location.item.name]) - else: - locations_target.append(0) + # A map from Archipelago's location IDs to the keys the static randomizer uses to identify + # locations. + location_ids_to_keys: Dict[int, str] = {} + for location in cast(List[DarkSouls3Location], self.multiworld.get_filled_locations(self.player)): + # Skip events and only look at this world's locations + if (location.address is not None and location.item.code is not None + and location.data.static): + location_ids_to_keys[location.address] = location.data.static slot_data = { "options": { - "enable_weapon_locations": self.multiworld.enable_weapon_locations[self.player].value, - "enable_shield_locations": self.multiworld.enable_shield_locations[self.player].value, - "enable_armor_locations": self.multiworld.enable_armor_locations[self.player].value, - "enable_ring_locations": self.multiworld.enable_ring_locations[self.player].value, - "enable_spell_locations": self.multiworld.enable_spell_locations[self.player].value, - "enable_key_locations": self.multiworld.enable_key_locations[self.player].value, - "enable_boss_locations": self.multiworld.enable_boss_locations[self.player].value, - "enable_npc_locations": self.multiworld.enable_npc_locations[self.player].value, - "enable_misc_locations": self.multiworld.enable_misc_locations[self.player].value, - "auto_equip": self.multiworld.auto_equip[self.player].value, - "lock_equip": self.multiworld.lock_equip[self.player].value, - "no_weapon_requirements": self.multiworld.no_weapon_requirements[self.player].value, - "death_link": self.multiworld.death_link[self.player].value, - "no_spell_requirements": self.multiworld.no_spell_requirements[self.player].value, - "no_equip_load": self.multiworld.no_equip_load[self.player].value, - "enable_dlc": self.multiworld.enable_dlc[self.player].value + "random_starting_loadout": self.options.random_starting_loadout.value, + "require_one_handed_starting_weapons": self.options.require_one_handed_starting_weapons.value, + "auto_equip": self.options.auto_equip.value, + "lock_equip": self.options.lock_equip.value, + "no_weapon_requirements": self.options.no_weapon_requirements.value, + "death_link": self.options.death_link.value, + "no_spell_requirements": self.options.no_spell_requirements.value, + "no_equip_load": self.options.no_equip_load.value, + "enable_dlc": self.options.enable_dlc.value, + "enable_ngp": self.options.enable_ngp.value, + "smooth_soul_locations": self.options.smooth_soul_items.value, + "smooth_upgrade_locations": self.options.smooth_upgrade_items.value, + "randomize_enemies": self.options.randomize_enemies.value, + "randomize_mimics_with_enemies": self.options.randomize_mimics_with_enemies.value, + "randomize_small_crystal_lizards_with_enemies": self.options.randomize_small_crystal_lizards_with_enemies.value, + "reduce_harmless_enemies": self.options.reduce_harmless_enemies.value, + "simple_early_bosses": self.options.simple_early_bosses.value, + "scale_enemies": self.options.scale_enemies.value, + "all_chests_are_mimics": self.options.all_chests_are_mimics.value, + "impatient_mimics": self.options.impatient_mimics.value, }, "seed": self.multiworld.seed_name, # to verify the server's multiworld "slot": self.multiworld.player_name[self.player], # to connect to server - "base_id": self.base_id, # to merge location and items lists - "locationsId": locations_id, - "locationsAddress": locations_address, - "locationsTarget": locations_target, - "itemsId": items_id, - "itemsAddress": items_address + # Reserializing here is silly, but it's easier for the static randomizer. + "random_enemy_preset": json.dumps(self.options.random_enemy_preset.value), + "yhorm": ( + f"{self.yhorm_location.name} {self.yhorm_location.id}" + if self.yhorm_location != default_yhorm_location + else None + ), + "apIdsToItemIds": ap_ids_to_ds3_ids, + "itemCounts": item_counts, + "locationIdsToKeys": location_ids_to_keys, } return slot_data + + @staticmethod + def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: + return slot_data diff --git a/worlds/dark_souls_3/detailed_location_descriptions.py b/worlds/dark_souls_3/detailed_location_descriptions.py new file mode 100644 index 0000000000..e20c700ab1 --- /dev/null +++ b/worlds/dark_souls_3/detailed_location_descriptions.py @@ -0,0 +1,97 @@ +# python -m worlds.dark_souls_3.detailed_location_descriptions \ +# worlds/dark_souls_3/detailed_location_descriptions.py +# +# This script downloads the static randomizer's descriptions for each location and adds them to +# the location documentation. + +from collections import defaultdict +import html +import os +import re +import requests +import yaml + +from .Locations import location_dictionary + + +location_re = re.compile(r'^([A-Z0-9]+): (.*?)(?:$| - )') + +if __name__ == '__main__': + # TODO: update this to the main branch of the main randomizer once Archipelago support is merged + url = 'https://raw.githubusercontent.com/nex3/SoulsRandomizers/archipelago-server/dist/Base/annotations.txt' + response = requests.get(url) + if response.status_code != 200: + raise Exception(f"Got {response.status_code} when downloading static randomizer locations") + annotations = yaml.load(response.text, Loader=yaml.Loader) + + static_to_archi_regions = { + area['Name']: area['Archipelago'] + for area in annotations['Areas'] + } + + descriptions_by_key = {slot['Key']: slot['Text'] for slot in annotations['Slots']} + + # A map from (region, item name) pairs to all the descriptions that match those pairs. + descriptions_by_location = defaultdict(list) + + # A map from item names to all the descriptions for those item names. + descriptions_by_item = defaultdict(list) + + for slot in annotations['Slots']: + region = static_to_archi_regions[slot['Area']] + for item in slot['DebugText']: + name = item.split(" - ")[0] + descriptions_by_location[(region, name)].append(slot['Text']) + descriptions_by_item[name].append(slot['Text']) + counts_by_location = { + location: len(descriptions) for (location, descriptions) in descriptions_by_location.items() + } + + location_names_to_descriptions = {} + for location in location_dictionary.values(): + if location.ap_code is None: continue + if location.static: + location_names_to_descriptions[location.name] = descriptions_by_key[location.static] + continue + + match = location_re.match(location.name) + if not match: + raise Exception(f"Location name \"{location.name}\" doesn't match expected format.") + + item_candidates = descriptions_by_item[match[2]] + if len(item_candidates) == 1: + location_names_to_descriptions[location.name] = item_candidates[0] + continue + + key = (match[1], match[2]) + if key not in descriptions_by_location: + raise Exception(f'No static randomizer location found matching "{match[1]}: {match[2]}".') + + candidates = descriptions_by_location[key] + if len(candidates) == 0: + raise Exception( + f'There are only {counts_by_location[key]} locations in the static randomizer ' + + f'matching "{match[1]}: {match[2]}", but there are more in Archipelago.' + ) + + location_names_to_descriptions[location.name] = candidates.pop(0) + + table = "\n" + for (name, description) in sorted( + location_names_to_descriptions.items(), + key = lambda pair: pair[0] + ): + table += f"\n" + table += "
Location nameDetailed description
{html.escape(name)}{html.escape(description)}
\n" + + with open(os.path.join(os.path.dirname(__file__), 'docs/locations_en.md'), 'r+') as f: + original = f.read() + start_flag = "\n" + start = original.index(start_flag) + len(start_flag) + end = original.index("") + + f.seek(0) + f.write(original[:start] + table + original[end:]) + f.truncate() + + print("Updated docs/locations_en.md!") diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index f31358bb9c..06227226aa 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,28 +1,201 @@ # Dark Souls III +Game Page | [Items] | [Locations] + +[Items]: /tutorial/Dark%20Souls%20III/items/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## What do I need to do to randomize DS3? + +See full instructions on [the setup page]. + +[the setup page]: /tutorial/Dark%20Souls%20III/setup/en + ## Where is the options page? -The [player options page for this game](../player-options) contains all the options you need to configure and export a -config file. +The [player options page for this game][options] contains all the options you +need to configure and export a config file. + +[options]: ../player-options ## What does randomization do to this game? -Items that can be picked up from static corpses, taken from chests, or earned from defeating enemies or NPCs can be -randomized. Common pickups like titanite shards or firebombs can be randomized as "progressive" items. That is, the -location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter where it was from. This is also what -happens when you randomize Estus Shards and Undead Bone Shards. +1. All item locations are randomized, including those in the overworld, in + shops, and dropped by enemies. Most locations can contain games from other + worlds, and any items from your world can appear in other players' worlds. -It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have -one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as -removing weapon requirements or auto-equipping whatever equipment you most recently received. +2. By default, all enemies and bosses are randomized. This can be disabled by + setting "Randomize Enemies" to false. -The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. +3. By default, the starting equipment for each class is randomized. This can be + disabled by setting "Randomize Starting Loadout" to false. -## What Dark Souls III items can appear in other players' worlds? +4. By setting the "Randomize Weapon Level" or "Randomize Infusion" options, you + can randomize whether the weapons you find will be upgraded or infused. -Practically anything can be found in other worlds including pieces of armor, upgraded weapons, key items, consumables, -spells, upgrade materials, etc... +There are also options that can make playing the game more convenient or +bring a new experience, like removing equip loads or auto-equipping weapons as +you pick them up. Check out [the options page][options] for more! -## What does another world's item look like in Dark Souls III? +## What's the goal? -In Dark Souls III, items which are sent to other worlds appear as Prism Stones. +Your goal is to find the four "Cinders of a Lord" items randomized into the +multiworld and defeat the boss in the Kiln of the First Flame. + +## Do I have to check every item in every area? + +Dark Souls III has about 1500 item locations, which is a lot of checks for a +single run! But you don't necessarily need to check all of them. Locations that +you can potentially miss, such as rewards for failable quests or soul +transposition items, will _never_ have items required for any game to progress. +The following types of locations are also guaranteed not to contain progression +items by default: + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +You can customize which locations are guaranteed not to contain progression +items by setting the `exclude_locations` field in your YAML to the [location +groups] you want to omit. For example, this is the default setting but without +"Hidden" so that hidden locations can contain progression items: + +[location groups]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + +```yaml +Dark Souls III: + exclude_locations: + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +This allows _all_ non-missable locations to have progression items, if you're in +for the long haul: + +```yaml +Dark Souls III: + exclude_locations: [] +``` + +## What if I don't want to do the whole game? + +If you want a shorter DS3 randomizer experience, you can exclude entire regions +from containing progression items. The items and enemies from those regions will +still be included in the randomization pool, but none of them will be mandatory. +For example, the following configuration just requires you to play the game +through Irithyll of the Boreal Valley: + +```yaml +Dark Souls III: + # Enable the DLC so it's included in the randomization pool + enable_dlc: true + + exclude_locations: + # Exclude late-game and DLC regions + - Anor Londo + - Lothric Castle + - Consumed King's Garden + - Untended Graves + - Grand Archives + - Archdragon Peak + - Painted World of Ariandel + - Dreg Heap + - Ringed City + + # Default exclusions + - Hidden + - Small Crystal Lizards + - Upgrade + - Small Souls + - Miscellaneous +``` + +## Where can I learn more about Dark Souls III locations? + +Location names have to pack a lot of information into very little space. To +better understand them, check out the [location guide], which explains all the +names used in locations and provides more detailed descriptions for each +individual location. + +[location guide]: /tutorial/Dark%20Souls%20III/locations/en + +## Where can I learn more about Dark Souls III items? + +Check out the [item guide], which explains the named groups available for items. + +[item guide]: /tutorial/Dark%20Souls%20III/items/en + +## What's new from 2.x.x? + +Version 3.0.0 of the Dark Souls III Archipelago client has a number of +substantial differences with the older 2.x.x versions. Improvements include: + +* Support for randomizing all item locations, not just unique items. + +* Support for randomizing items in shops, starting loadouts, Path of the Dragon, + and more. + +* Built-in integration with the enemy randomizer, including consistent seeding + for races. + +* Support for the latest patch for Dark Souls III, 1.15.2. Older patches are + *not* supported. + +* Optional smooth distribution for upgrade items, upgraded weapons, and soul + items so you're more likely to see weaker items earlier and more powerful + items later. + +* More detailed location names that indicate where a location is, not just what + it replaces. + +* Other players' item names are visible in DS3. + +* If you pick up items while static, they'll still send once you reconnect. + +However, 2.x.x YAMLs are not compatible with 3.0.0. You'll need to [generate a +new YAML configuration] for use with 3.x.x. + +[generating a new YAML configuration]: /games/Dark%20Souls%20III/player-options + +The following options have been removed: + +* `enable_boss_locations` is now controlled by the `soul_locations` option. + +* `enable_progressive_locations` was removed because all locations are now + individually randomized rather than replaced with a progressive list. + +* `pool_type` has been removed. Since there are no longer any non-randomized + items in randomized categories, there's not a meaningful distinction between + "shuffle" and "various" mode. + +* `enable_*_locations` options have all been removed. Instead, you can now add + [location group names] to the `exclude_locations` option to prevent them from + containing important items. + + [location group names]: /tutorial/Dark%20Souls%20III/locations/en#location-groups + + By default, the Hidden, Small Crystal Lizards, Upgrade, Small Souls, and + Miscellaneous groups are in `exclude_locations`. Once you've chosen your + excluded locations, you can set `excluded_locations: unrandomized` to preserve + the default vanilla item placements for all excluded locations. + +* `guaranteed_items`: In almost all cases, all items from the base game are now + included somewhere in the multiworld. + +In addition, the following options have changed: + +* The location names used in options like `exclude_locations` have changed. See + the [location guide] for a full description. diff --git a/worlds/dark_souls_3/docs/items_en.md b/worlds/dark_souls_3/docs/items_en.md new file mode 100644 index 0000000000..b9de5e500a --- /dev/null +++ b/worlds/dark_souls_3/docs/items_en.md @@ -0,0 +1,24 @@ +# Dark Souls III Items + +[Game Page] | Items | [Locations] + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Locations]: /tutorial/Dark%20Souls%20III/locations/en + +## Item Groups + +The Dark Souls III randomizer supports a number of item group names, which can +be used in YAML options like `local_items` to refer to many items at once: + +* **Progression:** Items which unlock locations. +* **Cinders:** All four Cinders of a Lord. Once you have these four, you can + fight Soul of Cinder and win the game. +* **Miscellaneous:** Generic stackable items, such as arrows, firebombs, buffs, + and so on. +* **Unique:** Items that are unique per NG cycle, such as scrolls, keys, ashes, + and so on. Doesn't include equipment, spells, or souls. +* **Boss Souls:** Souls that can be traded with Ludleth, including Soul of + Rosaria. +* **Small Souls:** Soul items, not including boss souls. +* **Upgrade:** Upgrade items, including titanite, gems, and Shriving Stones. +* **Healing:** Undead Bone Shards and Estus Shards. diff --git a/worlds/dark_souls_3/docs/locations_en.md b/worlds/dark_souls_3/docs/locations_en.md new file mode 100644 index 0000000000..ef07b84b2b --- /dev/null +++ b/worlds/dark_souls_3/docs/locations_en.md @@ -0,0 +1,2276 @@ +# Dark Souls III Locations + +[Game Page] | [Items] | Locations + +[Game Page]: /games/Dark%20Souls%20III/info/en +[Items]: /tutorial/Dark%20Souls%20III/items/en + +## Table of Contents + +* [Location Groups](#location-groups) +* [Understanding Location Names](#understanding-location-names) + * [HWL: High Wall of Lothric](#high-wall-of-lothric) + * [US: Undead Settlement](#undead-settlement) + * [RS: Road of Sacrifices](#road-of-sacrifices) + * [CD: Cathedral of the Deep](#cathedral-of-the-deep) + * [FK: Farron Keep](#farron-keep) + * [CC: Catacombs of Carthus](#catacombs-of-carthus) + * [SL: Smouldering Lake](#smouldering-lake) + * [IBV: Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) + * [ID: Irithyll Dungeon](#irithyll-dungeon) + * [PC: Profaned Capital](#profaned-capital) + * [AL: Anor Londo](#anor-londo) + * [LC: Lothric Castle](#lothric-castle) + * [CKG: Consumed King's Garden](#consumed-kings-garden) + * [GA: Grand Archives](#grand-archives) + * [UG: Untended Graves](#untended-graves) + * [AP: Archdragon Peak](#archdragon-peak) + * [PW1: Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) + * [PW2: Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) + * [DH: Dreg Heap](#dreg-heap) + * [RC: Ringed City](#ringed-city) +* [Detailed Location Descriptions](#detailed-location-descriptions) + +## Location Groups + +The Dark Souls III randomizer supports a number of location group names, which +can be used in YAML options like `exclude_locations` to refer to many locations +at once: + +* **Prominent:** A small number of locations that are in very obvious locations. + Mostly boss drops. Ideal for setting as priority locations. + +* **Progression:** Locations that contain items in vanilla which unlock other + locations. + +* **Boss Rewards:** Boss drops. Does not include soul transfusions or shop + items. + +* **Miniboss Rewards:** Miniboss drops. Minibosses are large enemies that don't + respawn after being killed and usually drop some sort of treasure, such as + Boreal Outrider Knights and Ravenous Crystal Lizards. Only includes enemies + considered minibosses by the enemy randomizer. + +* **Mimic Rewards:** Drops from enemies that are mimics in vanilla. + +* **Hostile NPC Rewards:** Drops from NPCs that are hostile to you. This + includes scripted invaders and initially-friendly NPCs that must be fought as + part of their quest. + +* **Friendly NPC Rewards:** Items given by friendly NPCs as part of their quests + or from non-violent interaction. + +* **Small Crystal Lizards:** Drops from small crystal lizards. + +* **Upgrade:** Locations that contain upgrade items in vanilla, including + titanite, gems, and Shriving Stones. + +* **Small Souls:** Locations that contain soul items in vanilla, not including + boss souls. + +* **Boss Souls:** Locations that contain boss souls in vanilla, as well as Soul + of Rosaria. + +* **Unique:** Locations that contain items in vanilla that are unique per NG + cycle, such as scrolls, keys, ashes, and so on. Doesn't cover equipment, + spells, or souls. + +* **Healing:** Locations that contain Undead Bone Shards and Estus Shards in + vanilla. + +* **Miscellaneous:** Locations that contain generic stackable items in vanilla, + such as arrows, firebombs, buffs, and so on. + +* **Hidden:** Locations that are particularly difficult to find, such as behind + illusory walls, down hidden drops, and so on. Does not include large locations + like Untended Graves or Archdragon Peak. + +* **Weapons:** Locations that contain weapons in vanilla. + +* **Shields:** Locations that contain shields in vanilla. + +* **Armor:** Locations that contain armor in vanilla. + +* **Rings:** Locations that contain rings in vanilla. + +* **Spells:** Locations that contain spells in vanilla. + +## Understanding Location Names + +All locations begin with an abbreviation indicating their general region. Most +locations have a set of landmarks that are used in location names to keep them +short. + +* **FS:** Firelink Shrine +* **FSBT:** Firelink Shrine belltower +* **HWL:** [High Wall of Lothric](#high-wall-of-lothric) +* **US:** [Undead Settlement](#undead-settlement) +* **RS:** [Road of Sacrifices](#road-of-sacrifices) +* **CD:** [Cathedral of the Deep](#cathedral-of-the-deep) +* **FK:** [Farron Keep](#farron-keep) +* **CC:** [Catacombs of Carthus](#catacombs-of-carthus) +* **SL:** [Smouldering Lake](#smouldering-lake) +* **IBV:** [Irithyll of the Boreal Valley](#irithyll-of-the-boreal-valley) +* **ID:** [Irithyll Dungeon](#irithyll-dungeon) +* **PC:** [Profaned Capital](#profaned-capital) +* **AL:** [Anor Londo](#anor-londo) +* **LC:** [Lothric Castle](#lothric-castle) +* **CKG:** [Consumed King's Garden](#consumed-kings-garden) +* **GA:** [Grand Archives](#grand-archives) +* **UG:** [Untended Graves](#untended-graves) +* **AP:** [Archdragon Peak](#archdragon-peak) +* **PW1:** [Painted World of Ariandel (Before Contraption)](#painted-world-of-ariandel-before-contraption) +* **PW2:** [Painted World of Ariandel (After Contraption)](#painted-world-of-ariandel-after-contraption) +* **DH:** [Dreg Heap](#dreg-heap) +* **RC:** [Ringed City](#ringed-city) + +General notes: + +* "Lizard" always refers to a small crystal lizard. + +* "Miniboss" are large enemies that don't respawn after being killed and usually + drop some sort of treasure, such as Boreal Outrider Knights and Ravenous + Crystal Lizards. + +* NPC quest items are always in the first location you can get them _without_ + killing the NPC or ending the quest early. + +### High Wall of Lothric + +* **Back tower:** The tower _behind_ the High Wall of Lothric bonfire, past the + path to the shortcut elevator. + +* **Corpse tower:** The first tower after the High Wall of Lothric bonfire, with + a dead Wyvern on top of it. + +* **Fire tower:** The second tower after the High Wall of Lothric bonfire, where + a living Wyvern lands and breathes fire at you. + +* **Flame plaza:** The open area with many items where the Wyvern breathes fire. + +* **Wall tower:** The third tower after the High Wall of Lothric bonfire, with + the Tower on the Wall bonfire. + +* **Fort:** The large building after the Tower on the Wall bonfire, with the + transforming hollow on top. + + * "Entry": The first room you enter after descending the ladder from the roof. + + * "Walkway": The top floor of the tall room, with a path around the edge + hidden by a large wheel. + + * "Mezzanine": The middle floor of the tall room, with a chest. + + * "Ground": The bottom floor of the tall room, with an anvil and many mobs. + +* **Fountain:** The large fountain with many dead knights around it, where the + Winged Knight patrols in vanilla. + +* **Shortcut:** The unlockable path between the promenade and the High Wall of + Lothric bonfire, including both the elevator and the area at its base. + +* **Promenade:** The long, wide path between the two boss arenas. + +### Undead Settlement + +* **Foot:** The area where you first appear, around the Foot of the High Wall + bonfire. + +* **Burning tree:** The tree near the beginning of the region, with the + Cathedral Evangelist in front of it in vanilla. + +* **Hanging corpse room:** The dark room to the left of the burning tree with + many hanging corpses inside, on the way to the Dilapidated Bridge bonfire. + +* **Back alley:** The path between buildings leading to the Dilapidated Bridge + bonfire. + +* **Stable:** The building complex across the bridge to the right of the burning + tree. + +* **White tree:** The birch tree by the Dilapidated Bridge bonfire, where the + giant shoots arrows. + +* **Sewer:** The underground passage between the chasm and the Dilapidated + Bridge bonfire. + +* **Chasm:** The chasm underneath the bridge on the way to the tower. It's + possible to get into the chasm without a key by dropping down next to Eygon of + Carim with a full health bar. + +* **Tower:** The tower at the end of the region with the giant archer at the + top. + +* **Tower village:** The village reachable from the tower, where the Fire Demon + patrols in vanilla. + +### Road of Sacrifices + +The area after the Crystal Sage is considered part of the Cathedral of the Deep +region. + +* **Road:** The path from the Road of Sacrifices bonfire to the Halfway Fortress + bonfire. + +* **Woods:** The wooded area on land, after the Halfway Fortress bonfire and + surrounding the Crucifixion Woods bonfire. + +* **Water:** The watery area, covered in crabs in vanilla. + +* **Deep water:** The area in the water near the ladder to Farron Keep, where + your walking is slowed. + +* **Stronghold:** The stone building complex on the way to Crystal Sage. + + * "Left room" is the room whose entrance is near the Crucifixion Woods + bonfire. + + * "Right room" is the room up the stairs closer to Farron Keep. + +* **Keep perimeter:** The building with the Black Knight and the locked door to + the Farron Keep Perimeter bonfire. + +### Cathedral of the Deep + +* **Path:** The path from Road of Sacrifices to the cathedral proper. + +* **Moat:** The circular path around the base of the front of the + cathedral, with the Ravenous Crystal Lizard and Corpse-Grubs in vanilla. + +* **Graveyard:** The area with respawning enemies up the hill from the Cleansing + Chapel bonfire. + +* **White tree:** The birch tree below the front doors of the chapel and across + the moat from the graveyard, where the giant shoots arrows if he's still + alive. + +* **Lower roofs:** The roofs, flying buttresses, and associated areas to the + right of the front door, which must be traversed before entering the + cathedral. + +* **Upper roofs:** The roofs, flying buttresses, and rafters leading to the + Rosaria's Bedchamber bonfire. + +* **Main hall:** The central and largest room in the cathedral, with the muck + that slows your movement. Divided into the south (with the sleeping giant in + vanilla) and east (with many items) wings, with north pointing towards the + door to the boss. + +* **Side chapel:** The room with rows of pews and the patrolling Cathedral + Knight in vanilla, to the side of the main hall. + +### Farron Keep + +* **Left island:** The large island with the ritual flame, to the left as you + leave the Farron Keep bonfire. + +* **Right island:** The large island with the ritual flame, to the right as you + leave the Farron Keep bonfire. + +* **Hidden cave:** A small cave in the far corner of the map, closest to the + right island. Near a bunch of basilisks in vanilla. + +* **Keep ruins:** The following two islands: + + * "Bonfire island": The island with the Keep Ruins bonfire. + * "Ritual island": The island with one of the three ritual fires. + +* **White tree**: The birch tree by the ramp down from the keep ruins bonfire + island, where the giant shoots arrows if he's still alive. + +* **Keep proper:** The building with the Old Wolf of Farron bonfire. + +* **Upper keep:** The area on top of the keep proper, reachable from the + elevator from the Old Wolf of Farron bonfire. + +* **Perimeter:** The area from near the Farron Keep Perimeter bonfire, including + the stone building and the path to the boss. + +### Catacombs of Carthus + +All the area up to the Small Doll wall into Irithyll is considered part of the +Catacombs of Carthus region. + +* **Atrium:** The large open area you first enter and the rooms attached to it. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the short stairs but at the top of the long + stairway that the skeleton ball rolls down. + +* **Crypt:** The enclosed area at the bottom of the long stairway that the + skeleton ball rolls down. + + * "Upper" is the floor the long stairway leads to that also contains the + Catacombs of Carthus bonfire. + * "Lower" is the floor with rats and bonewheels in vanilla. + * "Across" is the area reached by going up the set of stairs across from + the entrance downstairs. + +* **Cavern:** The even larger open area past the crypt with the rope bridge to + the boss arena. + +* **Tomb:** The area on the way to Smouldering Lake, reachable by cutting down + the rope bridge and climbing down it. + +* **Irithyll Bridge:** The outdoor bridge leading to Irithyll of the Boreal + Valley. + +### Smouldering Lake + +* **Lake:** The watery area you enter initially, where you get shot at by the + ballista. + +* **Side lake:** The small lake accessible via a passage from the larger one, in + which you face Horace the Hushed as part of his quest. + +* **Ruins main:** The area you first enter after the Demon Ruins bonfire. + + * "Upper" is the floor you begin on. + * "Lower" is the floor down the stairs. + +* **Antechamber:** The area up the flight of stairs near the +Old King's Antechamber bonfire. + +* **Ruins basement:** The area further down from ruins main lower, with many + basilisks and Knight Slayer Tsorig in vanilla. + +### Irithyll of the Boreal Valley + +This region starts _after_ the Small Doll wall and ends with Pontiff Sulyvahn. +Everything after that, including the contents of Sulyvahn's cathedral is +considered part of Anor Londo. + +* **Central:** The beginning of the region, from the Central Irithyll bonfire up + to the plaza. + +* **Dorhys:** The sobbing mob (a Cathedral Evangelist in vanilla) behind the + locked door opening onto central. Accessed through an illusory railing by the + crystal lizard just before the plaza. + +* **Plaza:** The area in front of and below the cathedral, with a locked door up + to the cathedral and a locked elevator to the Ascent. + +* **Descent:** The path from the Church of Yorshka bonfire down to the lake. + +* **Lake:** The open watery area outside the room with the Distant Manor + bonfire. + +* **Sewer:** The room between the lake and the beginning of the ascent, filled + with Sewer Centipedes in vanilla. + +* **Ascent:** The path up from the lake to the cathedral, through several + buildings and some open stairs. + +* **Great hall:** The building along the ascent with a large picture of + Gwynevere and several Silver Knights in vanilla. + +### Irithyll Dungeon + +In Irithyll Dungeon locations, "left" and "right" are always oriented as though +"near" is where you stand and "far" is where you're facing. (For example, you +enter the dungeon from the bonfire on the near left.) + +* **B1:** The floor on which the player enters the dungeon, with the Irithyll + Dungeon bonfire. + + * "Near" is the side of the dungeon with the bonfire. + * "Far" is the opposite side. + +* **B2:** The floor directly below B1, which can be reached by going down the + stairs or dropping. + + * "Near" is the same side of the dungeon as the bonfire. + * "Far" is the opposite side. + +* **Pit:** The large room with the Giant Slave and many Rats in vanilla. + +* **Pit lift:** The elevator from the pit up to B1 near, right to the Irithyll + Dungeon bonfire. + +* **B3:** The lowest floor, with Karla's cell, a lift back to B2, and the exit + onwards to the Profaned Capital. + + * "Near" is the side with Karla's cell and the path from the pit. + * "Far" is the opposite side with the mimic. + +* **B3 lift:** The elevator from B3 (near where you can use Path of the Dragon + to go to Archdragon Peak) up to B2. + +### Profaned Capital + +* **Tower:** The tower that contains the Profaned Capital bonfire. + +* **Swamp:** The pool of toxic liquid accessible by falling down out of the + lower floor of the tower, going into the corridor to the left, and falling + down a hole. + +* **Chapel:** The building in the swamp containing Monstrosities of Sin in + vanilla. + +* **Bridge:** The long bridge from the tower into the palace. + +* **Palace:** The large building carved into the wall of the cavern, full of + chalices and broken pillars. + +### Anor Londo + +This region includes everything after Sulyvahn's cathedral, including its upper +story. + +* **Light cathedral:** The cathedral in which you fight Pontiff Sulyvahn in + vanilla. + +* **Plaza:** The wide open area filled with Giant Slaves in vanilla. + +* **Walkway:** The path above the plaza leading to the second floor of the light + cathedral, with Deacons in vanilla. + +* **Buttresses:** The flying buttresses that you have to climb to get to the + spiral staircase. "Near" and "far" are relative to the light cathedral, so the + nearest buttress is the one that leads back to the walkway. + +* **Tomb:** The area past the illusory wall just before the spiral staircase, in + which you marry Anri during Yoel and Yuria's quest. + +* **Dark cathedral:** The darkened cathedral just before the Aldrich fight in + vanilla. + +### Lothric Castle + +This region covers everything up the ladder from the Dancer of the Boreal Valley +bonfire up to the door into Grand Archives, except the area to the left of the +ladder which is part of Consumed King's Garden. + +* **Lift:** The elevator from the room straight after the Dancer of the Boreal + Valley bonfire up to just before the boss fight. + +* **Ascent:** The set of stairways and turrets leading from the Lothric Castle + bonfire to the Dragon Barracks bonfire. + +* **Barracks:** The large building with two fire-breathing wyverns across from + the Dragon Barracks bonfire. + +* **Moat:** The ditch beneath the bridge leading to the barracks. + + * The "right path" leads to the right as you face the barracks, around and + above the stairs up to the Dragon Barracks bonfire. + +* **Plaza:** The open area in the center of the barracks, where the two wyverns + breathe fire. + + * "Left" is the enclosed area on the left as you're coming from the Dragon + Barracks bonfire, with the stairs down to the basement. + +* **Basement:** The room beneath plaza left, with the Boreal Outrider in + vanilla. + +* **Dark room:** The large darkened room on the right of the barracks as you're + coming from the Dragon Barracks bonfire, with firebomb-throwing Hollows in + vanilla. + + * "Lower" is the bottom floor that you enter onto from the plaza. + * "Upper" is the top floor with the door to the main hall. + * "Mid" is the middle floor accessible by climbing a ladder from lower or + going down stairs from upper. + +* **Main hall:** The central room of the barracks, behind the gate. + +* **Chapel:** The building to the right just before the stairs to the boss, with + a locked elevator to Grand Archives. + +* **Wyvern room:** The room where you can fight the Pus of Man infecting the + left wyvern, accessible by dropping down to the left of the stairs to the + boss. + +* **Altar:** The building containing the Altar of Sunlight, accessible by + climbing up a ladder onto a roof around the corner from the stairs to the + boss. + +### Consumed King's Garden + +This region covers everything to the left of the ladder up from the Dancer of +the Boreal Valley bonfire up to the illusory wall into Untended Graves. + +* **Balcony:** The walkway accessible by getting off the first elevator halfway + down. + +* **Rotunda:** The building in the center of the toxic pool, with a Cathedral + Knight on it in vanilla. + +* **Lone stairway:** A set of stairs leading nowhere in the far left of the main + area as you enter from the first elevator. + +* **Shortcut:** The path from the locked door into Lothric Castle, through the + room filled with thralls in vanilla, and down a lift. + +* **Tomb:** The area after the boss room. + +### Grand Archives + +* **1F:** The first floor of the Grand Archives, including the first wax pool. + +* **Dark room:** The unlit room on 1F to the right of the wax pool. + +* **2F:** The second floor of the grand archives. It's split into two sections + that are separated by retractable bookshelves. + + * "Early" is the first part you reach and has an outdoor balcony with a ladder + to 3F and a wax pool up a short set of stairs. + * "Late" is the part you can only reach by climbing down from F3, where you + encounter the teleporting miniboss for the final time. + +* **3F:** The third floor of the grand archives, where you encounter the + teleporting miniboss for the second time. Includes the area with a hidden room + with another miniboss. + +* **4F:** The topmost and most well-lit section of bookshelves, overlooking the + rest of the archives. + +* **Rooftops:** The outer rooftop area between 4F and 5F, with Gargoyles in + vanilla. + + * "Lower" is the balcony you can reach by dropping off the rooftops, as well + as the further rooftops leading down to the 2F early balcony. + +* **5F:** The topmost floor of the archives interior, accessible from the + rooftops, with a ladder down to 4F. + +* **Dome:** The domed roof of the Grand Archives, with Ascended Winged Knights + in vanilla. + +* **Rafters:** The narrow walkways above the Grand Archives, accessible by + dropping down from the dome. + +### Untended Graves + +* **Swamp:** The watery area immediately after the Untended graves bonfire, up + to the cemetery. + +* **Cemetery:** The area past where the Cemetery of Ash bonfire would be, up to + the boss arena. + +* **Environs:** The area after the boss and outside the abandoned Firelink + Shrine. + +* **Shrine:** The area inside the abandoned Firelink Shrine. + +### Archdragon Peak + +"Gesture" always means the Path of the Dragon gesture. + +* **Intro:** The first section, from where you warp in from Irithyll Dungeon up + to the first boss fight. + + * "Archway": The large stone archway in front of the boss door. + +* **Fort:** The arena where you fight Ancient Wyvern in vanilla. + + * "Overlook": The area down the stairs from where the Ancient Wyvern first + lands in vanilla, overlooking the fog. + + * "Rotunda": The top of the spiral staircase building, to the left before the + bridge with the chain-axe Man-Serpent in vanilla. + +* **Mausoleum:** The building with the Dragon-Kin Mausoleum bonfire, where + you're warped after the first boss fight. + +* **Walkway:** The path from the mausoleum to the belfry, looking out over + clouds. + + * "Building": The building along the walkway, just before the wyvern in + vanilla. + +* **Belfry:** The building with the Great Belfry bonfire, including the room + with the summoner. + +* **Plaza:** The arena that appears after you defeat Nameless King in vanilla. + +* **Summit:** The path up from the belfry to the final altar at the top of the + mountain. + +### Painted World of Ariandel (Before Contraption) + +This region covers the Ashes of Ariandel DLC up to the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter. + +* **Snowfield:** The area around the Snowfield bonfire, + + * "Upper": The area immediately after the Snowfield bonfire, before the + collapsing overhang, with the Followers in vanilla. + + * "Lower": The snowy tree-filled area after the collapsing overhang, with the + Wolves in vanilla. + + * "Village": The area with broken-down buildings and Millwood Knights in + vanilla. + + * "Tower": The tower by the village, with Millwood Knights in Vanilla. + +* **Bridge:** The rope bridge to the chapel. + + * "Near": The side of the bridge by the Rope Bridge Cave bonfire. + + * "Far": The side of the bridge by the Ariandel Chapel bonfire. + +* **Chapel:** The building with the Ariandel Chapel bonfire and Lady Friede. + +* **Depths:** The area reachable by cutting down the bridge and descending on + the far side, with the Depths of the Painting bonfire. + +* **Settlement:** The area reachable by cutting down the bridge and descending + on the near side, with the Corvian Settlement bonfire. Everything after the + slide down the hill is considered part of the settlement. + + * "Courtyard": The area in front of the settlement, immediately after the + slide. + + * "Main": The main road of the settlement leading up to the locked gate to the + library. Also includes the buildings that are immediately accessible from + this road. + + * "Loop": A side path that loops left from the main road and goes up and + behind the building with the bonfire. + + * "Back": The back alley of the settlement, accessible by dropping down to the + right of the locked gate to the library. Also includes the buildings that + are immediately accessible from this alley. + + * "Roofs": The village rooftops, first accessible by climbing a ladder from + the back alley. Also includes the buildings and items that are first + accessible from the roofs. + + * "Hall": The largest building in the settlement, with two Corvian Knights in + vanilla. + +* **Library:** The building where you use the contraption key, where Vilhelm + appears in vanilla. + +### Painted World of Ariandel (After Contraption) + +This region covers the Ashes of Ariandel DLC past the point where you must use +the Contraption Key to ascend to the second level of the building and first meet +the painter, including the basement beneath the chapel. + +* **Pass:** The mountainous area past the Snowy Mountain Pass bonfire. + +* **Pit:** The area with a large tree and numerous Millwood Knights in vanilla, + reached by a collapsing overhang in the pass. + +* **B1:** The floor immediately below the chapel, first accessible from the + pass. Filled with Giant Flies in vanilla. + +* **B2:** The floor below B1, with lots of fly eggs. Filled with even more Giant + Flies than B1 in vanilla. + +* **B3:** The floor below B2, accessible through an illusory wall. + +* **Rotunda:** The round arena out in the open, accessible by platforming down + tree roots from B3. + +### Dreg Heap + +* **Shop:** Items sold by the Stone-Humped Hag by The Dreg Heap bonfire. + +* **Castle:** The building with The Dreg Heap bonfire, up to the large fall into + the library. + +* **Library:** The building with the stained-glass window that you fall into + from the castle. + +* **Church:** The building below and to the right of the library, which the + pillar falls into to make a bridge. + +* **Pantry:** The set of rooms entered through a door near the fountain just + past the church, with boxes and barrels. + + * "Upstairs": The room with an open side, accessible through an illusory wall + in the furthest pantry room. + +* **Parapets:** The area with balconies and Overgrown Lothric Knights in + vanilla, accessible by taking the pillar bridge from the church, following + that path to the end, and dropping down to the right. + +* **Ruins:** The area around the Earthen Peak Ruins bonfire, up to the swamp. + +* **Swamp:** The area in and above the poisonous water, up to the point the + branches deposit you back on the ruins. + + * "Left": Left as you enter from the ruins, towards the cliff edge. + + * "Right": Right as you enter from the ruins, towards higher ground. + + * "Upper": The path up and over the swamp towards the Within Earthen Peak + Ruins bonfire. + +### Ringed City + +The "mid boss", "end boss", and "hidden boss" are the bosses who take the place +of Halflight, Gael, and Midir, respectively. + +* **Wall:** The large wall in which you spawn when you first enter the area, + with the Mausoleum Lookout bonfire. + + * "Top": The open-air top of the wall, where you first spawn in. + + * "Upper": The upper area of the wall, with the Ringed Inner Wall bonfire. + + * "Tower": The tiered tower leading down from the upper area to the stairs. + + * "Lower": The lower rooms of the wall, accessible from the lower cliff, with + an elevator back to upper. + + * "Hidden": The hidden floor accessible from the elevator from lower to upper, + from which you can reach Midir in vanilla. + +* **Streets:** The streets and skyways of the city proper. "Left" and "right" + are relative to the main staircase as you head down towards the swamp, "near" + and "far" are relative to Shira's chamber at the top of the stairs. + + * "Garden": The flower-filled back alley accessible from the left side of the + nearest bridge over the stairs. + + * "High": The higher areas in the far left where you can find the Locust + Preacher, accessible from a long ladder in the swamp. + + * "Monument": The area around the purging monument, which can only be accessed + by solving the "Show Your Humanity" puzzle. + +* **Swamp:** The wet area past the city streets. "Left" and "right" are relative + to heading out from the Ringed City Streets bonfire, and "near" and "far" are + relative to that bonfire as well. + +* **Lower cliff:** The cliffside path leading from the swamp into the shared + grave, where Midir breathes fire. + +* **Grave:** The cylindrical chamber with spiral stairs around the edges, + connecting the two cliffs, containing the Shared Grave bonfire. + +* **Upper cliff:** The cliffside path leading out of the grave to the lower + wall. + +* **Church path:** The sunlit path from the lower cliff up to the Church of + Filianore where you fight Halflight in vanilla. + +* **Ashes:** The final area, where you fight Gael in vanilla. + +## Detailed Location Descriptions + +These location descriptions were originally written by [Matt Gruen] for [the +static _Dark Souls III_ randomizer]. + +[Matt Gruen]: https://thefifthmatt.com/ +[the static _Dark Souls III_ randomizer]: https://www.nexusmods.com/darksouls3/mods/361 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Location nameDetailed description
AL: Aldrich Faithful - water reserves, talk to McDonnelGiven by Archdeacon McDonnel in Water Reserves.
AL: Aldrich's Ruby - dark cathedral, minibossDropped by the Deep Accursed who drops down when you open the Anor Londo Cathedral shortcut
AL: Anri's Straight Sword - Anri questDropped by Anri of Astora upon death or completing quest. In the Darkmoon Tomb with Lord of Hollows route, or given by Ludleth if summoned to defeat Aldrich.
AL: Blade of the Darkmoon - Yorshka with Darkmoon LoyaltyGiven by Yorshka after learning the Darkmoon Loyalty gesture from Sirris, or by killing her
AL: Brass Armor - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Gauntlets - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Helm - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Brass Leggings - tombBehind the illusory statue in the hallway leading to the Darkmoon Tomb
AL: Chameleon - tomb after marrying AnriDropped by the Stone-humped Hag assassin after Anri reaches the Church of Yorshka, either in the church or after marrying Anri
AL: Cinders of a Lord - AldrichDropped by Aldrich
AL: Crescent Moon Sword - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Dark Stoneplate Ring - by dark stairs up from plazaAfter the Pontiff fight, in the dark hallways to the left of the area with the Giant Slaves
AL: Deep Gem - water reservesIn the open in the Water Reserves
AL: Dragonslayer Greatarrow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Dragonslayer Greatbow - drop from nearest buttressDropping down from about halfway down the flying buttress closest to the entrance to the Darkmoon Tomb
AL: Drang Twinspears - plaza, NPC dropDropped by Drang Twinspears-wielding knight on the stairs leading up to the Anor Londo Silver Knights
AL: Easterner's Ashes - below top of furthest buttressDropping down from the rightmost flying buttress, or the rightmost set of stairs
AL: Ember - plaza, furtherAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Ember - plaza, right sideAfter the Pontiff fight, next to one of the Giant Slaves on the right side
AL: Ember - spiral staircase, bottomNext to the lever that summons the rotating Anor Londo stairs at the bottom
AL: Estus Shard - dark cathedral, by left stairsIn a chest on the floor of the Anor Londo cathedral
AL: Giant's Coal - by giant near dark cathedralOn the Giant Blacksmith's corpse in Anor Londo
AL: Golden Ritual Spear - light cathedral, mimic upstairsDrop from a mimic in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Havel's Ring+2 - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Human Dregs - water reservesIn the open in the Water Reserves
AL: Large Soul of a Weary Warrior - left of dark cathedral entranceIn front of the Anor Londo cathedral, slightly to the left
AL: Large Titanite Shard - balcony by dead giantsAfter the Pontiff fight, on the balcony to the right of the area with the Giant Slaves
AL: Large Titanite Shard - bottom of the furthest buttressAt the base of the rightmost flying buttress leading up to Anor Londo
AL: Large Titanite Shard - bottom of the nearest buttressOn the tower leading back from Anor Londo to the shortcut to Irithyll, down the flying buttress closest to the Darkmoon Tomb entrance.
AL: Large Titanite Shard - right after light cathedralAfter Pontiff's cathedral, hugging the wall to the right
AL: Large Titanite Shard - walkway, side path by cathedralAfter the Pontiff fight, going back from the Deacons area to the original cathedral, before a dropdown
AL: Moonlight Arrow - dark cathedral, up right stairsIn the Anor Londo cathedral, up the stairs on the right side
AL: Painting Guardian Gloves - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Gown - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Hood - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian Waistcloth - prison tower, raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Painting Guardian's Curved Sword - prison tower raftersOn the rafters dropping down from Yorshka's Prison Tower to the Church of Yorshka
AL: Proof of a Concord Kept - dark cathedral, up left stairsIn the Anor Londo cathedral, halfway down the stairs on the left side next to some Deacons
AL: Reversal Ring - tomb, chest in cornerIn a chest in Darkmoon Tomb
AL: Ring of Favor - water reserves, both minibossesDropped after killing both of Sulyvahn's Beasts in the Water Reserves
AL: Ring of Favor+1 - light cathedral, upstairsIn the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Silver Mask - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Simple Gem - light cathedral, lizard upstairsDropped by a Crystal Lizard in the higher levels of Pontiff's cathedral, accessible from the Deacons after the Pontiff fight
AL: Soul of AldrichDropped by Aldrich
AL: Soul of Rosaria - Leonhard dropDrop by Ringfinger Leonhard upon death. Includes Soul of Rosaria if invaded in Anor Londo.
AL: Soul of a Crestfallen Knight - right of dark cathedral entranceTo the right of the Anor Londo cathedral entrance, past the red-eyed Silver Knight
AL: Soul of a Weary Warrior - plaza, nearerAfter the Pontiff fight, in the middle of the area with the Giant Slaves
AL: Sun Princess Ring - dark cathedral, after bossIn the Anor Londo cathedral after defeating Aldrich, up the elevators in Gwynevere's Chamber
AL: Titanite Scale - top of ladder up to buttressesOn the platform after the stairs leading up to Anor Londo from the Water Reserves building
AL: Twinkling Titanite - lizard after light cathedral #1Dropped a Crystal Lizard straight after the Pontiff fight
AL: Twinkling Titanite - lizard after light cathedral #2Dropped a Crystal Lizard straight after the Pontiff fight
AL: Yorshka's Chime - kill YorshkaDropped by Yorshka upon death.
AP: Ancient Dragon Greatshield - intro, on archwayAfter the Archdragon Peak bonfire, on top of the arch in front of the Ancient Wyvern fight
AP: Calamity Ring - mausoleum, gesture at altarReceived using Path of the Dragon at the Altar by the Mausoleum bonfire
AP: Covetous Gold Serpent Ring+2 - plazaIn the Nameless King boss arena after he is defeated
AP: Dragon Chaser's Ashes - summit, side pathIn the run-up to the Dragon Altar after the Belfry bonfire, in a side path to the left side
AP: Dragon Head Stone - fort, boss dropDropped by Ancient Wyvern
AP: Dragon Tooth - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Dragonslayer Armor - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Gauntlets - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Helm - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Leggings - plazaIn the Nameless King boss arena after he is defeated
AP: Dragonslayer Spear - gate after mausoleumIn the gate connecting the Dragon-Kin Mausoleum area to the bridge where the Nameless King fight takes place
AP: Drakeblood Greatsword - mausoleum, NPC dropDropped by the Drakeblood Knight summoned by the Serpent-Man Summoner
AP: Dung Pie - fort, landing after second roomOn a landing going up the stairs from the Ancient Wyvern to the chainaxe Man-Serpent area
AP: Ember - belfry, below bellIn the area below the bell lever, either dropping down near the lever or going down the stairs from the open fountain area after the Belfry bonfire
AP: Ember - fort overlook #1From the right of where Ancient Wyvern first lands
AP: Ember - fort overlook #2From the right of where Ancient Wyvern first lands
AP: Ember - intro, by bonfireNext to the Archdragon Peak bonfire
AP: Great Magic Barrier - drop off belfry roofDropping down to the left from the area with the Havel Knight and the dead Wyvern
AP: Havel's Greatshield - belfry roof, NPC dropDropped from any of the Havel Knights
AP: Havel's Ring+1 - summit, after buildingJust past the building with all of the Man-Serpents on the way to the Dragon Altar, on the left side
AP: Homeward Bone - intro, path to bonfireFrom the start of the area, along the left path leading to the first bonfire
AP: Large Soul of a Crestfallen Knight - summit, by fountainIn the middle of the open fountain area after the Belfry bonfire
AP: Large Soul of a Nameless Soldier - fort, by stairs to first roomto the left of where the Ancient Wyvern lands
AP: Large Soul of a Weary Warrior - fort, centerWhere the Ancient Wyvern lands
AP: Lightning Bolt - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Lightning Clutch Ring - intro, left of boss doorTo the left of gate leading to Ancient Wyvern, past the Rock Lizard
AP: Lightning Gem - intro, side riseFrom the start of the area, up a ledge in between two forked paths toward the first bonfire
AP: Lightning Urn - fort, left of first room entranceOn the path to the left of where the Ancient Wyvern lands, left of the building entrance
AP: Ricard's Rapier - belfry, NPC dropDropped by the Richard Champion summoned by the Serpent-Man Summoner
AP: Ring of Steel Protection - fort overlook, beside stairsTo the right of the area where the Ancient Wyvern lands, dropping down onto the ledge
AP: Soul of a Crestfallen Knight - mausoleum, upstairsFrom the Mausoleum bonfire, up the second set of stairs to the right
AP: Soul of a Nameless Soldier - intro, right before archwayFrom the Archdragon Peak bonfire, going right before the arch before Ancient Wyvern
AP: Soul of a Weary Warrior - intro, first cliff edgeAt the very start of the area on the left cliff edge
AP: Soul of a Weary Warrior - walkway, building windowOn the way to the Belfry bonfire after the sagging wooden bridge, on a ledge visible in a room with a Crystal Lizard, accessible by a tricky jump or just going around the other side
AP: Soul of the Nameless KingDropped by Nameless King
AP: Stalk Dung Pie - fort overlookFrom the right of where Ancient Wyvern first lands
AP: Thunder Stoneplate Ring - walkway, up ladderAfter the long hallway after the Mausoleum bonfire, before the rope bridge, up the long ladder
AP: Titanite Chunk - fort, second room balconyAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left
AP: Titanite Chunk - intro, archway cornerFrom the Archdragon Peak bonfire, under the arch, immediately to the right
AP: Titanite Chunk - intro, behind rockAlmost at the Archdragon Peak bonfire, behind a rock in the area with many Man-Serpents
AP: Titanite Chunk - intro, left before archwayAfter the Archdragon Peak bonfire, going left before the arch before Ancient Wyvern
AP: Titanite Chunk - rotundaOn top of the ruined dome found going up spiral stairs to the left before the bridge with the chainaxe Man-Serpent
AP: Titanite Chunk - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Scale - mausoleum, downstairs balcony #1From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, downstairs balcony #2From the Mausoleum bonfire, up the stairs to the left, past the Rock Lizard
AP: Titanite Scale - mausoleum, upstairs balconyFrom the Mausoleum bonfire, up the first stairs to the right, going around toward the Man-Serpent Summoner, on the balcony on the side
AP: Titanite Scale - walkway buildingIn a chest after the sagging wooden bridge on the way to the Belfry, in the building with the Crystal Lizard
AP: Titanite Scale - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
AP: Titanite Slab - belfry roofNext to the Havel Knight by the dead Wyvern
AP: Titanite Slab - plazaIn the Nameless King boss arena after he is defeated
AP: Twinkling Dragon Torso Stone - summit, gesture at altarReceived using Path of the Dragon at the Altar after the Belfry bonfire. Hawkwood also uses the gesture there when summoned.
AP: Twinkling Titanite - belfry, by ladder to roofIn the chest before the ladder climbing up to the Havel Knight
AP: Twinkling Titanite - fort, down second room balcony ladderAfter going left of where Ancient Wyvern lands and left again, rather than going up the stairs to the right, go to the open area to the left and then down the ladder
AP: Twinkling Titanite - fort, end of raftersDropping down to the left of the Mausoleum bonfire, all the way down the wooden rafters
AP: Twinkling Titanite - walkway building, lizardDropped by Crystal Lizard in the building after the sagging wooden bridge toward the Belfry
AP: Twinkling Titanite - walkway, miniboss dropDropped by the second Ancient Wyvern patrolling the path up to the Belfry
CA: Coiled Sword - boss dropDropped by Iudex Gundyr
CA: Firebomb - down the cliff edgeAlong the cliff edge before the Iudex Gundyr fight, to the right
CA: Soul of a Deserted Corpse - right of spawnAt the very start of the game
CA: Soul of an Unknown Traveler - by minibossIn the area with the Ravenous Crystal Lizard
CA: Speckled Stoneplate Ring+1 - by minibossIn the area with the Ravenous Crystal Lizard, along the right wall
CA: Titanite Scale - miniboss dropDropped by Ravenous Crystal Lizard
CA: Titanite Shard - jump to coffinMaking a jump to a coffin after the Cemetery of Ash bonfire
CC: Black Blade - tomb, mimicDropped by the mimic before Smouldering Lake
CC: Black Bug Pellet - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Bloodred Moss Clump - atrium lower, down more stairsTo the left before going down the main stairwell in the Catacombs, past the skeleton ambush and where Anri is standing, near the Crystal Lizard
CC: Carthus Bloodring - crypt lower, end of side hallAt the very end of the Bonewheel Skeleton area
CC: Carthus Milkring - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots
CC: Carthus Pyromancy Tome - atrium lower, jump from bridgeDown the hallway to the right before going down the main stairwell in the Catacombs and through an illusory wall on the left, or making a difficult dropdown from the top-level platform
CC: Carthus Rouge - atrium upper, left after entranceTo the right after first entering the Catacombs
CC: Carthus Rouge - crypt across, cornerMaking a difficult jump between the hallway after the first Skeleton Ball and the area at the same level on the opposite side, or going up the stairs from the main hall
CC: Dark Gem - crypt lower, skeleton ball dropDropped by second Skeleton Ball after killing its sorcerer skeleton
CC: Ember - atrium, on long stairwayOn the main stairwell in Catacombs
CC: Ember - crypt lower, shortcut to cavernIn the short hallway with the level shortcut where Knight Slayer Tsorig invades
CC: Ember - crypt upper, end of hall past holeGoing right from the Catacombs bonfire, down the hall to the left, then to the right. After a hole that drops down into the Bonewheel Skeleton area.
CC: Fire Gem - cavern, lizardDropped by a Crystal Lizard found between the Catacombs main halls and the ledge overlooking the bridge you can cut down
CC: Grave Warden Pyromancy Tome - boss arenaIn Wolnir's arena, or in the back left of the room containing his bonfire if not picked up in the arena
CC: Grave Warden's Ashes - crypt across, cornerFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, on the far left side. Stairwell past the illusory wall is most direct.
CC: Homeward Bone - Irithyll bridgeFound right before the wall blocking access to Irithyll
CC: Large Soul of a Nameless Soldier - cavern, before bridgeIn the area where many many skeletons are before the bridge you can cut
CC: Large Soul of a Nameless Soldier - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Large Soul of an Unknown Traveler - crypt upper, hall middleGoing right from the Catacombs bonfire, then down the long hallway after the hallway to the left
CC: Large Titanite Shard - crypt across, middle hallFrom the Catacombs bonfire, down the stairs into the main hall and up the stairs to the other side, in a middle hallway
CC: Large Titanite Shard - crypt upper, skeleton ball hallGoing right from the Catacombs bonfire, to the end of the hallway where second Skeleton Ball rolls
CC: Large Titanite Shard - tomb lowerDown the ramp from the Fire Demon, where all the skeletons are
CC: Old Sage's Blindfold - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Pontiff's Right Eye - Irithyll bridge, miniboss dropDropped by killing Sulyvahn's Beast on the bridge to Irithyll or in the lake below
CC: Ring of Steel Protection+2 - atrium upper, drop onto pillarFrom the first bridge in Catacombs where the first skeletons are encountered, parallel to the long stairwell, walk off onto a pillar on the left side.
CC: Sharp Gem - atrium lower, right before exitDown the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of High Lord WolnirDropped by High Lord Wolnir
CC: Soul of a Demon - tomb, miniboss dropDropped by the Fire Demon before Smouldering Lake
CC: Soul of a Nameless Soldier - atrium lower, down hallAll the way down the hallway to the right before going down the main stairwell in the Catacombs
CC: Soul of a Nameless Soldier - atrium upper, up more stairsFrom the room before the Catacombs main stairwell, up the two ramps and to the end of the long hallway crossing the room
CC: Thunder Stoneplate Ring+1 - crypt upper, among potsAfter the first Skeleton Ball, in the hallway alcove with the many dark-exploding pots, behind one of the pillars
CC: Titanite Shard - atrium lower, corner by stairsTo the left before going down the main stairwell in the Catacombs, behind the pensive Carthus Cursed Sword Skeleton
CC: Titanite Shard - crypt lower, left of entranceIn the main hall after the Catacombs bonfire, down the stairs and to the left
CC: Titanite Shard - crypt lower, start of side hallIn the Bonewheel Skeleton area, on the left side under a Writhing Flesh
CC: Twinkling Titanite - atrium lower, lizard down more stairsDropped by a Crystal Lizard found to the left before going down the main stairwell in the Catacombs, past the skeleton ambush and past where Anri is standing
CC: Undead Bone Shard - crypt upper, skeleton ball dropDropped by first Skeleton Ball after killing its sorcerer skeleton
CC: Witch's Ring - tomb, hall before bonfireDown the ramp from the Fire Demon, straight down the hallway past the room with the Abandoned Tomb bonfire
CC: Yellow Bug Pellet - cavern, on overlookTo the right of the Carthus Curved Sword Skeleton overlooking the pit Horace falls into
CD: Aldrich's Sapphire - side chapel, miniboss dropDropped by the Deep Accursed
CD: Arbalest - upper roofs, end of furthest buttressBefore the rafters on the way to Rosaria, up a flying buttress, past a halberd-wielding Large Hollow Soldier to the right, and down another flying buttress to the right
CD: Archdeacon Holy Garb - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon Skirt - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Archdeacon White Crown - boss room after killing bossNear the Deacons of the Deep bonfire, found after resting at it
CD: Armor of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Astora Greatsword - graveyard, left of entranceDown one of the side paths to the left in the Reanimated Corpse area
CD: Barbed Straight Sword - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Black Eye Orb - Rosaria from Leonhard's questOn Rosaria's corpse after joining Rosaria's Fingers, exhausting Leonhard's dialogue there and reaching the Profaned Capital bonfire.
CD: Blessed Gem - upper roofs, raftersIn the rafters leading to Rosaria, guarded by a Cathedral Knight to the right
CD: Crest Shield - path, drop down by Cathedral of the Deep bonfireOn a grave near the Cathedral of the Deep bonfire, accessed by dropping down to the right
CD: Curse Ward Greatshield - by ladder from white tree to moatTaking a right after the Infested Corpse graveyard, before the shortcut ladder down to the Ravenous Crystal Lizard area
CD: Deep Braille Divine Tome - mimic by side chapelDropped by the Mimic before the room with the patrolling Cathedral Knight and Deep Accursed
CD: Deep Gem - down stairs by first elevatorComing from the room where you first see deacons, go down instead of continuing to the main cathedral room. Guarded by a pensive Cathedral Evangelist.
CD: Deep Ring - upper roofs, passive mob drop in first towerDropped by the passive Deacon on the way to Rosaria
CD: Drang Armor - main hall, eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Gauntlets - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Drang Hammers - main hall eastIn the Giant Slave muck pit leading up to Deacons, underneath the stairwell
CD: Drang Shoes - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Duel Charm - by first elevatorAfter opening the cathedral's backdoor, where the Deacon enemies are first seen, under a fountain that spouts poison
CD: Duel Charm - next to Patches in onion armorTo the right of the bridge leading to Rosaria, from the Deacons side. Patches will lower the bridge if you try to cross from this side.
CD: Ember - PatchesSold by Patches in Firelink Shrine
CD: Ember - by back doorPast the pair of Grave Wardens and the Cathedral backdoor against a wall, guarded by a greataxe-wielding Large Hollow Soldier
CD: Ember - edge of platform before bossOn the edge of the chapel before Deacons overlooking the Giant Slaves
CD: Ember - side chapel upstairs, up ladderUp a ladder and past the Cathedral Evangelist from the top level of the room with the patrolling Cathedral Knight and Deep Accursed
CD: Ember - side chapel, miniboss roomIn the room with the Deep Accursed
CD: Estus Shard - monument outside Cleansing ChapelRight outside of the Cleansing Chapel. Requires killing praying hollows.
CD: Executioner's Greatsword - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area
CD: Exploding Bolt - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Fading Soul - graveyard, far endIn an open area down one of the side paths to the left in the Reanimated Corpse area, next to the Executioner's Greatsword
CD: Gauntlets of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Helm of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Herald Armor - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Gloves - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Helm - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Herald Trousers - path, by fireGuarded by the Cathedral Evangelist after the Crystal Sage fight
CD: Heysel Pick - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Homeward Bone - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Horsehoof Ring - PatchesSold or dropped by Patches after he mentions Greirat
CD: Large Soul of an Unknown Traveler - by white tree #1In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - by white tree #2In the graveyard with the White Birch and Infested Corpses
CD: Large Soul of an Unknown Traveler - lower roofs, semicircle balconyOn the cathedral roof after climbing up the flying buttresses, on the edge of the semicircle platform balcony
CD: Large Soul of an Unknown Traveler - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Large Soul of an Unknown Traveler - main hall south, side pathDown a side path with poison-spouting fountains in the main cathedral room, accessible from the Cleansing Chapel shortcut, patrolled by a Cathedral Knight
CD: Large Soul of an Unknown Traveler - path, against outer wallFrom the Cathedral of the Deep bonfire after the Brigand, against the wall in the area with the dogs and crossbowmen
CD: Leggings of Thorns - Rosaria's Bed Chamber after killing KirkFound in Rosaria's Bed Chamber after killing Longfinger Kirk
CD: Lloyd's Sword Ring - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Maiden Gloves - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Hood - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Robe - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Maiden Skirt - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Notched Whip - Cleansing ChapelIn a corner of the Cleansing Chapel
CD: Paladin's Ashes - path, guarded by lower NPCAt the very start of the area, guarded by the Fallen Knight
CD: Pale Tongue - main hall eastIn the Giant Slave muck pit leading up to Deacons
CD: Pale Tongue - upper roofs, outdoors far endBefore the rafters on the way to Rosaria, up a flying buttress and straight right, passing a halberd-wielding Large Hollow Soldier
CD: Poisonbite Ring - moat, hall past minibossIn the pit with the Infested Corpse, accessible from the Ravenous Crystal Lizard area or from dropping down near the second Cleansing Chapel shortcut
CD: Red Bug Pellet - lower roofs, up stairs between buttressesIn the area after the cathedral roof against the wall of the cathedral, down the path from the Cathedral Evangelist.
CD: Red Bug Pellet - right of cathedral front doorsUp the stairs past the Infested Corpse graveyard and the left, toward the roof path to the right of the cathedral doors
CD: Red Sign Soapstone - passive mob drop by Rosaria's Bed ChamberDropped by passive Corpse-grub against the wall near the entrance to Rosaria's Bed Chamber
CD: Repair Powder - by white treeIn the graveyard with the White Birch and Infested Corpses
CD: Ring of Favor+2 - upper roofs, on buttressBefore the rafters on the way to Rosaria, up a flying buttress, behind a greataxe-wielding Large Hollow Soldier to the left
CD: Ring of the Evil Eye+1 - by stairs to bossBefore the stairs leading down into the Deacons fight
CD: Rosaria's Fingers - RosariaGiven by Rosaria.
CD: Rusted Coin - don't forgive PatchesGiven by Patches after not forgiving him after he lowers the bridge in Cathedral of the Deep.
CD: Rusted Coin - left of cathedral front doors, behind cratesUp the stairs past the Infested Corpse graveyard and to the left, hidden behind some crates to the left of the cathedral door
CD: Saint Bident - outside main hall south doorPast the cathedral doors guarded by the Giant Slave opposite to the Deacons fight
CD: Saint-tree Bellvine - moat, by waterIn the Infested Corpse moat beneath the Cathedral
CD: Seek Guidance - side chapel upstairsAbove the room with the patrolling Cathedral Knight and Deep Accursed, below a writhing flesh on the ceiling.
CD: Shotel - PatchesSold by Patches
CD: Small Doll - boss dropDropped by Deacons of the Deep
CD: Soul of a Nameless Soldier - ledge above main hall southOn the ledge where the Giant Slave slams his arms down
CD: Soul of a Nameless Soldier - lower roofs, side roomComing from the cathedral roof, past the three crossbowmen to the path patrolled by the halberd-wielding Large Hollow Soldier, in a room to the left with many thralls.
CD: Soul of a Nameless Soldier - main hall southIn the muck pit with the Giant Slave that can attack with his arms
CD: Soul of the Deacons of the DeepDropped by Deacons of the Deep
CD: Spider Shield - NPC drop on pathDropped by the brigand at the start of Cathedral of the Deep
CD: Spiked Shield - Kirk dropDropped by Longfinger Kirk when he invades in the cathedral central room
CD: Titanite Scale - moat, miniboss dropDropped by the Ravenous Crystal Lizard outside of the Cathedral
CD: Titanite Shard - Cleansing Chapel windowsill, by minibossOn the ledge dropping back down into Cleansing Chapel from the area with the Ravenous Crystal Lizard
CD: Titanite Shard - moat, far endBehind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Titanite Shard - moat, up a slopeUp one of the slopes in the Ravenous Crystal Lizard area
CD: Titanite Shard - outside building by white treePast the Infested Corpse graveyard to the left, hidden along the left wall of the building with the shortcut ladder and Curse Ward Greatshield
CD: Titanite Shard - path, side path by Cathedral of the Deep bonfireUp a path to the left after the Cathedral of the Deep bonfire, after the Fallen Knight and before the Brigand
CD: Twinkling Titanite - moat, lizard #1Dropped by the Crystal Lizard behind the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - moat, lizard #2Dropped by the Crystal Lizard under the cathedral near the Infested Corpse moat, going from the Ravenous Crystal Lizard
CD: Twinkling Titanite - path, lizard #1Dropped by the first Crystal Lizard after the Crystal Sage fight
CD: Twinkling Titanite - path, lizard #2Dropped by the second Crystal Lizard after the Crystal Sage fight
CD: Undead Bone Shard - gravestone by white treeIn the graveyard with the Infested Corpses, on a coffin partly hanging off of the ledge
CD: Undead Hunter Charm - lower roofs, up stairs between buttressesIn the area after the cathedral roof guarded by a Cathedral Evangelist. Can be jumped to from a flying buttress or by going around and back
CD: Winged Spear - kill PatchesDropped by Patches when killed in his own armor.
CD: Xanthous Crown - Heysel Corpse-Grub in Rosaria's Bed ChamberDropped by the Heysel Corpse-grub in Rosaria's Bed Chamber
CD: Young White Branch - by white tree #1By the White Birch tree in the Infested Corpse graveyard
CD: Young White Branch - by white tree #2By the White Birch tree in the Infested Corpse graveyard
CKG: Black Firebomb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Claw - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Dark Gem - under lone stairwayFollowing the left wall, behind the standalone set of stairs
CKG: Dragonscale Ring - shortcut, leave halfway down liftFrom the middle level of the second elevator, toward the Oceiros boss fight
CKG: Drakeblood Armor - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Gauntlets - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Helm - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Drakeblood Leggings - tomb, after killing AP mausoleum NPCOn the Drakeblood Knight after Oceiros fight, after defeating the Drakeblood Knight from the Serpent-Man Summoner
CKG: Estus Shard - balconyFrom the middle level of the first Consumed King's Gardens elevator, out the balcony and to the right
CKG: Human Pine Resin - by lone stairway bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, in a toxic pool
CKG: Human Pine Resin - toxic pool, past rotundaIn between two platforms near the middle of the garden, by a tree in a toxic pool
CKG: Magic Stoneplate Ring - mob drop before bossDropped by the Cathedral Knight closest to the Oceiros fog gate
CKG: Ring of Sacrifice - under balconyAlong the right wall of the garden, next to the first elevator building
CKG: Sage Ring+2 - balcony, drop onto rubble, jump backFrom the middle platform of the first elevator in the target, going out and dropping off to the left, and then running off onto the ruined arch behind.
CKG: Shadow Garb - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Gauntlets - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Leggings - under rotundaUnder the platform in the middle of the garden, in the toxic pool
CKG: Shadow Mask - under center platformUnder the platform in the middle of the garden, in the toxic pool
CKG: Soul of Consumed OceirosDropped by Consumed King Oceiros
CKG: Soul of a Weary Warrior - before first liftOn the path leading to the first elevator from Lothric Castle
CKG: Titanite Chunk - balcony, drop onto rubbleFrom the middle platform of the first elevator, dropping down to the left
CKG: Titanite Chunk - right of shortcut lift bottomOn the right side of the garden, following the wall past the entrance to the shortcut elevator building, all the way to the end
CKG: Titanite Chunk - shortcutRight inside of the shortcut door leading to Oceiros from Lothric/Dancer bonfire
CKG: Titanite Chunk - up lone stairwayFollowing the left wall of the garden, in and up the standalone set of stairs
CKG: Titanite Scale - shortcutIn the room leading to the Oceiros shortcut elevator from Lothric/Dancer, in the first floor alcove.
CKG: Titanite Scale - tomb, chest #1Chest after Oceiros fight
CKG: Titanite Scale - tomb, chest #2Chest after Oceiros fight
CKG: Wood Grain Ring+1 - by first elevator bottomBehind the first elevator going down into the garden, in the toxic pool
DH: Aquamarine Dagger - castle, up stairsUp the second flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Black Firebomb - ruins, up windmill from bonfireTo the left of the Earthen Peak Ruins bonfire, past the ruined windmill, next to many Poisonhorn bugs.
DH: Covetous Silver Serpent Ring+3 - pantry upstairs, drop downAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then dropping down after exiting the building from the last room.
DH: Desert Pyromancer Garb - ruins, by shack near cliffBehind a shack near the edge of the cliff of the area targeted by the second angel.
DH: Desert Pyromancer Gloves - swamp, far rightAfter dropping down in the poison swamp area, against the wall straight to the right.
DH: Desert Pyromancer Hood - swamp upper, tunnel endAt the end of the tunnel with Desert Pyromancy Zoey, to the right of the final branches.
DH: Desert Pyromancer Skirt - swamp right, by rootsIn the poison swamp, against a tree guarded by a few Poisonhorn bugs in the front right.
DH: Divine Blessing - library, after dropAfter the dropdown where an angel first targets you, behind you
DH: Divine Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Divine Blessing - swamp upper, building roofOn a rooftop of one of the buildings bordering the poison swamp. Can be reached by dropping down from the final tree branch and accessing the roof to the right.
DH: Ember - castle, behind spireAt the start of the area, behind a spire to the right of first drop down
DH: Ember - pantry, behind crates just before upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room end of the building to the right, up stairs past an illusory wall to the left, in the second-to-last room of the sequence, behind some crates to the left.
DH: Ember - ruins, alcove before swampIn an alcove providing cover from the second angel's projectiles, before dropping down in the poison swamp area.
DH: Ember - ruins, alcove on cliffIn the area with the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, dropping down to the right of the bonfire.
DH: Ember - shopSold by Stone-humped Hag, or in her ashes
DH: Flame Fan - swamp upper, NPC dropDropped by Desert Pyromancer Zoey
DH: Giant Door Shield - ruins, path below far shackDescending down a path from the edge of the cliff of the area targeted by the second angel, to the very end of the cliff.
DH: Great Soul Dregs - pantry upstairsAfter exiting the building with the Lothric Knights where the front crumbles, to the last room of the building to the right, up stairs past an illusory wall to the left, then all the way to the end of the last room.
DH: Harald Curved Greatsword - swamp left, under rootIn the back leftmost area of the poison swamp, underneath the tree branch leading up and out, guarded by a stationary Harald Legion Knight.
DH: Hidden Blessing - shopSold by Stone-humped Hag, or in her ashes
DH: Homeward Bone - end of path from churchImmediately before dropping into the area with the Earthen Peak Ruins bonfire, next to Gael's flag.
DH: Homeward Bone - swamp left, on rootAll the way to the end of a short path in the back leftmost area of the poison swamp, where you can plunge attack the stationary Harald Legion Knight.
DH: Large Soul of a Weary Warrior - parapets, hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, in a corner to the left.
DH: Large Soul of a Weary Warrior - swamp centerIn the middle of the poison swamp.
DH: Large Soul of a Weary Warrior - swamp, under overhangIn the cavern adjacent to the poison swamp, surrounded by a few Poisonhorn bugs.
DH: Lightning Urn - wall outside churchAfter the dropdown where an angel first targets you, against the wall on the left.
DH: Loincloth - swamp, left edgeIn the leftmost edge of the poison swamp after dropping down, guarded by 6 Poisonhorn bugs.
DH: Lothric War Banner - parapets, end of hallAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman and dropping down again, at the end of the hallway to the right.
DH: Murky Hand Scythe - library, behind bookshelvesAfter the first long drop into the building which looks like Grand Archives, to the left up the bookshelf stairs and behind the bookshelves
DH: Murky Longstaff - pantry, last roomAfter exiting the building with the Lothric Knights where the front crumbles, in the third-furthest room in the building to the right.
DH: Prism Stone - swamp upper, tunnel startNear the start of the tunnel with Desert Pyromancer Zoey.
DH: Projected Heal - parapets balconyAfter crossing the spire bridge that crashes into the building with the Lothric Knights, past Lapp's initial location, dropping down behind the murkman, against a wall in the area with the Lothric War Banner Knight and many murkmen.
DH: Purple Moss Clump - swamp shackIn the ruined shack with Poisonhorn bugs straight ahead of the dropdown into the poison swamp area.
DH: Ring of Favor+3 - swamp right, up rootUp the long branch close to the dropdown into the poison swamp area, in front of the cavern.
DH: Ring of Steel Protection+3 - ledge before churchAfter the dropdown where an angel first targets you, on an exposed edge to the left. Difficult to get without killing the angel.
DH: Rusted Coin - behind fountain after churchAfter exiting the building with the Lothric Knights where the front crumbles, behind the fountain on the right side.
DH: Rusted Gold Coin - shopSold by Stone-humped Hag, or in her ashes
DH: Siegbräu - LappGiven by Lapp after collecting the Titanite Slab in Earthen Peak Ruins, or left after Demon Princes fight, or dropped upon death if not given.
DH: Small Envoy Banner - boss dropFound in the small room after beating Demon Prince.
DH: Soul of a Crestfallen Knight - church, altarIn the building where the front crumbles, guarded by the two Lothric Knights at the front of the chapel.
DH: Soul of a Weary Warrior - castle overhangThe bait item at the start of the area which falls down with you into the ruined building below.
DH: Soul of the Demon PrinceDropped by Demon Prince
DH: Splitleaf Greatsword - shopSold by Stone-humped Hag, or in her ashes
DH: Titanite Chunk - castle, up stairsUp first flight of stairs to the left of the starting area with the murkmen, before the long drop
DH: Titanite Chunk - pantry, first roomAfter exiting the building with the Lothric Knights where the front crumbles, on a ledge in the first room of the building to the right.
DH: Titanite Chunk - path from church, by pillarBefore dropping into the area with the Earthen Peak Ruins bonfire, behind a pillar in front of a murkman pool.
DH: Titanite Chunk - ruins, by far shackIn front of a shack at the far edge of the cliff of the area targeted by the second angel. There is a shortcut dropdown to the left of the building.
DH: Titanite Chunk - ruins, path from bonfireAt the Earthen Peak Ruins bonfire, straight a bit then all the way left, near the edge of the cliff in the area targeted by the second angel.
DH: Titanite Chunk - swamp right, drop partway up rootPartway up the long branch close to the dropdown into the poison swamp area, in front of the cavern, dropping down to a branch to the left.
DH: Titanite Chunk - swamp, along buildingsAfter dropping down into the poison swamp, along the buildings on the left side.
DH: Titanite Chunk - swamp, path to upperPartway up the branch that leads out of the poison swamp, on a very exposed branch jutting out to the left.
DH: Titanite Scale - library, back of roomAfter the first long drop into the building which looks like Grand Archives, behind you at the back of the room
DH: Titanite Scale - swamp upper, drop and jump into towerAt the very end of the last tree branch before dropping down toward the Within Earthen Peak Ruins bonfire, drop down to the left instead. Make a jump into the interior of the overturned tower to the left.
DH: Titanite Slab - swamp, path under overhangDeep within the cavern adjacent to the poison swamp, to the back and then left. Alternatively, given by Lapp after exhausting dialogue near the bonfire and dying, or left after he moves on, or dropped upon death if not given.
DH: Twinkling Titanite - library, chandelierAfter the first long drop into the building which looks like Grand Archives, straight ahead hanging from a chandelier on the ground
DH: Twinkling Titanite - path after church, mob dropDropped the pilgrim responsible for the first angel encountered, below the spire bridge that forms by crashing into the building.
DH: Twinkling Titanite - ruins, alcove on cliff, mob dropDropped by the pilgrim responsible for the second angel, below the Within Earthen Peak Ruins bonfire. Can be accessed by dropping down from a cliff edge, or dropping down to the right of the bonfire.
DH: Twinkling Titanite - ruins, root near bonfireTreasure visible straight ahead of the Earthen Peak Ruins bonfire on a branch. Can be accessed by following the right wall from the bonfire until a point of access onto the branch is found.
DH: Twinkling Titanite - swamp upper, drop onto rootOn the final tree branches before dropping down toward the Within Earthen Peak Ruins bonfire, drop down on a smaller branch to the right. This loops back to the original branch.
DH: Twinkling Titanite - swamp upper, mob drop on roofDropped by the pilgrim responsible for the third angel in the swamp. Rather than heading left into the tunnel with Desert Pyromancy Zoey, go right onto a shack roof. Drop down onto a tree branch at the end, then drop down to another roof.
FK: Antiquated Dress - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Gloves - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Antiquated Skirt - hidden caveIn a chest in the cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Atonement - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Black Bow of Pharis - miniboss drop, by keep ruins near wallDropped the Elder Ghru on the left side of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Black Bug Pellet - perimeter, hill by boss doorOn the small hill to the right of the Abyss Watchers entrance, guarded by a spear-wielding Ghru Grunt
FK: Cinders of a Lord - Abyss WatcherDropped by Abyss Watchers
FK: Crown of Dusk - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Dark Stoneplate Ring+2 - keep ruins ritual island, behind wallHidden behind the right wall of the ritual fire before Keep Ruins
FK: Dragon Crest Shield - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Dreamchaser's Ashes - keep proper, illusory wallNear the Old Wolf of Farron bonfire, behind an illusory wall near the Crystal Lizard
FK: Ember - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Ember - perimeter, path to bossGuarded by a spear-wielding Ghru Grunt to the right of the main path leading up to Abyss Watchers
FK: Ember - upper keep, by miniboss #1Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Ember - upper keep, by miniboss #2Guarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Estus Shard - between Farron Keep bonfire and left islandStraight ahead from the Farron Keep bonfire to the ritual fire stairs, guarded by a slug
FK: Gold Pine Bundle - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Golden Scroll - hidden caveIn a cave found along the keep wall in the basilisk area, with the Elizabeth corpse
FK: Great Magic Weapon - perimeter, by door to Road of SacrificesNext to the shortcut leading from Farron Keep Perimeter back into Crucifixion Woods, past the Ravenous Crystal Lizard
FK: Greataxe - upper keep, by minibossGuarded by Stray Demon, up from the Old Wolf of Farron bonfire
FK: Greatsword - ramp by keep ruins ritual islandIn the middle of the swamp, on the pair of long ramps furthest from the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Havel's Armor - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Gauntlets - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Helm - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Havel's Leggings - upper keep, after killing AP belfry roof NPCAppears by Stray Demon, up from the Old Wolf of Farron bonfire, after the Havel Knight guarding the Titanite Slab in Archdragon Peak has been killed
FK: Heavy Gem - upper keep, lizard on stairsDropped by the Crystal Lizard that scurries up the stairs in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Hollow Gem - perimeter, drop down into swampDropping down from the Farron Keep Perimeter building, to the right past the bonfire, before the stairs going up
FK: Homeward Bone - right island, behind fireBehind the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Iron Flesh - Farron Keep bonfire, right after exitIn the open in the swamp, heading straight right from Farron Keep bonfire
FK: Large Soul of a Nameless Soldier - corner of keep and right islandHidden in a corner to the right of the stairs leading up to the ritual fire from the basilisk area
FK: Large Soul of a Nameless Soldier - near wall by right islandTo the left of the stairs leading up to the ritual fire from the Basilisk area, by the keep wall
FK: Large Soul of an Unknown Traveler - by white treeOn a tree close to the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Large Titanite Shard - upper keep, lizard by wyvernDropped by the farther Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Large Titanite Shard - upper keep, lizard in openDropped by the closer Crystal Lizard in the area dropping down from near Stray Demon, up from Old Wolf of Farron bonfire
FK: Lightning Spear - upper keep, far side of the wallUp the elevator from Old Wolf of Farron bonfire, and dropping down to Crystal Lizard area, in the open.
FK: Lingering Dragoncrest Ring - by white tree, miniboss dropDropped by the Greater Crab patrolling the birch tree where the Giant shoots arrows
FK: Magic Stoneplate Ring+1 - between right island and wallBehind a tree in the basilisk area, heading directly right from Farron Keep bonfire
FK: Manikin Claws - Londor Pale Shade dropDropped by Londor Pale Shade when he invades near the basilisks, if Yoel or Yuria have been betrayed
FK: Nameless Knight Armor - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Gauntlets - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Helm - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Nameless Knight Leggings - corner of keep and right islandFrom the Keep Ruins bonfire to the ritual fire stairs patrolled by Elder Ghrus, along the edge of the ritual fire hill
FK: Pharis's Hat - miniboss drop, by keep ruins near wallDropped the Elder Ghru in the back of the group of three to the left of the Keep Ruins bonfire, as approached from the ritual fire.
FK: Poison Gem - near wall by keep ruins bridgeFrom the left of the bridge leading from the ritual fire to the Keep Ruins bonfire, guarded by the three Elder Ghru
FK: Prism Stone - by left island stairsOn an island to the left of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Purple Moss Clump - Farron Keep bonfire, around right cornerAlong the inner wall of the keep, making an immediate right from Farron Keep bonfire
FK: Purple Moss Clump - keep ruins, ritual islandClose to the ritual fire before the Keep Ruins bonfire
FK: Purple Moss Clump - ramp directly in front of Farron Keep bonfireIn the middle of the swamp, on the pair of long ramps closest to the Farron Keep bonfire, going out forward and slightly right from the bonfire.
FK: Ragged Mask - Farron Keep bonfire, around left cornerAlong the inner wall of the keep, making an immediate left from Farron Keep bonfire, guarded by slugs
FK: Repair Powder - outside hidden caveAlong the keep wall in the basilisk area, outside of the cave with the Elizabeth corpse and Golden Scroll
FK: Rotten Pine Resin - left island, behind fireIn the area behind the ritual fire which is straight ahead of the Farron Keep bonfire
FK: Rotten Pine Resin - outside pavilion by left islandFrom the Farron Keep bonfire straight ahead to the pavilion guarded by the Darkwraith, just to the left of the ritual fire stairs
FK: Rusted Gold Coin - right island, behind wallHidden behind the right wall of the ritual fire with stairs guarded by Elder Ghrus/basilisks
FK: Sage's Coal - pavilion by left islandIn the pavilion guarded by a Darkwraith, straight ahead from the Farron Keep bonfire to the left of the ritual fire stairs
FK: Sage's Scroll - near wall by keep ruins bonfire islandAlong the keep inner wall, heading left from the stone doors past the crab area, surrounded by many Ghru enemies
FK: Shriving Stone - perimeter, just past stone doorsPast the stone doors, on the path leading up to Abyss Watchers by the Corvians
FK: Soul of a Nameless Soldier - by white treeNear the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Soul of a Stray Demon - upper keep, miniboss dropDropped by Stray Demon on the bridge above Farron Keep
FK: Soul of the Blood of the WolfDropped by Abyss Watchers
FK: Stone Parma - near wall by left islandAlong the inner wall of the keep, making a left from Farron Keep bonfire but before the area with the Darkwraith, guarded by a slug
FK: Sunlight Talisman - estus soup island, by ladder to keep properBy the pot of estus soup to the left of the stairs leading up to Old Wolf of Farron
FK: Titanite Scale - perimeter, miniboss dropDropped by Ravenous Crystal Lizard near the shortcut from Farron Keep back to Road of Sacrifices
FK: Titanite Shard - Farron Keep bonfire, left after exitAlong the inner wall of the keep, making a left from Farron Keep bonfire, by the second group of four slugs
FK: Titanite Shard - between left island and keep ruinsIn the swamp area with the Ghru Leaper between the Keep Ruins ritual fire and ritual fire straight ahead of Farron Keep bonfire, opposite from the keep wall
FK: Titanite Shard - by keep ruins ritual island stairsBy the stairs leading up to the Keep Ruins ritual fire from the middle of the swamp
FK: Titanite Shard - by ladder to keep properIn the swamp area close to the foot of the ladder leading to Old Wolf of Farron bonfire
FK: Titanite Shard - by left island stairsIn front of the stairs leading up to the ritual fire straight ahead of the Farron Keep bonfire
FK: Titanite Shard - keep ruins bonfire island, under rampUnder the ramp leading down from the Keep Ruins bonfire
FK: Titanite Shard - swamp by right islandBehind a tree patrolled by an Elder Ghru close to the ritual fire stairs
FK: Twinkling Dragon Head Stone - Hawkwood dropDropped by Hawkwood after killing him in the Abyss Watchers arena, after running up to the altar in Archdragon Peak. Twinkling Dragon Torso Stone needs to be acquired first.
FK: Twinkling Titanite - keep proper, lizardDropped by the Crystal Lizard on the balcony behind the Old Wolf of Farron bonfire
FK: Undead Bone Shard - pavilion by keep ruins bonfire islandIn a standalone pavilion down the ramp from Keep Ruins bonfire and to the right
FK: Watchdogs of Farron - Old WolfGiven by Old Wolf of Farron.
FK: Wolf Ring+1 - keep ruins bonfire island, outside buildingTo the right of the building with the Keep Ruins bonfire, when approached from the ritual fire
FK: Wolf's Blood Swordgrass - by ladder to keep properTo the left of the ladder leading up to the Old Wolf of Farron bonfire
FK: Young White Branch - by white tree #1Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FK: Young White Branch - by white tree #2Near the swamp birch tree patrolled by the greater crab, where the Giant shoots arrows
FS: Acid Surge - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Affinity - KarlaSold by Karla after recruiting her, or in her ashes
FS: Alluring Skull - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Arstor's Spear - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Aural Decoy - OrbeckSold by Orbeck
FS: Billed Mask - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Dress - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Fire Orb - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Flame - Karla for Grave Warden TomeSold by Karla after giving her the Grave Warden Pyromancy Tome
FS: Black Gauntlets - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Iron Armor - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Gauntlets - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Helm - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Iron Leggings - shop after killing TsorigSold by Handmaid after killing Knight Slayer Tsorig in Smouldering Lake
FS: Black Leggings - Yuria after killing KFF bossDropped by Yuria upon death or quest completion.
FS: Black Serpent - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Blessed Weapon - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Blue Tearstone Ring - GreiratGiven by Greirat upon rescuing him from the High Wall cell
FS: Boulder Heave - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Bountiful Light - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Bountiful Sunlight - Ludleth for RosariaBoss weapon for Rosaria, available after Leonhard is killed
FS: Broken Straight Sword - gravestone after bossNear the grave after Iudex Gundyr fight
FS: Budding Green Blossom - shop after killing Creighton and AL bossSold by Handmaid after receiving Silvercat Ring item lot from Sirris and defeating Aldrich
FS: Bursting Fireball - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Caressing Tears - IrinaSold by Irina after recruiting her, or in her ashes
FS: Carthus Beacon - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Carthus Flame Arc - Cornyx for Carthus TomeSold by Cornyx after giving him the Carthus Pyromancy Tome
FS: Cast Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Chaos Bed Vestiges - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Chaos Storm - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Clandestine Coat - shop with Orbeck's AshesSold by Handmaid after giving Orbeck's Ashes and reloading
FS: Cleric's Candlestick - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Cracked Red Eye Orb - LeonhardGiven by Ringfinger Leonhard in Firelink Shrine after reaching Tower on the Wall bonfire
FS: Crystal Hail - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Magic Weapon - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Crystal Sage's Rapier - Ludleth for SageBoss weapon for Crystal Sage
FS: Crystal Soul Spear - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Dancer's Armor - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Crown - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Enchanted Swords - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Dancer's Gauntlets - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dancer's Leggings - shop after killing LC entry bossSold by Handmaid after defeating Dancer of the Boreal Valley
FS: Dark Blade - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Dark Edge - KarlaSold by Karla after recruiting her, or in her ashes
FS: Dark Hand - Yoel/YuriaSold by Yuria
FS: Darkdrift - Yoel/YuriaDropped by Yuria upon death or quest completion.
FS: Darkmoon Longbow - Ludleth for AldrichBoss weapon for Aldrich
FS: Dead Again - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Deep Protection - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Deep Soul - Ludleth for DeaconsBoss weapon for Deacons of the Deep
FS: Demon's Fist - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Greataxe - Ludleth for Fire DemonBoss weapon for Fire Demon
FS: Demon's Scar - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Divine Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Divine Blessing - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Dragonscale Armor - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonscale Waistcloth - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Dragonslayer Greataxe - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Greatshield - Ludleth for DragonslayerBoss weapon for Dragonslayer Armour
FS: Dragonslayer Swordspear - Ludleth for NamelessBoss weapon for Nameless King
FS: Dried Finger - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: East-West Shield - tree by shrine entranceIn a tree to the left of the Firelink Shrine entrance
FS: Eastern Armor - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Gauntlets - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Helm - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Eastern Leggings - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Elite Knight Armor - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Gauntlets - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Helm - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Elite Knight Leggings - shop after Anri questSold by Handmaid after completing Anri's questline or killing Anri
FS: Ember - Dragon Chaser's AshesSold by Handmaid after giving Dragon Chaser's Ashes
FS: Ember - Grave Warden's AshesSold by Handmaid after giving Grave Warden's Ashes
FS: Ember - GreiratSold by Greirat after recruiting him, or in his ashes
FS: Ember - Greirat from USSold by Greirat after pillaging Undead Settlement
FS: Ember - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Ember - above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
FS: Ember - path right of Firelink entranceOn a cliffside to the right of the main path leading up to Firelink Shrine, guarded by a dog
FS: Ember - shopSold by Handmaid
FS: Ember - shop for Greirat's AshesSold by Handmaid after Greirat pillages Lothric Castle and handing in ashes
FS: Embraced Armor of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Executioner Armor - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Gauntlets - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Helm - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Executioner Leggings - shop after killing HoraceSold by Handmaid after killing Horace the Hushed
FS: Exile Armor - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Gauntlets - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Leggings - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Exile Mask - shop after killing NPCs in RSSold by Handmaid after killing the exiles just before Farron Keep
FS: Faraam Helm - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
FS: Farron Dart - OrbeckSold by Orbeck
FS: Farron Dart - shopSold by Handmaid
FS: Farron Flashsword - OrbeckSold by Orbeck
FS: Farron Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Farron Hail - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Farron Ring - HawkwoodGiven by Hawkwood, or dropped upon death, after defeating Abyss Watchers.
FS: Fire Orb - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Fire Surge - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Fire Whip - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Fireball - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Firelink Armor - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Gauntlets - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Greatsword - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Firelink Helm - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firelink Leggings - shop after placing all CindersSold by Handmaid after defeating Soul of Cinder
FS: Firestorm - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Flash Sweat - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Force - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Frayed Blade - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Friede's Great Scythe - Ludleth for FriedeBoss weapon for Sister Friede
FS: Gael's Greatsword - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Gauntlets of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Gnaw - Karla for Deep Braille TomeSold by Irina or Karla after giving one the Deep Braille Divine Tome
FS: Golden Bracelets - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Golden Crown - shop after killing AP bossSold by Handmaid after defeating Nameless King
FS: Grave Key - Mortician's AshesSold by Handmaid after giving Mortician's Ashes
FS: Great Chaos Fire Orb - Cornyx for Izalith TomeSold by Cornyx after giving him Izalith Pyromancy Tome
FS: Great Combustion - CornyxSold by Cornyx after recruiting him, or in his ashes
FS: Great Farron Dart - Orbeck for Sage's ScrollSold by Orbeck after giving him the Sage's Scroll
FS: Great Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Great Soul Arrow - OrbeckSold by Orbeck
FS: Greatsword of Judgment - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Gundyr's Armor - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Gauntlets - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Halberd - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Gundyr's Helm - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Gundyr's Leggings - shop after killing UG bossSold by Handmaid after defeating Champion Gundyr
FS: Havel's Ring - Ludleth for Stray DemonBoss weapon for Stray Demon
FS: Hawkwood's Shield - gravestone after Hawkwood leavesLeft by Hawkwood after defeating Abyss Watchers, Curse-Rotted Greatwood, Deacons of the Deep, and Crystal Sage
FS: Hawkwood's Swordgrass - Andre after gesture in AP summitGiven by Andre after praying at the Dragon Altar in Archdragon Peak, after acquiring Twinkling Dragon Torso Stone.
FS: Heal - IrinaSold by Irina after recruiting her, or in her ashes
FS: Heal Aid - shopSold by Handmaid
FS: Heavy Soul Arrow - OrbeckSold by Orbeck
FS: Heavy Soul Arrow - Yoel/YuriaSold by Yoel/Yuria
FS: Helm of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Hidden Blessing - Dreamchaser's AshesSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Hidden Blessing - Patches after searching GASold by Handmaid after giving Dreamchaser's Ashes, saying where they were found
FS: Hidden Body - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hidden Weapon - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Hollowslayer Greatsword - Ludleth for GreatwoodBoss weapon for Curse-Rotted Greatwood
FS: Homeward - IrinaSold by Irina after recruiting her, or in her ashes
FS: Homeward Bone - cliff edge after bossAlong the cliff edge straight ahead of the Iudex Gundyr fight
FS: Homeward Bone - path above shrine entranceTo the right of the Firelink Shrine entrance, up a slope and before the ledge on top of a coffin
FS: Homing Crystal Soulmass - Orbeck for Crystal ScrollSold by Orbeck after giving him the Crystal Scroll
FS: Homing Soulmass - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Karla's Coat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Coat - kill KarlaDropped from Karla upon death
FS: Karla's Gloves - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Gloves - kill KarlaDropped from Karla upon death
FS: Karla's Pointed Hat - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Pointed Hat - kill KarlaDropped from Karla upon death
FS: Karla's Trousers - Prisoner Chief's AshesSold by Handmaid after giving Prisoner Chief's Ashes
FS: Karla's Trousers - kill KarlaDropped from Karla upon death
FS: Leggings of Favor - shop after killing water reserve minibossesSold by Handmaid after killing Sulyvahn's Beasts in Water Reserve
FS: Leonhard's Garb - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Gauntlets - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Leonhard's Trousers - shop after killing LeonhardSold by Handmaid after killing Leonhard
FS: Life Ring - Dreamchaser's AshesSold by Handmaid after giving Dreamchaser's Ashes
FS: Lifehunt Scythe - Ludleth for AldrichBoss weapon for Aldrich
FS: Lift Chamber Key - LeonhardGiven by Ringfinger Leonhard after acquiring a Pale Tongue.
FS: Lightning Storm - Ludleth for NamelessBoss weapon for Nameless King
FS: Lloyd's Shield Ring - Paladin's AshesSold by Handmaid after giving Paladin's Ashes
FS: Londor Braille Divine Tome - Yoel/YuriaSold by Yuria
FS: Lorian's Armor - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Gauntlets - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Greatsword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Lorian's Helm - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lorian's Leggings - shop after killing GA bossSold by Handmaid after defeating Lothric, Younger Prince
FS: Lothric's Holy Sword - Ludleth for PrincesBoss weapon for Twin Princes
FS: Magic Barrier - Irina for Tome of LothricSold by Irina after giving her the Braille Divine Tome of Lothric
FS: Magic Shield - OrbeckSold by Orbeck
FS: Magic Shield - Yoel/YuriaSold by Yoel/Yuria
FS: Magic Weapon - OrbeckSold by Orbeck
FS: Magic Weapon - Yoel/YuriaSold by Yoel/Yuria
FS: Mail Breaker - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Master's Attire - NPC dropDropped by Sword Master
FS: Master's Gloves - NPC dropDropped by Sword Master
FS: Med Heal - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Millwood Knight Armor - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Gauntlets - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Helm - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Millwood Knight Leggings - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Moaning Shield - EygonDropped by Eygon of Carim
FS: Moonlight Greatsword - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: Morion Blade - Yuria for Orbeck's AshesGiven by Yuria after giving Orbeck's Ashes after she asks you to assassinate him, after he moves to Firelink Shrine. Can be done without killing Orbeck, by completing his questline.
FS: Morne's Armor - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Gauntlets - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Great Hammer - EygonDropped by Eygon of Carim
FS: Morne's Helm - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Morne's Leggings - shop after killing Eygon or LC bossSold by Handmaid after killing Eygon of Carim or defeating Dragonslayer Armour
FS: Old King's Great Hammer - Ludleth for Old Demon KingBoss weapon for Old Demon King
FS: Old Moonlight - Ludleth for MidirBoss weapon for Darkeater Midir
FS: Ordained Dress - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Hood - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Ordained Trousers - shop after killing PW2 bossSold by Handmaid after defeating Sister Friede
FS: Pale Shade Gloves - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Robe - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pale Shade Trousers - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Pestilent Mist - Orbeck for any scrollSold by Orbeck after giving him any scroll
FS: Poison Mist - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Pontiff's Left Eye - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Prisoner's Chain - Ludleth for ChampionBoss weapon for Champion Gundyr
FS: Profaned Greatsword - Ludleth for PontiffBoss weapon for Pontiff Sulyvahn
FS: Profuse Sweat - Cornyx for Great Swamp TomeSold by Cornyx after giving him the Great Swamp Pyromancy Tome
FS: Rapport - Karla for Quelana TomeSold by Karla after giving her the Quelana Pyromancy Tome
FS: Refined Gem - Captain's AshesSold by Handmaid after giving Captain's Ashes
FS: Repair - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Repeating Crossbow - Ludleth for GaelBoss weapon for Slave Knight Gael
FS: Replenishment - IrinaSold by Irina after recruiting her, or in her ashes
FS: Ring of Sacrifice - Yuria shopSold by Yuria, or by Handmaid after giving Hollow's Ashes
FS: Rose of Ariandel - Ludleth for FriedeBoss weapon for Sister Friede
FS: Rusted Gold Coin - don't forgive PatchesGiven by Patches after not forgiving him after he locks you in the Bell Tower.
FS: Sage's Big Hat - shop after killing RS bossSold by Handmaid after defeating Crystal Sage
FS: Saint's Ring - IrinaSold by Irina after recruiting her, or in her ashes
FS: Seething Chaos - Ludleth for Demon PrinceBoss weapon for Demon Prince
FS: Silvercat Ring - Sirris for killing CreightonGiven by Sirris talking to her in Firelink Shrine after invading and vanquishing Creighton.
FS: Skull Ring - kill LudlethDropped by Ludleth upon death, including after placing all cinders. Note that if killed before giving Transposing Kiln, transposition is not possible.
FS: Slumbering Dragoncrest Ring - Orbeck for buying four specific spellsGiven by Orbeck after purchasing the shop items corresponding to Aural Decoy, Farron Flashsword, Spook (starting items), and Pestilent Mist (after giving one scroll).
FS: Smough's Armor - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Gauntlets - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Helm - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Smough's Leggings - shop after killing AL bossSold by Handmaid after defeating Alrich, Devourer of Gods
FS: Sneering Mask - Yoel's room, kill Londor Pale Shade twiceIn Yoel/Yuria's area after defeating both Londor Pale Shade invasions
FS: Soothing Sunlight - Ludleth for DancerBoss weapon for Dancer of the Boreal Valley
FS: Soul Arrow - OrbeckSold by Orbeck
FS: Soul Arrow - Yoel/YuriaSold by Yoel/Yuria
FS: Soul Arrow - shopSold by Handmaid
FS: Soul Greatsword - OrbeckSold by Orbeck
FS: Soul Greatsword - Yoel/YuriaSold by Yoel/Yuria after using Draw Out True Strength
FS: Soul Spear - Orbeck for Logan's ScrollSold by Orbeck after giving him Logan's Scroll
FS: Soul of a Deserted Corpse - bell tower doorNext to the door requiring the Tower Key
FS: Spook - OrbeckSold by Orbeck
FS: Storm Curved Sword - Ludleth for NamelessBoss weapon for Nameless King
FS: Sunless Armor - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Gauntlets - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Leggings - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunless Talisman - Sirris, kill GA bossDropped by Sirris on death or quest completion.
FS: Sunless Veil - shop, Sirris quest, kill GA bossSold by Handmaid after completing Sirris' questline
FS: Sunlight Spear - Ludleth for CinderBoss weapon for Soul of Cinder
FS: Sunset Shield - by grave after killing Hodrick w/SirrisLeft by Sirris upon quest completion.
FS: Tears of Denial - Irina for Tome of CarimSold by Irina after giving her the Braille Divine Tome of Carim
FS: Titanite Scale - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Titanite Slab - shop after placing all CindersSold by Handmaid after placing all Cinders of a Lord on their thrones
FS: Tower Key - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Twinkling Titanite - Greirat from IBVSold by Greirat after pillaging Irithyll
FS: Twisted Wall of Light - Orbeck for Golden ScrollSold by Orbeck after giving him the Golden Scroll
FS: Uchigatana - NPC dropDropped by Sword Master
FS: Undead Legion Armor - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Gauntlet - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Helm - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Undead Legion Leggings - shop after killing FK bossSold by Handmaid after defeating Abyss Watchers
FS: Untrue Dark Ring - Yoel/YuriaSold by Yuria
FS: Untrue White Ring - Yoel/YuriaSold by Yuria
FS: Vordt's Great Hammer - Ludleth for VordtBoss weapon for Vordt of the Boreal Valley
FS: Vow of Silence - Karla for Londor TomeSold by Irina or Karla after giving one the Londor Braille Divine Tome
FS: Washing Pole - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: White Dragon Breath - Ludleth for OceirosBoss weapon for Oceiros, the Consumed King
FS: White Sign Soapstone - shopSold by both Shrine Handmaid and Untended Graves Handmaid
FS: Wolf Knight's Greatsword - Ludleth for Abyss WatchersBoss weapon for Abyss Watchers
FS: Wolf Ring+2 - left of boss room exitAfter Iudex Gundyr on the left
FS: Wolnir's Crown - shop after killing CC bossSold by Handmaid after defeating High Lord Wolnir
FS: Wolnir's Holy Sword - Ludleth for WolnirBoss weapon for High Lord Wolnir
FS: Wood Grain Ring - Easterner's AshesSold by Handmaid after giving Easterner's Ashes
FS: Xanthous Gloves - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Overcoat - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Xanthous Trousers - Xanthous AshesSold by Handmaid after giving Xanthous Ashes
FS: Yhorm's Great Machete - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Yhorm's Greatshield - Ludleth for YhormBoss weapon for Yhorm the Giant
FS: Young Dragon Ring - Orbeck for one scroll and buying three spellsGiven by Orbeck after purchasing four sorceries from him, and giving him one scroll, as a non-sorcerer.
FSBT: Armor of the Sun - crow for SiegbräuTrade Siegbräu with crow
FSBT: Blessed Gem - crow for Moaning ShieldTrade Moaning Shield with crow
FSBT: Covetous Silver Serpent Ring - illusory wall past raftersFrom the Firelink Shrine roof, past the rafters and an illusory wall
FSBT: Estus Ring - tower baseDropping down from the Bell Tower to where Irina eventually resides
FSBT: Estus Shard - raftersIn the Firelink Shrine rafters, accessible from the roof
FSBT: Fire Keeper Gloves - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Robe - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Skirt - partway down towerDropping down to the left after entering the Bell Tower. Align with the center of the closest floor tile row and run off the edge at full speed, aiming slightly left.
FSBT: Fire Keeper Soul - tower topAt the top of the Bell Tower
FSBT: Hello Carving - crow for Alluring SkullTrade Alluring Skull with crow
FSBT: Help me! Carving - crow for any sacred chimeTrade any Sacred Chime with crow
FSBT: Hollow Gem - crow for EleonoraTrade Eleonora with crow
FSBT: Homeward Bone - roofOn Firelink Shrine roof
FSBT: I'm sorry Carving - crow for Shriving StoneTrade Shriving Stone with crow
FSBT: Iron Bracelets - crow for Homeward BoneTrade Homeward Bone with crow
FSBT: Iron Helm - crow for Lightning UrnTrade Lightning Urn with crow
FSBT: Iron Leggings - crow for Seed of a Giant TreeTrade Seed of a Giant Tree with crow
FSBT: Large Titanite Shard - crow for FirebombTrade Firebomb or Rope Firebomb with crow
FSBT: Lightning Gem - crow for Xanthous CrownTrade Xanthous Crown with crow
FSBT: Lucatiel's Mask - crow for Vertebra ShackleTrade Vertebra Shackle with crow
FSBT: Porcine Shield - crow for Undead Bone ShardTrade Undead Bone Shard with crow
FSBT: Ring of Sacrifice - crow for Loretta's BoneTrade Loretta's Bone with crow
FSBT: Sunlight Shield - crow for Mendicant's StaffTrade Mendicant's Staff with crow
FSBT: Thank you Carving - crow for Hidden BlessingTrade Hidden Blessing with crow
FSBT: Titanite Chunk - crow for Black FirebombTrade Black Firebomb or Rope Black Firebomb with crow
FSBT: Titanite Scale - crow for Blacksmith HammerTrade Blacksmith Hammer with crow
FSBT: Titanite Slab - crow for Coiled Sword FragmentTrade Coiled Sword Fragment with crow
FSBT: Twinkling Titanite - crow for Large Leather ShieldTrade Large Leather Shield with crow
FSBT: Twinkling Titanite - crow for Prism StoneTrade Prism Stone with crow
FSBT: Twinkling Titanite - lizard behind FirelinkDropped by the Crystal Lizard behind Firelink Shrine. Can be accessed with tree jump by going all the way around the roof, left of the entrance to the rafters, or alternatively dropping down from the Bell Tower.
FSBT: Very good! Carving - crow for Divine BlessingTrade Divine Blessing with crow
GA: Avelyn - 1F, drop from 3F onto bookshelvesOn top of a bookshelf on the Archive first floor, accessible by going halfway up the stairs to the third floor, dropping down past the Grand Archives Scholar, and then dropping down again
GA: Black Hand Armor - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
GA: Black Hand Hat - shop after killing GA NPCSold by Handmaid after killing Black Hand Kumai
GA: Blessed Gem - raftersOn the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Chaos Gem - dark room, lizardDropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Cinders of a Lord - Lothric PrinceDropped by Twin Princes
GA: Crystal Chime - 1F, path from wax poolOn the Archives first floor, in the room with the Lothric Knight, to the right
GA: Crystal Gem - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Crystal Scroll - 2F late, miniboss dropDropped by the Grand Archives Crystal Sage
GA: Divine Blessing - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Divine Pillars of Light - cage above raftersIn a cage above the rafters high above the Archives, can be accessed by dropping down from the Winged Knight roof area
GA: Ember - 5F, by entranceOn a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, on the opposite side from the wax pool
GA: Estus Shard - dome, far balconyOn the Archives roof near the three Winged Knights, in a side area overlooking the ocean.
GA: Faraam Armor - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Faraam Boots - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Faraam Gauntlets - shop after killing GA NPCSold by Handmaid after killing Lion Knight Albert
GA: Fleshbite Ring - up stairs from 4FFrom the first shortcut elevator with the movable bookshelf, past the Scholars right before going outside onto the roof, in an alcove to the right with many Clawed Curse bookshelves
GA: Golden Wing Crest Shield - outside 5F, NPC dropDropped by Lion Knight Albert before the stairs leading up to Twin Princes
GA: Heavy Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Hollow Gem - rooftops lower, in hallGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, in a tunnel underneath the ledge
GA: Homeward Bone - 2F early balconyOn the Archives second floor, on the balcony with the ladder going up to the Crystal Sage
GA: Hunter's Ring - dome, very topAt the top of the ladder in roof the area with the Winged Knights
GA: Large Soul of a Crestfallen Knight - 4F, backIn the back of a Clawed Curse-heavy corridor of bookshelves, in the area with the Grand Archives Scholars and dropdown ladder, after the first shortcut elevator with the movable bookshelf
GA: Large Soul of a Crestfallen Knight - outside 5FIn the middle of the area with the three human NPCs attacking you, before the Grand Archives bonfire shortcut elevator
GA: Lingering Dragoncrest Ring+2 - dome, room behind spireNear the tower with the Winged Knights, up the stairs on the opposite side from the ladder leading up to the Hunter's Ring
GA: Onikiri and Ubadachi - outside 5F, NPC dropDropped by Black Hand Kamui before the stairs leading up to Twin Princes
GA: Outrider Knight Armor - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Gauntlets - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Helm - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Outrider Knight Leggings - 3F, behind illusory wall, miniboss dropDropped by an Outrider Knight past the Crystal Sage's third floor location and an illusory wall
GA: Power Within - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
GA: Refined Gem - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Sage Ring+1 - rafters, second level downOn the rafters high above the Grand Archives, dropping down from the cage to the high rafters to the rafters below with the Corpse-grub
GA: Sage's Crystal Staff - outside 5F, NPC dropDropped by Daughter of Crystal Kriemhild before the stairs leading up to Twin Princes
GA: Scholar Ring - 2F, between late and earlyOn the corpse of a sitting Archives Scholar between two bookshelves, accessible by activating a lever before crossing the bridge that is the Crystal Sage's final location
GA: Sharp Gem - rooftops, lizardDropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Shriving Stone - 2F late, by ladder from 3FGoing from the Crystal Sage's location on the third floor to its location on the bridge, after descending the ladder
GA: Soul Stream - 3F, behind illusory wallPast the Crystal Sage's third floor location, an illusory wall, and an Outrider Knight, on the corpse of a sitting Archives Scholar
GA: Soul of a Crestfallen Knight - 1F, loop left after dropOn the Archives first floor, hugging the left wall, on a ledge that loops back around to the left wall
GA: Soul of a Crestfallen Knight - path to domeOn balcony of the building with the second shortcut elevator down to the bonfire, accessible by going up the spiral stairs to the left
GA: Soul of a Nameless Soldier - dark roomOn the Archives first floor, after the wax pool, against a Clawed Curse bookshelf
GA: Soul of a Weary Warrior - rooftops, by lizardsOn the Archives roof, going up the first rooftop slope where a Gargoyle always attacks you
GA: Soul of the Twin PrincesDropped by Twin Princes
GA: Titanite Chunk - 1F, balconyOn the Archives first floor, on balcony overlooking the entrance opposite from the Grand Archives Scholars wax pool
GA: Titanite Chunk - 1F, path from wax poolOn the Archives first floor, toward the Lothric Knight, turning right to a ledge leading back to the entrance area
GA: Titanite Chunk - 1F, up right stairsGoing right after entering the Archives entrance and up the short flight of stairs
GA: Titanite Chunk - 2F, by wax poolUp the stairs from the Archives second floor on the right side from the entrance, in a corner near the small wax pool
GA: Titanite Chunk - 2F, right after dark roomExiting from the dark room with the Crystal Lizards on the first floor onto the second floor main room, then taking an immediate right
GA: Titanite Chunk - 5F, far balconyOn a balcony outside where Lothric Knight stands on the top floor of the Archives, accessing by going right from the final wax pool or by dropping down from the gargoyle area
GA: Titanite Chunk - rooftops, balconyGoing onto the roof and down the first ladder, all the way down the ledge facing the ocean to the right
GA: Titanite Chunk - rooftops lower, ledge by buttressGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, on a roof ledge to the right
GA: Titanite Chunk - rooftops, just before 5FOn the Archives roof, after a short dropdown, in the small area where the two Gargoyles attack you
GA: Titanite Scale - 1F, drop from 2F late onto bookshelves, lizardDropped by a Crystal Lizard on first floor bookshelves. Can be accessed by dropping down to the left at the end of the bridge which is the Crystal Sage's final location
GA: Titanite Scale - 1F, up stairs on bookshelfOn the Archives first floor, up a movable set of stairs near the large wax pool, on top of a bookshelf
GA: Titanite Scale - 2F, titanite scale atop bookshelfOn top of a bookshelf on the Archive second floor, accessible by going halfway up the stairs to the third floor and dropping down near a Grand Archives Scholar
GA: Titanite Scale - 3F, by ladder to 2F lateGoing from the Crystal Sage's location on the third floor to its location on the bridge, on the left side of the ladder you descend, behind a table
GA: Titanite Scale - 3F, corner up stairsFrom the Grand Archives third floor up past the thralls, in a corner with bookshelves to the left
GA: Titanite Scale - 5F, chest by exitIn a chest after the first elevator shortcut with the movable bookshelf, in the area with the Grand Archives Scholars, to the left of the stairwell leading up to the roof
GA: Titanite Scale - dark room, upstairsRight after going up the stairs to the Archives second floor, on the left guarded by a Grand Archives Scholar and a sequence of Clawed Curse bookshelves
GA: Titanite Scale - rooftops lower, path to 2FGoing onto the roof and down the first ladder, dropping down on either side from the ledge facing the ocean, then going past the corvians all the way to the left and making a jump
GA: Titanite Slab - 1F, after pulling 2F switchIn a chest on the Archives first floor, behind a bookshelf moved by pulling a lever in the middle of the second floor between two cursed bookshelves
GA: Titanite Slab - dome, kill all mobsDropped by killing all three Winged Knights on top of the Archives
GA: Titanite Slab - final elevator secretAt the bottom of the shortcut elevator right outside the Twin Princes fight. Requires sending the elevator up to the top from the middle, and then riding the lower elevator down.
GA: Twinkling Titanite - 1F, lizard by dropDropped by the Crystal Lizard on the Archives first floor along the left wall
GA: Twinkling Titanite - 2F, lizard by entranceDropped by the Crystal Lizard on the Archives second floor, going toward the stairs/balcony
GA: Twinkling Titanite - dark room, lizard #1Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - dark room, lizard #2Dropped by a Crystal Lizard on the Archives first floor in the dark room past the large wax pool
GA: Twinkling Titanite - rafters, down lower level ladderIn a chest reachable after dropping down from the Archives rafters and down a ladder near the Corpse-grub
GA: Twinkling Titanite - rooftops, lizard #1Dropped by one of the pair of Crystal Lizards, on the right side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - rooftops, lizard #2Dropped by one of the pair of Crystal Lizards, on the left side, found going up a slope past the gargoyle on the Archives roof
GA: Twinkling Titanite - up stairs from 4F, lizardDropped by a Crystal Lizard found heading from the first elevator shortcut with the movable bookshelf, on the right side up the stairs before exiting to the roof
GA: Undead Bone Shard - 5F, by entranceOn the corpse of a sitting Archives Scholar on a balcony high in the Archives overlooking the area with the Grand Archives Scholars with a shortcut ladder, near the final wax pool
GA: Witch's Locks - dark room, behind retractable bookshelfBehind a bookshelf in the dark room with the Crystal Lizards, moved by a lever in the same room
HWL: Astora Straight Sword - fort walkway, drop downIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel, dropping down past the edge
HWL: Basin of Vows - EmmaDropped by Emma upon killing her. This is possible to do at any time
HWL: Battle Axe - flame tower, mimicDropped by mimic in the building guarded by the fire-breathing wyvern
HWL: Binoculars - corpse tower, upper platformIn the area with the dead wyvern, at the top of a set of stairs past a Hollow Soldier
HWL: Black Firebomb - small roof over fountainAfter roof with Pus of Man, on the edge of another rooftop to the left where you can drop down into Winged Knight area
HWL: Broadsword - fort, room off walkwayIn the building with the Pus of Man on the roof, past the Lothric Knight in an alcove to the left
HWL: Cell Key - fort ground, down stairsIn the basement of the building with Pus of Man on the roof, down the stairs guarded by a dog
HWL: Claymore - flame plazaIn the area where the wyvern breathes fire, farthest away from the door
HWL: Club - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Ember - flame plazaIn the area where the wyvern breathes fire, in the open
HWL: Ember - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Ember - fountain #1In the area with the Winged Knight
HWL: Ember - fountain #2In the area with the Winged Knight
HWL: Estus Shard - fort ground, on anvilIn the basement of the building with the Pus of Man on the roof, on the blacksmith anvil
HWL: Firebomb - corpse tower, under tableIn the building near the dead wyvern, behind a table near the ladder you descend
HWL: Firebomb - fort roofNext to the Pus of Man on the roof
HWL: Firebomb - top of ladder to fountainBy the long ladder leading down to the area with the Winged Knight
HWL: Firebomb - wall tower, beamIn the building with the Tower on the Wall bonfire, on a wooden beam overhanging the lower levels
HWL: Fleshbite Ring+1 - fort roof, jump to other roofJumping from the roof with the Pus of Man to a nearby building with a fenced roof
HWL: Gold Pine Resin - corpse tower, dropDropping past the dead wyvern, down the left path from the High Wall bonfire
HWL: Green Blossom - fort walkway, hall behind wheelIn the building with the Pus of Man on the roof, past the Lothric Knight down a hallway obscured by a wooden wheel
HWL: Green Blossom - shortcut, lower courtyardIn the courtyard at the bottom of the shortcut elevator
HWL: Large Soul of a Deserted Corpse - flame plazaIn the area where the wyvern breathes fire, behind one of the praying statues
HWL: Large Soul of a Deserted Corpse - fort roofOn the edge of the roof with the Pus of Man
HWL: Large Soul of a Deserted Corpse - platform by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area)
HWL: Longbow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Lucerne - promenade, side pathOn one of the side paths from the main path connecting Dancer and Vordt fights, patrolled by a Lothric Knight
HWL: Mail Breaker - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire on the roof, before Greirat's cell
HWL: Rapier - fountain, cornerIn a corner in the area with the Winged Knight
HWL: Raw Gem - fort roof, lizardDropped by the Crystal Lizard on the rooftop after the Tower on the Wall bonfire
HWL: Red Eye Orb - wall tower, minibossDropped by the Darkwraith past the Lift Chamber Key
HWL: Refined Gem - promenade minibossDropped by the red-eyed Lothric Knight to the left of the Dancer's room entrance
HWL: Ring of Sacrifice - awning by fountainComing from the elevator shortcut, on a side path to the left (toward Winged Knight area), jumping onto a wooden support
HWL: Ring of the Evil Eye+2 - fort ground, far wallIn the basement of the building with the Pus of Man on the roof, on the far wall past the stairwell, behind some barrels
HWL: Silver Eagle Kite Shield - fort mezzanineIn the chest on the balcony overlooking the basement of the building with the Pus of Man on the roof
HWL: Small Lothric Banner - EmmaGiven by Emma, or dropped upon death
HWL: Soul of Boreal Valley VordtDropped by Vordt of the Boreal Valley
HWL: Soul of a Deserted Corpse - by wall tower doorRight before the entrance to the building with the Tower on the Wall bonfire
HWL: Soul of a Deserted Corpse - corpse tower, bottom floorDown the ladder of the building near the dead wyvern, on the way to the living wyvern
HWL: Soul of a Deserted Corpse - fort entry, cornerIn the corner of the room with a Lothric Knight, with the Pus of Man on the roof
HWL: Soul of a Deserted Corpse - fountain, path to promenadeIn between the Winged Knight area and the Dancer/Vordt corridor
HWL: Soul of a Deserted Corpse - path to back tower, by lift doorWhere the Greataxe Hollow Soldier patrols outside of the elevator shortcut entrance
HWL: Soul of a Deserted Corpse - path to corpse towerAt the very start, heading left from the High Wall bonfire
HWL: Soul of a Deserted Corpse - wall tower, right of exitExiting the building with the Tower on the Wall bonfire on the roof, immediately to the right
HWL: Soul of the DancerDropped by Dancer of the Boreal Valley
HWL: Standard Arrow - back towerDown the path from the right of the High Wall bonfire, where the Pus of Man and crossbowman are
HWL: Throwing Knife - shortcut, lift topAt the top of the elevator shortcut, opposite from the one-way door
HWL: Throwing Knife - wall tower, path to GreiratIn the basement of the building with the Tower on the Wall bonfire, in the room with the explosive barrels
HWL: Titanite Shard - back tower, transforming hollowDropped by the Pus of Man on the tower to the right of the High Wall bonfire after transformation
HWL: Titanite Shard - fort ground behind cratesBehind some wooden crates in the basement of the building with the Pus of Man on the roof
HWL: Titanite Shard - fort roof, transforming hollowDropped by the Pus of Man on the roof after the Tower on the Wall bonfire after transformation
HWL: Titanite Shard - fort, room off entryIn the building with the Pus of Man on the roof, in a room to the left and up the short stairs
HWL: Titanite Shard - wall tower, corner by bonfireOn the balcony with the Tower on the Wall bonfire
HWL: Undead Hunter Charm - fort, room off entry, in potIn the building with the Pus of Man on the roof, in a room to the left, in a pot you have to break
HWL: Way of Blue - EmmaGiven by Emma or dropped upon death.
IBV: Blood Gem - descent, platform before lakeIn front of the tree in the courtyard before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Blue Bug Pellet - ascent, in last buildingIn the final building before Pontiff's cathedral, coming from the sewer, on the first floor
IBV: Blue Bug Pellet - descent, dark roomIn the dark area with the Irithyllian slaves, to the left of the staircase
IBV: Budding Green Blossom - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Chloranthy Ring+1 - plaza, behind altarIn the area before and below Pontiff's cathedral, behind the central structure
IBV: Covetous Gold Serpent Ring+1 - descent, drop after dark roomAfter the dark area with the Irithyllian slaves, drop down to the right
IBV: Creighton's Steel Mask - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Divine Blessing - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Divine Blessing - great hall, mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Dorhys' Gnawing - Dorhys dropDropped by Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches and to the left
IBV: Dragonslayer's Axe - Creighton dropFollowing Sirris' questline, dropped by Creighton the Wanderer when he invades in the graveyard after the Church of Yorshka.
IBV: Dung Pie - sewer #1In the area with the sewer centipedes
IBV: Dung Pie - sewer #2In the area with the sewer centipedes
IBV: Ember - shortcut from church to cathedralAfter the gate shortcut from Church of Yorshka to Pontiff's cathedral
IBV: Emit Force - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Excrement-covered Ashes - sewer, by stairsIn the area with the sewer centipedes, before going up the stairs to the kitchen
IBV: Fading Soul - descent, cliff edge #1In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Fading Soul - descent, cliff edge #2In the graveyard down the stairs from the Church of Yorshka, at the cliff edge
IBV: Great Heal - lake, dead Corpse-GrubOn the Corpse-grub at the edge of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake wallOn the wall of the lake leading to the Distant Manor bonfire
IBV: Green Blossom - lake, by Distant ManorIn the lake close to the Distant Manor bonfire
IBV: Green Blossom - lake, by stairs from descentGoing down the stairs into the lake leading to the Distant Manor bonfire
IBV: Homeward Bone - descent, before gravestoneIn the graveyard down the stairs from the Church of Yorshka, in front of the grave with the Corvian
IBV: Kukri - descent, side pathDown the stairs from the graveyard after Church of Yorshka, before the group of dogs in the left path
IBV: Large Soul of a Nameless Soldier - ascent, after great hallBy the tree near the stairs from the sewer leading up to Pontiff's cathedral, where the first dogs attack you
IBV: Large Soul of a Nameless Soldier - central, by bonfireBy the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - central, by second fountainNext to the fountain up the stairs from the Central Irithyll bonfire
IBV: Large Soul of a Nameless Soldier - lake islandOn an island in the lake leading to the Distant Manor bonfire
IBV: Large Soul of a Nameless Soldier - stairs to plazaOn the path from Central Irithyll bonfire, before making the left toward Church of Yorshka
IBV: Large Titanite Shard - Distant Manor, under overhangUnder overhang next to second set of stairs leading from Distant Manor bonfire
IBV: Large Titanite Shard - ascent, by elevator doorOn the path from the sewer leading up to Pontiff's cathedral, to the right of the statue surrounded by dogs
IBV: Large Titanite Shard - ascent, down ladder in last buildingOutside the final building before Pontiff's cathedral, coming from the sewer, dropping down to the left before the entrance
IBV: Large Titanite Shard - central, balcony just before plazaFrom the Central Irithyll bonfire, on the balcony with the second Fire Witch.
IBV: Large Titanite Shard - central, side path after first fountainUp the stairs from the Central Irithyll bonfire, on a railing to the right
IBV: Large Titanite Shard - great hall, main floor mob dropOne-time drop from the Silver Knight staring at the painting in Irithyll
IBV: Large Titanite Shard - great hall, upstairs mob drop #1One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - great hall, upstairs mob drop #2One-time drop from the Silver Knight on the balcony of the room with the painting
IBV: Large Titanite Shard - path to DorhysBefore the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Large Titanite Shard - plaza, balcony overlooking ascentOn the path from Central Irithyll bonfire, instead of going left toward the Church of Yorshka, going right, on the balcony
IBV: Large Titanite Shard - plaza, by stairs to churchTo the left of the stairs leading up to the Church of Yorshka from Central Irithyll
IBV: Leo Ring - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Lightning Gem - plaza centerIn the area before and below Pontiff's cathedral, in the center guarded by the enemies
IBV: Magic Clutch Ring - plaza, illusory wallIn the area before and below Pontiff's cathedral, behind an illusory wall to the right
IBV: Mirrah Chain Gloves - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Leggings - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Mirrah Chain Mail - bridge after killing CreightonFollowing Sirris' questline, found on the bridge to Irithyll after being invaded by Creighton the Wanderer in the graveyard after the Church of Yorshka.
IBV: Proof of a Concord Kept - Church of Yorshka altarAt the altar in the Church of Yorshka
IBV: Rime-blue Moss Clump - central, by bonfireBy the Central Irithyll bonfire
IBV: Rime-blue Moss Clump - central, past second fountainFrom the Central Irithyll bonfire, to the left before the first Fire Witch.
IBV: Ring of Sacrifice - lake, right of stairs from descentNear the sewer centipede at the start of the lake leading to the Distant Manor bonfire
IBV: Ring of the Evil Eye - AnriGiven by Anri of Astora in the Church of Yorshka, or if told of Horace's whereabouts in the Catacombs
IBV: Ring of the Sun's First Born - fall from in front of cathedralDropping down from in front of Pontiff Sulyvahn's church toward the Church of Yorshka
IBV: Roster of Knights - descent, first landingOn the landing going down the stairs from Church of Yorshka to the graveyard
IBV: Rusted Gold Coin - Distant Manor, drop after stairsDropping down after the first set of stairs leading from Distant Manor bonfire
IBV: Rusted Gold Coin - descent, side pathDown the stairs from the graveyard after Church of Yorshka, guarded by the group of dogs in the left path
IBV: Shriving Stone - descent, dark room raftersOn the rafters in the dark area with the Irithyllian slaves
IBV: Siegbräu - SiegwardGiven by Siegward meeting him in the Irithyll kitchen after the Sewer Centipedes.
IBV: Smough's Great Hammer - great hall, chestIn a chest up the stairs in the room with the Silver Knight staring at the painting
IBV: Soul of Pontiff SulyvahnDropped by Pontiff Sulyvahn
IBV: Soul of a Weary Warrior - ascent, by final staircaseToward the end of the path from the sewer leading up to Pontiff's cathedral, to the left of the final staircase
IBV: Soul of a Weary Warrior - central, by first fountainBy the Central Irithyll bonfire
IBV: Soul of a Weary Warrior - central, railing by first fountainOn the railing overlooking the Central Irithyll bonfire, at the very start
IBV: Soul of a Weary Warrior - plaza, side room lowerDropping down from the path from Church of Yorshka to Pontiff, guarded by the pensive Fire Witch
IBV: Soul of a Weary Warrior - plaza, side room upperIn the path from Church of Yorshka to Pontiff's cathedral, at the broken ledge you can drop down onto the Fire Witch
IBV: Twinkling Titanite - central, lizard before plazaDropped by a Crystal Lizard past the Central Irithyll Fire Witches and to the left
IBV: Twinkling Titanite - descent, lizard behind illusory wallDropped by a Crystal Lizard behind an illusory wall before going down the stairs to the lake leading to the Distant Manor bonfire
IBV: Undead Bone Shard - descent, behind gravestoneIn the graveyard down the stairs from the Church of Yorshka, behind the grave with the Corvian
IBV: Witchtree Branch - by DorhysIn the area with Cathedral Evangelist Dorhys, past an illusory railing past the Central Irithyll Fire Witches
IBV: Wood Grain Ring+2 - ascent, right after great hallLeaving the building with the Silver Knight staring at the painting, instead of going left up the stairs, go right
IBV: Yorshka's Spear - descent, dark room rafters chestIn a chest in the rafters of the dark area with the Irithyllian slaves
ID: Alva Armor - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Gauntlets - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Helm - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Alva Leggings - B3 near, by Karla's cell, after killing AlvaIn the main Jailer cell block on the floor close to Karla's cell, if the invading Alva is killed
ID: Bellowing Dragoncrest Ring - drop from B1 towards pitDropping down from the Jailbreaker's Key shortcut at the end of the top corridor on the bonfire side in Irithyll Dungeon
ID: Covetous Gold Serpent Ring - Siegward's cellIn the Old Cell where Siegward is rescued
ID: Covetous Silver Serpent Ring+1 - pit lift, middle platformOn one of the platforms in elevator shaft of the shortcut elevator from the Giant Slave area to the Irithyll Dungeon bonfire
ID: Dark Clutch Ring - stairs between pit and B3, mimicDropped by the mimic found going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs, on the left side
ID: Dragon Torso Stone - B3, outside liftOn the balcony corpse in the Path of the Dragon pose
ID: Dragonslayer Lightning Arrow - pit, mimic in hallDropped by the mimic in the side corridor from where the Giant Slave is standing, before the long ladder
ID: Dung Pie - B3, by path from pitIn the room with the Giant Hound Rats
ID: Dung Pie - pit, miniboss dropDrop from the Giant Slave
ID: Dusk Crown Ring - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
ID: Ember - B3 centerAt the center pillar in the main Jailer cell block
ID: Ember - B3 far rightIn the main Jailer cell block, on the left side coming from the Profaned Capital
ID: Estus Shard - mimic on path from B2 to pitDropped by the mimic in the room after the outside area of Irithyll Dungeon overlooking Profaned Capital
ID: Fading Soul - B1 near, main hallOn the top corridor on the bonfire side in Irithyll Dungeon, close to the first Jailer
ID: Great Magic Shield - B2 near, mob drop in far left cellOne-time drop from the Infested Corpse in the bottom corridor on the bonfire side of Irithyll Dungeon, in the closest cell
ID: Homeward Bone - path from B2 to pitIn the part of Irithyll Dungeon overlooking the Profaned Capital, after exiting the last jail cell corridor
ID: Jailbreaker's Key - B1 far, cell after gateIn the cell of the top corridor opposite to the bonfire in Irithyll Dungeon
ID: Large Soul of a Nameless Soldier - B2 far, by liftTaking the elevator up from the area you can use Path of the Dragon, before the one-way door
ID: Large Soul of a Nameless Soldier - B2, hall by stairsAt the end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Large Soul of a Weary Warrior - just before Profaned CapitalIn the open area before the bridge leading into Profaned Capital from Irithyll Dungeon
ID: Large Titanite Shard - B1 far, rightmost cellIn a cell on the far end of the top corridor opposite to the bonfire in Irithyll Dungeon, nearby the Jailer
ID: Large Titanite Shard - B1 near, by doorAt the end of the top corridor on the bonfire side in Irithyll Dungeon, before the Jailbreaker's Key door
ID: Large Titanite Shard - B3 near, right cornerIn the main Jailer cell block, to the left of the hallway leading to the Path of the Dragon area
ID: Large Titanite Shard - after bonfire, second cell on rightIn the second cell on the right after Irithyll Dungeon bonfire
ID: Large Titanite Shard - pit #1On the floor where the Giant Slave is standing
ID: Large Titanite Shard - pit #2On the floor where the Giant Slave is standing
ID: Lightning Blade - B3 lift, middle platformOn the middle platform riding the elevator up from the Path of the Dragon area
ID: Lightning Bolt - awning over pitOn the wooden overhangs above the Giant Slave. Can be reached by dropping down after climbing the long ladder around the area where the Giant stands.
ID: Murakumo - Alva dropDropped by Alva, Seeker of the Spurned when he invades in the cliffside path to Irithyll Dungeon
ID: Old Cell Key - stairs between pit and B3In a chest found going past the Giant Slave to the sewer with the rats and the basilisks, up the stairs to the end, on the right side
ID: Old Sorcerer Boots - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Coat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Gauntlets - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Old Sorcerer Hat - B2 near, middle cellIn one of the cells on the bottom corridor on the bonfire side in Irithyll Dungeon, close to the bonfire, with many Infested Corpses
ID: Pale Pine Resin - B1 far, cell with broken wallIn the jail cell with the broken wall in the top corridor opposite to the bonfire in Irithyll Dungeon, near the passive Wretch on the wall
ID: Pickaxe - path from pit to B3Passing by the Giant Slave, before the tunnel with the rats and basilisks
ID: Prisoner Chief's Ashes - B2 near, locked cell by stairsIn the cell at the far end of the bottom corridor on the bonfire side in Irithyll Dungeon
ID: Profaned Coal - B3 far, left cellIn the room with the Wretches next to the main Jailer cell block, guarded by a Wretch
ID: Profaned Flame - pitOn the floor where the Giant Slave is standing
ID: Rusted Coin - after bonfire, first cell on leftIn the first cell on the left from the Irithyll dungeon bonfire
ID: Rusted Gold Coin - after bonfire, last cell on rightIn the third cell on the right from the Irithyll Dungeon bonfire
ID: Simple Gem - B2 far, cell by stairsIn the cell near the bottom corridor opposite to the bonfire in Irithyll Dungeon, adjacent to the room with three Jailers and Cage Spiders
ID: Soul of a Crestfallen Knight - balcony above pitUnder whether the Giant Slave is resting his head
ID: Soul of a Weary Warrior - by drop to pitAt the end of the room with many peasant hollows after the Estus Shard mimic
ID: Soul of a Weary Warrior - stairs between pit and B3Going past the Giant Slave to the sewer with the rats and the basilisks, up the first flight of stairs
ID: Titanite Chunk - balcony above pit, lizardDropped by the Crystal Lizard where the Giant Slave is resting his head
ID: Titanite Chunk - pit, miniboss dropDrop from the Giant Slave
ID: Titanite Scale - B2 far, lizardDropped by the Crystal Lizard on the bottom corridor opposite from the bonfire in Irithyll Dungeon where a Wretch attacks you
ID: Titanite Scale - B3 far, mimic in hallDropped by the mimic in the main Jailer cell block
ID: Titanite Slab - SiegwardGiven by Siegward after unlocking Old Cell or on quest completion
ID: Xanthous Ashes - B3 far, right cellIn the cell in the main Jailer cell block to the left of the Profaned Capital exit
KFF: Soul of the LordsDropped by Soul of Cinder
LC: Black Firebomb - dark room lowerIn the room with the firebomb-throwing hollows, against the wall on the lowest level
LC: Braille Divine Tome of Lothric - wyvern roomIn the room next to the second Pus of Man wyvern
LC: Caitha's Chime - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, and then dropping down again towards the Lothric knights
LC: Dark Stoneplate Ring+1 - wyvern room, balconyThrough the room next to the second Pus of Man wyvern, on the balcony outside
LC: Ember - by Dragon Barracks bonfireNear the Dragon Barracks bonfire
LC: Ember - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Ember - main hall, left of stairsTo the left of the stairs past the Dragon Barracks grate
LC: Ember - plaza centerIn the area where the Pus of Man wyverns breathe fire
LC: Ember - plaza, by gateOn the railing near the area where the Pus of Man wyverns breathe fire, before the gate
LC: Ember - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Gotthard Twinswords - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Grand Archives Key - by Grand Archives door, after PC and AL bossesBefore the door to the Grand Archives after Aldrich and Yhorm are killed
LC: Greatlance - overlooking Dragon Barracks bonfireGuarded by a pensive Lothric Knight after the Dragon Barracks bonfire and continuing up the stairs
LC: Hood of PrayerIn a chest right after the Lothric Castle bonfire
LC: Irithyll Rapier - basement, miniboss dropDropped by the Boreal Outrider Knight in the basement
LC: Knight's Ring - altarClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, past the Large Hollow Soldier, down into the room with the tables
LC: Large Soul of a Nameless Soldier - dark room midIn the room with the firebomb-throwing hollows, up the ladder
LC: Large Soul of a Nameless Soldier - moat, right pathFound on the ledge after dropping into the area with the Pus of Man transforming hollows and making the entire loop
LC: Large Soul of a Nameless Soldier - plaza left, by pillarIn the building to the left of the area where the Pus of Man wyverns breathe fire, against a pillar
LC: Large Soul of a Weary Warrior - ascent, last turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs and forwards
LC: Large Soul of a Weary Warrior - main hall, by leverOn a ledge to the right of the lever opening the grate
LC: Life Ring+2 - dark room mid, out door opposite wyvern, drop downPast the room with the firebomb-throwing hollows and Pus of Man wyvern, around to the front, dropping down past where the Titanite Chunk is
LC: Lightning Urn - moat, right path, first roomStarting the loop from where the Pus of Man hollows transform, behind some crates in the first room
LC: Lightning Urn - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Pale Pine Resin - dark room upper, by mimicIn the room with the firebomb-throwing hollows, next to the mimic in the far back left
LC: Raw Gem - plaza leftOn a balcony to the left of the area where the Pus of Man wyverns breathe fire, where the Hollow Soldier throws Undead Hunter Charms
LC: Red Tearstone Ring - chapel, drop onto roofFrom the chapel to the right of the Dragonslayer Armour fight, on the balcony to the left
LC: Refined Gem - plazaIn the area where the Pus of Man wyverns breathe fire
LC: Robe of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Rusted Coin - chapelIn the chapel to the right of the Dragonslayer Armour fight
LC: Sacred Bloom Shield - ascent, behind illusory wallUp the ladder where the Winged Knight is waiting, past an illusory wall
LC: Skirt of Prayer - ascent, chest at beginningIn a chest right after the Lothric Castle bonfire
LC: Sniper Bolt - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Sniper Crossbow - moat, right path endHanging from the arch passed under on the way to the Dragon Barracks bonfire. Can be accessed by dropping into the area with the Pus of Man transforming hollows and making the entire loop, but going left at the end
LC: Soul of Dragonslayer ArmourDropped by Dragonslayer Armour
LC: Soul of a Crestfallen Knight - by lift bottomGuarded by a buffed Lothric Knight straight from the Dancer bonfire
LC: Soul of a Crestfallen Knight - wyvern room, balconyOn a ledge accessible after the second Pus of Man wyvern is defeated
LC: Spirit Tree Crest Shield - basement, chestIn a chest in the basement with the Outrider Knight
LC: Sunlight Medal - by lift topNext to the shortcut elevator outside of the Dragonslayer Armour fight that goes down to the start of the area
LC: Sunlight Straight Sword - wyvern room, mimicDropped by the mimic in the room next to the second Pus of Man wyvern
LC: Thunder Stoneplate Ring+2 - chapel, drop onto roofDropping down from the chapel balcony where the Red Tearstone Ring is found, out on the edge
LC: Titanite Chunk - altar roofClimbing the ladder to the rooftop outside the Dragonslayer Armour fight, overlooking the tree
LC: Titanite Chunk - ascent, final turretRather than going up the stairs to the Dragon Barracks bonfire, continue straight down the stairs, then right
LC: Titanite Chunk - ascent, first balconyRight after the Lothric Castle bonfire, out on the balcony
LC: Titanite Chunk - ascent, turret before barricadesFrom the Lothric Castle bonfire, up the stairs, straight, and then down the stairs behind the barricade
LC: Titanite Chunk - dark room mid, out door opposite wyvernFrom the room with the firebomb-throwing hollows, past the Pus of Man Wyvern and back around the front, before the Crystal Lizard
LC: Titanite Chunk - dark room mid, pus of man mob dropDropped by the first Pus of Man wyvern
LC: Titanite Chunk - down stairs after bossDown the stairs to the right after Dragonslayer Armour
LC: Titanite Chunk - moat #1In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat #2In the center of the area where the Pus of Man hollows transform
LC: Titanite Chunk - moat, near ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the near side to the bonfire
LC: Titanite Chunk - wyvern room, wyvern foot mob dropDropped by the second Pus of Man wyvern
LC: Titanite Scale - altarIn a chest climbing the ladder to the rooftop outside the Dragonslayer Armour fight, continuing the loop past the Red-Eyed Lothric Knight
LC: Titanite Scale - basement, chestIn a chest in the basement with the Outrider Knight
LC: Titanite Scale - chapel, chestIn a chest in the chapel to the right of the Dragonslayer Armour fight
LC: Titanite Scale - dark room mid, out door opposite wyvernPassing through the room with the firebomb-throwing hollows and the Pus of Man wyvern around to the front, overlooking the area where the wyverns breathe fire
LC: Titanite Scale - dark room, upper balconyIn the room with the firebomb-throwing hollows, at the very top on a balcony to the right
LC: Titanite Scale - dark room, upper, mimicDropped by the crawling mimic at the top of the room with the firebomb-throwing hollows
LC: Twinkling Titanite - ascent, side roomIn the room where the Winged Knight drops down
LC: Twinkling Titanite - basement, chest #1In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - basement, chest #2In a chest in the basement with the Outrider Knight
LC: Twinkling Titanite - dark room mid, out door opposite wyvern, lizardDropped by the Crystal Lizard after the room with the firebomb-throwing hollows around the front
LC: Twinkling Titanite - moat, left sideBehind one of the Pus of Man transforming hollows, to the left of the bridge to the wyvern fire-breathing area
LC: Twinkling Titanite - moat, right path, lizardDropped by the Crystal Lizard near the thieves after dropping down to the area with the Pus of Man transforming hollows
LC: Undead Bone Shard - moat, far ledgeDropping down from the bridge where the Pus of Man wyverns breathe fire on the far side from the bonfire
LC: Winged Knight Armor - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Gauntlets - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Helm - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
LC: Winged Knight Leggings - ascent, behind illusory wallIn the area where the Winged Knight drops down, up the ladder and past the illusory wall
PC: Blooming Purple Moss Clump - walkway above swampAt the right end of the plank before dropping down into the Profaned Capital toxic pool
PC: Cinders of a Lord - Yhorm the GiantDropped by Yhorm the Giant
PC: Court Sorcerer Gloves - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Hood - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Robe - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer Trousers - chapel, second floorOn the second floor of the Monstrosity of Sin building in front of the Monstrosity of Sin
PC: Court Sorcerer's Staff - chapel, mimic on second floorDropped by the mimic on the second floor of the Monstrosity of Sin building
PC: Cursebite Ring - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Eleonora - chapel ground floor, kill mobDropped by the Monstrosity of Sin on the first floor, furthest away from the door
PC: Ember - palace, far roomTo the right of the Profaned Flame, in the room with the many Jailers looking at the mimics
PC: Flame Stoneplate Ring+1 - chapel, drop from roof towards entranceDropping down from the roof connected to the second floor of the Monstrosity of Sin building, above the main entrance to the building
PC: Greatshield of Glory - palace, mimic in far roomDropped by the left mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Jailer's Key Ring - hall past chapelPast the Profaned Capital Court Sorcerer, in the corridor overlooking the Irithyll Dungeon Giant Slave area
PC: Large Soul of a Weary Warrior - bridge, far endOn the way from the Profaned Capital bonfire toward the Profaned Flame, crossing the bridge without dropping down
PC: Logan's Scroll - chapel roof, NPC dropDropped by the court sorcerer above the toxic pool
PC: Magic Stoneplate Ring+2 - tower baseAt the base of the Profaned Capital structure, going all the way around the outside wall clockwise
PC: Onislayer Greatarrow - bridgeItem on the bridge descending from the Profaned Capital bonfire into the Profaned Flame building
PC: Onislayer Greatbow - drop from bridgeFrom the bridge leading from the Profaned Capital bonfire to Yhorm, onto the ruined pillars shortcut to the right, behind you after the first dropdown.
PC: Pierce Shield - SiegwardDropped by Siegward upon death or quest completion, and sold by Patches while Siegward is in the well.
PC: Poison Arrow - chapel roofAt the far end of the roof with the Court Sorcerer
PC: Poison Gem - swamp, below hallsIn the inner cave of the Profaned Capital toxic pool
PC: Purging Stone - chapel ground floorAt the back of the room with the three Monstrosities of Sin on the first floor
PC: Purging Stone - swamp, by chapel ladderIn the middle of the Profaned Capital toxic pool, near the ladder to the Court Sorcerer
PC: Rubbish - chapel, down stairs from second floorHanging corpse visible from Profaned Capital accessible from the second floor of the building with the Monstrosities of Sin, in the back right
PC: Rusted Coin - below bridge #1Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - below bridge #2Among the rubble before the steps leading up to the Profaned Flame
PC: Rusted Coin - tower exteriorTreasure visible on a ledge in the Profaned Capital bonfire. Can be accessed by climbing a ladder outside the main structure.
PC: Rusted Gold Coin - halls above swampIn the corridors leading to the Profaned Capital toxic pool
PC: Rusted Gold Coin - palace, mimic in far roomDropped by the right mimic surrounded by the Jailers to the right of the Profaned Flame
PC: Shriving Stone - swamp, by chapel doorAt the far end of the Profaned Capital toxic pool, to the left of the door leading to the Monstrosities of Sin
PC: Siegbräu - Siegward after killing bossGiven by Siegward after helping him defeat Yhorm the Giant. You must talk to him before Emma teleports you.
PC: Soul of Yhorm the GiantDropped by Yhorm the Giant
PC: Storm Ruler - SiegwardDropped by Siegward upon death or quest completion.
PC: Storm Ruler - boss roomTo the right of Yhorm's throne
PC: Twinkling Titanite - halls above swamp, lizard #1Dropped by the second Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Twinkling Titanite - halls above swamp, lizard #2Dropped by the first Crystal Lizard in the corridors before the Profaned Capital toxic pool
PC: Undead Bone Shard - by bonfireOn the corpse of Laddersmith Gilligan next to the Profaned Capital bonfire
PC: Wrath of the Gods - chapel, drop from roofDropping down from the roof of the Monstrosity of Sin building where the Court Sorcerer is
PW1: Black Firebomb - snowfield lower, path to bonfireDropping down after the first snow overhang and following the wall on the left, past the rotting bed descending toward the second bonfire
PW1: Blessed Gem - snowfield, behind towerBehind the Millwood Knight tower in the first area, approach from the right side
PW1: Budding Green Blossom - settlement courtyard, ledgeAfter sliding down the slope on the way to Corvian Settlement, dropping down hugging the left wall
PW1: Captain's Ashes - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder
PW1: Chillbite Ring - FriedeGiven by Sister Friede while she is sitting in the Ariandel Chapel, or on the stool after she moves.
PW1: Contraption Key - library, NPC dropDropped by Sir Vilhelm
PW1: Crow Quills - settlement loop, jump into courtyardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Go right and jump past some barrels onto the central platform.
PW1: Crow Talons - settlement roofs, near bonfireAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, into the building, then looping around onto its roof.
PW1: Dark Gem - settlement back, egg buildingDropping down to the right of the gate guarded by a Corvian Knight in Corvian Settlement, inside of the last building on the right
PW1: Ember - roots above depthsIn the tree branch area after climbing down the rope bridge, hugging a right wall past a Follower Javelin wielder
PW1: Ember - settlement main, left building after bridgeCrossing the bridge after Corvian Settlement bonfire, in the building to the left.
PW1: Ember - settlement, building near bonfireIn the first building in Corvian Settlement next to the bonfire building
PW1: Ethereal Oak Shield - snowfield tower, 3FIn the Millwood Knight tower on a Millwood Knight corpse, after climbing the first ladder, then going down the staircase
PW1: Follower Javelin - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Follower Sabre - roots above depthsOn a tree branch after climbing down the rope bridge. Rather than hugging a right wall toward a Follower Javelin wielder, drop off to the left.
PW1: Frozen Weapon - snowfield lower, egg zoneDropping down after the first snow overhang, in the rotting bed along the left side
PW1: Heavy Gem - snowfield villageBefore the Millwood Knight tower, on the far side of one of the ruined walls targeted by the archer
PW1: Hollow Gem - beside chapelTo the right of the entrance to the Ariandel
PW1: Homeward Bone - depths, up hillIn the Depths of the Painting, up a hill next to the giant crabs.
PW1: Homeward Bone - snowfield village, outcroppingDropping down after the first snow overhang and following the cliff on the right, making a sharp right after a ruined wall segment before approaching the Millwood Knight tower
PW1: Large Soul of a Weary Warrior - settlement hall roofOn top of the chapel with the Corvian Knight to the left of Vilhelm's building
PW1: Large Soul of a Weary Warrior - snowfield tower, 6FAt the very top of the Millwood Knight tower after climbing up the second ladder, on a Millwood Knight corpse
PW1: Large Soul of an Unknown Traveler - below snowfield village overhangUp the slope to the left of the Millwood Knight tower, dropping down after a snow overhang, then several more ledges.
PW1: Large Soul of an Unknown Traveler - settlement backIn Corvian Settlement, on the ground before the ladder climbing onto the rooftops
PW1: Large Soul of an Unknown Traveler - settlement courtyard, cliffAfter sliding down the slope on the way to Corvian Settlement, on a cliff to the right and behind
PW1: Large Soul of an Unknown Traveler - settlement loop, by bonfireCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. On the corpse in a hole in the wall leading back to the bonfire.
PW1: Large Soul of an Unknown Traveler - settlement roofs, balconyAfter climbing the ladder onto Corvian Settlement rooftops, dropping down on a bridge to the left, on the other side of the bridge.
PW1: Large Soul of an Unknown Traveler - settlement, by ladder to bonfireTo the right of the ladder leading up to Corvian Settlement bonfire.
PW1: Large Soul of an Unknown Traveler - snowfield lower, by cliffDropping down after the first snow overhang, between the forest and the cliff edge, before where the large wolf drops down
PW1: Large Soul of an Unknown Traveler - snowfield lower, path back upDropping down after the first snow overhang, follow the right wall around and up a slope, past the Followers
PW1: Large Soul of an Unknown Traveler - snowfield lower, path to villageDropping down after the first snow overhang and following the cliff on the right, on a tree past where the large wolf jumps down
PW1: Large Soul of an Unknown Traveler - snowfield upperGoing straight after the first bonfire, to the left of the caving snow overhand
PW1: Large Titanite Shard - lizard under bridge nearDropped by a Crystal Lizard after the Rope Bridge Cave on the way to Corvian Settlement
PW1: Large Titanite Shard - settlement loop, lizardCrossing the bridge after Corvian Settlement bonfire, follow the left edge past another bridge until a dropdown point looping back to the bonfire. Hug the bonfire building's outer wall along the right side.
PW1: Large Titanite Shard - snowfield lower, left from fallDropping down after the first snow overhang, guarded by a Tree Woman overlooking the rotting bed along the left wall
PW1: Millwood Battle Axe - snowfield tower, 5FIn the Milkwood Knight tower, either dropping down from rafters after climbing the second ladder or making a risky jump
PW1: Millwood Greatarrow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Millwood Greatbow - snowfield village, loop back to lowerDropping down after the first snow overhang and following the cliff on the right, making the full loop around, up the slope leading towards where the large wolf drops down
PW1: Onyx Blade - library, NPC dropDropped by Sir Vilhelm
PW1: Poison Gem - snowfield upper, forward from bonfireFollowing the left wall from the start, guarded by a Giant Fly
PW1: Rime-blue Moss Clump - below bridge farIn a small alcove to the right after climbing down the rope bridge
PW1: Rime-blue Moss Clump - snowfield upper, overhangOn the first snow overhang at the start. It drops down at the same time you do.
PW1: Rime-blue Moss Clump - snowfield upper, starting caveIn the starting cave
PW1: Rusted Coin - right of libraryTo the right of Vilhelm's building
PW1: Rusted Coin - snowfield lower, straight from fallDropping down after the first snow overhang, shortly straight ahead
PW1: Rusted Gold Coin - settlement roofs, roof near second ladderAfter climbing the second ladder on the Corvian Settlement rooftops, immediately dropping off the bridge to the right, on a rooftop
PW1: Shriving Stone - below bridge nearAfter the Rope Bridge Cave bonfire, dropping down before the bridge, following the ledge all the way to the right
PW1: Simple Gem - settlement, lowest level, behind gateCrossing the bridge after Corvian Settlement bonfire, follow the left edge until a bridge, then drop down on the right side. Guarded by a Sewer Centipede.
PW1: Slave Knight Armor - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Gauntlets - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Hood - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Slave Knight Leggings - settlement roofs, drop by ladderIn Corvian Settlement, rather than climbing up a ladder leading to a bridge to the roof of the chapel with the Corvian Knight, dropping down a hole to the left of the ladder into the building below.
PW1: Snap Freeze - depths, far end, mob dropIn the Depths of the Painting, past the giant crabs, guarded by a special Tree Woman. Killing her drops down a very long nearby ladder.
PW1: Soul of a Crestfallen Knight - settlement hall, raftersIn the rafters of the chapel with the Corvian Knight to the left of Vilhelm's building. Can drop down from the windows exposed to the roof.
PW1: Soul of a Weary Warrior - snowfield tower, 1FAt the bottom of the Millwood Knight tower on a Millwood Knight corpse
PW1: Titanite Slab - CorvianGiven by the Corvian NPC in the building next to Corvian Settlement bonfire.
PW1: Titanite Slab - depths, up secret ladderIn the Depths of the Painting, past the giant crabs, killing a special Tree Woman drops down a very long nearby ladder. Climb the ladder and also the ladder after that one.
PW1: Twinkling Titanite - roots, lizardDropped by a Crystal Lizard in the tree branch area after climbing down the rope bridge, before the ledge with the Follower Javelin wielder
PW1: Twinkling Titanite - settlement roofs, lizard before hallDropped by a Crystal Lizard on a bridge in Corvian Settlement before the rooftop of the chapel with the Corvian Knight inside.
PW1: Twinkling Titanite - snowfield tower, 3F lizardDropped by a Crystal Lizard in the Millwood Knight tower, climbing up the first ladder and descending the stairs down
PW1: Valorheart - boss dropDropped by Champion's Gravetender
PW1: Way of White Corona - settlement hall, by altarIn the chapel with the Corvian Knight to the left of Vilhelm's building, in front of the altar.
PW1: Young White Branch - right of libraryTo the right of Vilhelm's building
PW2: Blood Gem - B2, centerOn the lower level of the Ariandel Chapel basement, in the middle
PW2: Dung Pie - B1On the higher level of the Ariandel Chapel basement, on a wooden beam overlooking the lower level
PW2: Earth Seeker - pit caveIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, in the cave
PW2: Ember - pass, central alcoveAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, in a small alcove along the left wall
PW2: Floating Chaos - NPC dropDropped by Livid Pyromancer Dunnel when he invades while embered, whether boss is defeated or not. On the second level of Priscilla's building above the Gravetender fight, accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Follower Shield - pass, far cliffsideAfter the Snowy Mountain Pass bonfire, going left of the bell stuck in the ground, on the cliff ledge past the open area, to the left
PW2: Follower Torch - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going all the way down the slope on the edge of the map. Guarded by a Follower
PW2: Homeward Bone - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Large Soul of a Crestfallen Knight - pit, by treeIn the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, by the tree
PW2: Large Titanite Shard - pass, far side pathOn the way to the Ariandel Chapel basement, where the first wolf enemies reappear, going partway down the slope on the edge of the map
PW2: Large Titanite Shard - pass, just before B1On the way to Ariandel Chapel basement, past the Millwood Knights and before the first rotten tree that can be knocked down
PW2: Prism Stone - pass, tree by beginningUp the slope and to the left after the Snowy Mountain Pass, straight ahead by a tree
PW2: Pyromancer's Parting Flame - rotundaOn the second level of Priscilla's building above the Gravetender fight. Can be accessed from the lowest level of the Ariandel Chapel basement, past an illusory wall nearly straight left of the mechanism that moves the statue, then carefully dropping down tree branches.
PW2: Quakestone Hammer - pass, side path near B1On the way to Ariandel Chapel basement, rather than going right past the two Millwood Knights, go left, guarded by a very strong Millwood Knight
PW2: Soul of Sister FriedeDropped by Sister Friede
PW2: Soul of a Crestfallen Knight - pit edge #1In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Soul of a Crestfallen Knight - pit edge #2In the area after Snowy Mountain Pass with the giant tree and Earth Seeker Millwood Knight, along the edge
PW2: Titanite Chunk - pass, by kickable treeAfter the Snowy Mountain Pass bonfire, on a ledge to the right of the slope with the bell stuck in the ground, behind a tree
PW2: Titanite Chunk - pass, cliff overlooking bonfireOn a cliff overlooking the Snowy Mountain Pass bonfire. Requires following the left wall
PW2: Titanite Slab - boss dropOne-time drop after killing Father Ariandel and Friede (phase 2) for the first time.
PW2: Twinkling Titanite - B3, lizard #1Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Twinkling Titanite - B3, lizard #2Dropped by a Crystal Lizard past an illusory wall nearly straight left of the mechanism that moves the statue in the lowest level of the Ariandel Chapel basement
PW2: Vilhelm's Armor - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Gauntlets - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's HelmOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
PW2: Vilhelm's Leggings - B2, along wallOn the lower level of the Ariandel Chapel basement, along a wall to the left of the contraption that turns the statue
RC: Antiquated Plain Garb - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Black Witch Garb - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Hat - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Trousers - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Black Witch Veil - swamp near right, by sunken churchTo the left of the submerged building with 4 Ringed Knights, near a spear-wielding knight.
RC: Black Witch Wrappings - streets gardenGuarded by Alva (invades whether embered or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Blessed Gem - grave, down lowest stairsIn Shared Grave, after dropping down near Gael's flag and dropping down again, behind you. Or from the bonfire, go back through the side tunnel with the skeletons and down the stairs after that.
RC: Blindfold Mask - grave, NPC dropDropped by Moaning Knight (invades whether embered or not, or boss defeated or not) in Shared Grave.
RC: Blood of the Dark Soul - end boss dropDropped by Slave Knight Gael
RC: Budding Green Blossom - church pathOn the way to the Halflight building.
RC: Budding Green Blossom - wall top, flowers by stairsIn a patch of flowers to the right of the stairs leading up to the first Judicator along the left wall of the courtyard are Mausoleum Lookout.
RC: Budding Green Blossom - wall top, in flower clusterAlong the left wall of the courtyard after Mausoleum Lookout, in a patch of flowers.
RC: Chloranthy Ring+3 - wall hidden, drop onto statueFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, dropping back down toward the way to Filianore, onto a platform with a Gwyn statue. Try to land on the platform rather than the statue.
RC: Church Guardian Shiv - swamp far left, in buildingInside of the building at the remote end of the muck pit surrounded by praying Hollow Clerics.
RC: Covetous Gold Serpent Ring+3 - streets, by LappGoing up the very long ladder from the muck pit, then up some stairs, to the left, and across the bridge, in a building past the Ringed Knights. Also where Lapp can be found to tell him of the Purging Monument.
RC: Crucifix of the Mad King - ashes, NPC dropDropped by Shira, who invades you (ember not required) in the far-future version of her room
RC: Dark Gem - swamp near, by stairsIn the middle of the muck pit, close to the long stairs.
RC: Divine Blessing - streets monument, mob dropDropped by the Judicator near the Purging Monument area. Requires solving "Show Your Humanity" puzzle.
RC: Divine Blessing - wall top, mob dropDropped by the Judicator after the Mausoleum Lookup bonfire.
RC: Dragonhead Greatshield - lower cliff, under bridgeDown a slope to the right of the bridge where Midir first assaults you, past a sword-wielding Ringed Knight, under the bridge.
RC: Dragonhead Shield - streets monument, across bridgeFound in Purging Monument area, across the bridge from the monument. Requires solving "Show Your Humanity" puzzle.
RC: Ember - wall hidden, statue roomFrom the mid level of the Ringed Inner Wall elevator that leads to the Midir fight, in the room with the illusory statue.
RC: Ember - wall top, by statueAlong the left wall of the courtyard after Mausoleum Lookout, in front of a tall monument.
RC: Ember - wall upper, balconyOn the balcony attached to the room with the Ringed Inner Wall bonfire.
RC: Filianore's Spear Ornament - mid boss dropDropped by Halflight, Spear of the Church
RC: Filianore's Spear Ornament - wall hidden, by ladderNext the ladder leading down to the chapel before the Midir fight in the Ringed Inner Wall building.
RC: Havel's Ring+3 - streets high, drop from building oppositeDropping down from the building where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Hidden Blessing - swamp center, mob dropDropped by Judicator patrolling the muck pit.
RC: Hidden Blessing - wall top, tomb under platformIn a tomb underneath the platform with the first Judicator, accessed by approaching from Mausoleum Lookout bonfire.
RC: Hollow Gem - wall upper, path to towerHeading down the cursed stairs after Ringed Inner Wall bonfire and another short flight of stairs, hanging on a balcony.
RC: Iron Dragonslayer Armor - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Gauntlets - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Helm - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Iron Dragonslayer Leggings - swamp far, miniboss dropDropped by Dragonslayer Armour at the far end of the muck pit.
RC: Lapp's Armor - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Gauntlets - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Helm - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Lapp's Leggings - LappLeft at Lapp's final location in Shared Grave after his quest is complete, or sold by Shrine Handmaid upon killing Lapp.
RC: Large Soul of a Crestfallen Knight - streets monument, across bridgeFound in Purging Monument area, on the other side of the bridge leading to the monument. Requires solving "Show Your Humanity" puzzle.
RC: Large Soul of a Crestfallen Knight - streets, far stairsToward the bottom of the stairs leading down to the muck pit.
RC: Large Soul of a Weary Warrior - lower cliff, endToward the end of the upper path attacked Midir's fire-breathing.
RC: Large Soul of a Weary Warrior - swamp center, by peninsulaIn the muck pit approaching where the Judicator patrols from the stairs.
RC: Large Soul of a Weary Warrior - wall lower, past two illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past two illusory walls on the right side of the ascending stairs.
RC: Large Soul of a Weary Warrior - wall top, right of small tombIn the open toward the end of the courtyard after the Mausoleum Lookout bonfire, on the right side of the small tomb.
RC: Ledo's Great Hammer - streets high, opposite building, NPC dropDropped by Silver Knight Ledo (invades whether embered or not, or boss defeated or not) in the building down the path to the right after climbing the very long ladder from the muck area.
RC: Lightning Arrow - wall lower, past three illusory wallsIn the Ringed Inner Wall building coming from Shared Grave, past three illusory walls on the right side of the ascending stairs.
RC: Lightning Gem - grave, room after first dropIn Shared Grave, in the first room encountered after falling down from the crumbling stairs and continuing upward.
RC: Mossfruit - streets near left, path to gardenPartway down the stairs from Shira, across the bridge.
RC: Mossfruit - streets, far left alcoveNear the bottom of the stairs before the muck pit, in an alcove to the left.
RC: Preacher's Right Arm - swamp near right, by towerIn the muck pit behind a crystal-covered structure, close to the Ringed City Streets shortcut entrance.
RC: Prism Stone - swamp near, railing by bonfireOn the balcony of the path leading up to Ringed City Streets bonfire from the muck pit.
RC: Purging Stone - wall top, by door to upperAt the end of the path from Mausoleum Lookup to Ringed Inner Wall, just outside the door.
RC: Ring of the Evil Eye+3 - grave, mimicDropped by mimic in Shared Grave. In one of the rooms after dropping down near Gael's flag and then dropping down again.
RC: Ringed Knight Paired Greatswords - church path, mob dropDropped by Ringed Knight with paired greatswords before Filianore building.
RC: Ringed Knight Spear - streets, down far right hallIn a courtyard guarded by a spear-wielding Ringed Knight. Can be accessed from a hallway filled with cursed clerics on the right side going down the long stairs, or by climbing up the long ladder from the muck pit and dropping down past the Locust Preacher.
RC: Ringed Knight Straight Sword - swamp near, tower on peninsulaOn a monument next to the Ringed City Streets building. Can be easily accessed after unlocking the shortcut by following the left wall inside and then outside the building.
RC: Ritual Spear Fragment - church pathTo the right of the Paired Greatswords Ringed Knight on the way to Halflight.
RC: Rubbish - lower cliff, middleIn the middle of the upper path attacked Midir's fire-breathing, after the first alcove.
RC: Rubbish - swamp far, by crystalIn the remote end of the muck pit, next to a massive crystal structure between a giant tree and the building with praying Hollow Clerics, guarded by several Locust Preachers.
RC: Ruin Armor - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Gauntlets - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Helm - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Ruin Leggings - wall top, under stairs to bonfireUnderneath the stairs leading down from Mausoleum Lookout.
RC: Sacred Chime of Filianore - ashes, NPC dropGiven by Shira after accepting her request to kill Midir, or dropped by her in post-Filianore Ringed City.
RC: Shira's Armor - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Crown - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Gloves - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shira's Trousers - Shira's room after killing ashes NPCFound in Shira's room in Ringed City after killing her in post-Filianore Ringed City.
RC: Shriving Stone - wall tower, bottom floor centerIn the cylindrical building before the long stairs with many Harald Legion Knights, in the center structure on the first floor.
RC: Siegbräu - LappGiven by Lapp within the Ringed Inner Wall.
RC: Simple Gem - grave, up stairs after first dropIn Shared Grave, following the path after falling down from the crumbling stairs and continuing upward.
RC: Soul of Darkeater MidirDropped by Darkeater Midir
RC: Soul of Slave Knight GaelDropped by Slave Knight Gael
RC: Soul of a Crestfallen Knight - swamp far, behind crystalBehind a crystal structure at the far end of the muck pit, close to the building with the praying Hollow Clerics before Dragonslayer Armour.
RC: Soul of a Crestfallen Knight - swamp near left, nookIn the muck pit behind all of the Hollow Clerics near the very long ladder.
RC: Soul of a Crestfallen Knight - wall top, under dropAfter dropping down onto the side path on the right side of the Mausoleum Lookout courtyard to where the Crystal Lizard is, behind you.
RC: Soul of a Weary Warrior - lower cliff, by first alcoveIn front of the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Soul of a Weary Warrior - swamp centerIn the middle of the muck pit where the Judicator is patrolling.
RC: Soul of a Weary Warrior - swamp right, by sunken churchIn between where the Judicator patrols in the muck pit and the submerged building with the 4 Ringed Knights. Provides some shelter from his arrows.
RC: Spears of the Church - hidden boss dropDropped by Darkeater Midir
RC: Titanite Chunk - streets high, building oppositeDown a path past the room where Silver Knight Ledo invades. The building is up the very long ladder from the muck pit, down the path all the way to the right.
RC: Titanite Chunk - streets, near left dropNear the top of the stairs by Shira, dropping down in an alcove to the left.
RC: Titanite Chunk - swamp center, peninsula edgeAlong the edge of the muck pit close to where the Judicator patrols.
RC: Titanite Chunk - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: Titanite Chunk - swamp near left, by spire topAt the edge of the muck pit, on the opposite side of the wall from the very long ladder.
RC: Titanite Chunk - swamp near right, behind rockAt the very edge of the muck pit, to the left of the submerged building with 4 Ringed Knights.
RC: Titanite Chunk - wall top, among gravesAlong the right edge of the courtyard after Mausoleum Lookout in a cluster of graves.
RC: Titanite Chunk - wall upper, courtyard alcoveIn the courtyard where the first Ringed Knight is seen, along the right wall into an alcove.
RC: Titanite Scale - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Titanite Scale - lower cliff, first alcoveIn the first alcove providing shelter from Midir's fire-breathing on the way to Shared Grave.
RC: Titanite Scale - lower cliff, lower pathAfter dropping down from the upper path attacked by Midir's fire-breathing to the lower path.
RC: Titanite Scale - lower cliff, path under bridgePartway down a slope to the right of the bridge where Midir first assaults you.
RC: Titanite Scale - swamp far, by minibossIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - swamp far, lagoon entranceIn the area at the far end of the muck pit with the Dragonslayer Armour.
RC: Titanite Scale - upper cliff, bridgeOn the final bridge where Midir attacks before you knock him off.
RC: Titanite Scale - wall lower, lizardDropped by the Crystal Lizard on the stairs going up from Shared Grave to Ringed Inner Wall elevator.
RC: Titanite Scale - wall top, behind spawnBehind you at the very start of the level.
RC: Titanite Slab - ashes, NPC dropGiven by Shira after defeating Midir, or dropped by her in post-Filianore Ringed City.
RC: Titanite Slab - ashes, mob dropDropped by the Ringed Knight wandering around near Gael's arena
RC: Titanite Slab - mid boss dropDropped by Halflight, Spear of the Church
RC: Twinkling Titanite - church path, left of boss doorDropping down to the left of the door leading to Halflight.
RC: Twinkling Titanite - grave, lizard past first dropDropped by the Crystal Lizard right after the crumbling stairs in Shared Grave.
RC: Twinkling Titanite - streets high, lizardDropped by the Crystal Lizard which runs across the bridge after climbing the very long ladder up from the muck pit.
RC: Twinkling Titanite - swamp near leftAt the left edge of the muck pit coming from the stairs, guarded by a Preacher Locust.
RC: Twinkling Titanite - swamp near right, on sunken churchFollowing the sloped roof of the submerged building with the 4 Ringed Knights, along the back wall
RC: Twinkling Titanite - wall top, lizard on side pathDropped by the first Crystal Lizard on the side path on the right side of the Mausoleum Lookout courtyard
RC: Twinkling Titanite - wall tower, jump from chandelierIn the cylindrical building before the long stairs with many Harald Legion Knights. Carefully drop down to the chandelier in the center, then jump to the second floor. The item is on a ledge.
RC: Violet Wrappings - wall hidden, before bossIn the chapel before the Midir fight in the Ringed Inner Wall building.
RC: White Birch Bow - swamp far left, up hillUp a hill at the edge of the muck pit with the Hollow Clerics.
RC: White Preacher Head - swamp near, nook right of stairsPast the balcony to the right of the Ringed City Streets bonfire room entrance. Can be accessed by dropping down straight after from the bonfire, then around to the left.
RC: Wolf Ring+3 - street gardens, NPC dropDropped by Alva (invades whether embered or not, or boss defeated or not), partway down the stairs from Shira, across the bridge, and past the Ringed Knight.
RC: Young White Branch - swamp far left, by white tree #1Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #2Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RC: Young White Branch - swamp far left, by white tree #3Next to a small birch tree at the edge of the muck pit, between the hill with the aggressive Hollow Clerics and the building with the praying Hollow Clerics outside.
RS: Blue Bug Pellet - broken stairs by OrbeckOn the broken stairs leading down from Orbeck's area, on the opposite side from Orbeck
RS: Blue Sentinels - HoraceGiven by Horace the Hushed by first "talking" to him, or upon death.
RS: Braille Divine Tome of Carim - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Brigand Armor - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Axe - beneath roadAt the start of the path leading down to the Madwoman in Road of Sacrifices
RS: Brigand Gauntlets - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Hood - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Trousers - beneath roadIn the middle of the path where the Madwoman waits in Road of Sacrifices
RS: Brigand Twindaggers - beneath roadAt the end of the path guarded by the Madwoman in Road of Sacrifices
RS: Butcher Knife - NPC drop beneath roadDropped by the Butcher Knife-wielding madwoman near the start of Road of Sacrifices
RS: Chloranthy Ring+2 - road, drop across from carriageFound dropping down from the first Storyteller Corvian on the left side rather than the right side. You can then further drop down to where the madwoman is, after healing.
RS: Conjurator Boots - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Hood - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Manchettes - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Conjurator Robe - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Crystal Gem - stronghold, lizardDropped by the Crystal Lizard in the building before Crystal Sage
RS: Ember - right of Halfway Fortress entranceOn the ledge with the Corvian with the Storyteller Staff, to the right of the Halfway Fortress entrance
RS: Ember - right of fire behind stronghold left roomBehind the building before Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side or go under bridge on right side
RS: Estus Shard - left of fire behind stronghold left roomBehind the building leading to Crystal Sage, approached from Crucifixion Woods bonfire. Can drop down on left side of go under bridge on right side
RS: Exile Greatsword - NPC drop by Farron KeepDropped by the greatsword-wielding Exile Knight before the ladder down to Farron Keep
RS: Fading Soul - woods by Crucifixion Woods bonfireDropping down from the Crucifixion Woods bonfire toward the Halfway Fortress, guarded by dogs
RS: Fallen Knight Armor - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Gauntlets - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Helm - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Fallen Knight Trousers - water's edge by Farron KeepOn the edge of the water surrounding the building where you descend into Farron Keep
RS: Farron Coal - keep perimeterAt the end of the Farron Keep Perimeter building on Crucifixion Woods side, behind the Black Knight
RS: Golden Falcon Shield - path from stronghold right room to Farron KeepHalfway up the stairs to the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area, go straight and follow the path down
RS: Grass Crest Shield - water by Crucifixion Woods bonfireDropping down into the crab area from Crucifixion Woods, on the other side of a tree from the greater crab
RS: Great Club - NPC drop by Farron KeepDropped by the club-wielding Exile Knight before the ladder down to Farron Keep
RS: Great Swamp Pyromancy Tome - deep waterIn the deep water part of the Crucifixion Woods crab area, between a large tree and the keep wall
RS: Great Swamp Ring - miniboss drop, by Farron KeepDropped by Greater Crab in Crucifixion Woods close to the Farron Keep outer wall
RS: Green Blossom - by deep waterIn the Crucifixion Woods crab area out in the open, close to the edge of the deep water area
RS: Green Blossom - water beneath strongholdIn the Crucifixion Woods crab area close to the Crucifixion Woods bonfire, along the left wall of the water area, to the right of the entrance to the building before Crystal Sage
RS: Heretic's Staff - stronghold left roomIn the building before Crystal Sage, entering from near Crucifixion Woods, in a corner under the first stairwell and balcony
RS: Heysel Pick - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
RS: Homeward Bone - balcony by Farron KeepAt the far end of the building where you descend into Farron Keep, by the balcony
RS: Large Soul of an Unknown Traveler - left of stairs to Farron KeepIn the area before you descend into Farron Keep, before the stairs to the far left
RS: Lingering Dragoncrest Ring+1 - waterOn a tree by the greater crab near the Crucifixion Woods bonfire, after the Grass Crest Shield tree
RS: Morne's Ring - drop from bridge to Halfway FortressDropping down before the bridge leading up to Halfway Fortress from Road of Sacrifices, guarded by the maggot belly dog
RS: Ring of Sacrifice - stronghold, drop from right room balconyDrop down from the platform behind the sorcerer in the building before Crystal Sage, entering from the stairs leading up from the crab area
RS: Sage Ring - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sellsword Armor - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Gauntlet - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Helm - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Trousers - keep perimeter balconyIn the Farron Keep Perimeter building on Crucifixion Woods side, on the balcony on the right side overlooking the Black Knight
RS: Sellsword Twinblades - keep perimeterIn the Farron Keep Perimeter building on Crucifixion Woods side, behind and to the right of the Black Knight
RS: Shriving Stone - road, by startDropping down to the left of the first Corvian enemy in Road of Sacrifices
RS: Sorcerer Gloves - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Hood - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Robe - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Sorcerer Trousers - water beneath strongholdIn an alcove under the building before Crystal Sage, guarded by a Lycanthrope, accessible from the swamp or from dropping down
RS: Soul of a Crystal SageDropped by Crystal Sage
RS: Soul of an Unknown Traveler - drop along wall from Halfway FortressFrom Halfway Fortress, hug the right wall and drop down twice on the way to the crab area
RS: Soul of an Unknown Traveler - right of door to stronghold leftOut in the open to the right of the building before Crystal Sage, as entered from Crucifixion Woods bonfire
RS: Soul of an Unknown Traveler - road, by wagonTo the right of the overturned wagon descending from the Road of Sacrifices bonfire
RS: Titanite Shard - road, on bridge after you go underCrossing the bridge you go under after the first Road of Sacrifices bonfire, after a sleeping Corvian and another Corvian guarding the pickup
RS: Titanite Shard - water by Halfway FortressDropping down into the Crucifixion Woods crab area right after Halfway Fortress, on the left wall heading toward the Black Knight building, guarded by dog
RS: Titanite Shard - woods, left of path from Halfway FortressHugging the left wall from Halfway Fortress to Crystal Sage, behind you after the first dropdown
RS: Titanite Shard - woods, surrounded by enemiesHugging the left wall from Halfway Fortress to the Crystal Sage bonfire, after a dropdown surrounded by seven Poisonhorn bugs
RS: Twin Dragon Greatshield - woods by Crucifixion Woods bonfireIn the middle of the area with the Poisonhorn bugs and Lycanthrope Hunters, following the wall where the bugs guard a Titanite Shard
RS: Xanthous Crown - Heysel dropDropped by Heysel when she invades in Road of Sacrifices
SL: Black Iron Greatshield - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Black Knight Sword - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, guarded by a Black Knight
SL: Bloodbite Ring+1 - behind ballistaBehind the ballista, overlooking Smouldering Lake
SL: Chaos Gem - antechamber, lizard at end of long hallDropped by the Crystal Lizard found from the Antechamber bonfire, toward the Demon Cleric and to the right, then all the way down
SL: Chaos Gem - lake, far end by mobIn Smouldering Lake along the wall underneath the ballista, all the way to the left past two crabs
SL: Dragonrider Bow - by ladder from ruins basement to ballistaAfter climbing up the ladder after the Black Knight in Demon Ruins, falling back down to a ledge
SL: Ember - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Ember - ruins main lower, path to antechamberGoing down the stairs from the Antechamber bonfire, to the right, at the end of the short hallway to the next right
SL: Ember - ruins main upper, hall end by holeIn the Demon Ruins, hugging the right wall from the Demon Ruins bonfire, or making a jump from the illusory hall corridor from Antechamber bonfire
SL: Ember - ruins main upper, just after entranceBehind the first Demon Cleric from the Demon Ruins bonfire
SL: Estus Shard - antechamber, illusory wallBehind an illusory wall and Smouldering Writhing Flesh-filled corridor from Antechamber bonfire
SL: Flame Stoneplate Ring+2 - ruins main lower, illusory wall in far hallOn the far exit of the Demon Ruins main hall, past an illusory wall, past the Black Knight, hidden in a corner
SL: Fume Ultra Greatsword - ruins basement, NPC dropDropped by Knight Slayer Tsorig in Smouldering Lake
SL: Homeward Bone - path to ballistaIn the area targeted by the ballista after the long ladder guarded by the Black Knight, before the Bonewheel Skeletons
SL: Izalith Pyromancy Tome - antechamber, room near bonfireIn the room straight down from the Antechamber bonfire, past a Demon Cleric, surrounded by many Ghrus.
SL: Izalith Staff - ruins basement, second illusory wall behind chestPast an illusory wall to the left of the Large Hound Rat in Demon Ruins, and then past another illusory wall, before the basilisk area
SL: Knight Slayer's Ring - ruins basement, NPC dropDropped by Knight Slayer Tsorig after invading in the Catacombs
SL: Large Titanite Shard - lake, by entranceIn the middle of Smouldering Lake, close to the Abandoned Tomb
SL: Large Titanite Shard - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Large Titanite Shard - lake, by tree #1In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, by tree #2In the middle of Smouldering Lake, by a tree before the hallway to the pit
SL: Large Titanite Shard - lake, straight from entranceIn the middle of Smouldering Lake, in between Abandoned Tomb and Demon Ruins
SL: Large Titanite Shard - ledge by Demon Ruins bonfireOn a corpse hanging off the ledge outside the Demon Ruins bonfire
SL: Large Titanite Shard - ruins basement, illusory wall in upper hallIn a chest past an illusory wall to the left of the Large Hound Rat in Demon Ruins, before the basilisk area
SL: Large Titanite Shard - side lake #1In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Large Titanite Shard - side lake #2In the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
SL: Lightning Stake - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Llewellyn Shield - Horace dropDropped by Horace the Hushed upon death or quest completion.
SL: Quelana Pyromancy Tome - ruins main lower, illusory wall in grey roomAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall
SL: Sacred Flame - ruins basement, in lavaIn the lava pit under the Black Knight, by Knight Slayer Tsorig
SL: Shield of Want - lake, by minibossIn the middle of Smouldering Lake, under the Carthus Sandworm
SL: Soul of a Crestfallen Knight - ruins basement, above lavaNext to the Black Knight in Demon Ruins
SL: Soul of the Old Demon KingDropped by Old Demon King in Smouldering Lake
SL: Speckled Stoneplate Ring - lake, ballista breaks bricksBehind a destructible wall in Smouldering Lake which the ballista has to destroy
SL: Titanite Chunk - path to side lake, lizardDropped by the second Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Titanite Scale - ruins basement, path to lavaIn the area with Basilisks on the way to the ballista
SL: Toxic Mist - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, in the middle of the lava pit.
SL: Twinkling Titanite - path to side lake, lizardDropped by the first Crystal Lizard in the cave leading to the pit where Horace can be found in Smouldering Lake
SL: Undead Bone Shard - lake, miniboss dropDropped by the giant Carthus Sandworm
SL: Undead Bone Shard - ruins main lower, left after stairsIn the close end of the Demon Ruins main hall, right below a Smouldering Writhing Flesh
SL: White Hair Talisman - ruins main lower, in lavaAt the far end of the Demon Ruins main hall to the right, where the rats are, then another right and past the illusory wall, at the far end of the lava pit.
SL: Yellow Bug Pellet - side lakeIn the Smouldering Lake pit where Horace can be found, following the right wall from Abandoned Tomb
UG: Ashen Estus Ring - swamp, path opposite bonfireIn the coffin similar to your initial spawn location, guarded by Corvians
UG: Black Knight Glaive - boss arenaIn the Champion Gundyr boss area
UG: Blacksmith Hammer - shrine, Andre's roomWhere Andre sits in Firelink Shrine
UG: Chaos Blade - environs, left of shrineWhere Sword Master is in Firelink Shrine
UG: Coiled Sword Fragment - shrine, dead bonfireIn the dead Firelink Shrine bonfire
UG: Ember - shopSold by Untended Graves Handmaid
UG: Eyes of a Fire Keeper - shrine, Irina's roomBehind an illusory wall, in the same location Irina sits in Firelink Shrine
UG: Hidden Blessing - cemetery, behind coffinBehind the coffin that had a Titanite Shard in Cemetery of Ash
UG: Hornet Ring - environs, right of main path after killing FK bossOn a cliffside to the right of the main path leading up to dark Firelink Shrine, after Abyss Watchers is defeated.
UG: Life Ring+3 - shrine, behind big throneBehind Prince Lothric's throne
UG: Priestess Ring - shopSold or dropped by Untended Graves Handmaid. Killing her is not recommended
UG: Ring of Steel Protection+1 - environs, behind bell towerBehind Bell Tower to the right
UG: Shriving Stone - swamp, by bonfireAt the very start of the area
UG: Soul of Champion GundyrDropped by Champion Gundyr
UG: Soul of a Crestfallen Knight - environs, above shrine entranceAbove the Firelink Shrine entrance, up the stairs/slope from either left or right of the entrance
UG: Soul of a Crestfallen Knight - swamp, centerClose to where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, left path by fountainIn a path to the left of where Ashen Estus Flask was in Cemetery of Ash
UG: Titanite Chunk - swamp, right path by fountainIn a path to the right of where Ashen Estus Flask was in Cemetery of Ash
UG: Wolf Knight Armor - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Gauntlets - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Helm - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
UG: Wolf Knight Leggings - shop after killing FK bossSold by Untended Graves Handmaid after defeating Abyss Watchers
US: Alluring Skull - foot, behind carriageGuarded by two dogs after the Foot of the High Wall bonfire
US: Alluring Skull - on the way to tower, behind buildingAfter the ravine bridge leading to Eygon and the Giant's tower, wrapping around the building to the right.
US: Alluring Skull - tower village building, upstairsUp the stairs of the building with Cage Spiders after the Fire Demon, before the dogs
US: Bloodbite Ring - miniboss in sewerDropped by the large rat in the sewers with grave access
US: Blue Wooden Shield - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Caduceus Round Shield - right after stable exitAfter exiting the building across the bridge to the right of the first Undead Settlement building, to the left
US: Caestus - sewerIn the tunnel with the Giant Hound Rat and Grave Key door, from the ravine bridge toward Dilapidated Bridge bonfire
US: Charcoal Pine Bundle - first building, bottom floorDown the stairs in the first building
US: Charcoal Pine Bundle - first building, middle floorOn the bottom floor of the first building
US: Charcoal Pine Resin - hanging corpse roomIn the building after the burning tree and Cathedral Evangelist, in the room with the many hanging corpses
US: Chloranthy Ring - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Cleric Blue Robe - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Gloves - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Hat - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cleric Trousers - graveyard by white treeAfter Dilapidated Bridge bonfire, in the back of the Giant's arrow area. Guarded by a flamberge-wielding thrall.
US: Cornyx's Garb - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Garb - kill CornyxDropped by Cornyx
US: Cornyx's Skirt - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Skirt - kill CornyxDropped by Cornyx
US: Cornyx's Wrap - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Cornyx's Wrap - kill CornyxDropped by Cornyx
US: Covetous Silver Serpent Ring+2 - tower village, drop down from roofAt the back of a roof near the end of the Fire Demon loop, dropping down past where Flynn's Ring is
US: Ember - behind burning treeBehind the burning tree with the Cathedral Evangelist
US: Ember - bridge on the way to towerOn the ravine bridge leading toward Eygon and the Giant's tower
US: Ember - by stairs to bossNext to the stairs leading up to Curse-Rotted Greatwood fight, near a tree guarded by a dog
US: Ember - by white treeNear the Birch Tree where giant shoots arrows
US: Ember - tower basement, minibossIn the room with the Outrider Knight
US: Estus Shard - under burning treeIn front of the burning tree guarded by the Cathedral Evangelist
US: Fading Soul - by white treeNear the Birch Tree where giant shoots arrows
US: Fading Soul - outside stableIn the thrall area to the right of the bridge to the right of the burning tree with the Cathedral Evangelist
US: Fire Clutch Ring - wooden walkway past stableFrom the area bombarded by firebombs above the Cliff Underside bonfire
US: Fire Gem - tower village, miniboss dropDropped by the Fire Demon you fight with Siegward
US: Firebomb - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a rooftop overlooking the Cliff Underside area.
US: Flame Stoneplate Ring - hanging corpse by Mound-Maker transportOn a hanging corpse in the area with the Pit of Hollows cage manservant, after the thrall area, overlooking the entrance to the Giant's tower.
US: Flynn's Ring - tower village, rooftopOn the roof toward the end of the Fire Demon loop, past the Cathedral Evangelists
US: Great Scythe - building by white tree, balconyOn the balcony of the building before Curse-Rotted Greatwood, coming from Dilapidated Bridge bonfire
US: Hand Axe - by CornyxNext to Cornyx's cell
US: Hawk Ring - Giant ArcherDropped by Giant, either by killing him or collecting all of the birch tree items locations in the base game.
US: Heavy Gem - HawkwoodGiven or dropped by Hawkwood after defeating Curse-Rotted Greatwood or Crystal Sage
US: Heavy Gem - chasm, lizardDrop by Crystal Lizard in ravine accessible by Grave Key or dropping down near Eygon.
US: Homeward Bone - foot, drop overlookUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Homeward Bone - stable roofIn the thrall area across the bridge from the first Undead Settlement building, on a roof overlooking the ravine bridge.
US: Homeward Bone - tower village, jump from roofAt the end of the loop from the Siegward Demon fight, after dropping down from the roof onto the tower with Chloranthy Ring, to the right of the tower entrance
US: Homeward Bone - tower village, right at startUnder Foot of the High Wall bonfire, around where Yoel can be first met
US: Human Pine Resin - tower village building, chest upstairsIn a chest after Fire Demon. Cage Spiders activate open opening it.
US: Irithyll Straight Sword - miniboss drop, by Road of SacrificesDropped by the Boreal Outright Knight before Road of Sacrifices
US: Kukri - hanging corpse above burning treeHanging corpse high above the burning tree with the Cathedral Evangelist. Must be shot down with an arrow or projective.
US: Large Club - tower village, by minibossIn the Fire Demon area
US: Large Soul of a Deserted Corpse - across from Foot of the High WallOn the opposite tower from the Foot of the High Wall bonfire
US: Large Soul of a Deserted Corpse - around corner by Cliff UndersideAfter going up the stairs from Curse-Rotted Greatwood to Cliff Underside area, on a cliff edge to the right
US: Large Soul of a Deserted Corpse - by white treeNear the Birch Tree where giant shoots arrows
US: Large Soul of a Deserted Corpse - hanging corpse room, over stairsOn a hanging corpse in the building after the burning tree. Can be knocked down by dropping onto the stairs through the broken railing.
US: Large Soul of a Deserted Corpse - on the way to tower, by wellAfter the ravine bridge leading toward Eygon and the Giant's tower, next to the well to the right
US: Large Soul of a Deserted Corpse - stableIn the building with stables across the bridge and to the right from the first Undead Settlement building
US: Life Ring+1 - tower on the way to villageOn the wooden rafters near where Siegward is waiting for Fire Demon
US: Loincloth - by Velka statueNext to the Velka statue. Requires Grave Key or dropping down near Eygon and backtracking through the skeleton area.
US: Loretta's Bone - first building, hanging corpse on balconyOn a hanging corpse after the first building, can be knocked down by rolling into it
US: Mirrah Gloves - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Trousers - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mirrah Vest - tower village, jump from roofAt the end of the Fire Demon loop, in the tower where you have to drop down after the roof
US: Mortician's Ashes - graveyard by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, at the close end of the graveyard
US: Mound-makers - HodrickGiven by Hodrick if accessing the Pit of Hollows before fighting Curse-Rotted Greatwood, or dropped after invading him with Sirris.
US: Northern Armor - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Gloves - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Helm - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Northern Trousers - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Old Sage's Blindfold - kill CornyxDropped by Cornyx
US: Pale Tongue - tower village, hanging corpseHanging corpse in the Fire Demon fight area, can be knocked down by rolling into it
US: Partizan - hanging corpse above Cliff UndersideOn a hanging corpse on the path from Cliff Underside to Cornyx's cage. Must be shot down with an arrow or projective.
US: Plank Shield - outside stable, by NPCIn the thrall area across the bridge from the first Undead Settlement building, on a cliff edge overlooking the ravine bridge.
US: Poisonbite Ring+1 - graveyard by white tree, near wellBehind the well in the back of area where the Giant shoots arrows, nearby where the flamberge-wielding thrall drops down.
US: Pyromancy Flame - CornyxGiven by Cornyx in Firelink Shrine or dropped.
US: Red Bug Pellet - tower village building, basementOn the floor of the building after the Fire Demon encounter
US: Red Hilted Halberd - chasm cryptIn the skeleton area accessible from Grave Key or dropping down from near Eygon
US: Red and White Shield - chasm, hanging corpseOn a hanging corpse in the ravine accessible with the Grave Key or dropping down near Eygon, to the entrance of Irina's prison. Must be shot down with an arrow or projective.
US: Reinforced Club - by white treeNear the Birch Tree where giant shoots arrows
US: Repair Powder - first building, balconyOn the balcony of the first Undead Settlement building
US: Rusted Coin - awning above Dilapidated BridgeOn a wooden ledge near the Dilapidated Bridge bonfire. Must be jumped to from near Cathedral Evangelist enemy
US: Saint's Talisman - chasm, by ladderFrom the ravine accessible via Grave Key or dropping near Eygon, before ladder leading up to Irina of Carim
US: Sharp Gem - lizard by Dilapidated BridgeDrop by Crystal Lizard near Dilapidated Bridge bonfire.
US: Siegbräu - SiegwardGiven by Siegward after helping him defeat the Fire Demon.
US: Small Leather Shield - first building, hanging corpse by entranceHanging corpse in the first building, to the right of the entrance
US: Soul of a Nameless Soldier - top of towerAt the top of the tower where Giant shoots arrows
US: Soul of an Unknown Traveler - back alley, past cratesAfter exiting the building after the burning tree on the way to the Dilapidated Bridge bonfire. Hidden behind some crates between two buildings on the right.
US: Soul of an Unknown Traveler - chasm cryptIn the skeleton area accessible Grave Key or dropping down from near Eygon
US: Soul of an Unknown Traveler - pillory past stableIn the area bombarded by firebombs above the Cliff Underside bonfire
US: Soul of an Unknown Traveler - portcullis by burning treeBehind a grate to the left of the burning tree and Cathedral Evangelist
US: Soul of the Rotted GreatwoodDropped by Curse Rotted Greatwood
US: Spotted Whip - by Cornyx's cage after Cuculus questAppears next to Cornyx's cage after defeating Old Demon King with Cuculus surviving
US: Sunset Armor - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Gauntlets - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Helm - Pit of Hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Sunset Leggings - pit of hollows after killing Hodrick w/SirrisFound in Pit of Hollows after completing Sirris' questline.
US: Titanite Shard - back alley, side pathOn a side path to the right of the Cathedral Evangelist before the Dilapidated Bridge bonfire
US: Titanite Shard - back alley, up ladderNext to the Cathedral Evangelist close to the Dilapidated Bridge bonfire
US: Titanite Shard - chasm #1In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - chasm #2In the ravine accessible from Grave Key or dropping down from near Eygon
US: Titanite Shard - lower path to Cliff UndersideAt the end of the cliffside path next to Cliff Underside bonfire, guarded by a Hollow Peasant wielding a four-pronged plow.
US: Titanite Shard - porch after burning treeIn front of the building after the burning tree and Cathedral Evangelist
US: Tower Key - kill IrinaDropped by Irina of Carim
US: Transposing Kiln - boss dropDropped by Curse Rotted Greatwood
US: Undead Bone Shard - by white treeIn the area past the Dilapidated Bridge bonfire, where the Giant is shooting arrows, jumping to the floating platform on the right
US: Wargod Wooden Shield - Pit of HollowsIn the Pit of Hollows
US: Warrior of Sunlight - hanging corpse room, drop through holeDropping through a hole in the floor in the first building after the burning tree.
US: Whip - back alley, behind wooden wallIn one of the houses between building after the burning tree and the Dilapidated Bridge bonfire
US: Young White Branch - by white tree #1Near the Birch Tree where giant shoots arrows
US: Young White Branch - by white tree #2Near the Birch Tree where giant shoots arrows
+ diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 61215dbc60..ed90289a8b 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -7,48 +7,49 @@ ## Optional Software -- [Dark Souls III Maptracker Pack](https://github.com/Br00ty/DS3_AP_Maptracker/releases/latest), for use with [Poptracker](https://github.com/black-sliver/PopTracker/releases) +- Map tracker not yet updated for 3.0.0 -## General Concept +## Setting Up - -**This mod can ban you permanently from the FromSoftware servers if used online.** - -The Dark Souls III AP Client is a dinput8.dll triggered when launching Dark Souls III. This .dll file will launch a command -prompt where you can read information about your run and write any command to interact with the Archipelago server. +First, download the client from the link above. It doesn't need to go into any particular directory; +it'll automatically locate _Dark Souls III_ in your Steam installation folder. -This client has only been tested with the Official Steam version of the game at version 1.15. It does not matter which DLCs are installed. However, you will have to downpatch your Dark Souls III installation from current patch. +Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This +is the latest version, so you don't need to do any downpatching! However, if you've already +downpatched your game to use an older version of the randomizer, you'll need to reinstall the latest +version before using this version. -## Downpatching Dark Souls III +### One-Time Setup -To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database. +Before you first connect to a multiworld, you need to generate the local data files for your world's +randomized item and (optionally) enemy locations. You only need to do this once per multiworld. -1. Launch Steam (in online mode). -2. Press the Windows Key + R. This will open the Run window. -3. Open the Steam console by typing the following string: `steam://open/console`. Steam should now open in Console Mode. -4. Insert the string of the depot you wish to download. For the AP-supported v1.15, you will want to use: `download_depot 374320 374321 4471176929659548333`. -5. Steam will now download the depot. Note: There is no progress bar for the download in Steam, but it is still downloading in the background. -6. Back up your existing game executable (`DarkSoulsIII.exe`) found in `\Steam\steamapps\common\DARK SOULS III\Game`. Easiest way to do this is to move it to another directory. If you have file extensions enabled, you can instead rename the executable to `DarkSoulsIII.exe.bak`. -7. Return to the Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like `\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX`. -8. Take the `DarkSoulsIII.exe` from that folder and place it in `\Steam\steamapps\common\DARK SOULS III\Game`. -9. Back up and delete your save file (`DS30000.sl2`) in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type `%appdata%` and hit enter. Alternatively: open File Explorer > View > Hidden Items and follow `C:\Users\\AppData\Roaming\DarkSoulsIII\`. -10. If you did all these steps correctly, you should be able to confirm your game version in the upper-left corner after launching Dark Souls III. +1. Before you first connect to a multiworld, run `randomizer\DS3Randomizer.exe`. +2. Put in your Archipelago room address (usually something like `archipelago.gg:12345`), your player + name (also known as your "slot name"), and your password if you have one. -## Installing the Archipelago mod +3. Click "Load" and wait a minute or two. -Get the `dinput8.dll` from the [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) and -add it at the root folder of your game (e.g. `SteamLibrary\steamapps\common\DARK SOULS III\Game`) +### Running and Connecting the Game -## Joining a MultiWorld Game +To run _Dark Souls III_ in Archipelago mode: -1. Run Steam in offline mode to avoid being banned. -2. Launch Dark Souls III. -3. Type in `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME} password:{PASSWORD}` in the "Windows Command Prompt" that opened. For example: `/connect archipelago.gg:38281 "Example Name" password:"Example Password"`. The password parameter is only necessary if your game requires one. -4. Once connected, create a new game, choose a class and wait for the others before starting. -5. You can quit and launch at anytime during a game. +1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the + DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. -## Where do I get a config file? +2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that + you can use to interact with the Archipelago server. + +3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the + appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`. + +4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have + control of your character and the connection is established. + +## Frequently Asked Questions + +### Where do I get a config file? The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to configure your personal options and export them into a config file. diff --git a/worlds/dark_souls_3/test/TestDarkSouls3.py b/worlds/dark_souls_3/test/TestDarkSouls3.py new file mode 100644 index 0000000000..e590cd732b --- /dev/null +++ b/worlds/dark_souls_3/test/TestDarkSouls3.py @@ -0,0 +1,27 @@ +from test.TestBase import WorldTestBase + +from worlds.dark_souls_3.Items import item_dictionary +from worlds.dark_souls_3.Locations import location_tables +from worlds.dark_souls_3.Bosses import all_bosses + +class DarkSouls3Test(WorldTestBase): + game = "Dark Souls III" + + def testLocationDefaultItems(self): + for locations in location_tables.values(): + for location in locations: + if location.default_item_name: + self.assertIn(location.default_item_name, item_dictionary) + + def testLocationsUnique(self): + names = set() + for locations in location_tables.values(): + for location in locations: + self.assertNotIn(location.name, names) + names.add(location.name) + + def testBossLocations(self): + all_locations = {location.name for locations in location_tables.values() for location in locations} + for boss in all_bosses: + for location in boss.locations: + self.assertIn(location, all_locations) diff --git a/worlds/dark_souls_3/test/__init__.py b/worlds/dark_souls_3/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 6e41c6067208d0286f56694f4281d03a0aaf6dad Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 13:13:01 +0100 Subject: [PATCH 098/222] Core: Check parent_region.can_reach first in Location.can_reach (#3724) * Core: Check parent_region.can_reach first in Location.can_reach The comment about self.access_rule computing faster on average appears to no longer be correct with the current caching system for region accessibility, resulting in self.parent_region.can_reach computing faster on average. Generation of template yamls for each game that does not require a rom to generate, generated with `python -O .\Generate.py --seed 1` (all durations averaged over at 4 or 5 generations): Full generation with `spoiler: 1` and no progression balancing: 89.9s -> 72.6s Only output from above case: 2.6s -> 2.2s Full generation with `spoiler: 3` and no progression balancing: 769.9s -> 627.1s Only playthrough calculation + paths from above case: 680.5s -> 555.3s Full generation with `spoiler: 1` with default progression balancing: 123.5s -> 98.3s Only progression balancing from above case: 11.3s -> 9.6s * Update BaseClasses.py * Update BaseClasses.py * Update BaseClasses.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 81601506d0..34e7248415 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1128,9 +1128,9 @@ class Location: and (not check_access or self.can_reach(state)))) def can_reach(self, state: CollectionState) -> bool: - # self.access_rule computes faster on average, so placing it first for faster abort + # Region.can_reach is just a cache lookup, so placing it first for faster abort on average assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and self.parent_region.can_reach(state) + return self.parent_region.can_reach(state) and self.access_rule(state) def place_locked_item(self, item: Item): if self.item: From 30f97dd7dead5cad3a9d7a1eecf3737a210098c7 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 13:25:39 +0100 Subject: [PATCH 099/222] Core: Speed up CollectionState.copy() using built-in copy methods (#3678) All the types being copied are built-in types with their own `copy()` methods, so using the `copy` module was a bit overkill and also slower. This patch replaces the use of the `copy` module in `CollectionState.copy()` with using the built-in `.copy()` methods. The copying of `reachable_regions` and `blocked_connections` was also iterating the keys of each dictionary and then looking up the value in the dictionary for that key. It is faster, and I think more readable, to iterate the dictionary's `.items()` instead. For me, when generating a multiworld including the template yaml of every world with `python -O .\Generate.py --skip_output`, this patch saves about 2.1s. The overall generation duration for these yamls varies quite a lot, but averages around 160s for me, so on average this patch reduced overall generation duration (excluding output duration) by around 1.3%. Timing comparisons were made by calling time.perf_counter() at the start and end of `CollectionState.copy()`'s body, and summing the differences between the starts and ends of the method body into a global variable that was printed at the end of generation. Additional timing comparisons were made, using the `timeit` module, of the individual function calls or dictionary comprehensions used to perform the copying. The main performance cost was `copy.deepcopy()`, which gets slow as the number of keys multiplied by the number of values within the sets/Counters gets large, e.g., to deepcopy a `dict[int, Counter[str]]` with 100 keys and where each Counter contains 100 keys was 30x slower than most other tested copying methods. Increasing the number of dict keys or Counter keys only makes it slower. --- BaseClasses.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 34e7248415..092f330bcb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,7 +1,6 @@ from __future__ import annotations import collections -import copy import itertools import functools import logging @@ -719,14 +718,14 @@ class CollectionState(): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = copy.deepcopy(self.prog_items) - ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in - self.reachable_regions} - ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in - self.blocked_connections} - ret.events = copy.copy(self.events) - ret.path = copy.copy(self.path) - ret.locations_checked = copy.copy(self.locations_checked) + ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} + ret.reachable_regions = {player: region_set.copy() for player, region_set in + self.reachable_regions.items()} + ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in + self.blocked_connections.items()} + ret.events = self.events.copy() + ret.path = self.path.copy() + ret.locations_checked = self.locations_checked.copy() for function in self.additional_copy_functions: ret = function(self, ret) return ret From ac7590e621be1662e9522022670c5949e0195cfa Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Fri, 9 Aug 2024 16:02:41 +0100 Subject: [PATCH 100/222] HK: fix iterating all worlds instead of only HK worlds in stage_pre_fill (#3750) Would cause generation to fail when generating with HK and another game. Mistake in 6803c373e5ff. --- worlds/hk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 99277378a1..cbb9096061 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -467,7 +467,7 @@ class HKWorld(World): worlds = [world for world in multiworld.get_game_worlds(cls.game) if world.options.Goal in ["any", "grub_hunt"]] if worlds: grubs = [item for item in multiworld.get_items() if item.name == "Grub"] - all_grub_players = [world.player for world in multiworld.worlds.values() if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] + all_grub_players = [world.player for world in worlds if world.options.GrubHuntGoal == GrubHuntGoal.special_range_names["all"]] if all_grub_players: group_lookup = defaultdict(set) From c66a8605da1ba1aaaaac57bc2722ee1b585d4c5d Mon Sep 17 00:00:00 2001 From: Kaito Sinclaire Date: Fri, 9 Aug 2024 08:04:59 -0700 Subject: [PATCH 101/222] DOOM, DOOM II: Update steam URLs (#3746) --- worlds/doom_1993/docs/setup_en.md | 2 +- worlds/doom_ii/docs/setup_en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/doom_1993/docs/setup_en.md b/worlds/doom_1993/docs/setup_en.md index 8906efac9c..5d96e6a805 100644 --- a/worlds/doom_1993/docs/setup_en.md +++ b/worlds/doom_1993/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM_1993/) +- [DOOM 1993 (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software diff --git a/worlds/doom_ii/docs/setup_en.md b/worlds/doom_ii/docs/setup_en.md index 87054ab307..ec6697c76d 100644 --- a/worlds/doom_ii/docs/setup_en.md +++ b/worlds/doom_ii/docs/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2300/DOOM_II/) +- [DOOM II (e.g. Steam version)](https://store.steampowered.com/app/2280/DOOM__DOOM_II/) - [Archipelago Crispy DOOM](https://github.com/Daivuk/apdoom/releases) ## Optional Software From a6f376b02e48e98f253b42ca66fb99eaa927a1ab Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:38:42 -0400 Subject: [PATCH 102/222] TLOZ: world: multiworld (#3752) --- worlds/tloz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 8ea5f3e18c..c8c76bd85a 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -110,8 +110,8 @@ class TLoZWorld(World): if v is not None: location_name_to_id[k] = v + base_id - def __init__(self, world: MultiWorld, player: int): - super().__init__(world, player) + def __init__(self, multiworld: MultiWorld, player: int): + super().__init__(multiworld, player) self.generator_in_use = threading.Event() self.rom_name_available_event = threading.Event() self.levels = None From 9dba39b6064b162124885f556b7b72476774907e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:08:24 +0200 Subject: [PATCH 103/222] SoE: fix determinism (#3745) Fixes randomly placed ingredients not being deterministic (depending on settings) and in turn also fixes logic not being deterministic if they get replaced by fragments. --- worlds/soe/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 3baed165d8..161c749fd6 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -188,6 +188,7 @@ class SoEWorld(World): connect_name: str _halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name] + _fillers = sorted(item_name_groups["Ingredients"]) def __init__(self, multiworld: "MultiWorld", player: int): self.connect_name_available_event = threading.Event() @@ -469,7 +470,7 @@ class SoEWorld(World): multidata["connect_names"][self.connect_name] = payload def get_filler_item_name(self) -> str: - return self.random.choice(list(self.item_name_groups["Ingredients"])) + return self.random.choice(self._fillers) class SoEItem(Item): From 8e06ab4f688c5e32350bea51ae47f8fcb3dd3e71 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 10 Aug 2024 06:49:32 -0500 Subject: [PATCH 104/222] Core: fix invalid __package__ of zipped worlds (#3686) * fix invalid package fix * add comment describing fix --- worlds/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/__init__.py b/worlds/__init__.py index bb2fe866d0..c277ac9ca1 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -73,7 +73,12 @@ class WorldSource: else: # TODO: remove with 3.8 support mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) - mod.__package__ = f"worlds.{mod.__package__}" + if mod.__package__ is not None: + mod.__package__ = f"worlds.{mod.__package__}" + else: + # load_module does not populate package, we'll have to assume mod.__name__ is correct here + # probably safe to remove with 3.8 support + mod.__package__ = f"worlds.{mod.__name__}" mod.__name__ = f"worlds.{mod.__name__}" sys.modules[mod.__name__] = mod with warnings.catch_warnings(): From 68a92b0c6fe5b011da20ab5338c3f23757f1876d Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 11 Aug 2024 08:47:17 -0400 Subject: [PATCH 105/222] Clique: Update to new options API (#3759) --- worlds/clique/Items.py | 13 ++++++++----- worlds/clique/Locations.py | 11 +++++++---- worlds/clique/Options.py | 16 ++++++++-------- worlds/clique/Rules.py | 13 ++++++++----- worlds/clique/__init__.py | 32 ++++++++++++++++---------------- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/worlds/clique/Items.py b/worlds/clique/Items.py index 5474f58b82..81e2540bac 100644 --- a/worlds/clique/Items.py +++ b/worlds/clique/Items.py @@ -1,6 +1,9 @@ -from typing import Callable, Dict, NamedTuple, Optional +from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import Item, ItemClassification, MultiWorld +from BaseClasses import Item, ItemClassification + +if TYPE_CHECKING: + from . import CliqueWorld class CliqueItem(Item): @@ -10,7 +13,7 @@ class CliqueItem(Item): class CliqueItemData(NamedTuple): code: Optional[int] = None type: ItemClassification = ItemClassification.filler - can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True + can_create: Callable[["CliqueWorld"], bool] = lambda world: True item_data_table: Dict[str, CliqueItemData] = { @@ -21,11 +24,11 @@ item_data_table: Dict[str, CliqueItemData] = { "Button Activation": CliqueItemData( code=69696968, type=ItemClassification.progression, - can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), + can_create=lambda world: world.options.hard_mode, ), "A Cool Filler Item (No Satisfaction Guaranteed)": CliqueItemData( code=69696967, - can_create=lambda multiworld, player: False # Only created from `get_filler_item_name`. + can_create=lambda world: False # Only created from `get_filler_item_name`. ), "The Urge to Push": CliqueItemData( type=ItemClassification.progression, diff --git a/worlds/clique/Locations.py b/worlds/clique/Locations.py index 144becae53..900b497eb4 100644 --- a/worlds/clique/Locations.py +++ b/worlds/clique/Locations.py @@ -1,6 +1,9 @@ -from typing import Callable, Dict, NamedTuple, Optional +from typing import Callable, Dict, NamedTuple, Optional, TYPE_CHECKING -from BaseClasses import Location, MultiWorld +from BaseClasses import Location + +if TYPE_CHECKING: + from . import CliqueWorld class CliqueLocation(Location): @@ -10,7 +13,7 @@ class CliqueLocation(Location): class CliqueLocationData(NamedTuple): region: str address: Optional[int] = None - can_create: Callable[[MultiWorld, int], bool] = lambda multiworld, player: True + can_create: Callable[["CliqueWorld"], bool] = lambda world: True locked_item: Optional[str] = None @@ -22,7 +25,7 @@ location_data_table: Dict[str, CliqueLocationData] = { "The Item on the Desk": CliqueLocationData( region="The Button Realm", address=69696968, - can_create=lambda multiworld, player: bool(getattr(multiworld, "hard_mode")[player]), + can_create=lambda world: world.options.hard_mode, ), "In the Player's Mind": CliqueLocationData( region="The Button Realm", diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py index 7976dcb621..d88a128990 100644 --- a/worlds/clique/Options.py +++ b/worlds/clique/Options.py @@ -1,6 +1,5 @@ -from typing import Dict - -from Options import Choice, Option, Toggle +from dataclasses import dataclass +from Options import Choice, Toggle, PerGameCommonOptions, StartInventoryPool class HardMode(Toggle): @@ -25,10 +24,11 @@ class ButtonColor(Choice): option_black = 11 -clique_options: Dict[str, type(Option)] = { - "color": ButtonColor, - "hard_mode": HardMode, +@dataclass +class CliqueOptions(PerGameCommonOptions): + color: ButtonColor + hard_mode: HardMode + start_inventory_from_pool: StartInventoryPool # DeathLink is always on. Always. - # "death_link": DeathLink, -} + # death_link: DeathLink diff --git a/worlds/clique/Rules.py b/worlds/clique/Rules.py index 5ae1d2c68e..63ecd4e9e1 100644 --- a/worlds/clique/Rules.py +++ b/worlds/clique/Rules.py @@ -1,10 +1,13 @@ -from typing import Callable +from typing import Callable, TYPE_CHECKING -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState + +if TYPE_CHECKING: + from . import CliqueWorld -def get_button_rule(multiworld: MultiWorld, player: int) -> Callable[[CollectionState], bool]: - if getattr(multiworld, "hard_mode")[player]: - return lambda state: state.has("Button Activation", player) +def get_button_rule(world: "CliqueWorld") -> Callable[[CollectionState], bool]: + if world.options.hard_mode: + return lambda state: state.has("Button Activation", world.player) return lambda state: True diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index b5cc74d94a..3d06e477eb 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -1,10 +1,10 @@ -from typing import List +from typing import List, Dict, Any from BaseClasses import Region, Tutorial from worlds.AutoWorld import WebWorld, World from .Items import CliqueItem, item_data_table, item_table from .Locations import CliqueLocation, location_data_table, location_table, locked_locations -from .Options import clique_options +from .Options import CliqueOptions from .Regions import region_data_table from .Rules import get_button_rule @@ -38,7 +38,8 @@ class CliqueWorld(World): game = "Clique" web = CliqueWebWorld() - option_definitions = clique_options + options: CliqueOptions + options_dataclass = CliqueOptions location_name_to_id = location_table item_name_to_id = item_table @@ -48,7 +49,7 @@ class CliqueWorld(World): def create_items(self) -> None: item_pool: List[CliqueItem] = [] for name, item in item_data_table.items(): - if item.code and item.can_create(self.multiworld, self.player): + if item.code and item.can_create(self): item_pool.append(self.create_item(name)) self.multiworld.itempool += item_pool @@ -61,41 +62,40 @@ class CliqueWorld(World): # Create locations. for region_name, region_data in region_data_table.items(): - region = self.multiworld.get_region(region_name, self.player) + region = self.get_region(region_name) region.add_locations({ location_name: location_data.address for location_name, location_data in location_data_table.items() - if location_data.region == region_name and location_data.can_create(self.multiworld, self.player) + if location_data.region == region_name and location_data.can_create(self) }, CliqueLocation) region.add_exits(region_data_table[region_name].connecting_regions) # Place locked locations. for location_name, location_data in locked_locations.items(): # Ignore locations we never created. - if not location_data.can_create(self.multiworld, self.player): + if not location_data.can_create(self): continue locked_item = self.create_item(location_data_table[location_name].locked_item) - self.multiworld.get_location(location_name, self.player).place_locked_item(locked_item) + self.get_location(location_name).place_locked_item(locked_item) # Set priority location for the Big Red Button! - self.multiworld.priority_locations[self.player].value.add("The Big Red Button") + self.options.priority_locations.value.add("The Big Red Button") def get_filler_item_name(self) -> str: return "A Cool Filler Item (No Satisfaction Guaranteed)" def set_rules(self) -> None: - button_rule = get_button_rule(self.multiworld, self.player) - self.multiworld.get_location("The Big Red Button", self.player).access_rule = button_rule - self.multiworld.get_location("In the Player's Mind", self.player).access_rule = button_rule + button_rule = get_button_rule(self) + self.get_location("The Big Red Button").access_rule = button_rule + self.get_location("In the Player's Mind").access_rule = button_rule # Do not allow button activations on buttons. - self.multiworld.get_location("The Big Red Button", self.player).item_rule =\ - lambda item: item.name != "Button Activation" + self.get_location("The Big Red Button").item_rule = lambda item: item.name != "Button Activation" # Completion condition. self.multiworld.completion_condition[self.player] = lambda state: state.has("The Urge to Push", self.player) - def fill_slot_data(self): + def fill_slot_data(self) -> Dict[str, Any]: return { - "color": getattr(self.multiworld, "color")[self.player].current_key + "color": self.options.color.current_key } From 09e052c750fcd1fa2e56afb8e9ec39e95775e382 Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 12 Aug 2024 00:24:09 +0200 Subject: [PATCH 106/222] Timespinner: Fix eels check logic #3777 --- worlds/timespinner/Locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 86839f0f21..f99dd76155 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -135,11 +135,11 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio LocationData('Upper Lake Serene', 'Lake Serene: Pyramid keys room', 1337104), LocationData('Upper Lake Serene', 'Lake Serene (Upper): Chicken ledge', 1337174), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Deep dive', 1337105), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Under the eels', 1337106), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Under the eels', 1337106, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Water spikes room', 1337107), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater secret', 1337108, logic.can_break_walls), LocationData('Lower Lake Serene', 'Lake Serene (Lower): T chest', 1337109, lambda state: flooded.flood_lake_serene or logic.has_doublejump_of_npc(state)), - LocationData('Lower Lake Serene', 'Lake Serene (Lower): Past the eels', 1337110), + LocationData('Left Side forest Caves', 'Lake Serene (Lower): Past the eels', 1337110, lambda state: state.has('Water Mask', player)), LocationData('Lower Lake Serene', 'Lake Serene (Lower): Underwater pedestal', 1337111, lambda state: flooded.flood_lake_serene or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Shroom jump room', 1337112, lambda state: flooded.flood_maw or logic.has_doublejump(state)), LocationData('Caves of Banishment (upper)', 'Caves of Banishment (Maw): Secret room', 1337113, lambda state: logic.can_break_walls(state) and (not flooded.flood_maw or state.has('Water Mask', player))), From 21bbf5fb95bcf6c1cc842197ca44ec253c5daeb4 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 11 Aug 2024 18:24:30 -0400 Subject: [PATCH 107/222] TUNIC: Add note to Universal Tracker stuff #3772 --- worlds/tunic/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 5253e99514..6657b464ed 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -410,7 +410,9 @@ class TunicWorld(World): return slot_data # for the universal tracker, doesn't get called in standard gen + # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md @staticmethod def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]: # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough + # we are using re_gen_passthrough over modifying the world here due to complexities with ER return slot_data From ae0abd38217d03bba2f2ab78ee6a2311c5fdb995 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 11 Aug 2024 17:57:59 -0500 Subject: [PATCH 108/222] Core: change start inventory from pool to warn when nothing to remove (#3158) * makes start inventory from pool warn and fixes the itempool to match when it can not find a matching item to remove * calc the difference correctly * save new filler and non-removed items differently so we don't remove existing items at random --- Main.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Main.py b/Main.py index ce054dcd39..6dc03aaa55 100644 --- a/Main.py +++ b/Main.py @@ -151,6 +151,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # Because some worlds don't actually create items during create_items this has to be as late as possible. if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids): new_items: List[Item] = [] + old_items: List[Item] = [] depletion_pool: Dict[int, Dict[str, int]] = { player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", @@ -169,20 +170,24 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No depletion_pool[item.player][item.name] -= 1 # quick abort if we have found all items if not target: - new_items.extend(multiworld.itempool[i+1:]) + old_items.extend(multiworld.itempool[i+1:]) break else: - new_items.append(item) + old_items.append(item) # leftovers? if target: for player, remaining_items in depletion_pool.items(): remaining_items = {name: count for name, count in remaining_items.items() if count} if remaining_items: - raise Exception(f"{multiworld.get_player_name(player)}" + logger.warning(f"{multiworld.get_player_name(player)}" f" is trying to remove items from their pool that don't exist: {remaining_items}") - assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change." - multiworld.itempool[:] = new_items + # find all filler we generated for the current player and remove until it matches + removables = [item for item in new_items if item.player == player] + for _ in range(sum(remaining_items.values())): + new_items.remove(removables.pop()) + assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change." + multiworld.itempool[:] = new_items + old_items multiworld.link_items() From a3e54a951fb2ea322fe5a13ba09024e57425a4a2 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 11 Aug 2024 19:53:40 -0400 Subject: [PATCH 109/222] Undertale: Fix slot_data and options.as_dict() (#3774) * Undertale: Fixing slot_data * Booleans were difficult --- worlds/undertale/__init__.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 9084c77b00..9f09bb3452 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -67,12 +67,15 @@ class UndertaleWorld(World): "only_flakes": bool(self.options.only_flakes.value), "no_equips": bool(self.options.no_equips.value), "key_hunt": bool(self.options.key_hunt.value), - "key_pieces": self.options.key_pieces.value, - "rando_love": bool(self.options.rando_love.value), - "rando_stats": bool(self.options.rando_stats.value), + "key_pieces": int(self.options.key_pieces.value), + "rando_love": bool(self.options.rando_love and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), + "rando_stats": bool(self.options.rando_stats and (self.options.route_required == "genocide" or self.options.route_required == "all_routes")), "prog_armor": bool(self.options.prog_armor.value), "prog_weapons": bool(self.options.prog_weapons.value), - "rando_item_button": bool(self.options.rando_item_button.value) + "rando_item_button": bool(self.options.rando_item_button.value), + "route_required": int(self.options.route_required.value), + "temy_include": int(self.options.temy_include.value) + } def get_filler_item_name(self): @@ -220,16 +223,7 @@ class UndertaleWorld(World): link_undertale_areas(self.multiworld, self.player) def fill_slot_data(self): - slot_data = self._get_undertale_data() - for option_name in self.options.as_dict(): - option = getattr(self.multiworld, option_name)[self.player] - if (option_name == "rando_love" or option_name == "rando_stats") and \ - self.options.route_required != "genocide" and \ - self.options.route_required != "all_routes": - option.value = False - if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: - slot_data[option_name] = int(option.value) - return slot_data + return self._get_undertale_data() def create_item(self, name: str) -> Item: item_data = item_table[name] From 67520adceae7d7a1222afd319ab274868f3bd3b9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 11 Aug 2024 20:13:45 -0400 Subject: [PATCH 110/222] Core: Error on empty options.as_dict (#3773) * Error on empty options.as_dict * ValueError instead * Apply suggestions from code review Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Options.py b/Options.py index d040828509..e22ee1ee63 100644 --- a/Options.py +++ b/Options.py @@ -1236,6 +1236,7 @@ class CommonOptions(metaclass=OptionsMetaProperty): :param option_names: names of the options to return :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` """ + assert option_names, "options.as_dict() was used without any option names." option_results = {} for option_name in option_names: if option_name in type(self).type_hints: From 50330cf32f05e5e1afb65b51f4fd5c01391c3534 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 12 Aug 2024 19:32:14 +0200 Subject: [PATCH 111/222] Core: Remove broken unused code from Options.py (#3781) "Unused" is a baseless assertion, but this code path has been crashing on the first statement for 6 months and noone's complained --- Options.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/Options.py b/Options.py index e22ee1ee63..ecde6275f1 100644 --- a/Options.py +++ b/Options.py @@ -1518,31 +1518,3 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: f.write(res) - - -if __name__ == "__main__": - - from worlds.alttp.Options import Logic - import argparse - - map_shuffle = Toggle - compass_shuffle = Toggle - key_shuffle = Toggle - big_key_shuffle = Toggle - hints = Toggle - test = argparse.Namespace() - test.logic = Logic.from_text("no_logic") - test.map_shuffle = map_shuffle.from_text("ON") - test.hints = hints.from_text('OFF') - try: - test.logic = Logic.from_text("overworld_glitches_typo") - except KeyError as e: - print(e) - try: - test.logic_owg = Logic.from_text("owg") - except KeyError as e: - print(e) - if test.map_shuffle: - print("map_shuffle is on") - print(f"Hints are {bool(test.hints)}") - print(test) From dcaa2f7b971d9b41de7a84331008a095223ac997 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:02:09 -0400 Subject: [PATCH 112/222] Core: Two Small Fixes (#3782) --- BaseClasses.py | 2 +- Launcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 092f330bcb..97e792cc5c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1427,7 +1427,7 @@ class Spoiler: # Maybe move the big bomb over to the Event system instead? if any(exit_path == 'Pyramid Fairy' for path in self.paths.values() for (_, exit_path) in path): - if multiworld.mode[player] != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': self.paths[str(multiworld.get_region('Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Big Bomb Shop', player)) else: diff --git a/Launcher.py b/Launcher.py index e4b65be93a..6b66b2a3a6 100644 --- a/Launcher.py +++ b/Launcher.py @@ -266,7 +266,7 @@ def run_gui(): if file and component: run_component(component, file) else: - logging.warning(f"unable to identify component for {filename}") + logging.warning(f"unable to identify component for {file}") def _stop(self, *largs): # ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm. From 96d48a923a34a1cd4c5f64a5f2e12acc657dd041 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 15:28:05 -0500 Subject: [PATCH 113/222] Core: recontextualize `CollectionState.collect` (#3723) * Core: renamed `CollectionState.collect` arg from `event` to `prevent_sweep` and remove forced collection * Update TestDungeon.py --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- BaseClasses.py | 8 +-- test/bases.py | 2 +- worlds/alttp/test/dungeons/TestDungeon.py | 4 +- worlds/oot/EntranceShuffle.py | 2 +- worlds/oot/__init__.py | 2 +- worlds/stardew_valley/test/TestCrops.py | 8 +-- .../stardew_valley/test/TestDynamicGoals.py | 34 ++++++------ worlds/stardew_valley/test/TestLogic.py | 2 +- worlds/stardew_valley/test/__init__.py | 8 +-- .../test/assertion/world_assert.py | 4 +- .../stardew_valley/test/rules/TestArcades.py | 52 +++++++++---------- .../test/rules/TestBuildings.py | 14 ++--- .../test/rules/TestCookingRecipes.py | 32 ++++++------ .../test/rules/TestCraftingRecipes.py | 26 +++++----- .../test/rules/TestDonations.py | 6 +-- .../test/rules/TestFriendship.py | 34 ++++++------ .../stardew_valley/test/rules/TestShipping.py | 2 +- worlds/stardew_valley/test/rules/TestTools.py | 40 +++++++------- .../stardew_valley/test/rules/TestWeapons.py | 16 +++--- 19 files changed, 146 insertions(+), 150 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 97e792cc5c..95f24af265 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -863,19 +863,15 @@ class CollectionState(): ) # Item related - def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: + def collect(self, item: Item, prevent_sweep: bool = False, location: Optional[Location] = None) -> bool: if location: self.locations_checked.add(location) changed = self.multiworld.worlds[item.player].collect(self, item) - if not changed and event: - self.prog_items[item.player][item.name] += 1 - changed = True - self.stale[item.player] = True - if changed and not event: + if changed and not prevent_sweep: self.sweep_for_events() return changed diff --git a/test/bases.py b/test/bases.py index 5c2d241cbb..9fb223af2a 100644 --- a/test/bases.py +++ b/test/bases.py @@ -23,7 +23,7 @@ class TestBase(unittest.TestCase): state = CollectionState(self.multiworld) for item in items: item.classification = ItemClassification.progression - state.collect(item, event=True) + state.collect(item, prevent_sweep=True) state.sweep_for_events() state.update_reachable_regions(1) self._state_cache[self.multiworld, tuple(items)] = state diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 91fc462c4e..796bfeec3f 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase): for item in items: item.classification = ItemClassification.progression - state.collect(item, event=True) # event=True prevents running sweep_for_events() and picking up + state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_events() and picking up state.sweep_for_events() # key drop keys repeatedly - self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") \ No newline at end of file + self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}") diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index bbdc30490c..058fdbed00 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -796,7 +796,7 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() - time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) + time_travel_state.collect(ootworld.create_item('Time Travel'), prevent_sweep=True) time_travel_state._oot_update_age_reachable_regions(player) # Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 89f10a5a2d..24be303f82 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1388,7 +1388,7 @@ class OOTWorld(World): self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: - all_state.collect(self.create_item("Scarecrow Song"), event=True) + all_state.collect(self.create_item("Scarecrow Song"), prevent_sweep=True) all_state.stale[self.player] = True return all_state diff --git a/worlds/stardew_valley/test/TestCrops.py b/worlds/stardew_valley/test/TestCrops.py index 38b736367b..362e6bf27e 100644 --- a/worlds/stardew_valley/test/TestCrops.py +++ b/worlds/stardew_valley/test/TestCrops.py @@ -11,10 +11,10 @@ class TestCropsanityRules(SVTestBase): harvest_cactus = self.world.logic.region.can_reach_location("Harvest Cactus Fruit") self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), event=False) - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) - self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), event=False) + self.multiworld.state.collect(self.world.create_item("Cactus Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) + self.multiworld.state.collect(self.world.create_item("Desert Obelisk"), prevent_sweep=False) self.assert_rule_false(harvest_cactus, self.multiworld.state) - self.multiworld.state.collect(self.world.create_item("Greenhouse"), event=False) + self.multiworld.state.collect(self.world.create_item("Greenhouse"), prevent_sweep=False) self.assert_rule_true(harvest_cactus, self.multiworld.state) diff --git a/worlds/stardew_valley/test/TestDynamicGoals.py b/worlds/stardew_valley/test/TestDynamicGoals.py index fe1bfb5f30..bfa58dd340 100644 --- a/worlds/stardew_valley/test/TestDynamicGoals.py +++ b/worlds/stardew_valley/test/TestDynamicGoals.py @@ -12,29 +12,29 @@ from ..strings.wallet_item_names import Wallet def collect_fishing_abilities(tester: SVTestBase): for i in range(4): - tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APTool.axe), event=False) - tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), event=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.fishing_rod), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.pickaxe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APTool.axe), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(APWeapon.weapon), prevent_sweep=False) for i in range(10): - tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Combat Level"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Mining Level"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Fishing Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Combat Level"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Mining Level"), prevent_sweep=False) for i in range(17): - tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Spring"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Summer"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Fall"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Winter"), event=False) - tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), event=False) - tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), event=False) - tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), event=False) + tester.multiworld.state.collect(tester.world.create_item("Progressive Mine Elevator"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Spring"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Summer"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False) + tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False) def create_and_collect(tester: SVTestBase, item_name: str) -> StardewItem: item = tester.world.create_item(item_name) - tester.multiworld.state.collect(item, event=False) + tester.multiworld.state.collect(item, prevent_sweep=False) return item diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 65f7352a5e..da00a0f43e 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -12,7 +12,7 @@ from ..options import BundleRandomization def collect_all(mw): for item in mw.get_items(): - mw.state.collect(item, event=True) + mw.state.collect(item, prevent_sweep=True) class LogicTestBase(RuleAssertMixin, TestCase): diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 7e82ea91e4..c2c2a6a20b 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -257,16 +257,16 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): return super().run_default_tests def collect_lots_of_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) 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) + self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) def collect_all_the_money(self): - self.multiworld.state.collect(self.world.create_item("Shipping Bin"), event=False) + self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False) 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) + self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False) def collect_everything(self): non_event_items = [item for item in self.multiworld.get_items() if item.code] diff --git a/worlds/stardew_valley/test/assertion/world_assert.py b/worlds/stardew_valley/test/assertion/world_assert.py index c1c24bdf75..9717283454 100644 --- a/worlds/stardew_valley/test/assertion/world_assert.py +++ b/worlds/stardew_valley/test/assertion/world_assert.py @@ -33,14 +33,14 @@ class WorldAssertMixin(RuleAssertMixin, TestCase): self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_cannot_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_item_was_not_necessary_for_victory(self, item: StardewItem, multiworld: MultiWorld): self.assert_can_reach_victory(multiworld) multiworld.state.remove(item) self.assert_can_reach_victory(multiworld) - multiworld.state.collect(item, event=False) + multiworld.state.collect(item, prevent_sweep=False) self.assert_can_reach_victory(multiworld) def assert_can_win(self, multiworld: MultiWorld): diff --git a/worlds/stardew_valley/test/rules/TestArcades.py b/worlds/stardew_valley/test/rules/TestArcades.py index fb62a45637..2922ecfb5d 100644 --- a/worlds/stardew_valley/test/rules/TestArcades.py +++ b/worlds/stardew_valley/test/rules/TestArcades.py @@ -19,8 +19,8 @@ class TestArcadeMachinesLogic(SVTestBase): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=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)) @@ -28,8 +28,8 @@ class TestArcadeMachinesLogic(SVTestBase): self.remove(boots) self.remove(gun) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(boots, prevent_sweep=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)) @@ -37,10 +37,10 @@ class TestArcadeMachinesLogic(SVTestBase): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=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)) @@ -50,13 +50,13 @@ class TestArcadeMachinesLogic(SVTestBase): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=True) + self.multiworld.state.collect(drop, prevent_sweep=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)) @@ -69,17 +69,17 @@ class TestArcadeMachinesLogic(SVTestBase): 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.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(boots, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(gun, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(ammo, prevent_sweep=True) + self.multiworld.state.collect(life, prevent_sweep=True) + self.multiworld.state.collect(drop, prevent_sweep=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)) diff --git a/worlds/stardew_valley/test/rules/TestBuildings.py b/worlds/stardew_valley/test/rules/TestBuildings.py index b00e4138a1..2c276d8b5c 100644 --- a/worlds/stardew_valley/test/rules/TestBuildings.py +++ b/worlds/stardew_valley/test/rules/TestBuildings.py @@ -23,11 +23,11 @@ class TestBuildingLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=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)}") @@ -35,13 +35,13 @@ class TestBuildingLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True) self.assertTrue(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state)) def test_big_shed_blueprint(self): @@ -53,10 +53,10 @@ class TestBuildingLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=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.multiworld.state.collect(self.create_item("Progressive Shed"), prevent_sweep=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/TestCookingRecipes.py b/worlds/stardew_valley/test/rules/TestCookingRecipes.py index 81a91d1e74..7ab9d61cb9 100644 --- a/worlds/stardew_valley/test/rules/TestCookingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCookingRecipes.py @@ -17,14 +17,14 @@ class TestRecipeLearnLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -42,21 +42,21 @@ class TestRecipeReceiveLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Radish Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(spring, prevent_sweep=False) + self.multiworld.state.collect(qos, prevent_sweep=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.multiworld.state.collect(self.create_item("Radish Salad Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) def test_get_chefsanity_check_recipe(self): @@ -64,20 +64,20 @@ class TestRecipeReceiveLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Spring"), prevent_sweep=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.multiworld.state.collect(seeds, prevent_sweep=False) + self.multiworld.state.collect(summer, prevent_sweep=False) + self.multiworld.state.collect(house, prevent_sweep=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.multiworld.state.collect(self.create_item("The Queen of Sauce"), prevent_sweep=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 index 59d41f6a63..93c325ae5c 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -25,7 +25,7 @@ class TestCraftsanityLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Marble Brazier Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) def test_can_learn_crafting_recipe(self): @@ -38,16 +38,16 @@ class TestCraftsanityLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=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.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), event=False) + self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -62,16 +62,16 @@ class TestCraftsanityWithFestivalsLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=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.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) - self.multiworld.state.collect(self.create_item("Torch Recipe"), event=False) + self.multiworld.state.collect(self.create_item("Torch Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) @@ -92,7 +92,7 @@ class TestNoCraftsanityLogic(SVTestBase): 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("Pumpkin Seeds"), prevent_sweep=False) self.collect_lots_of_money() rule = self.world.logic.crafting.can_craft(recipe) result = rule(self.multiworld.state) @@ -113,11 +113,11 @@ class TestNoCraftsanityWithFestivalsLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Pumpkin Seeds"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=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.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=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 index 84ceac50ff..984a3ebc38 100644 --- a/worlds/stardew_valley/test/rules/TestDonations.py +++ b/worlds/stardew_valley/test/rules/TestDonations.py @@ -18,7 +18,7 @@ class TestDonationLogicAll(SVTestBase): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in locations_by_tag[LocationTags.MUSEUM_DONATIONS]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -39,7 +39,7 @@ class TestDonationLogicRandomized(SVTestBase): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in donation_locations: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) @@ -58,7 +58,7 @@ class TestDonationLogicMilestones(SVTestBase): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=False) for donation in locations_by_tag[LocationTags.MUSEUM_MILESTONES]: self.assertTrue(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestFriendship.py b/worlds/stardew_valley/test/rules/TestFriendship.py index 43c5e55c7f..fb186ca994 100644 --- a/worlds/stardew_valley/test/rules/TestFriendship.py +++ b/worlds/stardew_valley/test/rules/TestFriendship.py @@ -11,34 +11,34 @@ class TestFriendsanityDatingRules(SVTestBase): 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) + self.multiworld.state.collect(self.create_item("Fall"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Beach Bridge"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=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) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Weapon"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Barn"), prevent_sweep=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) + self.multiworld.state.collect(self.create_item("Foraging Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Farming Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Mining Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Combat Level"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Mine Elevator"), prevent_sweep=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.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 6, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 8, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=False) self.assert_can_reach_heart_up_to(npc, 10, step) - self.multiworld.state.collect(self.create_item(heart_name), event=False) + self.multiworld.state.collect(self.create_item(heart_name), prevent_sweep=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): diff --git a/worlds/stardew_valley/test/rules/TestShipping.py b/worlds/stardew_valley/test/rules/TestShipping.py index 378933b7d7..973d8d3ada 100644 --- a/worlds/stardew_valley/test/rules/TestShipping.py +++ b/worlds/stardew_valley/test/rules/TestShipping.py @@ -76,7 +76,7 @@ class TestShipsanityEverything(SVTestBase): 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) + self.multiworld.state.collect(bin_item, prevent_sweep=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/TestTools.py b/worlds/stardew_valley/test/rules/TestTools.py index a1fb152812..5f0fe8ef3f 100644 --- a/worlds/stardew_valley/test/rules/TestTools.py +++ b/worlds/stardew_valley/test/rules/TestTools.py @@ -21,30 +21,30 @@ class TestProgressiveToolsLogic(SVTestBase): self.assert_rule_false(sturgeon_rule, self.multiworld.state) summer = self.create_item("Summer") - self.multiworld.state.collect(summer, event=False) + self.multiworld.state.collect(summer, prevent_sweep=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.multiworld.state.collect(fishing_rod, prevent_sweep=False) + self.multiworld.state.collect(fishing_rod, prevent_sweep=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.multiworld.state.collect(fishing_level, prevent_sweep=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.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=False) + self.multiworld.state.collect(fishing_level, prevent_sweep=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.multiworld.state.collect(winter, prevent_sweep=False) self.assert_rule_true(sturgeon_rule, self.multiworld.state) self.remove(fishing_rod) @@ -53,24 +53,24 @@ class TestProgressiveToolsLogic(SVTestBase): 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.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Progressive Axe"), prevent_sweep=False) + self.multiworld.state.collect(self.create_item("Summer"), prevent_sweep=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.multiworld.state.collect(fall, prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) tuesday = self.create_item("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=False) + self.multiworld.state.collect(tuesday, prevent_sweep=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.multiworld.state.collect(rare_seed, prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) self.remove(fall) @@ -80,11 +80,11 @@ class TestProgressiveToolsLogic(SVTestBase): green_house = self.create_item("Greenhouse") self.collect(self.create_item(Event.fall_farming)) - self.multiworld.state.collect(green_house, event=False) + self.multiworld.state.collect(green_house, prevent_sweep=False) self.assert_rule_false(rule, self.multiworld.state) friday = self.create_item("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=False) + self.multiworld.state.collect(friday, prevent_sweep=False) self.assertTrue(self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state)) self.remove(green_house) @@ -111,7 +111,7 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=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]: @@ -125,7 +125,7 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase): 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) + self.multiworld.state.collect(self.create_item(railroad_item), prevent_sweep=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) diff --git a/worlds/stardew_valley/test/rules/TestWeapons.py b/worlds/stardew_valley/test/rules/TestWeapons.py index 77887f8eca..972170b93c 100644 --- a/worlds/stardew_valley/test/rules/TestWeapons.py +++ b/worlds/stardew_valley/test/rules/TestWeapons.py @@ -10,16 +10,16 @@ class TestWeaponsLogic(SVTestBase): } 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.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive Pickaxe"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Progressive House"), prevent_sweep=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.multiworld.state.collect(self.create_item("Bus Repair"), prevent_sweep=True) + self.multiworld.state.collect(self.create_item("Skull Key"), prevent_sweep=True) self.GiveItemAndCheckReachableMine("Progressive Sword", 1) self.GiveItemAndCheckReachableMine("Progressive Dagger", 1) @@ -43,7 +43,7 @@ class TestWeaponsLogic(SVTestBase): 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) + self.multiworld.state.collect(item, prevent_sweep=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) From 8e7ea06f39248b93f02f9640eae9a3d21c805fdb Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 17:17:42 -0500 Subject: [PATCH 114/222] Core: dump all item placements for generation failures. (#3237) * Core: dump all item placements for generation failures * pass the multiworld from remaining fill * change how the args get handled to fix formatting --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- Fill.py | 19 +++++++++++++------ Main.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Fill.py b/Fill.py index 5185bbb60e..15d5842e29 100644 --- a/Fill.py +++ b/Fill.py @@ -12,7 +12,12 @@ from worlds.generic.Rules import add_item_rule class FillError(RuntimeError): - pass + def __init__(self, *args: typing.Union[str, typing.Any], **kwargs) -> None: + if "multiworld" in kwargs and isinstance(args[0], str): + placements = (args[0] + f"\nAll Placements:\n" + + f"{[(loc, loc.item) for loc in kwargs['multiworld'].get_filled_locations()]}") + args = (placements, *args[1:]) + super().__init__(*args) def _log_fill_progress(name: str, placed: int, total_items: int) -> None: @@ -212,7 +217,7 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) item_pool.extend(unplaced_items) @@ -299,7 +304,7 @@ def remaining_fill(multiworld: MultiWorld, f"Unfilled locations:\n" f"{', '.join(str(location) for location in locations)}\n" f"Already placed {len(placements)}:\n" - f"{', '.join(str(place) for place in placements)}") + f"{', '.join(str(place) for place in placements)}", multiworld=multiworld) itempool.extend(unplaced_items) @@ -506,7 +511,8 @@ def distribute_items_restrictive(multiworld: MultiWorld, if progitempool: raise FillError( f"Not enough locations for progression items. " - f"There are {len(progitempool)} more progression items than there are available locations." + f"There are {len(progitempool)} more progression items than there are available locations.", + multiworld=multiworld, ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) @@ -523,7 +529,8 @@ def distribute_items_restrictive(multiworld: MultiWorld, if excludedlocations: raise FillError( f"Not enough filler items for excluded locations. " - f"There are {len(excludedlocations)} more excluded locations than filler or trap items." + f"There are {len(excludedlocations)} more excluded locations than filler or trap items.", + multiworld=multiworld, ) restitempool = filleritempool + usefulitempool @@ -589,7 +596,7 @@ def flood_items(multiworld: MultiWorld) -> None: if candidate_item_to_place is not None: item_to_place = candidate_item_to_place else: - raise FillError('No more progress items left to place.') + raise FillError('No more progress items left to place.', multiworld=multiworld) # find item to replace with progress item location_list = multiworld.get_reachable_locations() diff --git a/Main.py b/Main.py index 6dc03aaa55..edae5d7b19 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,8 @@ from typing import Dict, List, Optional, Set, Tuple, Union import worlds from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region -from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items +from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, distribute_planned, \ + flood_items from Options import StartInventoryPool from Utils import __version__, output_path, version_tuple, get_settings from settings import get_settings @@ -346,7 +347,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No output_file_futures.append(pool.submit(write_multidata)) if not check_accessibility_task.result(): if not multiworld.can_beat_game(): - raise Exception("Game appears as unbeatable. Aborting.") + raise FillError("Game appears as unbeatable. Aborting.", multiworld=multiworld) else: logger.warning("Location Accessibility requirements not fulfilled.") From 169da1b1e021bda141f7049cae591bb5c67d37df Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 13 Aug 2024 17:31:26 -0500 Subject: [PATCH 115/222] Tests: fix the all games multiworld test (#3788) --- test/multiworld/test_multiworlds.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/multiworld/test_multiworlds.py b/test/multiworld/test_multiworlds.py index 5289cac6c3..8415ac4c84 100644 --- a/test/multiworld/test_multiworlds.py +++ b/test/multiworld/test_multiworlds.py @@ -55,7 +55,7 @@ class TestAllGamesMultiworld(MultiworldTestBase): all_worlds = list(AutoWorldRegister.world_types.values()) self.multiworld = setup_multiworld(all_worlds, ()) for world in self.multiworld.worlds.values(): - world.options.accessibility.value = Accessibility.option_locations + world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) with self.subTest("filling multiworld", seed=self.multiworld.seed): distribute_items_restrictive(self.multiworld) @@ -66,8 +66,8 @@ class TestAllGamesMultiworld(MultiworldTestBase): class TestTwoPlayerMulti(MultiworldTestBase): def test_two_player_single_game_fills(self) -> None: """Tests that a multiworld of two players for each registered game world can generate.""" - for world in AutoWorldRegister.world_types.values(): - self.multiworld = setup_multiworld([world, world], ()) + for world_type in AutoWorldRegister.world_types.values(): + self.multiworld = setup_multiworld([world_type, world_type], ()) for world in self.multiworld.worlds.values(): world.options.accessibility.value = Accessibility.option_full self.assertSteps(gen_steps) From 0af31c71e0a8e3930cf24aec717fdea644054314 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 13 Aug 2024 20:35:08 -0400 Subject: [PATCH 116/222] TUNIC: Swap from multiworld.get to world.get for applicable things (#3789) * Swap from multiworld.get to world.get for applicable things * Why was this even here in the first place? --- worlds/tunic/__init__.py | 23 ++--- worlds/tunic/er_rules.py | 181 ++++++++++++++++++------------------- worlds/tunic/er_scripts.py | 2 +- worlds/tunic/rules.py | 180 ++++++++++++++++++------------------ 4 files changed, 190 insertions(+), 196 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 6657b464ed..47c66591f9 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -155,8 +155,7 @@ class TunicWorld(World): if is_mismatched: raise Exception(f"TUNIC: Conflict between seed group {group}'s plando " f"connection {group_cxn.entrance} <-> {group_cxn.exit} and " - f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " - f"connection {cxn.entrance} <-> {cxn.exit}") + f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}") if new_cxn: cls.seed_groups[group]["plando"].value.append(cxn) @@ -187,17 +186,17 @@ class TunicWorld(World): if self.options.laurels_location: laurels = self.create_item("Hero's Laurels") if self.options.laurels_location == "6_coins": - self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 6 Coins").place_locked_item(laurels) elif self.options.laurels_location == "10_coins": - self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) + self.get_location("Coins in the Well - 10 Coins").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.get_location("Secret Gathering Place - 10 Fairy Reward").place_locked_item(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.get_location(location).place_locked_item(hex_item) items_to_create[rgb_hexagon] = 0 items_to_create[gold_hexagon] -= 3 @@ -297,15 +296,15 @@ class TunicWorld(World): self.multiworld.regions.append(region) for region_name, exits in tunic_regions.items(): - region = self.multiworld.get_region(region_name, self.player) + region = self.get_region(region_name) region.add_exits(exits) for location_name, location_id in self.location_name_to_id.items(): - region = self.multiworld.get_region(location_table[location_name].region, self.player) + region = self.get_region(location_table[location_name].region) location = TunicLocation(self.player, location_name, location_id, region) region.locations.append(location) - victory_region = self.multiworld.get_region("Spirit Arena", self.player) + victory_region = self.get_region("Spirit Arena") victory_location = TunicLocation(self.player, "The Heir", None, victory_region) victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) @@ -339,10 +338,8 @@ class TunicWorld(World): name, connection = paths[location.parent_region] except KeyError: # logic bug, proceed with warning since it takes a long time to update AP - warning(f"{location.name} is not logically accessible for " - f"{self.multiworld.get_file_safe_player_name(self.player)}. " - "Creating entrance hint Inaccessible. " - "Please report this to the TUNIC rando devs.") + warning(f"{location.name} is not logically accessible for {self.player_name}. " + "Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.") hint_text = "Inaccessible" else: while connection != ("Menu", None): diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 81e9d48b4a..a54ea23bcc 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1318,222 +1318,221 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player), + set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), lambda state: state.has("Activate Furnace Fuse", player)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + set_rule(world.get_location("Library Hall - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), + set_rule(world.get_location("Overworld - [Southwest] Flowers Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), + set_rule(world.get_location("Overworld - [East] Weathervane Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), + set_rule(world.get_location("Overworld - [Northeast] Flowers Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), + set_rule(world.get_location("Overworld - [Southwest] Haiku Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), + set_rule(world.get_location("Overworld - [Northwest] Golden Obelisk Page"), lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player), + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player), + set_rule(world.get_location("Caustic Light Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player), + set_rule(world.get_location("Cube Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player), + set_rule(world.get_location("Old House - Holy Cross Door Page"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player), + set_rule(world.get_location("Maze Cave - Maze Room Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player), + set_rule(world.get_location("Patrol Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player), + set_rule(world.get_location("Ruined Passage - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player), + set_rule(world.get_location("Hourglass Cave - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player), + set_rule(world.get_location("Secret Gathering Place - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + set_rule(world.get_location("Coins in the Well - 6 Coins"), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: ( + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: ( 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), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), 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), + set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), + set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) # Ruined Atoll - set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), + set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player)) # Beneath the Vault - set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), + set_rule(world.get_location("Beneath the Fortress - Bridge"), lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), lambda state: state.has(laurels, player)) # Ziggurat # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig - set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), 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), + set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), 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), + set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player)) # nmg - kill Librarian with a lure, or gun I guess - set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + set_rule(world.get_location("Librarian - Hexagon Green"), lambda state: (has_sword(state, player) or options.logic_rules) 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), + set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: state.has(fire_wand, player) and has_sword(state, player)) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) # these two swamp checks really want you to kill the big skeleton first - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) # Hero's Grave and Far Shore - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + set_rule(world.get_location("Hero's Grave - Tooth Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + set_rule(world.get_location("Hero's Grave - Ash Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + set_rule(world.get_location("Hero's Grave - Flowers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + set_rule(world.get_location("Hero's Grave - Effigy Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + set_rule(world.get_location("Far Shore - Secret Chest"), lambda state: state.has(laurels, player)) # Events - set_rule(multiworld.get_location("Eastern Bell", player), + set_rule(world.get_location("Eastern Bell"), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Western Bell", player), + set_rule(world.get_location("Western Bell"), lambda state: (has_stick(state, player) or state.has(fire_wand, player))) - set_rule(multiworld.get_location("Furnace Fuse", player), + set_rule(world.get_location("Furnace Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player), + set_rule(world.get_location("South and West Fortress Exterior Fuses"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player), + set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Beneath the Vault Fuse", player), + set_rule(world.get_location("Beneath the Vault Fuse"), lambda state: state.has("Activate South and West Fortress Exterior Fuses", player)) - set_rule(multiworld.get_location("Eastern Vault West Fuses", player), + set_rule(world.get_location("Eastern Vault West Fuses"), lambda state: state.has("Activate Beneath the Vault Fuse", player)) - set_rule(multiworld.get_location("Eastern Vault East Fuse", player), + set_rule(world.get_location("Eastern Vault East Fuse"), 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), + set_rule(world.get_location("Quarry Connector Fuse"), lambda state: has_ability(prayer, state, world) and state.has(grapple, player)) - set_rule(multiworld.get_location("Quarry Fuse", player), + set_rule(world.get_location("Quarry Fuse"), lambda state: state.has("Activate Quarry Connector Fuse", player)) - set_rule(multiworld.get_location("Ziggurat Fuse", player), + set_rule(world.get_location("Ziggurat Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("West Garden Fuse", player), + set_rule(world.get_location("West Garden Fuse"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Library Fuse", player), + set_rule(world.get_location("Library Fuse"), lambda state: has_ability(prayer, state, world)) # Shop - set_rule(multiworld.get_location("Shop - Potion 1", player), + set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Potion 2", player), + set_rule(world.get_location("Shop - Potion 2"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 1", player), + set_rule(world.get_location("Shop - Coin 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 2", player), + set_rule(world.get_location("Shop - Coin 2"), lambda state: has_sword(state, player)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index a4295cf9f2..e7c8fd58d0 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -130,7 +130,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} dead_ends: List[Portal] = [] two_plus: List[Portal] = [] - player_name = world.multiworld.get_player_name(world.player) + player_name = world.player_name portal_map = portal_mapping.copy() logic_rules = world.options.logic_rules.value fixed_shop = world.options.fixed_shop diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 73eb811890..2ff588da90 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -87,41 +87,40 @@ def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: 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 = \ + world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \ lambda state: has_ability(holy_cross, state, world) - multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \ + world.get_entrance("Overworld -> Beneath the Well").access_rule = \ lambda state: has_stick(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \ + world.get_entrance("Overworld -> Dark Tomb").access_rule = \ lambda state: has_lantern(state, world) - multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ + world.get_entrance("Overworld -> West Garden").access_rule = \ lambda state: state.has(laurels, player) \ or can_ladder_storage(state, world) - multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ + world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ lambda state: state.has(laurels, player) \ 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 = \ + world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ lambda state: has_lantern(state, world) and has_ability(prayer, state, world) - multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \ + world.get_entrance("Ruined Atoll -> Library").access_rule = \ lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) - multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \ + world.get_entrance("Overworld -> Quarry").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, world)) - multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \ + world.get_entrance("Quarry Back -> Quarry").access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) - multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \ + world.get_entrance("Quarry -> Lower Quarry").access_rule = \ lambda state: has_mask(state, world) - multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ + world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) - multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ + world.get_entrance("Swamp -> Cathedral").access_rule = \ 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 = \ + world.get_entrance("Overworld -> Spirit Arena").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)) @@ -130,210 +129,209 @@ def set_region_rules(world: "TunicWorld") -> None: def set_location_rules(world: "TunicWorld") -> None: - multiworld = world.multiworld player = world.player options = world.options - forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player) + forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) # Ability Shuffle Exclusive Rules - set_rule(multiworld.get_location("Far Shore - Page Pickup", player), + set_rule(world.get_location("Far Shore - Page Pickup"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player), + set_rule(world.get_location("Fortress Courtyard - Chest Near Cave"), 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), + set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), 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), + set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player), + set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player), + set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player), + set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"), lambda state: has_ability(prayer, state, world)) - set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player), + set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player), + set_rule(world.get_location("Library Hall - Holy Cross Chest"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player), + set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player), + set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player), + set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"), lambda state: has_ability(holy_cross, state, world)) - set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), + set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"), lambda state: has_ability(holy_cross, state, world)) # Overworld - set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player), + set_rule(world.get_location("Overworld - [Southwest] Fountain Page"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), + set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player), + set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Far Shore - Secret Chest", player), + set_rule(world.get_location("Far Shore - Secret Chest"), 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), + set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Old House - Normal Chest", player), + set_rule(world.get_location("Old House - Normal Chest"), lambda state: 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 - Holy Cross Chest", player), + set_rule(world.get_location("Old House - Holy Cross Chest"), 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), + set_rule(world.get_location("Old House - Shield Pickup"), lambda state: 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("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), + set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), + set_rule(world.get_location("Overworld - [Southwest] From West Garden"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), + set_rule(world.get_location("Overworld - [West] Chest After Bell"), lambda state: state.has(laurels, player) 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), + set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) - set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), + set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), + set_rule(world.get_location("Special Shop - Secret Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player), + set_rule(world.get_location("Sealed Temple - Holy Cross Chest"), 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), + set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: 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("West Furnace - Lantern Pickup", player), + set_rule(world.get_location("West Furnace - Lantern Pickup"), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) - set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"), lambda state: state.has(fairies, player, 10)) - set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), + set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"), lambda state: state.has(fairies, player, 20)) - set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), + set_rule(world.get_location("Coins in the Well - 3 Coins"), lambda state: state.has(coins, player, 3)) - set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), + set_rule(world.get_location("Coins in the Well - 6 Coins"), lambda state: state.has(coins, player, 6)) - set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player), + set_rule(world.get_location("Coins in the Well - 10 Coins"), lambda state: state.has(coins, player, 10)) - set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player), + set_rule(world.get_location("Coins in the Well - 15 Coins"), lambda state: state.has(coins, player, 15)) # East Forest - set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player), + set_rule(world.get_location("East Forest - Lower Grapple Chest"), lambda state: state.has(grapple, player)) - set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player), + set_rule(world.get_location("East Forest - Lower Dash Chest"), lambda state: state.has_all({grapple, laurels}, player)) - set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), + set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: 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), + set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player), + set_rule(world.get_location("West Garden - [West] In Flooded Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player), + set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"), 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), + set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), 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), + set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), + set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), lambda state: state.has(laurels, player) 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), + set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), + set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), lambda state: state.has(laurels, player) or state.has(key, player, 2)) - set_rule(multiworld.get_location("Librarian - Hexagon Green", player), + set_rule(world.get_location("Librarian - Hexagon Green"), lambda state: has_sword(state, player) or options.logic_rules) # Frog's Domain - set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), + set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player), + set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"), lambda state: state.has_any({grapple, laurels}, player)) - set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player), + set_rule(world.get_location("Frog's Domain - Escape Chest"), lambda state: state.has_any({grapple, laurels}, player)) # Eastern Vault Fortress - set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player), + set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), + set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), 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), + set_rule(world.get_location("Fortress Arena - Hexagon Red"), 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), + set_rule(world.get_location("Beneath the Fortress - Bridge"), 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), + set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"), lambda state: has_stick(state, player) and has_lantern(state, world)) # Quarry - set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player), + set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player), + set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player)) # nmg - kill boss scav with orb + firecracker, or similar - set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), + set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) # Swamp - set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), + set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) - set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), + set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player), + set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player), + set_rule(world.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial"), lambda state: state.has(laurels, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), + set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) # Hero's Grave - set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), + set_rule(world.get_location("Hero's Grave - Tooth Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player), + set_rule(world.get_location("Hero's Grave - Mushroom Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player), + set_rule(world.get_location("Hero's Grave - Ash Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player), + set_rule(world.get_location("Hero's Grave - Flowers Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player), + set_rule(world.get_location("Hero's Grave - Effigy Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) - set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player), + set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) # Shop - set_rule(multiworld.get_location("Shop - Potion 1", player), + set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Potion 2", player), + set_rule(world.get_location("Shop - Potion 2"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 1", player), + set_rule(world.get_location("Shop - Coin 1"), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Shop - Coin 2", player), + set_rule(world.get_location("Shop - Coin 2"), lambda state: has_sword(state, player)) From 9fbaa6050f40aded0546ea143743e1b3db559aa9 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Wed, 14 Aug 2024 00:21:42 -0400 Subject: [PATCH 117/222] I have no idea (#3791) --- WebHostLib/templates/weightedOptions/macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index a1d3196971..68d3968a17 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -138,7 +138,7 @@ id="{{ option_name }}-{{ key }}" name="{{ option_name }}||{{ key }}" value="1" - checked="{{ "checked" if key in option.default else "" }}" + {{ "checked" if key in option.default }} />
From a40744e6db904d6445f7c39c4117fb2e2096edcb Mon Sep 17 00:00:00 2001 From: Spineraks Date: Fri, 6 Sep 2024 22:50:57 +0200 Subject: [PATCH 201/222] Yacht Dice: logic fix and several other fixes (#3878) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/Rules.py | 2 +- worlds/yachtdice/YachtWeights.py | 3040 +++++++++--------------------- worlds/yachtdice/__init__.py | 40 +- 3 files changed, 938 insertions(+), 2144 deletions(-) diff --git a/worlds/yachtdice/Rules.py b/worlds/yachtdice/Rules.py index 1db5cebccd..d99f5b1474 100644 --- a/worlds/yachtdice/Rules.py +++ b/worlds/yachtdice/Rules.py @@ -29,7 +29,7 @@ class Category: mean_score = 0 for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items(): mean_score += key * value / 100000 - return mean_score * self.quantity + return mean_score class ListState: diff --git a/worlds/yachtdice/YachtWeights.py b/worlds/yachtdice/YachtWeights.py index ee387fdf21..5f647f3420 100644 --- a/worlds/yachtdice/YachtWeights.py +++ b/worlds/yachtdice/YachtWeights.py @@ -17,77 +17,77 @@ yacht_weights = { ("Category Ones", 0, 7): {0: 100000}, ("Category Ones", 0, 8): {0: 100000}, ("Category Ones", 1, 0): {0: 100000}, - ("Category Ones", 1, 1): {0: 83416, 1: 16584}, - ("Category Ones", 1, 2): {0: 69346, 1: 30654}, - ("Category Ones", 1, 3): {0: 57756, 1: 42244}, - ("Category Ones", 1, 4): {0: 48709, 1: 51291}, - ("Category Ones", 1, 5): {0: 40214, 1: 59786}, + ("Category Ones", 1, 1): {0: 100000}, + ("Category Ones", 1, 2): {0: 100000}, + ("Category Ones", 1, 3): {0: 100000}, + ("Category Ones", 1, 4): {0: 100000}, + ("Category Ones", 1, 5): {0: 100000}, ("Category Ones", 1, 6): {0: 33491, 1: 66509}, ("Category Ones", 1, 7): {0: 27838, 1: 72162}, ("Category Ones", 1, 8): {0: 23094, 1: 76906}, ("Category Ones", 2, 0): {0: 100000}, - ("Category Ones", 2, 1): {0: 69715, 1: 30285}, - ("Category Ones", 2, 2): {0: 48066, 1: 51934}, - ("Category Ones", 2, 3): {0: 33544, 1: 48585, 2: 17871}, - ("Category Ones", 2, 4): {0: 23342, 1: 50092, 2: 26566}, - ("Category Ones", 2, 5): {0: 16036, 1: 48250, 2: 35714}, - ("Category Ones", 2, 6): {0: 11355, 1: 44545, 2: 44100}, - ("Category Ones", 2, 7): {0: 7812, 1: 40248, 2: 51940}, - ("Category Ones", 2, 8): {0: 5395, 1: 35484, 2: 59121}, + ("Category Ones", 2, 1): {0: 100000}, + ("Category Ones", 2, 2): {0: 100000}, + ("Category Ones", 2, 3): {0: 33544, 1: 66456}, + ("Category Ones", 2, 4): {0: 23342, 1: 76658}, + ("Category Ones", 2, 5): {0: 16036, 2: 83964}, + ("Category Ones", 2, 6): {0: 11355, 2: 88645}, + ("Category Ones", 2, 7): {0: 7812, 2: 92188}, + ("Category Ones", 2, 8): {0: 5395, 2: 94605}, ("Category Ones", 3, 0): {0: 100000}, - ("Category Ones", 3, 1): {0: 57462, 1: 42538}, - ("Category Ones", 3, 2): {0: 33327, 1: 44253, 2: 22420}, - ("Category Ones", 3, 3): {0: 19432, 1: 42237, 2: 38331}, - ("Category Ones", 3, 4): {0: 11191, 1: 36208, 2: 38606, 3: 13995}, - ("Category Ones", 3, 5): {0: 6536, 1: 28891, 2: 43130, 3: 21443}, - ("Category Ones", 3, 6): {0: 3697, 1: 22501, 2: 44196, 3: 29606}, - ("Category Ones", 3, 7): {0: 2134, 2: 60499, 3: 37367}, - ("Category Ones", 3, 8): {0: 1280, 2: 53518, 3: 45202}, + ("Category Ones", 3, 1): {0: 100000}, + ("Category Ones", 3, 2): {0: 33327, 1: 66673}, + ("Category Ones", 3, 3): {0: 19432, 2: 80568}, + ("Category Ones", 3, 4): {0: 11191, 2: 88809}, + ("Category Ones", 3, 5): {0: 35427, 2: 64573}, + ("Category Ones", 3, 6): {0: 26198, 2: 73802}, + ("Category Ones", 3, 7): {0: 18851, 3: 81149}, + ("Category Ones", 3, 8): {0: 13847, 3: 86153}, ("Category Ones", 4, 0): {0: 100000}, - ("Category Ones", 4, 1): {0: 48178, 1: 38635, 2: 13187}, - ("Category Ones", 4, 2): {0: 23349, 1: 40775, 2: 35876}, - ("Category Ones", 4, 3): {0: 11366, 1: 32547, 2: 35556, 3: 20531}, - ("Category Ones", 4, 4): {0: 5331, 1: 23241, 2: 37271, 3: 34157}, - ("Category Ones", 4, 5): {0: 2640, 2: 49872, 3: 47488}, - ("Category Ones", 4, 6): {0: 1253, 2: 39816, 3: 39298, 4: 19633}, - ("Category Ones", 4, 7): {0: 6915, 2: 24313, 3: 41680, 4: 27092}, - ("Category Ones", 4, 8): {0: 4228, 3: 61312, 4: 34460}, + ("Category Ones", 4, 1): {0: 100000}, + ("Category Ones", 4, 2): {0: 23349, 2: 76651}, + ("Category Ones", 4, 3): {0: 11366, 2: 88634}, + ("Category Ones", 4, 4): {0: 28572, 3: 71428}, + ("Category Ones", 4, 5): {0: 17976, 3: 82024}, + ("Category Ones", 4, 6): {0: 1253, 3: 98747}, + ("Category Ones", 4, 7): {0: 31228, 3: 68772}, + ("Category Ones", 4, 8): {0: 23273, 4: 76727}, ("Category Ones", 5, 0): {0: 100000}, - ("Category Ones", 5, 1): {0: 40042, 1: 40202, 2: 19756}, - ("Category Ones", 5, 2): {0: 16212, 1: 35432, 2: 31231, 3: 17125}, - ("Category Ones", 5, 3): {0: 6556, 1: 23548, 2: 34509, 3: 35387}, - ("Category Ones", 5, 4): {0: 2552, 2: 44333, 3: 32048, 4: 21067}, - ("Category Ones", 5, 5): {0: 8783, 2: 23245, 3: 34614, 4: 33358}, - ("Category Ones", 5, 6): {0: 4513, 3: 49603, 4: 32816, 5: 13068}, - ("Category Ones", 5, 7): {0: 2295, 3: 40470, 4: 37869, 5: 19366}, - ("Category Ones", 5, 8): {0: 73, 3: 33115, 4: 40166, 5: 26646}, + ("Category Ones", 5, 1): {0: 100000}, + ("Category Ones", 5, 2): {0: 16212, 2: 83788}, + ("Category Ones", 5, 3): {0: 30104, 3: 69896}, + ("Category Ones", 5, 4): {0: 2552, 3: 97448}, + ("Category Ones", 5, 5): {0: 32028, 4: 67972}, + ("Category Ones", 5, 6): {0: 21215, 4: 78785}, + ("Category Ones", 5, 7): {0: 2295, 4: 97705}, + ("Category Ones", 5, 8): {0: 1167, 4: 98833}, ("Category Ones", 6, 0): {0: 100000}, - ("Category Ones", 6, 1): {0: 33501, 1: 40042, 2: 26457}, - ("Category Ones", 6, 2): {0: 11326, 1: 29379, 2: 32368, 3: 26927}, - ("Category Ones", 6, 3): {0: 3764, 2: 46660, 3: 28928, 4: 20648}, - ("Category Ones", 6, 4): {0: 1231, 2: 29883, 3: 31038, 4: 37848}, - ("Category Ones", 6, 5): {0: 4208, 3: 41897, 4: 30878, 5: 23017}, - ("Category Ones", 6, 6): {0: 1850, 3: 30396, 4: 33022, 5: 34732}, - ("Category Ones", 6, 7): {0: 5503, 4: 48099, 5: 32432, 6: 13966}, - ("Category Ones", 6, 8): {0: 2896, 4: 39616, 5: 37005, 6: 20483}, + ("Category Ones", 6, 1): {0: 33501, 1: 66499}, + ("Category Ones", 6, 2): {0: 40705, 2: 59295}, + ("Category Ones", 6, 3): {0: 3764, 3: 96236}, + ("Category Ones", 6, 4): {0: 9324, 4: 90676}, + ("Category Ones", 6, 5): {0: 4208, 4: 95792}, + ("Category Ones", 6, 6): {0: 158, 5: 99842}, + ("Category Ones", 6, 7): {0: 5503, 5: 94497}, + ("Category Ones", 6, 8): {0: 2896, 5: 97104}, ("Category Ones", 7, 0): {0: 100000}, - ("Category Ones", 7, 1): {0: 27838, 1: 39224, 2: 32938}, - ("Category Ones", 7, 2): {0: 7796, 1: 23850, 2: 31678, 3: 23224, 4: 13452}, - ("Category Ones", 7, 3): {0: 2247, 2: 35459, 3: 29131, 4: 33163}, - ("Category Ones", 7, 4): {0: 5252, 3: 41207, 4: 28065, 5: 25476}, - ("Category Ones", 7, 5): {0: 174, 3: 29347, 4: 28867, 5: 26190, 6: 15422}, - ("Category Ones", 7, 6): {0: 4625, 4: 38568, 5: 30596, 6: 26211}, - ("Category Ones", 7, 7): {0: 230, 4: 30109, 5: 32077, 6: 37584}, - ("Category Ones", 7, 8): {0: 5519, 5: 45718, 6: 33357, 7: 15406}, + ("Category Ones", 7, 1): {0: 27838, 2: 72162}, + ("Category Ones", 7, 2): {0: 7796, 3: 92204}, + ("Category Ones", 7, 3): {0: 13389, 4: 86611}, + ("Category Ones", 7, 4): {0: 5252, 4: 94748}, + ("Category Ones", 7, 5): {0: 9854, 5: 90146}, + ("Category Ones", 7, 6): {0: 4625, 5: 95375}, + ("Category Ones", 7, 7): {0: 30339, 6: 69661}, + ("Category Ones", 7, 8): {0: 5519, 6: 94481}, ("Category Ones", 8, 0): {0: 100000}, - ("Category Ones", 8, 1): {0: 23156, 1: 37295, 2: 26136, 3: 13413}, - ("Category Ones", 8, 2): {0: 5472, 2: 48372, 3: 25847, 4: 20309}, - ("Category Ones", 8, 3): {0: 8661, 3: 45896, 4: 24664, 5: 20779}, - ("Category Ones", 8, 4): {0: 2807, 3: 29707, 4: 27157, 5: 23430, 6: 16899}, - ("Category Ones", 8, 5): {0: 5173, 4: 36033, 5: 27792, 6: 31002}, - ("Category Ones", 8, 6): {0: 255, 4: 25642, 5: 27508, 6: 27112, 7: 19483}, - ("Category Ones", 8, 7): {0: 4236, 5: 35323, 6: 30438, 7: 30003}, - ("Category Ones", 8, 8): {0: 310, 5: 27692, 6: 30830, 7: 41168}, + ("Category Ones", 8, 1): {0: 23156, 2: 76844}, + ("Category Ones", 8, 2): {0: 5472, 3: 94528}, + ("Category Ones", 8, 3): {0: 8661, 4: 91339}, + ("Category Ones", 8, 4): {0: 12125, 5: 87875}, + ("Category Ones", 8, 5): {0: 5173, 5: 94827}, + ("Category Ones", 8, 6): {0: 8872, 6: 91128}, + ("Category Ones", 8, 7): {0: 4236, 6: 95764}, + ("Category Ones", 8, 8): {0: 9107, 7: 90893}, ("Category Twos", 0, 0): {0: 100000}, ("Category Twos", 0, 1): {0: 100000}, ("Category Twos", 0, 2): {0: 100000}, @@ -98,8 +98,8 @@ yacht_weights = { ("Category Twos", 0, 7): {0: 100000}, ("Category Twos", 0, 8): {0: 100000}, ("Category Twos", 1, 0): {0: 100000}, - ("Category Twos", 1, 1): {0: 83475, 2: 16525}, - ("Category Twos", 1, 2): {0: 69690, 2: 30310}, + ("Category Twos", 1, 1): {0: 100000}, + ("Category Twos", 1, 2): {0: 100000}, ("Category Twos", 1, 3): {0: 57818, 2: 42182}, ("Category Twos", 1, 4): {0: 48418, 2: 51582}, ("Category Twos", 1, 5): {0: 40301, 2: 59699}, @@ -107,68 +107,68 @@ yacht_weights = { ("Category Twos", 1, 7): {0: 28182, 2: 71818}, ("Category Twos", 1, 8): {0: 23406, 2: 76594}, ("Category Twos", 2, 0): {0: 100000}, - ("Category Twos", 2, 1): {0: 69724, 2: 30276}, - ("Category Twos", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Twos", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Twos", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Twos", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Twos", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Twos", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Twos", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Twos", 2, 1): {0: 100000}, + ("Category Twos", 2, 2): {0: 48238, 2: 51762}, + ("Category Twos", 2, 3): {0: 33290, 4: 66710}, + ("Category Twos", 2, 4): {0: 23136, 4: 76864}, + ("Category Twos", 2, 5): {0: 16146, 4: 83854}, + ("Category Twos", 2, 6): {0: 11083, 4: 88917}, + ("Category Twos", 2, 7): {0: 7662, 4: 92338}, + ("Category Twos", 2, 8): {0: 5354, 4: 94646}, ("Category Twos", 3, 0): {0: 100000}, - ("Category Twos", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Twos", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Twos", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Twos", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Twos", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Twos", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Twos", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Twos", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Twos", 3, 1): {0: 58021, 2: 41979}, + ("Category Twos", 3, 2): {0: 33548, 4: 66452}, + ("Category Twos", 3, 3): {0: 19375, 4: 80625}, + ("Category Twos", 3, 4): {0: 10998, 4: 89002}, + ("Category Twos", 3, 5): {0: 6519, 6: 93481}, + ("Category Twos", 3, 6): {0: 3619, 6: 96381}, + ("Category Twos", 3, 7): {0: 2195, 6: 97805}, + ("Category Twos", 3, 8): {0: 13675, 6: 86325}, ("Category Twos", 4, 0): {0: 100000}, - ("Category Twos", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Twos", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Twos", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Twos", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Twos", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Twos", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Twos", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Twos", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Twos", 4, 1): {0: 48235, 2: 51765}, + ("Category Twos", 4, 2): {0: 23289, 4: 76711}, + ("Category Twos", 4, 3): {0: 11177, 6: 88823}, + ("Category Twos", 4, 4): {0: 5499, 6: 94501}, + ("Category Twos", 4, 5): {0: 18356, 6: 81644}, + ("Category Twos", 4, 6): {0: 11169, 8: 88831}, + ("Category Twos", 4, 7): {0: 6945, 8: 93055}, + ("Category Twos", 4, 8): {0: 4091, 8: 95909}, ("Category Twos", 5, 0): {0: 100000}, - ("Category Twos", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Twos", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Twos", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Twos", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Twos", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Twos", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Twos", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Twos", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Twos", 5, 1): {0: 40028, 4: 59972}, + ("Category Twos", 5, 2): {0: 16009, 6: 83991}, + ("Category Twos", 5, 3): {0: 6489, 6: 93511}, + ("Category Twos", 5, 4): {0: 16690, 8: 83310}, + ("Category Twos", 5, 5): {0: 9016, 8: 90984}, + ("Category Twos", 5, 6): {0: 4602, 8: 95398}, + ("Category Twos", 5, 7): {0: 13627, 10: 86373}, + ("Category Twos", 5, 8): {0: 8742, 10: 91258}, ("Category Twos", 6, 0): {0: 100000}, - ("Category Twos", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Twos", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Twos", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Twos", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Twos", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Twos", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Twos", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Twos", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Twos", 6, 1): {0: 33502, 4: 66498}, + ("Category Twos", 6, 2): {0: 11210, 6: 88790}, + ("Category Twos", 6, 3): {0: 3673, 6: 96327}, + ("Category Twos", 6, 4): {0: 9291, 8: 90709}, + ("Category Twos", 6, 5): {0: 441, 8: 99559}, + ("Category Twos", 6, 6): {0: 10255, 10: 89745}, + ("Category Twos", 6, 7): {0: 5646, 10: 94354}, + ("Category Twos", 6, 8): {0: 14287, 12: 85713}, ("Category Twos", 7, 0): {0: 100000}, - ("Category Twos", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Twos", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Twos", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Twos", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Twos", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Twos", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Twos", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Twos", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Twos", 7, 1): {0: 27683, 4: 72317}, + ("Category Twos", 7, 2): {0: 7824, 6: 92176}, + ("Category Twos", 7, 3): {0: 13167, 8: 86833}, + ("Category Twos", 7, 4): {0: 564, 10: 99436}, + ("Category Twos", 7, 5): {0: 9824, 10: 90176}, + ("Category Twos", 7, 6): {0: 702, 12: 99298}, + ("Category Twos", 7, 7): {0: 10186, 12: 89814}, + ("Category Twos", 7, 8): {0: 942, 12: 99058}, ("Category Twos", 8, 0): {0: 100000}, - ("Category Twos", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Twos", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Twos", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Twos", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Twos", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Twos", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Twos", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Twos", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Twos", 8, 1): {0: 23378, 4: 76622}, + ("Category Twos", 8, 2): {0: 5420, 8: 94580}, + ("Category Twos", 8, 3): {0: 8560, 10: 91440}, + ("Category Twos", 8, 4): {0: 12199, 12: 87801}, + ("Category Twos", 8, 5): {0: 879, 12: 99121}, + ("Category Twos", 8, 6): {0: 9033, 14: 90967}, + ("Category Twos", 8, 7): {0: 15767, 14: 84233}, + ("Category Twos", 8, 8): {2: 9033, 14: 90967}, ("Category Threes", 0, 0): {0: 100000}, ("Category Threes", 0, 1): {0: 100000}, ("Category Threes", 0, 2): {0: 100000}, @@ -179,7 +179,7 @@ yacht_weights = { ("Category Threes", 0, 7): {0: 100000}, ("Category Threes", 0, 8): {0: 100000}, ("Category Threes", 1, 0): {0: 100000}, - ("Category Threes", 1, 1): {0: 83343, 3: 16657}, + ("Category Threes", 1, 1): {0: 100000}, ("Category Threes", 1, 2): {0: 69569, 3: 30431}, ("Category Threes", 1, 3): {0: 57872, 3: 42128}, ("Category Threes", 1, 4): {0: 48081, 3: 51919}, @@ -189,67 +189,67 @@ yacht_weights = { ("Category Threes", 1, 8): {0: 23240, 3: 76760}, ("Category Threes", 2, 0): {0: 100000}, ("Category Threes", 2, 1): {0: 69419, 3: 30581}, - ("Category Threes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Threes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Threes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Threes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Threes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Threes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Threes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Threes", 2, 2): {0: 48202, 3: 51798}, + ("Category Threes", 2, 3): {0: 33376, 6: 66624}, + ("Category Threes", 2, 4): {0: 23276, 6: 76724}, + ("Category Threes", 2, 5): {0: 16092, 6: 83908}, + ("Category Threes", 2, 6): {0: 11232, 6: 88768}, + ("Category Threes", 2, 7): {0: 7589, 6: 92411}, + ("Category Threes", 2, 8): {0: 5447, 6: 94553}, ("Category Threes", 3, 0): {0: 100000}, - ("Category Threes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Threes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Threes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Threes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Threes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Threes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Threes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Threes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Threes", 3, 1): {0: 57964, 3: 42036}, + ("Category Threes", 3, 2): {0: 33637, 6: 66363}, + ("Category Threes", 3, 3): {0: 19520, 6: 80480}, + ("Category Threes", 3, 4): {0: 11265, 6: 88735}, + ("Category Threes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Threes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Threes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Threes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Threes", 4, 0): {0: 100000}, - ("Category Threes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Threes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Threes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Threes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Threes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Threes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Threes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Threes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Threes", 4, 1): {0: 48121, 6: 51879}, + ("Category Threes", 4, 2): {0: 23296, 6: 76704}, + ("Category Threes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Threes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Threes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Threes", 4, 6): {0: 11267, 9: 88733}, + ("Category Threes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Threes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Threes", 5, 0): {0: 100000}, - ("Category Threes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Threes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Threes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Threes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Threes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Threes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Threes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Threes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Threes", 5, 1): {0: 40183, 6: 59817}, + ("Category Threes", 5, 2): {0: 16197, 6: 83803}, + ("Category Threes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Threes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Threes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Threes", 5, 6): {0: 4652, 12: 95348}, + ("Category Threes", 5, 7): {0: 2365, 12: 97635}, + ("Category Threes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Threes", 6, 0): {0: 100000}, - ("Category Threes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Threes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Threes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Threes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Threes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Threes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Threes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Threes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Threes", 6, 1): {0: 33473, 6: 66527}, + ("Category Threes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Threes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Threes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Threes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Threes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Threes", 6, 7): {0: 5519, 15: 94481}, + ("Category Threes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Threes", 7, 0): {0: 100000}, - ("Category Threes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Threes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Threes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Threes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Threes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Threes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Threes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Threes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Threes", 7, 1): {0: 27933, 6: 72067}, + ("Category Threes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Threes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Threes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Threes", 7, 5): {0: 9894, 15: 90106}, + ("Category Threes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Threes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Threes", 7, 8): {0: 5710, 18: 94290}, ("Category Threes", 8, 0): {0: 100000}, - ("Category Threes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Threes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Threes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Threes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Threes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Threes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Threes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Threes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, + ("Category Threes", 8, 1): {0: 23337, 6: 76663}, + ("Category Threes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Threes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Threes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Threes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Threes", 8, 6): {0: 8804, 18: 91196}, + ("Category Threes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Threes", 8, 8): {0: 9252, 21: 90748}, ("Category Fours", 0, 0): {0: 100000}, ("Category Fours", 0, 1): {0: 100000}, ("Category Fours", 0, 2): {0: 100000}, @@ -270,67 +270,67 @@ yacht_weights = { ("Category Fours", 1, 8): {0: 23431, 4: 76569}, ("Category Fours", 2, 0): {0: 100000}, ("Category Fours", 2, 1): {0: 69379, 4: 30621}, - ("Category Fours", 2, 2): {0: 48538, 4: 42240, 8: 9222}, + ("Category Fours", 2, 2): {0: 48538, 4: 51462}, ("Category Fours", 2, 3): {0: 33756, 4: 48555, 8: 17689}, ("Category Fours", 2, 4): {0: 23070, 4: 49916, 8: 27014}, ("Category Fours", 2, 5): {0: 16222, 4: 48009, 8: 35769}, ("Category Fours", 2, 6): {0: 11125, 4: 44400, 8: 44475}, ("Category Fours", 2, 7): {0: 7919, 4: 40216, 8: 51865}, - ("Category Fours", 2, 8): {0: 5348, 4: 35757, 8: 58895}, + ("Category Fours", 2, 8): {0: 5348, 8: 94652}, ("Category Fours", 3, 0): {0: 100000}, - ("Category Fours", 3, 1): {0: 57914, 4: 34622, 8: 7464}, + ("Category Fours", 3, 1): {0: 57914, 4: 42086}, ("Category Fours", 3, 2): {0: 33621, 4: 44110, 8: 22269}, - ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 30898, 12: 7524}, - ("Category Fours", 3, 4): {0: 11125, 4: 36011, 8: 39024, 12: 13840}, - ("Category Fours", 3, 5): {0: 6367, 4: 29116, 8: 43192, 12: 21325}, - ("Category Fours", 3, 6): {0: 3643, 4: 22457, 8: 44477, 12: 29423}, - ("Category Fours", 3, 7): {0: 2178, 4: 16802, 8: 43275, 12: 37745}, - ("Category Fours", 3, 8): {0: 1255, 4: 12301, 8: 41132, 12: 45312}, + ("Category Fours", 3, 3): {0: 19153, 4: 42425, 8: 38422}, + ("Category Fours", 3, 4): {0: 11125, 8: 88875}, + ("Category Fours", 3, 5): {0: 6367, 8: 72308, 12: 21325}, + ("Category Fours", 3, 6): {0: 3643, 8: 66934, 12: 29423}, + ("Category Fours", 3, 7): {0: 2178, 8: 60077, 12: 37745}, + ("Category Fours", 3, 8): {0: 1255, 8: 53433, 12: 45312}, ("Category Fours", 4, 0): {0: 100000}, - ("Category Fours", 4, 1): {0: 48465, 4: 38398, 8: 13137}, - ("Category Fours", 4, 2): {0: 23296, 4: 40911, 8: 27073, 12: 8720}, - ("Category Fours", 4, 3): {0: 11200, 4: 33191, 8: 35337, 12: 20272}, - ("Category Fours", 4, 4): {0: 5447, 4: 23066, 8: 37441, 12: 26861, 16: 7185}, - ("Category Fours", 4, 5): {0: 2533, 4: 15668, 8: 34781, 12: 34222, 16: 12796}, - ("Category Fours", 4, 6): {0: 1314, 4: 10001, 8: 29850, 12: 39425, 16: 19410}, - ("Category Fours", 4, 7): {0: 592, 4: 6231, 8: 24250, 12: 41917, 16: 27010}, - ("Category Fours", 4, 8): {0: 302, 8: 23055, 12: 41866, 16: 34777}, + ("Category Fours", 4, 1): {0: 48465, 4: 51535}, + ("Category Fours", 4, 2): {0: 23296, 4: 40911, 12: 35793}, + ("Category Fours", 4, 3): {0: 11200, 8: 68528, 12: 20272}, + ("Category Fours", 4, 4): {0: 5447, 8: 60507, 12: 34046}, + ("Category Fours", 4, 5): {0: 2533, 8: 50449, 16: 47018}, + ("Category Fours", 4, 6): {0: 1314, 8: 39851, 12: 39425, 16: 19410}, + ("Category Fours", 4, 7): {0: 6823, 12: 66167, 16: 27010}, + ("Category Fours", 4, 8): {0: 4189, 12: 61034, 16: 34777}, ("Category Fours", 5, 0): {0: 100000}, - ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 16028, 12: 3630}, - ("Category Fours", 5, 2): {0: 15946, 4: 35579, 8: 31158, 12: 13998, 16: 3319}, - ("Category Fours", 5, 3): {0: 6479, 4: 23705, 8: 34575, 12: 24783, 16: 10458}, - ("Category Fours", 5, 4): {0: 2635, 4: 13889, 8: 30079, 12: 32428, 16: 17263, 20: 3706}, - ("Category Fours", 5, 5): {0: 1160, 4: 7756, 8: 23332, 12: 34254, 16: 25803, 20: 7695}, - ("Category Fours", 5, 6): {0: 434, 8: 20773, 12: 32910, 16: 32752, 20: 13131}, - ("Category Fours", 5, 7): {0: 169, 8: 13536, 12: 29123, 16: 37701, 20: 19471}, - ("Category Fours", 5, 8): {0: 1267, 8: 7340, 12: 24807, 16: 40144, 20: 26442}, + ("Category Fours", 5, 1): {0: 40215, 4: 40127, 8: 19658}, + ("Category Fours", 5, 2): {0: 15946, 8: 66737, 12: 17317}, + ("Category Fours", 5, 3): {0: 6479, 8: 58280, 16: 35241}, + ("Category Fours", 5, 4): {0: 2635, 8: 43968, 16: 53397}, + ("Category Fours", 5, 5): {0: 8916, 12: 57586, 16: 33498}, + ("Category Fours", 5, 6): {0: 4682, 12: 49435, 20: 45883}, + ("Category Fours", 5, 7): {0: 2291, 12: 40537, 16: 37701, 20: 19471}, + ("Category Fours", 5, 8): {0: 75, 16: 73483, 20: 26442}, ("Category Fours", 6, 0): {0: 100000}, - ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 20225, 12: 6287}, - ("Category Fours", 6, 2): {0: 11175, 4: 29824, 8: 32381, 12: 19179, 16: 7441}, - ("Category Fours", 6, 3): {0: 3698, 4: 16329, 8: 29939, 12: 29071, 16: 15808, 20: 5155}, - ("Category Fours", 6, 4): {0: 1284, 4: 7889, 8: 21748, 12: 31107, 16: 25281, 20: 12691}, - ("Category Fours", 6, 5): {0: 462, 8: 17601, 12: 27817, 16: 31233, 20: 18386, 24: 4501}, - ("Category Fours", 6, 6): {0: 1783, 8: 8344, 12: 22156, 16: 32690, 20: 26192, 24: 8835}, - ("Category Fours", 6, 7): {0: 767, 12: 20974, 16: 31490, 20: 32639, 24: 14130}, - ("Category Fours", 6, 8): {0: 357, 12: 13912, 16: 27841, 20: 37380, 24: 20510}, + ("Category Fours", 6, 1): {0: 33632, 4: 39856, 8: 26512}, + ("Category Fours", 6, 2): {0: 11175, 8: 62205, 12: 26620}, + ("Category Fours", 6, 3): {0: 3698, 8: 46268, 16: 50034}, + ("Category Fours", 6, 4): {0: 9173, 12: 52855, 20: 37972}, + ("Category Fours", 6, 5): {0: 4254, 12: 41626, 20: 54120}, + ("Category Fours", 6, 6): {0: 1783, 16: 63190, 24: 35027}, + ("Category Fours", 6, 7): {0: 5456, 16: 47775, 24: 46769}, + ("Category Fours", 6, 8): {0: 2881, 16: 39229, 24: 57890}, ("Category Fours", 7, 0): {0: 100000}, - ("Category Fours", 7, 1): {0: 27821, 4: 39289, 8: 23327, 12: 9563}, - ("Category Fours", 7, 2): {0: 7950, 4: 24026, 8: 31633, 12: 23169, 16: 13222}, - ("Category Fours", 7, 3): {0: 2194, 4: 11153, 8: 24107, 12: 29411, 16: 21390, 20: 11745}, - ("Category Fours", 7, 4): {0: 560, 8: 19291, 12: 26330, 16: 28118, 20: 18174, 24: 7527}, - ("Category Fours", 7, 5): {0: 1858, 8: 7862, 12: 19425, 16: 29003, 20: 26113, 24: 15739}, - ("Category Fours", 7, 6): {0: 679, 12: 16759, 16: 25831, 20: 30724, 24: 20147, 28: 5860}, - ("Category Fours", 7, 7): {0: 13, 12: 10063, 16: 20524, 20: 31843, 24: 27368, 28: 10189}, - ("Category Fours", 7, 8): {4: 864, 16: 19910, 20: 30153, 24: 33428, 28: 15645}, + ("Category Fours", 7, 1): {0: 27821, 4: 39289, 12: 32890}, + ("Category Fours", 7, 2): {0: 7950, 8: 55659, 16: 36391}, + ("Category Fours", 7, 3): {0: 2194, 12: 64671, 20: 33135}, + ("Category Fours", 7, 4): {0: 5063, 12: 41118, 20: 53819}, + ("Category Fours", 7, 5): {0: 171, 16: 57977, 24: 41852}, + ("Category Fours", 7, 6): {0: 4575, 16: 38694, 24: 56731}, + ("Category Fours", 7, 7): {0: 252, 20: 62191, 28: 37557}, + ("Category Fours", 7, 8): {4: 5576, 20: 45351, 28: 49073}, ("Category Fours", 8, 0): {0: 100000}, - ("Category Fours", 8, 1): {0: 23275, 4: 37161, 8: 25964, 12: 13600}, - ("Category Fours", 8, 2): {0: 5421, 4: 19014, 8: 29259, 12: 25812, 16: 14387, 20: 6107}, - ("Category Fours", 8, 3): {0: 1277, 4: 7349, 8: 18330, 12: 27186, 16: 25138, 20: 14371, 24: 6349}, - ("Category Fours", 8, 4): {0: 289, 8: 11929, 12: 20282, 16: 26960, 20: 23292, 24: 12927, 28: 4321}, - ("Category Fours", 8, 5): {0: 835, 12: 16706, 16: 23588, 20: 27754, 24: 20767, 28: 10350}, - ("Category Fours", 8, 6): {0: 21, 12: 8911, 16: 17296, 20: 27398, 24: 27074, 28: 15457, 32: 3843}, - ("Category Fours", 8, 7): {0: 745, 16: 15069, 20: 23737, 24: 30628, 28: 22590, 32: 7231}, - ("Category Fours", 8, 8): {0: 1949, 16: 7021, 20: 18630, 24: 31109, 28: 29548, 32: 11743}, + ("Category Fours", 8, 1): {0: 23275, 8: 76725}, + ("Category Fours", 8, 2): {0: 5421, 8: 48273, 16: 46306}, + ("Category Fours", 8, 3): {0: 8626, 12: 45516, 20: 45858}, + ("Category Fours", 8, 4): {0: 2852, 16: 56608, 24: 40540}, + ("Category Fours", 8, 5): {0: 5049, 20: 63834, 28: 31117}, + ("Category Fours", 8, 6): {0: 269, 20: 53357, 28: 46374}, + ("Category Fours", 8, 7): {0: 4394, 24: 65785, 28: 29821}, + ("Category Fours", 8, 8): {0: 266, 24: 58443, 32: 41291}, ("Category Fives", 0, 0): {0: 100000}, ("Category Fives", 0, 1): {0: 100000}, ("Category Fives", 0, 2): {0: 100000}, @@ -350,8 +350,8 @@ yacht_weights = { ("Category Fives", 1, 7): {0: 27730, 5: 72270}, ("Category Fives", 1, 8): {0: 23210, 5: 76790}, ("Category Fives", 2, 0): {0: 100000}, - ("Category Fives", 2, 1): {0: 69299, 5: 27864, 10: 2837}, - ("Category Fives", 2, 2): {0: 48156, 5: 42526, 10: 9318}, + ("Category Fives", 2, 1): {0: 69299, 5: 30701}, + ("Category Fives", 2, 2): {0: 48156, 5: 51844}, ("Category Fives", 2, 3): {0: 33225, 5: 49153, 10: 17622}, ("Category Fives", 2, 4): {0: 23218, 5: 50075, 10: 26707}, ("Category Fives", 2, 5): {0: 15939, 5: 48313, 10: 35748}, @@ -359,59 +359,59 @@ yacht_weights = { ("Category Fives", 2, 7): {0: 7822, 5: 40388, 10: 51790}, ("Category Fives", 2, 8): {0: 5386, 5: 35636, 10: 58978}, ("Category Fives", 3, 0): {0: 100000}, - ("Category Fives", 3, 1): {0: 58034, 5: 34541, 10: 7425}, - ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 19403, 15: 2904}, - ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 30794, 15: 7492}, + ("Category Fives", 3, 1): {0: 58034, 5: 41966}, + ("Category Fives", 3, 2): {0: 33466, 5: 44227, 10: 22307}, + ("Category Fives", 3, 3): {0: 19231, 5: 42483, 10: 38286}, ("Category Fives", 3, 4): {0: 11196, 5: 36192, 10: 38673, 15: 13939}, - ("Category Fives", 3, 5): {0: 6561, 5: 29163, 10: 43014, 15: 21262}, - ("Category Fives", 3, 6): {0: 3719, 5: 22181, 10: 44611, 15: 29489}, - ("Category Fives", 3, 7): {0: 2099, 5: 16817, 10: 43466, 15: 37618}, - ("Category Fives", 3, 8): {0: 1281, 5: 12473, 10: 40936, 15: 45310}, + ("Category Fives", 3, 5): {0: 6561, 10: 72177, 15: 21262}, + ("Category Fives", 3, 6): {0: 3719, 10: 66792, 15: 29489}, + ("Category Fives", 3, 7): {0: 2099, 10: 60283, 15: 37618}, + ("Category Fives", 3, 8): {0: 1281, 10: 53409, 15: 45310}, ("Category Fives", 4, 0): {0: 100000}, ("Category Fives", 4, 1): {0: 48377, 5: 38345, 10: 13278}, - ("Category Fives", 4, 2): {0: 23126, 5: 40940, 10: 27041, 15: 8893}, - ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 17250, 20: 3208}, - ("Category Fives", 4, 4): {0: 5362, 5: 23073, 10: 37379, 15: 26968, 20: 7218}, - ("Category Fives", 4, 5): {0: 2655, 5: 15662, 10: 34602, 15: 34186, 20: 12895}, - ("Category Fives", 4, 6): {0: 1291, 5: 9959, 10: 29833, 15: 39417, 20: 19500}, - ("Category Fives", 4, 7): {0: 623, 5: 6231, 10: 24360, 15: 41779, 20: 27007}, - ("Category Fives", 4, 8): {0: 313, 10: 23001, 15: 41957, 20: 34729}, + ("Category Fives", 4, 2): {0: 23126, 5: 40940, 15: 35934}, + ("Category Fives", 4, 3): {0: 11192, 5: 32597, 10: 35753, 15: 20458}, + ("Category Fives", 4, 4): {0: 5362, 10: 60452, 20: 34186}, + ("Category Fives", 4, 5): {0: 2655, 10: 50264, 15: 34186, 20: 12895}, + ("Category Fives", 4, 6): {0: 1291, 10: 39792, 15: 39417, 20: 19500}, + ("Category Fives", 4, 7): {0: 6854, 15: 66139, 20: 27007}, + ("Category Fives", 4, 8): {0: 4150, 15: 61121, 20: 34729}, ("Category Fives", 5, 0): {0: 100000}, - ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 16029, 15: 3499}, - ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 13793, 20: 3266}, - ("Category Fives", 5, 3): {0: 6526, 5: 23716, 10: 34430, 15: 25017, 20: 10311}, - ("Category Fives", 5, 4): {0: 2615, 5: 13975, 10: 30133, 15: 32247, 20: 17219, 25: 3811}, - ("Category Fives", 5, 5): {0: 1063, 5: 7876, 10: 23203, 15: 34489, 20: 25757, 25: 7612}, - ("Category Fives", 5, 6): {0: 429, 5: 4091, 10: 16696, 15: 32855, 20: 32891, 25: 13038}, - ("Category Fives", 5, 7): {0: 159, 10: 13509, 15: 29416, 20: 37778, 25: 19138}, - ("Category Fives", 5, 8): {0: 1179, 10: 7453, 15: 24456, 20: 40615, 25: 26297}, + ("Category Fives", 5, 1): {0: 39911, 5: 40561, 10: 19528}, + ("Category Fives", 5, 2): {0: 16178, 5: 35517, 10: 31246, 15: 17059}, + ("Category Fives", 5, 3): {0: 6526, 10: 58146, 20: 35328}, + ("Category Fives", 5, 4): {0: 2615, 10: 44108, 15: 32247, 20: 21030}, + ("Category Fives", 5, 5): {0: 1063, 10: 31079, 15: 34489, 25: 33369}, + ("Category Fives", 5, 6): {0: 4520, 15: 49551, 20: 32891, 25: 13038}, + ("Category Fives", 5, 7): {0: 2370, 15: 40714, 20: 37778, 25: 19138}, + ("Category Fives", 5, 8): {0: 1179, 15: 31909, 20: 40615, 25: 26297}, ("Category Fives", 6, 0): {0: 100000}, - ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 20181, 15: 6176}, - ("Category Fives", 6, 2): {0: 11322, 5: 29613, 10: 32664, 15: 19004, 20: 7397}, - ("Category Fives", 6, 3): {0: 3765, 5: 16288, 10: 29770, 15: 29233, 20: 15759, 25: 5185}, - ("Category Fives", 6, 4): {0: 1201, 5: 8226, 10: 21518, 15: 31229, 20: 25160, 25: 12666}, - ("Category Fives", 6, 5): {0: 433, 10: 17879, 15: 27961, 20: 30800, 25: 18442, 30: 4485}, - ("Category Fives", 6, 6): {0: 141, 10: 10040, 15: 22226, 20: 32744, 25: 26341, 30: 8508}, - ("Category Fives", 6, 7): {0: 772, 10: 4724, 15: 16206, 20: 31363, 25: 32784, 30: 14151}, - ("Category Fives", 6, 8): {0: 297, 15: 13902, 20: 28004, 25: 37178, 30: 20619}, + ("Category Fives", 6, 1): {0: 33476, 5: 40167, 10: 26357}, + ("Category Fives", 6, 2): {0: 11322, 10: 62277, 20: 26401}, + ("Category Fives", 6, 3): {0: 3765, 10: 46058, 20: 50177}, + ("Category Fives", 6, 4): {0: 1201, 15: 60973, 25: 37826}, + ("Category Fives", 6, 5): {0: 4307, 15: 41966, 20: 30800, 25: 22927}, + ("Category Fives", 6, 6): {0: 1827, 15: 30580, 20: 32744, 30: 34849}, + ("Category Fives", 6, 7): {0: 5496, 20: 47569, 25: 32784, 30: 14151}, + ("Category Fives", 6, 8): {0: 2920, 20: 39283, 25: 37178, 30: 20619}, ("Category Fives", 7, 0): {0: 100000}, - ("Category Fives", 7, 1): {0: 27826, 5: 39154, 10: 23567, 15: 9453}, - ("Category Fives", 7, 2): {0: 7609, 5: 24193, 10: 31722, 15: 23214, 20: 10140, 25: 3122}, - ("Category Fives", 7, 3): {0: 2262, 5: 11013, 10: 24443, 15: 29307, 20: 21387, 25: 11588}, - ("Category Fives", 7, 4): {0: 618, 5: 4583, 10: 14761, 15: 26159, 20: 28335, 25: 18050, 30: 7494}, - ("Category Fives", 7, 5): {0: 183, 10: 9616, 15: 19685, 20: 28915, 25: 26000, 30: 12883, 35: 2718}, - ("Category Fives", 7, 6): {0: 670, 15: 16878, 20: 25572, 25: 30456, 30: 20695, 35: 5729}, - ("Category Fives", 7, 7): {0: 255, 15: 9718, 20: 20696, 25: 31883, 30: 27333, 35: 10115}, - ("Category Fives", 7, 8): {0: 927, 15: 4700, 20: 15292, 25: 30298, 30: 33015, 35: 15768}, + ("Category Fives", 7, 1): {0: 27826, 5: 39154, 15: 33020}, + ("Category Fives", 7, 2): {0: 7609, 10: 55915, 20: 36476}, + ("Category Fives", 7, 3): {0: 2262, 10: 35456, 20: 62282}, + ("Category Fives", 7, 4): {0: 5201, 15: 40920, 25: 53879}, + ("Category Fives", 7, 5): {0: 1890, 20: 56509, 30: 41601}, + ("Category Fives", 7, 6): {0: 4506, 20: 38614, 25: 30456, 30: 26424}, + ("Category Fives", 7, 7): {0: 2107, 25: 60445, 35: 37448}, + ("Category Fives", 7, 8): {0: 5627, 25: 45590, 30: 33015, 35: 15768}, ("Category Fives", 8, 0): {0: 100000}, - ("Category Fives", 8, 1): {0: 23333, 5: 37259, 10: 25947, 15: 10392, 20: 3069}, - ("Category Fives", 8, 2): {0: 5425, 5: 18915, 10: 29380, 15: 25994, 20: 14056, 25: 6230}, - ("Category Fives", 8, 3): {0: 1258, 5: 7317, 10: 18783, 15: 27375, 20: 24542, 25: 14322, 30: 6403}, - ("Category Fives", 8, 4): {0: 271, 10: 11864, 15: 20267, 20: 27158, 25: 23589, 30: 12529, 35: 4322}, - ("Category Fives", 8, 5): {0: 943, 10: 4260, 15: 12456, 20: 23115, 25: 27968, 30: 20704, 35: 10554}, - ("Category Fives", 8, 6): {0: 281, 15: 8625, 20: 17201, 25: 27484, 30: 27178, 35: 15414, 40: 3817}, - ("Category Fives", 8, 7): {0: 746, 20: 14964, 25: 23717, 30: 30426, 35: 22677, 40: 7470}, - ("Category Fives", 8, 8): {0: 261, 20: 8927, 25: 18714, 30: 31084, 35: 29126, 40: 11888}, + ("Category Fives", 8, 1): {0: 23333, 5: 37259, 15: 39408}, + ("Category Fives", 8, 2): {0: 5425, 10: 48295, 20: 46280}, + ("Category Fives", 8, 3): {0: 1258, 15: 53475, 25: 45267}, + ("Category Fives", 8, 4): {0: 2752, 20: 56808, 30: 40440}, + ("Category Fives", 8, 5): {0: 5203, 20: 35571, 30: 59226}, + ("Category Fives", 8, 6): {0: 1970, 25: 51621, 35: 46409}, + ("Category Fives", 8, 7): {0: 4281, 25: 35146, 30: 30426, 40: 30147}, + ("Category Fives", 8, 8): {0: 2040, 30: 56946, 40: 41014}, ("Category Sixes", 0, 0): {0: 100000}, ("Category Sixes", 0, 1): {0: 100000}, ("Category Sixes", 0, 2): {0: 100000}, @@ -431,8 +431,8 @@ yacht_weights = { ("Category Sixes", 1, 7): {0: 28251, 6: 71749}, ("Category Sixes", 1, 8): {0: 23206, 6: 76794}, ("Category Sixes", 2, 0): {0: 100000}, - ("Category Sixes", 2, 1): {0: 69463, 6: 27651, 12: 2886}, - ("Category Sixes", 2, 2): {0: 47896, 6: 42794, 12: 9310}, + ("Category Sixes", 2, 1): {0: 69463, 6: 30537}, + ("Category Sixes", 2, 2): {0: 47896, 6: 52104}, ("Category Sixes", 2, 3): {0: 33394, 6: 48757, 12: 17849}, ("Category Sixes", 2, 4): {0: 23552, 6: 49554, 12: 26894}, ("Category Sixes", 2, 5): {0: 16090, 6: 48098, 12: 35812}, @@ -440,59 +440,59 @@ yacht_weights = { ("Category Sixes", 2, 7): {0: 7737, 6: 40480, 12: 51783}, ("Category Sixes", 2, 8): {0: 5379, 6: 35672, 12: 58949}, ("Category Sixes", 3, 0): {0: 100000}, - ("Category Sixes", 3, 1): {0: 57718, 6: 34818, 12: 7464}, - ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 19159, 18: 2903}, - ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 30952, 18: 7436}, + ("Category Sixes", 3, 1): {0: 57718, 6: 42282}, + ("Category Sixes", 3, 2): {0: 33610, 6: 44328, 12: 22062}, + ("Category Sixes", 3, 3): {0: 19366, 6: 42246, 12: 38388}, ("Category Sixes", 3, 4): {0: 11144, 6: 36281, 12: 38817, 18: 13758}, ("Category Sixes", 3, 5): {0: 6414, 6: 28891, 12: 43114, 18: 21581}, - ("Category Sixes", 3, 6): {0: 3870, 6: 22394, 12: 44318, 18: 29418}, - ("Category Sixes", 3, 7): {0: 2188, 6: 16803, 12: 43487, 18: 37522}, - ("Category Sixes", 3, 8): {0: 1289, 6: 12421, 12: 41082, 18: 45208}, + ("Category Sixes", 3, 6): {0: 3870, 12: 66712, 18: 29418}, + ("Category Sixes", 3, 7): {0: 2188, 12: 60290, 18: 37522}, + ("Category Sixes", 3, 8): {0: 1289, 12: 53503, 18: 45208}, ("Category Sixes", 4, 0): {0: 100000}, ("Category Sixes", 4, 1): {0: 48197, 6: 38521, 12: 13282}, - ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 26935, 18: 8731}, - ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 17390, 24: 3157}, - ("Category Sixes", 4, 4): {0: 5324, 6: 23265, 12: 37209, 18: 26929, 24: 7273}, - ("Category Sixes", 4, 5): {0: 2658, 6: 15488, 12: 34685, 18: 34476, 24: 12693}, - ("Category Sixes", 4, 6): {0: 1282, 6: 9997, 12: 29855, 18: 39379, 24: 19487}, - ("Category Sixes", 4, 7): {0: 588, 6: 6202, 12: 24396, 18: 41935, 24: 26879}, - ("Category Sixes", 4, 8): {0: 317, 6: 3863, 12: 19042, 18: 42180, 24: 34598}, + ("Category Sixes", 4, 2): {0: 23155, 6: 41179, 12: 35666}, + ("Category Sixes", 4, 3): {0: 11256, 6: 32609, 12: 35588, 18: 20547}, + ("Category Sixes", 4, 4): {0: 5324, 12: 60474, 18: 34202}, + ("Category Sixes", 4, 5): {0: 2658, 12: 50173, 18: 34476, 24: 12693}, + ("Category Sixes", 4, 6): {0: 1282, 12: 39852, 18: 39379, 24: 19487}, + ("Category Sixes", 4, 7): {0: 588, 12: 30598, 18: 41935, 24: 26879}, + ("Category Sixes", 4, 8): {0: 4180, 18: 61222, 24: 34598}, ("Category Sixes", 5, 0): {0: 100000}, - ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 16206, 18: 3497}, - ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 13612, 24: 3281}, - ("Category Sixes", 5, 3): {0: 6456, 6: 23539, 12: 34585, 18: 25020, 24: 10400}, - ("Category Sixes", 5, 4): {0: 2581, 6: 13980, 12: 30355, 18: 32198, 24: 17115, 30: 3771}, - ("Category Sixes", 5, 5): {0: 1119, 6: 7775, 12: 23063, 18: 34716, 24: 25568, 30: 7759}, - ("Category Sixes", 5, 6): {0: 392, 6: 4171, 12: 16724, 18: 32792, 24: 32829, 30: 13092}, - ("Category Sixes", 5, 7): {0: 197, 12: 13627, 18: 29190, 24: 37560, 30: 19426}, - ("Category Sixes", 5, 8): {0: 1246, 12: 7404, 18: 24560, 24: 40134, 30: 26656}, + ("Category Sixes", 5, 1): {0: 40393, 6: 39904, 12: 19703}, + ("Category Sixes", 5, 2): {0: 16202, 6: 35664, 12: 31241, 18: 16893}, + ("Category Sixes", 5, 3): {0: 6456, 12: 58124, 18: 25020, 24: 10400}, + ("Category Sixes", 5, 4): {0: 2581, 12: 44335, 18: 32198, 24: 20886}, + ("Category Sixes", 5, 5): {0: 1119, 12: 30838, 18: 34716, 24: 33327}, + ("Category Sixes", 5, 6): {0: 4563, 18: 49516, 24: 32829, 30: 13092}, + ("Category Sixes", 5, 7): {0: 2315, 18: 40699, 24: 37560, 30: 19426}, + ("Category Sixes", 5, 8): {0: 1246, 18: 31964, 24: 40134, 30: 26656}, ("Category Sixes", 6, 0): {0: 100000}, - ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 12: 20198, 18: 6268}, - ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 18: 19196, 24: 7514}, - ("Category Sixes", 6, 3): {0: 3787, 6: 16266, 12: 29873, 18: 29107, 24: 15863, 30: 5104}, - ("Category Sixes", 6, 4): {0: 1286, 6: 8066, 12: 21653, 18: 31264, 24: 25039, 30: 12692}, - ("Category Sixes", 6, 5): {0: 413, 6: 3777, 12: 13962, 18: 27705, 24: 30919, 30: 18670, 36: 4554}, - ("Category Sixes", 6, 6): {0: 146, 12: 10040, 18: 22320, 24: 32923, 30: 26086, 36: 8485}, - ("Category Sixes", 6, 7): {0: 814, 12: 4698, 18: 16286, 24: 31577, 30: 32487, 36: 14138}, - ("Category Sixes", 6, 8): {0: 328, 18: 14004, 24: 28064, 30: 37212, 36: 20392}, + ("Category Sixes", 6, 1): {0: 33316, 6: 40218, 18: 26466}, + ("Category Sixes", 6, 2): {0: 11256, 6: 29444, 12: 32590, 24: 26710}, + ("Category Sixes", 6, 3): {0: 3787, 12: 46139, 18: 29107, 24: 20967}, + ("Category Sixes", 6, 4): {0: 1286, 12: 29719, 18: 31264, 24: 25039, 30: 12692}, + ("Category Sixes", 6, 5): {0: 4190, 18: 41667, 24: 30919, 30: 23224}, + ("Category Sixes", 6, 6): {0: 1804, 18: 30702, 24: 32923, 30: 34571}, + ("Category Sixes", 6, 7): {0: 51, 24: 53324, 30: 32487, 36: 14138}, + ("Category Sixes", 6, 8): {0: 2886, 24: 39510, 30: 37212, 36: 20392}, ("Category Sixes", 7, 0): {0: 100000}, - ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 12: 23499, 18: 9665}, - ("Category Sixes", 7, 2): {0: 7883, 6: 23846, 12: 31558, 18: 23295, 24: 10316, 30: 3102}, - ("Category Sixes", 7, 3): {0: 2186, 6: 10928, 12: 24321, 18: 29650, 24: 21177, 30: 9209, 36: 2529}, - ("Category Sixes", 7, 4): {0: 603, 6: 4459, 12: 14673, 18: 26303, 24: 28335, 30: 18228, 36: 7399}, - ("Category Sixes", 7, 5): {0: 172, 12: 9654, 18: 19381, 24: 29254, 30: 25790, 36: 12992, 42: 2757}, - ("Category Sixes", 7, 6): {0: 704, 12: 3864, 18: 13039, 24: 25760, 30: 30698, 36: 20143, 42: 5792}, - ("Category Sixes", 7, 7): {0: 257, 18: 9857, 24: 20557, 30: 31709, 36: 27546, 42: 10074}, - ("Category Sixes", 7, 8): {0: 872, 18: 4658, 24: 15419, 30: 30259, 36: 33183, 42: 15609}, + ("Category Sixes", 7, 1): {0: 27852, 6: 38984, 18: 33164}, + ("Category Sixes", 7, 2): {0: 7883, 12: 55404, 24: 36713}, + ("Category Sixes", 7, 3): {0: 2186, 12: 35249, 18: 29650, 30: 32915}, + ("Category Sixes", 7, 4): {0: 5062, 18: 40976, 24: 28335, 36: 25627}, + ("Category Sixes", 7, 5): {0: 1947, 18: 27260, 24: 29254, 30: 25790, 36: 15749}, + ("Category Sixes", 7, 6): {0: 4568, 24: 38799, 30: 30698, 42: 25935}, + ("Category Sixes", 7, 7): {0: 2081, 24: 28590, 30: 31709, 36: 37620}, + ("Category Sixes", 7, 8): {0: 73, 30: 51135, 36: 33183, 42: 15609}, ("Category Sixes", 8, 0): {0: 100000}, - ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 10483, 24: 3123}, - ("Category Sixes", 8, 2): {0: 5280, 6: 18943, 12: 29664, 18: 25777, 24: 14170, 30: 6166}, - ("Category Sixes", 8, 3): {0: 1246, 6: 7112, 12: 18757, 18: 27277, 24: 24802, 30: 14351, 36: 6455}, - ("Category Sixes", 8, 4): {0: 301, 12: 12044, 18: 20247, 24: 27146, 30: 23403, 36: 12524, 42: 4335}, - ("Category Sixes", 8, 5): {0: 859, 12: 4241, 18: 12477, 24: 23471, 30: 27655, 36: 20803, 42: 10494}, - ("Category Sixes", 8, 6): {0: 277, 18: 8656, 24: 17373, 30: 27347, 36: 27024, 42: 15394, 48: 3929}, - ("Category Sixes", 8, 7): {0: 766, 18: 3503, 24: 11451, 30: 23581, 36: 30772, 42: 22654, 48: 7273}, - ("Category Sixes", 8, 8): {6: 262, 24: 8866, 30: 18755, 36: 31116, 42: 28870, 48: 12131}, + ("Category Sixes", 8, 1): {0: 23220, 6: 37213, 12: 25961, 18: 13606}, + ("Category Sixes", 8, 2): {0: 5280, 12: 48607, 18: 25777, 30: 20336}, + ("Category Sixes", 8, 3): {0: 1246, 12: 25869, 18: 27277, 30: 45608}, + ("Category Sixes", 8, 4): {0: 2761, 18: 29831, 24: 27146, 36: 40262}, + ("Category Sixes", 8, 5): {0: 5100, 24: 35948, 30: 27655, 42: 31297}, + ("Category Sixes", 8, 6): {0: 2067, 30: 51586, 36: 27024, 42: 19323}, + ("Category Sixes", 8, 7): {0: 4269, 30: 35032, 36: 30772, 48: 29927}, + ("Category Sixes", 8, 8): {6: 2012, 30: 25871, 36: 31116, 42: 28870, 48: 12131}, ("Category Choice", 0, 0): {0: 100000}, ("Category Choice", 0, 1): {0: 100000}, ("Category Choice", 0, 2): {0: 100000}, @@ -503,77 +503,77 @@ yacht_weights = { ("Category Choice", 0, 7): {0: 100000}, ("Category Choice", 0, 8): {0: 100000}, ("Category Choice", 1, 0): {0: 100000}, - ("Category Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Choice", 2, 0): {0: 100000}, - ("Category Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Choice", 3, 0): {0: 100000}, - ("Category Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Choice", 4, 0): {0: 100000}, - ("Category Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Choice", 5, 0): {0: 100000}, - ("Category Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Choice", 6, 0): {0: 100000}, - ("Category Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Choice", 6, 2): {8: 1504, 16: 8676, 20: 10032, 22: 14673, 26: 27312, 27: 16609, 29: 12133, 31: 9061}, - ("Category Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Choice", 7, 0): {0: 100000}, - ("Category Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Choice", 8, 0): {0: 100000}, - ("Category Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Choice", 8, 2): {11: 5900, 26: 10331, 29: 11435, 31: 14533, 34: 23939, 36: 13855, 38: 10165, 40: 9842}, - ("Category Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Choice", 8, 5): {16: 4986, 32: 9031, 35: 10214, 37: 14528, 41: 25608, 42: 16131, 44: 11245, 46: 8257}, - ("Category Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Inverse Choice", 0, 0): {0: 100000}, ("Category Inverse Choice", 0, 1): {0: 100000}, ("Category Inverse Choice", 0, 2): {0: 100000}, @@ -584,104 +584,77 @@ yacht_weights = { ("Category Inverse Choice", 0, 7): {0: 100000}, ("Category Inverse Choice", 0, 8): {0: 100000}, ("Category Inverse Choice", 1, 0): {0: 100000}, - ("Category Inverse Choice", 1, 1): {1: 16642, 3: 33501, 5: 33218, 6: 16639}, - ("Category Inverse Choice", 1, 2): {1: 10921, 3: 22060, 5: 39231, 6: 27788}, - ("Category Inverse Choice", 1, 3): {1: 9416, 4: 27917, 5: 22740, 6: 39927}, - ("Category Inverse Choice", 1, 4): {1: 15490, 3: 15489, 6: 69021}, - ("Category Inverse Choice", 1, 5): {1: 12817, 3: 12757, 6: 74426}, - ("Category Inverse Choice", 1, 6): {1: 10513, 3: 10719, 6: 78768}, - ("Category Inverse Choice", 1, 7): {1: 8893, 6: 91107}, - ("Category Inverse Choice", 1, 8): {1: 14698, 6: 85302}, + ("Category Inverse Choice", 1, 1): {1: 33315, 5: 66685}, + ("Category Inverse Choice", 1, 2): {1: 10921, 5: 89079}, + ("Category Inverse Choice", 1, 3): {1: 27995, 6: 72005}, + ("Category Inverse Choice", 1, 4): {1: 15490, 6: 84510}, + ("Category Inverse Choice", 1, 5): {1: 6390, 6: 93610}, + ("Category Inverse Choice", 1, 6): {1: 34656, 6: 65344}, + ("Category Inverse Choice", 1, 7): {1: 28829, 6: 71171}, + ("Category Inverse Choice", 1, 8): {1: 23996, 6: 76004}, ("Category Inverse Choice", 2, 0): {0: 100000}, - ("Category Inverse Choice", 2, 1): {2: 8504, 6: 32987, 8: 30493, 11: 28016}, - ("Category Inverse Choice", 2, 2): {2: 3714, 7: 33270, 9: 25859, 11: 37157}, - ("Category Inverse Choice", 2, 3): {2: 5113, 5: 10402, 8: 25783, 10: 24173, 12: 34529}, - ("Category Inverse Choice", 2, 4): {2: 1783, 4: 8908, 8: 23189, 10: 22115, 12: 44005}, - ("Category Inverse Choice", 2, 5): {2: 7575, 8: 20444, 11: 38062, 12: 33919}, - ("Category Inverse Choice", 2, 6): {2: 5153, 9: 26383, 11: 25950, 12: 42514}, - ("Category Inverse Choice", 2, 7): {2: 3638, 7: 15197, 9: 14988, 12: 66177}, - ("Category Inverse Choice", 2, 8): {2: 2448, 7: 13306, 9: 12754, 12: 71492}, + ("Category Inverse Choice", 2, 1): {2: 16796, 8: 83204}, + ("Category Inverse Choice", 2, 2): {2: 22212, 10: 77788}, + ("Category Inverse Choice", 2, 3): {2: 29002, 11: 70998}, + ("Category Inverse Choice", 2, 4): {2: 22485, 11: 77515}, + ("Category Inverse Choice", 2, 5): {2: 28019, 12: 71981}, + ("Category Inverse Choice", 2, 6): {2: 23193, 12: 76807}, + ("Category Inverse Choice", 2, 7): {2: 11255, 8: 38369, 12: 50376}, + ("Category Inverse Choice", 2, 8): {2: 9297, 12: 90703}, ("Category Inverse Choice", 3, 0): {0: 100000}, - ("Category Inverse Choice", 3, 1): {3: 4589, 6: 11560, 9: 21469, 11: 25007, 13: 28332, 15: 9043}, - ("Category Inverse Choice", 3, 2): {3: 1380, 6: 8622, 9: 14417, 12: 23457, 14: 24807, 17: 27317}, - ("Category Inverse Choice", 3, 3): {3: 1605, 7: 9370, 10: 13491, 13: 24408, 15: 23065, 17: 28061}, - ("Category Inverse Choice", 3, 4): {3: 7212, 13: 32000, 15: 22707, 17: 38081}, - ("Category Inverse Choice", 3, 5): {3: 7989, 11: 10756, 14: 23811, 16: 21668, 18: 35776}, - ("Category Inverse Choice", 3, 6): {3: 3251, 10: 10272, 14: 21653, 17: 37049, 18: 27775}, - ("Category Inverse Choice", 3, 7): {3: 1018, 9: 8591, 15: 28080, 17: 26469, 18: 35842}, - ("Category Inverse Choice", 3, 8): {3: 6842, 15: 25118, 17: 24534, 18: 43506}, + ("Category Inverse Choice", 3, 1): {3: 25983, 12: 74017}, + ("Category Inverse Choice", 3, 2): {3: 24419, 14: 75581}, + ("Category Inverse Choice", 3, 3): {3: 24466, 15: 75534}, + ("Category Inverse Choice", 3, 4): {3: 25866, 16: 74134}, + ("Category Inverse Choice", 3, 5): {3: 30994, 17: 69006}, + ("Category Inverse Choice", 3, 6): {3: 13523, 13: 41606, 17: 44871}, + ("Category Inverse Choice", 3, 7): {3: 28667, 18: 71333}, + ("Category Inverse Choice", 3, 8): {3: 23852, 18: 76148}, ("Category Inverse Choice", 4, 0): {0: 100000}, - ("Category Inverse Choice", 4, 1): {4: 5386, 9: 10561, 13: 28501, 15: 21902, 17: 23999, 19: 9651}, - ("Category Inverse Choice", 4, 2): {4: 7510, 12: 10646, 16: 28145, 18: 22596, 19: 17705, 21: 13398}, - ("Category Inverse Choice", 4, 3): {4: 2392, 11: 8547, 14: 13300, 18: 29887, 20: 21680, 21: 15876, 23: 8318}, - ("Category Inverse Choice", 4, 4): {4: 2258, 12: 8230, 15: 12216, 19: 31486, 21: 20698, 23: 25112}, - ("Category Inverse Choice", 4, 5): {4: 2209, 13: 8484, 16: 11343, 19: 21913, 21: 21675, 23: 34376}, - ("Category Inverse Choice", 4, 6): {4: 2179, 14: 8704, 17: 12056, 20: 23300, 22: 20656, 24: 33105}, - ("Category Inverse Choice", 4, 7): {5: 7652, 19: 20489, 21: 20365, 23: 26176, 24: 25318}, - ("Category Inverse Choice", 4, 8): {5: 3231, 16: 8958, 21: 28789, 23: 25837, 24: 33185}, + ("Category Inverse Choice", 4, 1): {4: 1125, 7: 32443, 16: 66432}, + ("Category Inverse Choice", 4, 2): {4: 18156, 14: 39494, 18: 42350}, + ("Category Inverse Choice", 4, 3): {4: 538, 9: 32084, 20: 67378}, + ("Category Inverse Choice", 4, 4): {4: 30873, 21: 69127}, + ("Category Inverse Choice", 4, 5): {4: 31056, 22: 68944}, + ("Category Inverse Choice", 4, 6): {4: 22939, 19: 43956, 23: 33105}, + ("Category Inverse Choice", 4, 7): {5: 16935, 19: 41836, 23: 41229}, + ("Category Inverse Choice", 4, 8): {5: 31948, 24: 68052}, ("Category Inverse Choice", 5, 0): {0: 100000}, - ("Category Inverse Choice", 5, 1): {5: 1575, 10: 8293, 13: 12130, 17: 28045, 20: 40099, 23: 9858}, - ("Category Inverse Choice", 5, 2): {5: 3298, 14: 10211, 17: 13118, 21: 28204, 24: 34078, 26: 11091}, - ("Category Inverse Choice", 5, 3): {6: 2633, 15: 8316, 18: 11302, 22: 26605, 24: 20431, 26: 22253, 28: 8460}, - ("Category Inverse Choice", 5, 4): {5: 4084, 17: 9592, 20: 13422, 24: 28620, 26: 20353, 27: 14979, 29: 8950}, - ("Category Inverse Choice", 5, 5): {6: 348, 14: 8075, 20: 10195, 22: 14679, 25: 22335, 28: 28253, 29: 16115}, - ("Category Inverse Choice", 5, 6): {7: 3204, 19: 9258, 22: 11859, 25: 21412, 27: 20895, 29: 33372}, - ("Category Inverse Choice", 5, 7): {8: 2983, 20: 9564, 23: 12501, 26: 22628, 29: 34285, 30: 18039}, - ("Category Inverse Choice", 5, 8): {9: 323, 17: 8259, 25: 20762, 27: 20118, 29: 25318, 30: 25220}, + ("Category Inverse Choice", 5, 1): {5: 21998, 15: 38001, 19: 40001}, + ("Category Inverse Choice", 5, 2): {5: 26627, 19: 38217, 23: 35156}, + ("Category Inverse Choice", 5, 3): {6: 22251, 24: 77749}, + ("Category Inverse Choice", 5, 4): {5: 27098, 22: 39632, 26: 33270}, + ("Category Inverse Choice", 5, 5): {6: 1166, 16: 32131, 27: 66703}, + ("Category Inverse Choice", 5, 6): {7: 1177, 17: 32221, 28: 66602}, + ("Category Inverse Choice", 5, 7): {8: 25048, 25: 42590, 29: 32362}, + ("Category Inverse Choice", 5, 8): {9: 18270, 25: 41089, 29: 40641}, ("Category Inverse Choice", 6, 0): {0: 100000}, - ("Category Inverse Choice", 6, 1): {6: 6102, 17: 21746, 21: 26524, 23: 25004, 25: 11086, 27: 9538}, - ("Category Inverse Choice", 6, 2): { - 8: 1504, - 16: 8676, - 20: 10032, - 22: 14673, - 26: 27312, - 27: 16609, - 29: 12133, - 31: 9061, - }, - ("Category Inverse Choice", 6, 3): {6: 1896, 18: 8914, 22: 10226, 24: 14822, 28: 27213, 31: 28868, 33: 8061}, - ("Category Inverse Choice", 6, 4): {9: 441, 17: 8018, 25: 22453, 29: 26803, 32: 32275, 34: 10010}, - ("Category Inverse Choice", 6, 5): {10: 1788, 21: 8763, 25: 10319, 27: 14763, 31: 30144, 33: 23879, 35: 10344}, - ("Category Inverse Choice", 6, 6): {13: 876, 21: 8303, 28: 24086, 31: 21314, 34: 28149, 35: 17272}, - ("Category Inverse Choice", 6, 7): {12: 3570, 25: 9625, 28: 11348, 31: 20423, 33: 20469, 35: 34565}, - ("Category Inverse Choice", 6, 8): {12: 3450, 26: 9544, 29: 12230, 32: 22130, 35: 33671, 36: 18975}, + ("Category Inverse Choice", 6, 1): {6: 27848, 23: 72152}, + ("Category Inverse Choice", 6, 2): {8: 27078, 27: 72922}, + ("Category Inverse Choice", 6, 3): {6: 27876, 29: 72124}, + ("Category Inverse Choice", 6, 4): {9: 30912, 31: 69088}, + ("Category Inverse Choice", 6, 5): {10: 27761, 28: 38016, 32: 34223}, + ("Category Inverse Choice", 6, 6): {13: 25547, 29: 39452, 33: 35001}, + ("Category Inverse Choice", 6, 7): {12: 767, 22: 32355, 34: 66878}, + ("Category Inverse Choice", 6, 8): {12: 25224, 31: 41692, 35: 33084}, ("Category Inverse Choice", 7, 0): {0: 100000}, - ("Category Inverse Choice", 7, 1): {7: 1237, 15: 8100, 21: 23947, 25: 25361, 27: 22186, 31: 19169}, - ("Category Inverse Choice", 7, 2): {10: 2086, 20: 8960, 26: 23657, 30: 25264, 31: 15759, 33: 12356, 35: 11918}, - ("Category Inverse Choice", 7, 3): {10: 4980, 24: 9637, 27: 11247, 29: 15046, 33: 33492, 35: 13130, 37: 12468}, - ("Category Inverse Choice", 7, 4): {13: 2260, 24: 8651, 30: 23022, 34: 25656, 37: 29910, 39: 10501}, - ("Category Inverse Choice", 7, 5): {12: 3879, 27: 8154, 30: 10292, 32: 14692, 36: 27425, 38: 23596, 40: 11962}, - ("Category Inverse Choice", 7, 6): {14: 1957, 27: 8230, 33: 23945, 37: 29286, 39: 24519, 41: 12063}, - ("Category Inverse Choice", 7, 7): {16: 599, 26: 8344, 34: 22981, 37: 20883, 40: 28045, 42: 19148}, - ("Category Inverse Choice", 7, 8): {14: 3639, 31: 8907, 34: 10904, 37: 20148, 39: 20219, 41: 21627, 42: 14556}, + ("Category Inverse Choice", 7, 1): {7: 1237, 15: 32047, 27: 66716}, + ("Category Inverse Choice", 7, 2): {10: 27324, 31: 72676}, + ("Category Inverse Choice", 7, 3): {10: 759, 20: 32233, 34: 67008}, + ("Category Inverse Choice", 7, 4): {13: 26663, 35: 73337}, + ("Category Inverse Choice", 7, 5): {12: 29276, 37: 70724}, + ("Category Inverse Choice", 7, 6): {14: 26539, 38: 73461}, + ("Category Inverse Choice", 7, 7): {16: 24675, 35: 38365, 39: 36960}, + ("Category Inverse Choice", 7, 8): {14: 2, 19: 31688, 40: 68310}, ("Category Inverse Choice", 8, 0): {0: 100000}, - ("Category Inverse Choice", 8, 1): {10: 752, 17: 8385, 24: 21460, 26: 15361, 29: 23513, 31: 12710, 35: 17819}, - ("Category Inverse Choice", 8, 2): { - 11: 5900, - 26: 10331, - 29: 11435, - 31: 14533, - 34: 23939, - 36: 13855, - 38: 10165, - 40: 9842, - }, - ("Category Inverse Choice", 8, 3): {12: 2241, 26: 8099, 32: 20474, 34: 14786, 38: 31140, 40: 11751, 42: 11509}, - ("Category Inverse Choice", 8, 4): {16: 1327, 27: 8361, 34: 19865, 36: 15078, 40: 32325, 42: 12218, 44: 10826}, - ("Category Inverse Choice", 8, 5): { - 16: 4986, - 32: 9031, - 35: 10214, - 37: 14528, - 41: 25608, - 42: 16131, - 44: 11245, - 46: 8257, - }, - ("Category Inverse Choice", 8, 6): {16: 2392, 32: 8742, 38: 23237, 42: 26333, 45: 30725, 47: 8571}, - ("Category Inverse Choice", 8, 7): {20: 1130, 32: 8231, 39: 22137, 43: 28783, 45: 25221, 47: 14498}, - ("Category Inverse Choice", 8, 8): {20: 73, 28: 8033, 40: 21670, 43: 20615, 46: 28105, 48: 21504}, + ("Category Inverse Choice", 8, 1): {10: 23768, 25: 38280, 30: 37952}, + ("Category Inverse Choice", 8, 2): {11: 27666, 31: 38472, 36: 33862}, + ("Category Inverse Choice", 8, 3): {12: 24387, 33: 37477, 38: 38136}, + ("Category Inverse Choice", 8, 4): {16: 23316, 35: 38117, 40: 38567}, + ("Category Inverse Choice", 8, 5): {16: 30949, 42: 69051}, + ("Category Inverse Choice", 8, 6): {16: 26968, 43: 73032}, + ("Category Inverse Choice", 8, 7): {20: 24559, 44: 75441}, + ("Category Inverse Choice", 8, 8): {20: 1, 23: 22731, 41: 37835, 45: 39433}, ("Category Pair", 0, 0): {0: 100000}, ("Category Pair", 0, 1): {0: 100000}, ("Category Pair", 0, 2): {0: 100000}, @@ -791,7 +764,7 @@ yacht_weights = { ("Category Three of a Kind", 2, 7): {0: 100000}, ("Category Three of a Kind", 2, 8): {0: 100000}, ("Category Three of a Kind", 3, 0): {0: 100000}, - ("Category Three of a Kind", 3, 1): {0: 97222, 20: 2778}, + ("Category Three of a Kind", 3, 1): {0: 100000}, ("Category Three of a Kind", 3, 2): {0: 88880, 20: 11120}, ("Category Three of a Kind", 3, 3): {0: 78187, 20: 21813}, ("Category Three of a Kind", 3, 4): {0: 67476, 20: 32524}, @@ -881,7 +854,7 @@ yacht_weights = { ("Category Four of a Kind", 3, 7): {0: 100000}, ("Category Four of a Kind", 3, 8): {0: 100000}, ("Category Four of a Kind", 4, 0): {0: 100000}, - ("Category Four of a Kind", 4, 1): {0: 99516, 30: 484}, + ("Category Four of a Kind", 4, 1): {0: 100000}, ("Category Four of a Kind", 4, 2): {0: 96122, 30: 3878}, ("Category Four of a Kind", 4, 3): {0: 89867, 30: 10133}, ("Category Four of a Kind", 4, 4): {0: 81771, 30: 18229}, @@ -1304,7 +1277,7 @@ yacht_weights = { ("Category Yacht", 5, 7): {0: 67007, 50: 32993}, ("Category Yacht", 5, 8): {0: 58618, 50: 41382}, ("Category Yacht", 6, 0): {0: 100000}, - ("Category Yacht", 6, 1): {0: 99571, 50: 429}, + ("Category Yacht", 6, 1): {0: 100000}, ("Category Yacht", 6, 2): {0: 94726, 50: 5274}, ("Category Yacht", 6, 3): {0: 84366, 50: 15634}, ("Category Yacht", 6, 4): {0: 70782, 50: 29218}, @@ -1313,7 +1286,7 @@ yacht_weights = { ("Category Yacht", 6, 7): {0: 33578, 50: 66422}, ("Category Yacht", 6, 8): {0: 25079, 50: 74921}, ("Category Yacht", 7, 0): {0: 100000}, - ("Category Yacht", 7, 1): {0: 98833, 50: 1167}, + ("Category Yacht", 7, 1): {0: 100000}, ("Category Yacht", 7, 2): {0: 87511, 50: 12489}, ("Category Yacht", 7, 3): {0: 68252, 50: 31748}, ("Category Yacht", 7, 4): {0: 49065, 50: 50935}, @@ -1346,51 +1319,51 @@ yacht_weights = { ("Category Distincts", 2, 6): {1: 1, 2: 99999}, ("Category Distincts", 2, 7): {2: 100000}, ("Category Distincts", 2, 8): {2: 100000}, - ("Category Distincts", 3, 1): {1: 2760, 2: 41714, 3: 55526}, - ("Category Distincts", 3, 2): {1: 78, 3: 99922}, + ("Category Distincts", 3, 1): {1: 2760, 3: 97240}, + ("Category Distincts", 3, 2): {1: 15014, 3: 84986}, ("Category Distincts", 3, 3): {1: 4866, 3: 95134}, ("Category Distincts", 3, 4): {2: 1659, 3: 98341}, ("Category Distincts", 3, 5): {2: 575, 3: 99425}, ("Category Distincts", 3, 6): {2: 200, 3: 99800}, ("Category Distincts", 3, 7): {2: 69, 3: 99931}, ("Category Distincts", 3, 8): {2: 22, 3: 99978}, - ("Category Distincts", 4, 1): {1: 494, 3: 71611, 4: 27895}, - ("Category Distincts", 4, 2): {1: 1893, 3: 36922, 4: 61185}, - ("Category Distincts", 4, 3): {2: 230, 4: 99770}, - ("Category Distincts", 4, 4): {2: 21, 4: 99979}, + ("Category Distincts", 4, 1): {1: 16634, 3: 83366}, + ("Category Distincts", 4, 2): {1: 1893, 4: 98107}, + ("Category Distincts", 4, 3): {2: 19861, 4: 80139}, + ("Category Distincts", 4, 4): {2: 9879, 4: 90121}, ("Category Distincts", 4, 5): {2: 4906, 4: 95094}, ("Category Distincts", 4, 6): {3: 2494, 4: 97506}, ("Category Distincts", 4, 7): {3: 1297, 4: 98703}, ("Category Distincts", 4, 8): {3: 611, 4: 99389}, - ("Category Distincts", 5, 1): {1: 5798, 3: 38538, 4: 55664}, - ("Category Distincts", 5, 2): {2: 196, 4: 68119, 5: 31685}, - ("Category Distincts", 5, 3): {2: 3022, 4: 44724, 5: 52254}, - ("Category Distincts", 5, 4): {3: 722, 4: 31632, 5: 67646}, - ("Category Distincts", 5, 5): {3: 215, 4: 21391, 5: 78394}, - ("Category Distincts", 5, 6): {3: 55, 5: 99945}, - ("Category Distincts", 5, 7): {3: 15, 5: 99985}, + ("Category Distincts", 5, 1): {1: 5798, 4: 94202}, + ("Category Distincts", 5, 2): {2: 11843, 4: 88157}, + ("Category Distincts", 5, 3): {2: 3022, 5: 96978}, + ("Category Distincts", 5, 4): {3: 32354, 5: 67646}, + ("Category Distincts", 5, 5): {3: 21606, 5: 78394}, + ("Category Distincts", 5, 6): {3: 14525, 5: 85475}, + ("Category Distincts", 5, 7): {3: 9660, 5: 90340}, ("Category Distincts", 5, 8): {3: 6463, 5: 93537}, - ("Category Distincts", 6, 1): {1: 2027, 3: 22985, 4: 50464, 5: 24524}, - ("Category Distincts", 6, 2): {2: 3299, 4: 35174, 5: 61527}, - ("Category Distincts", 6, 3): {3: 417, 5: 79954, 6: 19629}, - ("Category Distincts", 6, 4): {3: 7831, 5: 61029, 6: 31140}, - ("Category Distincts", 6, 5): {3: 3699, 5: 54997, 6: 41304}, - ("Category Distincts", 6, 6): {4: 1557, 5: 47225, 6: 51218}, - ("Category Distincts", 6, 7): {4: 728, 5: 40465, 6: 58807}, - ("Category Distincts", 6, 8): {4: 321, 5: 33851, 6: 65828}, - ("Category Distincts", 7, 1): {1: 665, 4: 57970, 5: 41365}, - ("Category Distincts", 7, 2): {2: 839, 5: 75578, 6: 23583}, - ("Category Distincts", 7, 3): {3: 6051, 5: 50312, 6: 43637}, - ("Category Distincts", 7, 4): {3: 1796, 5: 38393, 6: 59811}, - ("Category Distincts", 7, 5): {4: 529, 5: 27728, 6: 71743}, - ("Category Distincts", 7, 6): {4: 164, 6: 99836}, - ("Category Distincts", 7, 7): {4: 53, 6: 99947}, - ("Category Distincts", 7, 8): {4: 14, 6: 99986}, - ("Category Distincts", 8, 1): {1: 7137, 4: 36582, 5: 56281}, - ("Category Distincts", 8, 2): {2: 233, 5: 59964, 6: 39803}, - ("Category Distincts", 8, 3): {3: 1976, 5: 34748, 6: 63276}, - ("Category Distincts", 8, 4): {4: 389, 5: 21008, 6: 78603}, - ("Category Distincts", 8, 5): {4: 78, 6: 99922}, + ("Category Distincts", 6, 1): {1: 25012, 4: 74988}, + ("Category Distincts", 6, 2): {2: 3299, 5: 96701}, + ("Category Distincts", 6, 3): {3: 17793, 5: 82207}, + ("Category Distincts", 6, 4): {3: 7831, 5: 92169}, + ("Category Distincts", 6, 5): {3: 3699, 6: 96301}, + ("Category Distincts", 6, 6): {4: 1557, 6: 98443}, + ("Category Distincts", 6, 7): {4: 728, 6: 99272}, + ("Category Distincts", 6, 8): {4: 321, 6: 99679}, + ("Category Distincts", 7, 1): {1: 13671, 5: 86329}, + ("Category Distincts", 7, 2): {2: 19686, 5: 80314}, + ("Category Distincts", 7, 3): {3: 6051, 6: 93949}, + ("Category Distincts", 7, 4): {3: 1796, 6: 98204}, + ("Category Distincts", 7, 5): {4: 28257, 6: 71743}, + ("Category Distincts", 7, 6): {4: 19581, 6: 80419}, + ("Category Distincts", 7, 7): {4: 13618, 6: 86382}, + ("Category Distincts", 7, 8): {4: 9545, 6: 90455}, + ("Category Distincts", 8, 1): {1: 7137, 5: 92863}, + ("Category Distincts", 8, 2): {2: 9414, 6: 90586}, + ("Category Distincts", 8, 3): {3: 1976, 6: 98024}, + ("Category Distincts", 8, 4): {4: 21397, 6: 78603}, + ("Category Distincts", 8, 5): {4: 12592, 6: 87408}, ("Category Distincts", 8, 6): {4: 7177, 6: 92823}, ("Category Distincts", 8, 7): {4: 4179, 6: 95821}, ("Category Distincts", 8, 8): {5: 2440, 6: 97560}, @@ -1404,8 +1377,8 @@ yacht_weights = { ("Category Two times Ones", 0, 7): {0: 100000}, ("Category Two times Ones", 0, 8): {0: 100000}, ("Category Two times Ones", 1, 0): {0: 100000}, - ("Category Two times Ones", 1, 1): {0: 83475, 2: 16525}, - ("Category Two times Ones", 1, 2): {0: 69690, 2: 30310}, + ("Category Two times Ones", 1, 1): {0: 100000}, + ("Category Two times Ones", 1, 2): {0: 100000}, ("Category Two times Ones", 1, 3): {0: 57818, 2: 42182}, ("Category Two times Ones", 1, 4): {0: 48418, 2: 51582}, ("Category Two times Ones", 1, 5): {0: 40301, 2: 59699}, @@ -1413,68 +1386,68 @@ yacht_weights = { ("Category Two times Ones", 1, 7): {0: 28182, 2: 71818}, ("Category Two times Ones", 1, 8): {0: 23406, 2: 76594}, ("Category Two times Ones", 2, 0): {0: 100000}, - ("Category Two times Ones", 2, 1): {0: 69724, 2: 30276}, - ("Category Two times Ones", 2, 2): {0: 48238, 2: 42479, 4: 9283}, - ("Category Two times Ones", 2, 3): {0: 33290, 2: 48819, 4: 17891}, - ("Category Two times Ones", 2, 4): {0: 23136, 2: 49957, 4: 26907}, - ("Category Two times Ones", 2, 5): {0: 16146, 2: 48200, 4: 35654}, - ("Category Two times Ones", 2, 6): {0: 11083, 2: 44497, 4: 44420}, - ("Category Two times Ones", 2, 7): {0: 7662, 2: 40343, 4: 51995}, - ("Category Two times Ones", 2, 8): {0: 5354, 2: 35526, 4: 59120}, + ("Category Two times Ones", 2, 1): {0: 100000}, + ("Category Two times Ones", 2, 2): {0: 48238, 2: 51762}, + ("Category Two times Ones", 2, 3): {0: 33290, 4: 66710}, + ("Category Two times Ones", 2, 4): {0: 23136, 4: 76864}, + ("Category Two times Ones", 2, 5): {0: 16146, 4: 83854}, + ("Category Two times Ones", 2, 6): {0: 11083, 4: 88917}, + ("Category Two times Ones", 2, 7): {0: 7662, 4: 92338}, + ("Category Two times Ones", 2, 8): {0: 5354, 4: 94646}, ("Category Two times Ones", 3, 0): {0: 100000}, - ("Category Two times Ones", 3, 1): {0: 58021, 2: 34522, 4: 7457}, - ("Category Two times Ones", 3, 2): {0: 33548, 2: 44261, 4: 22191}, - ("Category Two times Ones", 3, 3): {0: 19375, 2: 42372, 4: 30748, 6: 7505}, - ("Category Two times Ones", 3, 4): {0: 10998, 2: 36435, 4: 38569, 6: 13998}, - ("Category Two times Ones", 3, 5): {0: 6519, 2: 28838, 4: 43283, 6: 21360}, - ("Category Two times Ones", 3, 6): {0: 3619, 2: 22498, 4: 44233, 6: 29650}, - ("Category Two times Ones", 3, 7): {0: 2195, 2: 16979, 4: 43684, 6: 37142}, - ("Category Two times Ones", 3, 8): {0: 1255, 2: 12420, 4: 40920, 6: 45405}, + ("Category Two times Ones", 3, 1): {0: 58021, 2: 41979}, + ("Category Two times Ones", 3, 2): {0: 33548, 4: 66452}, + ("Category Two times Ones", 3, 3): {0: 19375, 4: 80625}, + ("Category Two times Ones", 3, 4): {0: 10998, 4: 89002}, + ("Category Two times Ones", 3, 5): {0: 6519, 6: 93481}, + ("Category Two times Ones", 3, 6): {0: 3619, 6: 96381}, + ("Category Two times Ones", 3, 7): {0: 2195, 6: 97805}, + ("Category Two times Ones", 3, 8): {0: 13675, 6: 86325}, ("Category Two times Ones", 4, 0): {0: 100000}, - ("Category Two times Ones", 4, 1): {0: 48235, 2: 38602, 4: 13163}, - ("Category Two times Ones", 4, 2): {0: 23289, 2: 40678, 4: 27102, 6: 8931}, - ("Category Two times Ones", 4, 3): {0: 11177, 2: 32677, 4: 35702, 6: 20444}, - ("Category Two times Ones", 4, 4): {0: 5499, 2: 23225, 4: 37240, 6: 26867, 8: 7169}, - ("Category Two times Ones", 4, 5): {0: 2574, 2: 15782, 4: 34605, 6: 34268, 8: 12771}, - ("Category Two times Ones", 4, 6): {0: 1259, 4: 39616, 6: 39523, 8: 19602}, - ("Category Two times Ones", 4, 7): {0: 622, 4: 30426, 6: 41894, 8: 27058}, - ("Category Two times Ones", 4, 8): {0: 4091, 4: 18855, 6: 42309, 8: 34745}, + ("Category Two times Ones", 4, 1): {0: 48235, 2: 51765}, + ("Category Two times Ones", 4, 2): {0: 23289, 4: 76711}, + ("Category Two times Ones", 4, 3): {0: 11177, 6: 88823}, + ("Category Two times Ones", 4, 4): {0: 5499, 6: 94501}, + ("Category Two times Ones", 4, 5): {0: 18356, 6: 81644}, + ("Category Two times Ones", 4, 6): {0: 11169, 8: 88831}, + ("Category Two times Ones", 4, 7): {0: 6945, 8: 93055}, + ("Category Two times Ones", 4, 8): {0: 4091, 8: 95909}, ("Category Two times Ones", 5, 0): {0: 100000}, - ("Category Two times Ones", 5, 1): {0: 40028, 2: 40241, 4: 19731}, - ("Category Two times Ones", 5, 2): {0: 16009, 2: 35901, 4: 31024, 6: 17066}, - ("Category Two times Ones", 5, 3): {0: 6489, 2: 23477, 4: 34349, 6: 25270, 8: 10415}, - ("Category Two times Ones", 5, 4): {0: 2658, 2: 14032, 4: 30199, 6: 32214, 8: 20897}, - ("Category Two times Ones", 5, 5): {0: 1032, 4: 31627, 6: 33993, 8: 25853, 10: 7495}, - ("Category Two times Ones", 5, 6): {0: 450, 4: 20693, 6: 32774, 8: 32900, 10: 13183}, - ("Category Two times Ones", 5, 7): {0: 2396, 4: 11231, 6: 29481, 8: 37636, 10: 19256}, - ("Category Two times Ones", 5, 8): {0: 1171, 6: 31564, 8: 40798, 10: 26467}, + ("Category Two times Ones", 5, 1): {0: 40028, 4: 59972}, + ("Category Two times Ones", 5, 2): {0: 16009, 6: 83991}, + ("Category Two times Ones", 5, 3): {0: 6489, 6: 93511}, + ("Category Two times Ones", 5, 4): {0: 16690, 8: 83310}, + ("Category Two times Ones", 5, 5): {0: 9016, 8: 90984}, + ("Category Two times Ones", 5, 6): {0: 4602, 8: 95398}, + ("Category Two times Ones", 5, 7): {0: 13627, 10: 86373}, + ("Category Two times Ones", 5, 8): {0: 8742, 10: 91258}, ("Category Two times Ones", 6, 0): {0: 100000}, - ("Category Two times Ones", 6, 1): {0: 33502, 2: 40413, 4: 26085}, - ("Category Two times Ones", 6, 2): {0: 11210, 2: 29638, 4: 32701, 6: 18988, 8: 7463}, - ("Category Two times Ones", 6, 3): {0: 3673, 2: 16459, 4: 29795, 6: 29102, 8: 20971}, - ("Category Two times Ones", 6, 4): {0: 1243, 4: 30025, 6: 31053, 8: 25066, 10: 12613}, - ("Category Two times Ones", 6, 5): {0: 4194, 4: 13949, 6: 28142, 8: 30723, 10: 22992}, - ("Category Two times Ones", 6, 6): {0: 1800, 6: 30677, 8: 32692, 10: 26213, 12: 8618}, - ("Category Two times Ones", 6, 7): {0: 775, 6: 21013, 8: 31410, 10: 32532, 12: 14270}, - ("Category Two times Ones", 6, 8): {0: 2855, 6: 11432, 8: 27864, 10: 37237, 12: 20612}, + ("Category Two times Ones", 6, 1): {0: 33502, 4: 66498}, + ("Category Two times Ones", 6, 2): {0: 11210, 6: 88790}, + ("Category Two times Ones", 6, 3): {0: 3673, 6: 96327}, + ("Category Two times Ones", 6, 4): {0: 9291, 8: 90709}, + ("Category Two times Ones", 6, 5): {0: 441, 8: 99559}, + ("Category Two times Ones", 6, 6): {0: 10255, 10: 89745}, + ("Category Two times Ones", 6, 7): {0: 5646, 10: 94354}, + ("Category Two times Ones", 6, 8): {0: 14287, 12: 85713}, ("Category Two times Ones", 7, 0): {0: 100000}, - ("Category Two times Ones", 7, 1): {0: 27683, 2: 39060, 4: 23574, 6: 9683}, - ("Category Two times Ones", 7, 2): {0: 7824, 2: 24031, 4: 31764, 6: 23095, 8: 13286}, - ("Category Two times Ones", 7, 3): {0: 2148, 2: 11019, 4: 24197, 6: 29599, 8: 21250, 10: 11787}, - ("Category Two times Ones", 7, 4): {0: 564, 4: 19036, 6: 26395, 8: 28409, 10: 18080, 12: 7516}, - ("Category Two times Ones", 7, 5): {0: 1913, 6: 27198, 8: 29039, 10: 26129, 12: 15721}, - ("Category Two times Ones", 7, 6): {0: 54, 6: 17506, 8: 25752, 10: 30413, 12: 26275}, - ("Category Two times Ones", 7, 7): {0: 2179, 8: 28341, 10: 32054, 12: 27347, 14: 10079}, - ("Category Two times Ones", 7, 8): {0: 942, 8: 19835, 10: 30248, 12: 33276, 14: 15699}, + ("Category Two times Ones", 7, 1): {0: 27683, 4: 72317}, + ("Category Two times Ones", 7, 2): {0: 7824, 6: 92176}, + ("Category Two times Ones", 7, 3): {0: 13167, 8: 86833}, + ("Category Two times Ones", 7, 4): {0: 564, 10: 99436}, + ("Category Two times Ones", 7, 5): {0: 9824, 10: 90176}, + ("Category Two times Ones", 7, 6): {0: 702, 12: 99298}, + ("Category Two times Ones", 7, 7): {0: 10186, 12: 89814}, + ("Category Two times Ones", 7, 8): {0: 942, 12: 99058}, ("Category Two times Ones", 8, 0): {0: 100000}, - ("Category Two times Ones", 8, 1): {0: 23378, 2: 37157, 4: 26082, 6: 13383}, - ("Category Two times Ones", 8, 2): {0: 5420, 2: 19164, 4: 29216, 6: 25677, 8: 20523}, - ("Category Two times Ones", 8, 3): {0: 1271, 4: 26082, 6: 27054, 8: 24712, 10: 20881}, - ("Category Two times Ones", 8, 4): {0: 2889, 6: 29552, 8: 27389, 10: 23232, 12: 16938}, - ("Category Two times Ones", 8, 5): {0: 879, 6: 16853, 8: 23322, 10: 27882, 12: 20768, 14: 10296}, - ("Category Two times Ones", 8, 6): {0: 2041, 8: 24140, 10: 27398, 12: 27048, 14: 19373}, - ("Category Two times Ones", 8, 7): {0: 74, 8: 15693, 10: 23675, 12: 30829, 14: 22454, 16: 7275}, - ("Category Two times Ones", 8, 8): {2: 2053, 10: 25677, 12: 31310, 14: 28983, 16: 11977}, + ("Category Two times Ones", 8, 1): {0: 23378, 4: 76622}, + ("Category Two times Ones", 8, 2): {0: 5420, 8: 94580}, + ("Category Two times Ones", 8, 3): {0: 8560, 10: 91440}, + ("Category Two times Ones", 8, 4): {0: 12199, 12: 87801}, + ("Category Two times Ones", 8, 5): {0: 879, 12: 99121}, + ("Category Two times Ones", 8, 6): {0: 9033, 14: 90967}, + ("Category Two times Ones", 8, 7): {0: 15767, 14: 84233}, + ("Category Two times Ones", 8, 8): {2: 9033, 14: 90967}, ("Category Half of Sixes", 0, 0): {0: 100000}, ("Category Half of Sixes", 0, 1): {0: 100000}, ("Category Half of Sixes", 0, 2): {0: 100000}, @@ -1485,7 +1458,7 @@ yacht_weights = { ("Category Half of Sixes", 0, 7): {0: 100000}, ("Category Half of Sixes", 0, 8): {0: 100000}, ("Category Half of Sixes", 1, 0): {0: 100000}, - ("Category Half of Sixes", 1, 1): {0: 83343, 3: 16657}, + ("Category Half of Sixes", 1, 1): {0: 100000}, ("Category Half of Sixes", 1, 2): {0: 69569, 3: 30431}, ("Category Half of Sixes", 1, 3): {0: 57872, 3: 42128}, ("Category Half of Sixes", 1, 4): {0: 48081, 3: 51919}, @@ -1495,1558 +1468,387 @@ yacht_weights = { ("Category Half of Sixes", 1, 8): {0: 23240, 3: 76760}, ("Category Half of Sixes", 2, 0): {0: 100000}, ("Category Half of Sixes", 2, 1): {0: 69419, 3: 30581}, - ("Category Half of Sixes", 2, 2): {0: 48202, 3: 42590, 6: 9208}, - ("Category Half of Sixes", 2, 3): {0: 33376, 3: 48849, 6: 17775}, - ("Category Half of Sixes", 2, 4): {0: 23276, 3: 49810, 6: 26914}, - ("Category Half of Sixes", 2, 5): {0: 16092, 3: 47718, 6: 36190}, - ("Category Half of Sixes", 2, 6): {0: 11232, 3: 44515, 6: 44253}, - ("Category Half of Sixes", 2, 7): {0: 7589, 3: 40459, 6: 51952}, - ("Category Half of Sixes", 2, 8): {0: 5447, 3: 35804, 6: 58749}, + ("Category Half of Sixes", 2, 2): {0: 48202, 3: 51798}, + ("Category Half of Sixes", 2, 3): {0: 33376, 6: 66624}, + ("Category Half of Sixes", 2, 4): {0: 23276, 6: 76724}, + ("Category Half of Sixes", 2, 5): {0: 16092, 6: 83908}, + ("Category Half of Sixes", 2, 6): {0: 11232, 6: 88768}, + ("Category Half of Sixes", 2, 7): {0: 7589, 6: 92411}, + ("Category Half of Sixes", 2, 8): {0: 5447, 6: 94553}, ("Category Half of Sixes", 3, 0): {0: 100000}, - ("Category Half of Sixes", 3, 1): {0: 57964, 3: 34701, 6: 7335}, - ("Category Half of Sixes", 3, 2): {0: 33637, 3: 44263, 6: 22100}, - ("Category Half of Sixes", 3, 3): {0: 19520, 3: 42382, 6: 30676, 9: 7422}, - ("Category Half of Sixes", 3, 4): {0: 11265, 3: 35772, 6: 39042, 9: 13921}, - ("Category Half of Sixes", 3, 5): {0: 6419, 3: 28916, 6: 43261, 9: 21404}, - ("Category Half of Sixes", 3, 6): {0: 3810, 3: 22496, 6: 44388, 9: 29306}, - ("Category Half of Sixes", 3, 7): {0: 2174, 3: 16875, 6: 43720, 9: 37231}, - ("Category Half of Sixes", 3, 8): {0: 1237, 3: 12471, 6: 41222, 9: 45070}, + ("Category Half of Sixes", 3, 1): {0: 57964, 3: 42036}, + ("Category Half of Sixes", 3, 2): {0: 33637, 6: 66363}, + ("Category Half of Sixes", 3, 3): {0: 19520, 6: 80480}, + ("Category Half of Sixes", 3, 4): {0: 11265, 6: 88735}, + ("Category Half of Sixes", 3, 5): {0: 6419, 6: 72177, 9: 21404}, + ("Category Half of Sixes", 3, 6): {0: 3810, 6: 66884, 9: 29306}, + ("Category Half of Sixes", 3, 7): {0: 2174, 6: 60595, 9: 37231}, + ("Category Half of Sixes", 3, 8): {0: 1237, 6: 53693, 9: 45070}, ("Category Half of Sixes", 4, 0): {0: 100000}, - ("Category Half of Sixes", 4, 1): {0: 48121, 3: 38786, 6: 13093}, - ("Category Half of Sixes", 4, 2): {0: 23296, 3: 40989, 6: 26998, 9: 8717}, - ("Category Half of Sixes", 4, 3): {0: 11233, 3: 32653, 6: 35710, 9: 20404}, - ("Category Half of Sixes", 4, 4): {0: 5463, 3: 23270, 6: 37468, 9: 26734, 12: 7065}, - ("Category Half of Sixes", 4, 5): {0: 2691, 3: 15496, 6: 34539, 9: 34635, 12: 12639}, - ("Category Half of Sixes", 4, 6): {0: 1221, 3: 10046, 6: 29811, 9: 39190, 12: 19732}, - ("Category Half of Sixes", 4, 7): {0: 599, 6: 30742, 9: 41614, 12: 27045}, - ("Category Half of Sixes", 4, 8): {0: 309, 6: 22719, 9: 42236, 12: 34736}, + ("Category Half of Sixes", 4, 1): {0: 48121, 6: 51879}, + ("Category Half of Sixes", 4, 2): {0: 23296, 6: 76704}, + ("Category Half of Sixes", 4, 3): {0: 11233, 6: 68363, 9: 20404}, + ("Category Half of Sixes", 4, 4): {0: 5463, 6: 60738, 9: 33799}, + ("Category Half of Sixes", 4, 5): {0: 2691, 6: 50035, 12: 47274}, + ("Category Half of Sixes", 4, 6): {0: 11267, 9: 88733}, + ("Category Half of Sixes", 4, 7): {0: 6921, 9: 66034, 12: 27045}, + ("Category Half of Sixes", 4, 8): {0: 4185, 9: 61079, 12: 34736}, ("Category Half of Sixes", 5, 0): {0: 100000}, - ("Category Half of Sixes", 5, 1): {0: 40183, 3: 40377, 6: 19440}, - ("Category Half of Sixes", 5, 2): {0: 16197, 3: 35494, 6: 30937, 9: 17372}, - ("Category Half of Sixes", 5, 3): {0: 6583, 3: 23394, 6: 34432, 9: 25239, 12: 10352}, - ("Category Half of Sixes", 5, 4): {0: 2636, 3: 14072, 6: 30134, 9: 32371, 12: 20787}, - ("Category Half of Sixes", 5, 5): {0: 1075, 3: 7804, 6: 23010, 9: 34811, 12: 25702, 15: 7598}, - ("Category Half of Sixes", 5, 6): {0: 418, 6: 20888, 9: 32809, 12: 32892, 15: 12993}, - ("Category Half of Sixes", 5, 7): {0: 2365, 6: 11416, 9: 29072, 12: 37604, 15: 19543}, - ("Category Half of Sixes", 5, 8): {0: 1246, 6: 7425, 9: 24603, 12: 40262, 15: 26464}, + ("Category Half of Sixes", 5, 1): {0: 40183, 6: 59817}, + ("Category Half of Sixes", 5, 2): {0: 16197, 6: 83803}, + ("Category Half of Sixes", 5, 3): {0: 6583, 6: 57826, 9: 35591}, + ("Category Half of Sixes", 5, 4): {0: 2636, 9: 76577, 12: 20787}, + ("Category Half of Sixes", 5, 5): {0: 8879, 9: 57821, 12: 33300}, + ("Category Half of Sixes", 5, 6): {0: 4652, 12: 95348}, + ("Category Half of Sixes", 5, 7): {0: 2365, 12: 97635}, + ("Category Half of Sixes", 5, 8): {0: 8671, 12: 64865, 15: 26464}, ("Category Half of Sixes", 6, 0): {0: 100000}, - ("Category Half of Sixes", 6, 1): {0: 33473, 3: 40175, 6: 20151, 9: 6201}, - ("Category Half of Sixes", 6, 2): {0: 11147, 3: 29592, 6: 32630, 9: 19287, 12: 7344}, - ("Category Half of Sixes", 6, 3): {0: 3628, 3: 16528, 6: 29814, 9: 29006, 12: 15888, 15: 5136}, - ("Category Half of Sixes", 6, 4): {0: 1262, 3: 8236, 6: 21987, 9: 30953, 12: 24833, 15: 12729}, - ("Category Half of Sixes", 6, 5): {0: 416, 6: 17769, 9: 27798, 12: 31197, 15: 18256, 18: 4564}, - ("Category Half of Sixes", 6, 6): {0: 1796, 6: 8372, 9: 22175, 12: 32897, 15: 26264, 18: 8496}, - ("Category Half of Sixes", 6, 7): {0: 791, 9: 21074, 12: 31385, 15: 32666, 18: 14084}, - ("Category Half of Sixes", 6, 8): {0: 20, 9: 14150, 12: 28320, 15: 36982, 18: 20528}, + ("Category Half of Sixes", 6, 1): {0: 33473, 6: 66527}, + ("Category Half of Sixes", 6, 2): {0: 11147, 6: 62222, 9: 26631}, + ("Category Half of Sixes", 6, 3): {0: 3628, 9: 75348, 12: 21024}, + ("Category Half of Sixes", 6, 4): {0: 9498, 9: 52940, 15: 37562}, + ("Category Half of Sixes", 6, 5): {0: 4236, 12: 72944, 15: 22820}, + ("Category Half of Sixes", 6, 6): {0: 10168, 12: 55072, 15: 34760}, + ("Category Half of Sixes", 6, 7): {0: 5519, 15: 94481}, + ("Category Half of Sixes", 6, 8): {0: 2968, 15: 76504, 18: 20528}, ("Category Half of Sixes", 7, 0): {0: 100000}, - ("Category Half of Sixes", 7, 1): {0: 27933, 3: 39105, 6: 23338, 9: 9624}, - ("Category Half of Sixes", 7, 2): {0: 7794, 3: 23896, 6: 31832, 9: 23110, 12: 13368}, - ("Category Half of Sixes", 7, 3): {0: 2138, 3: 11098, 6: 24140, 9: 29316, 12: 21386, 15: 11922}, - ("Category Half of Sixes", 7, 4): {0: 590, 6: 19385, 9: 26233, 12: 28244, 15: 18118, 18: 7430}, - ("Category Half of Sixes", 7, 5): {0: 1941, 6: 7953, 9: 19439, 12: 28977, 15: 26078, 18: 15612}, - ("Category Half of Sixes", 7, 6): {0: 718, 9: 16963, 12: 25793, 15: 30535, 18: 20208, 21: 5783}, - ("Category Half of Sixes", 7, 7): {0: 2064, 9: 7941, 12: 20571, 15: 31859, 18: 27374, 21: 10191}, - ("Category Half of Sixes", 7, 8): {0: 963, 12: 19864, 15: 30313, 18: 33133, 21: 15727}, + ("Category Half of Sixes", 7, 1): {0: 27933, 6: 72067}, + ("Category Half of Sixes", 7, 2): {0: 7794, 6: 55728, 12: 36478}, + ("Category Half of Sixes", 7, 3): {0: 2138, 9: 64554, 15: 33308}, + ("Category Half of Sixes", 7, 4): {0: 5238, 12: 69214, 15: 25548}, + ("Category Half of Sixes", 7, 5): {0: 9894, 15: 90106}, + ("Category Half of Sixes", 7, 6): {0: 4656, 15: 69353, 18: 25991}, + ("Category Half of Sixes", 7, 7): {0: 10005, 15: 52430, 18: 37565}, + ("Category Half of Sixes", 7, 8): {0: 5710, 18: 94290}, ("Category Half of Sixes", 8, 0): {0: 100000}, - ("Category Half of Sixes", 8, 1): {0: 23337, 3: 37232, 6: 25968, 9: 13463}, - ("Category Half of Sixes", 8, 2): {0: 5310, 3: 18930, 6: 29232, 9: 26016, 12: 14399, 15: 6113}, - ("Category Half of Sixes", 8, 3): {0: 1328, 3: 7328, 6: 18754, 9: 27141, 12: 24703, 15: 14251, 18: 6495}, - ("Category Half of Sixes", 8, 4): {0: 2719, 6: 9554, 9: 20607, 12: 26898, 15: 23402, 18: 12452, 21: 4368}, - ("Category Half of Sixes", 8, 5): {0: 905, 9: 16848, 12: 23248, 15: 27931, 18: 20616, 21: 10452}, - ("Category Half of Sixes", 8, 6): {0: 1914, 9: 6890, 12: 17302, 15: 27235, 18: 27276, 21: 19383}, - ("Category Half of Sixes", 8, 7): {0: 800, 12: 15127, 15: 23682, 18: 30401, 21: 22546, 24: 7444}, - ("Category Half of Sixes", 8, 8): {0: 2041, 12: 7211, 15: 18980, 18: 30657, 21: 29074, 24: 12037}, - ("Category Twos and Threes", 1, 1): {0: 66466, 3: 33534}, - ("Category Twos and Threes", 1, 2): {0: 55640, 3: 44360}, - ("Category Twos and Threes", 1, 3): {0: 46223, 3: 53777}, - ("Category Twos and Threes", 1, 4): {0: 38552, 3: 61448}, - ("Category Twos and Threes", 1, 5): {0: 32320, 3: 67680}, - ("Category Twos and Threes", 1, 6): {0: 26733, 3: 73267}, - ("Category Twos and Threes", 1, 7): {0: 22289, 3: 77711}, - ("Category Twos and Threes", 1, 8): {0: 18676, 3: 81324}, - ("Category Twos and Threes", 2, 1): {0: 44565, 2: 21965, 3: 25172, 5: 8298}, - ("Category Twos and Threes", 2, 2): {0: 30855, 3: 51429, 6: 17716}, - ("Category Twos and Threes", 2, 3): {0: 21509, 3: 51178, 6: 27313}, - ("Category Twos and Threes", 2, 4): {0: 14935, 3: 48581, 6: 36484}, - ("Category Twos and Threes", 2, 5): {0: 10492, 3: 44256, 6: 45252}, - ("Category Twos and Threes", 2, 6): {0: 10775, 3: 35936, 6: 53289}, - ("Category Twos and Threes", 2, 7): {0: 7375, 3: 32469, 6: 60156}, - ("Category Twos and Threes", 2, 8): {0: 5212, 3: 35730, 6: 59058}, - ("Category Twos and Threes", 3, 1): {0: 29892, 2: 22136, 3: 27781, 6: 20191}, - ("Category Twos and Threes", 3, 2): {0: 17285, 3: 44257, 6: 38458}, - ("Category Twos and Threes", 3, 3): {0: 9889, 3: 36505, 6: 40112, 8: 13494}, - ("Category Twos and Threes", 3, 4): {0: 5717, 3: 28317, 6: 43044, 9: 22922}, - ("Category Twos and Threes", 3, 5): {0: 5795, 3: 19123, 6: 45004, 9: 30078}, - ("Category Twos and Threes", 3, 6): {0: 3273, 3: 21888, 6: 36387, 9: 38452}, - ("Category Twos and Threes", 3, 7): {0: 1917, 3: 16239, 6: 35604, 9: 46240}, - ("Category Twos and Threes", 3, 8): {0: 1124, 3: 12222, 6: 33537, 9: 53117}, - ("Category Twos and Threes", 4, 1): {0: 19619, 3: 46881, 6: 33500}, - ("Category Twos and Threes", 4, 2): {0: 9395, 3: 33926, 6: 37832, 9: 18847}, - ("Category Twos and Threes", 4, 3): {0: 4538, 3: 22968, 6: 38891, 9: 33603}, - ("Category Twos and Threes", 4, 4): {0: 4402, 3: 12654, 6: 35565, 9: 34784, 11: 12595}, - ("Category Twos and Threes", 4, 5): {0: 2065, 3: 14351, 6: 23592, 9: 38862, 12: 21130}, - ("Category Twos and Threes", 4, 6): {0: 1044, 3: 9056, 6: 20013, 9: 41255, 12: 28632}, - ("Category Twos and Threes", 4, 7): {0: 6310, 7: 24021, 9: 34297, 12: 35372}, - ("Category Twos and Threes", 4, 8): {0: 3694, 6: 18611, 9: 34441, 12: 43254}, - ("Category Twos and Threes", 5, 1): {0: 13070, 3: 33021, 5: 24568, 6: 16417, 8: 12924}, - ("Category Twos and Threes", 5, 2): {0: 5213, 3: 24275, 6: 37166, 9: 24746, 11: 8600}, - ("Category Twos and Threes", 5, 3): {0: 4707, 3: 10959, 6: 31388, 9: 33265, 12: 19681}, - ("Category Twos and Threes", 5, 4): {0: 1934, 3: 12081, 6: 17567, 9: 35282, 12: 33136}, - ("Category Twos and Threes", 5, 5): {0: 380, 2: 7025, 6: 13268, 9: 33274, 12: 33255, 14: 12798}, - ("Category Twos and Threes", 5, 6): {0: 3745, 6: 15675, 9: 22902, 12: 44665, 15: 13013}, - ("Category Twos and Threes", 5, 7): {0: 1969, 6: 10700, 9: 19759, 12: 39522, 15: 28050}, - ("Category Twos and Threes", 5, 8): {0: 13, 2: 7713, 10: 23957, 12: 32501, 15: 35816}, - ("Category Twos and Threes", 6, 1): {0: 8955, 3: 26347, 5: 24850, 8: 39848}, - ("Category Twos and Threes", 6, 2): {0: 2944, 3: 16894, 6: 32156, 9: 37468, 12: 10538}, - ("Category Twos and Threes", 6, 3): {0: 2484, 3: 13120, 6: 15999, 9: 32271, 12: 24898, 14: 11228}, - ("Category Twos and Threes", 6, 4): {0: 320, 2: 6913, 6: 10814, 9: 28622, 12: 31337, 15: 21994}, - ("Category Twos and Threes", 6, 5): {0: 3135, 6: 12202, 9: 16495, 12: 33605, 15: 26330, 17: 8233}, - ("Category Twos and Threes", 6, 6): {0: 98, 3: 8409, 9: 12670, 12: 31959, 15: 38296, 18: 8568}, - ("Category Twos and Threes", 6, 7): {0: 4645, 9: 15210, 12: 21906, 15: 44121, 18: 14118}, - ("Category Twos and Threes", 6, 8): {0: 2367, 9: 10679, 12: 18916, 15: 38806, 18: 29232}, - ("Category Twos and Threes", 7, 1): {0: 5802, 3: 28169, 6: 26411, 9: 31169, 11: 8449}, - ("Category Twos and Threes", 7, 2): {0: 4415, 6: 34992, 9: 31238, 12: 20373, 14: 8982}, - ("Category Twos and Threes", 7, 3): {0: 471, 2: 8571, 6: 10929, 9: 28058, 12: 28900, 14: 14953, 16: 8118}, - ("Category Twos and Threes", 7, 4): {0: 3487, 6: 12139, 9: 14001, 12: 30314, 15: 23096, 18: 16963}, - ("Category Twos and Threes", 7, 5): {0: 40, 2: 7460, 12: 36006, 15: 31388, 18: 25106}, - ("Category Twos and Threes", 7, 6): {0: 3554, 9: 11611, 12: 15116, 15: 32501, 18: 27524, 20: 9694}, - ("Category Twos and Threes", 7, 7): {0: 157, 6: 8396, 13: 19880, 15: 22333, 18: 39121, 21: 10113}, - ("Category Twos and Threes", 7, 8): {0: 31, 5: 4682, 12: 14446, 15: 20934, 18: 44127, 21: 15780}, - ("Category Twos and Threes", 8, 1): {0: 3799, 3: 22551, 6: 23754, 8: 29290, 10: 11990, 12: 8616}, - ("Category Twos and Threes", 8, 2): {0: 902, 4: 14360, 6: 13750, 9: 29893, 13: 30770, 15: 10325}, - ("Category Twos and Threes", 8, 3): {0: 2221, 4: 8122, 9: 23734, 12: 28527, 16: 28942, 18: 8454}, - ("Category Twos and Threes", 8, 4): {0: 140, 3: 8344, 12: 33635, 15: 28711, 18: 20093, 20: 9077}, - ("Category Twos and Threes", 8, 5): {0: 3601, 9: 10269, 12: 12458, 15: 28017, 18: 24815, 21: 20840}, - ("Category Twos and Threes", 8, 6): {0: 4104, 11: 10100, 15: 25259, 18: 30949, 21: 29588}, - ("Category Twos and Threes", 8, 7): {0: 3336, 12: 10227, 15: 14149, 18: 31155, 21: 29325, 23: 11808}, - ("Category Twos and Threes", 8, 8): {3: 7, 5: 7726, 16: 17997, 18: 21517, 21: 40641, 24: 12112}, - ("Category Sum of Odds", 1, 1): {0: 50084, 1: 16488, 3: 16584, 5: 16844}, - ("Category Sum of Odds", 1, 2): {0: 44489, 3: 27886, 5: 27625}, - ("Category Sum of Odds", 1, 3): {0: 27892, 3: 32299, 5: 39809}, - ("Category Sum of Odds", 1, 4): {0: 30917, 3: 19299, 5: 49784}, - ("Category Sum of Odds", 1, 5): {0: 25892, 3: 15941, 5: 58167}, - ("Category Sum of Odds", 1, 6): {0: 21678, 3: 13224, 5: 65098}, - ("Category Sum of Odds", 1, 7): {0: 17840, 3: 11191, 5: 70969}, - ("Category Sum of Odds", 1, 8): {0: 14690, 5: 85310}, - ("Category Sum of Odds", 2, 1): {0: 24611, 1: 19615, 3: 22234, 6: 25168, 8: 8372}, - ("Category Sum of Odds", 2, 2): {0: 11216, 3: 33181, 6: 32416, 8: 15414, 10: 7773}, - ("Category Sum of Odds", 2, 3): {0: 13730, 3: 17055, 5: 34933, 8: 18363, 10: 15919}, - ("Category Sum of Odds", 2, 4): {0: 9599, 3: 11842, 5: 34490, 8: 19129, 10: 24940}, - ("Category Sum of Odds", 2, 5): {0: 6652, 5: 40845, 8: 18712, 10: 33791}, - ("Category Sum of Odds", 2, 6): {0: 10404, 5: 20970, 8: 26124, 10: 42502}, - ("Category Sum of Odds", 2, 7): {0: 7262, 5: 26824, 8: 15860, 10: 50054}, - ("Category Sum of Odds", 2, 8): {0: 4950, 5: 23253, 8: 14179, 10: 57618}, - ("Category Sum of Odds", 3, 1): {0: 12467, 1: 16736, 4: 20970, 6: 29252, 8: 11660, 10: 8915}, - ("Category Sum of Odds", 3, 2): {0: 8635, 3: 15579, 6: 27649, 9: 30585, 13: 17552}, - ("Category Sum of Odds", 3, 3): {0: 5022, 6: 32067, 8: 21631, 11: 24032, 13: 17248}, - ("Category Sum of Odds", 3, 4): {0: 8260, 6: 17955, 8: 18530, 11: 28631, 13: 14216, 15: 12408}, - ("Category Sum of Odds", 3, 5): {0: 4685, 5: 13863, 8: 14915, 11: 30363, 13: 16370, 15: 19804}, - ("Category Sum of Odds", 3, 6): {0: 2766, 5: 10213, 8: 11372, 10: 30968, 13: 17133, 15: 27548}, - ("Category Sum of Odds", 3, 7): {0: 543, 3: 8448, 10: 28784, 13: 26258, 15: 35967}, - ("Category Sum of Odds", 3, 8): {0: 3760, 6: 8911, 11: 27672, 13: 16221, 15: 43436}, - ("Category Sum of Odds", 4, 1): {0: 18870, 5: 28873, 6: 18550, 9: 20881, 11: 12826}, - ("Category Sum of Odds", 4, 2): {0: 7974, 6: 23957, 9: 27982, 11: 15953, 13: 13643, 15: 10491}, - ("Category Sum of Odds", 4, 3): {0: 1778, 3: 8154, 8: 25036, 11: 24307, 13: 18030, 15: 14481, 18: 8214}, - ("Category Sum of Odds", 4, 4): {0: 1862, 4: 8889, 8: 11182, 11: 21997, 13: 19483, 16: 20879, 20: 15708}, - ("Category Sum of Odds", 4, 5): {0: 5687, 7: 8212, 11: 18674, 13: 17578, 16: 25572, 18: 12704, 20: 11573}, - ("Category Sum of Odds", 4, 6): {0: 6549, 11: 17161, 13: 15290, 16: 28355, 18: 14865, 20: 17780}, - ("Category Sum of Odds", 4, 7): {0: 5048, 10: 11824, 13: 12343, 16: 29544, 18: 15947, 20: 25294}, - ("Category Sum of Odds", 4, 8): {0: 3060, 10: 8747, 15: 29415, 18: 25762, 20: 33016}, - ("Category Sum of Odds", 5, 1): {0: 3061, 3: 22078, 6: 26935, 9: 23674, 11: 15144, 14: 9108}, - ("Category Sum of Odds", 5, 2): {0: 5813, 7: 19297, 9: 14666, 11: 17165, 14: 21681, 16: 10586, 18: 10792}, - ("Category Sum of Odds", 5, 3): {0: 3881, 6: 9272, 9: 10300, 11: 13443, 14: 24313, 16: 13969, 19: 16420, 21: 8402}, - ("Category Sum of Odds", 5, 4): {0: 4213, 8: 9656, 13: 24199, 16: 22188, 18: 16440, 20: 14313, 23: 8991}, - ("Category Sum of Odds", 5, 5): {0: 4997, 10: 9128, 13: 11376, 16: 20859, 18: 17548, 21: 20120, 25: 15972}, - ("Category Sum of Odds", 5, 6): { - 0: 4581, - 11: 8516, - 14: 11335, - 16: 10647, - 18: 16866, - 21: 24256, - 23: 11945, - 25: 11854, - }, - ("Category Sum of Odds", 5, 7): {0: 176, 6: 8052, 16: 17535, 18: 14878, 21: 27189, 23: 14100, 25: 18070}, - ("Category Sum of Odds", 5, 8): {0: 2, 2: 6622, 15: 12097, 18: 12454, 21: 28398, 23: 15254, 25: 25173}, - ("Category Sum of Odds", 6, 1): {0: 11634, 4: 12188, 6: 16257, 9: 23909, 11: 13671, 13: 13125, 16: 9216}, - ("Category Sum of Odds", 6, 2): {0: 1403, 4: 8241, 10: 22151, 12: 14245, 14: 15279, 17: 19690, 21: 18991}, - ("Category Sum of Odds", 6, 3): { - 0: 6079, - 9: 10832, - 12: 10094, - 14: 13221, - 17: 22538, - 19: 12673, - 21: 15363, - 24: 9200, - }, - ("Category Sum of Odds", 6, 4): {0: 5771, 11: 9419, 16: 22239, 19: 22715, 21: 12847, 23: 12798, 25: 9237, 28: 4974}, - ("Category Sum of Odds", 6, 5): { - 0: 2564, - 11: 8518, - 17: 20753, - 19: 14121, - 21: 13179, - 23: 15752, - 25: 14841, - 28: 10272, - }, - ("Category Sum of Odds", 6, 6): {0: 4310, 14: 8668, 19: 20891, 21: 12052, 23: 16882, 26: 19954, 30: 17243}, - ("Category Sum of Odds", 6, 7): { - 0: 5233, - 16: 8503, - 19: 11127, - 21: 10285, - 23: 16141, - 26: 23993, - 28: 12043, - 30: 12675, - }, - ("Category Sum of Odds", 6, 8): {0: 510, 12: 8107, 21: 17013, 23: 14396, 26: 26771, 28: 13964, 30: 19239}, - ("Category Sum of Odds", 7, 1): { - 0: 2591, - 2: 8436, - 5: 11759, - 7: 13733, - 9: 15656, - 11: 14851, - 13: 12301, - 15: 11871, - 18: 8802, - }, - ("Category Sum of Odds", 7, 2): { - 0: 4730, - 8: 8998, - 11: 10573, - 13: 13099, - 15: 13819, - 17: 13594, - 19: 12561, - 21: 12881, - 24: 9745, - }, - ("Category Sum of Odds", 7, 3): { - 0: 2549, - 9: 8523, - 15: 19566, - 17: 12251, - 19: 13562, - 21: 13473, - 23: 11918, - 27: 18158, - }, - ("Category Sum of Odds", 7, 4): {0: 6776, 14: 9986, 19: 20914, 22: 21006, 24: 12685, 26: 10835, 30: 17798}, - ("Category Sum of Odds", 7, 5): { - 0: 2943, - 14: 8009, - 20: 20248, - 22: 11896, - 24: 14166, - 26: 12505, - 28: 13136, - 30: 10486, - 33: 6611, - }, - ("Category Sum of Odds", 7, 6): { - 2: 1990, - 15: 8986, - 22: 19198, - 24: 13388, - 26: 12513, - 28: 15893, - 30: 15831, - 35: 12201, - }, - ("Category Sum of Odds", 7, 7): { - 4: 559, - 14: 8153, - 21: 11671, - 24: 12064, - 26: 11473, - 28: 16014, - 31: 20785, - 33: 10174, - 35: 9107, - }, - ("Category Sum of Odds", 7, 8): {0: 3, 8: 5190, 21: 8049, 24: 10585, 28: 25255, 31: 24333, 33: 12445, 35: 14140}, - ("Category Sum of Odds", 8, 1): {0: 7169, 7: 19762, 9: 14044, 11: 14858, 13: 13399, 15: 10801, 17: 11147, 20: 8820}, - ("Category Sum of Odds", 8, 2): { - 0: 7745, - 11: 10927, - 14: 10849, - 16: 13103, - 18: 13484, - 20: 12487, - 22: 10815, - 24: 11552, - 27: 9038, - }, - ("Category Sum of Odds", 8, 3): { - 0: 3867, - 12: 9356, - 18: 19408, - 20: 12379, - 22: 12519, - 24: 12260, - 26: 11008, - 28: 10726, - 31: 8477, - }, - ("Category Sum of Odds", 8, 4): { - 1: 3971, - 15: 9176, - 21: 18732, - 23: 12900, - 25: 13405, - 27: 11603, - 29: 10400, - 33: 19813, - }, - ("Category Sum of Odds", 8, 5): { - 1: 490, - 12: 8049, - 20: 9682, - 23: 10177, - 25: 12856, - 27: 12369, - 29: 12781, - 32: 18029, - 34: 11315, - 38: 4252, - }, - ("Category Sum of Odds", 8, 6): { - 4: 86, - 11: 8038, - 22: 9157, - 25: 10729, - 27: 11053, - 29: 13606, - 31: 12383, - 33: 14068, - 35: 12408, - 38: 8472, - }, - ("Category Sum of Odds", 8, 7): { - 6: 1852, - 20: 8020, - 27: 17455, - 29: 12898, - 31: 12181, - 33: 15650, - 35: 17577, - 40: 14367, - }, - ("Category Sum of Odds", 8, 8): { - 4: 8, - 11: 8008, - 26: 10314, - 29: 11446, - 31: 10714, - 33: 16060, - 36: 21765, - 38: 10622, - 40: 11063, - }, - ("Category Sum of Evens", 1, 1): {0: 49585, 2: 16733, 4: 16854, 6: 16828}, - ("Category Sum of Evens", 1, 2): {0: 33244, 2: 11087, 4: 28025, 6: 27644}, - ("Category Sum of Evens", 1, 3): {0: 22259, 4: 42357, 6: 35384}, - ("Category Sum of Evens", 1, 4): {0: 18511, 4: 35651, 6: 45838}, - ("Category Sum of Evens", 1, 5): {0: 15428, 4: 29656, 6: 54916}, - ("Category Sum of Evens", 1, 6): {0: 12927, 4: 24370, 6: 62703}, - ("Category Sum of Evens", 1, 7): {0: 14152, 4: 17087, 6: 68761}, - ("Category Sum of Evens", 1, 8): {0: 11920, 4: 14227, 6: 73853}, - ("Category Sum of Evens", 2, 1): {0: 25229, 2: 16545, 4: 19538, 6: 21987, 10: 16701}, - ("Category Sum of Evens", 2, 2): {0: 11179, 4: 27164, 6: 24451, 8: 13966, 10: 15400, 12: 7840}, - ("Category Sum of Evens", 2, 3): {0: 8099, 4: 16354, 6: 20647, 8: 17887, 10: 24736, 12: 12277}, - ("Category Sum of Evens", 2, 4): {0: 5687, 4: 11219, 6: 20711, 8: 14290, 10: 26976, 12: 21117}, - ("Category Sum of Evens", 2, 5): {0: 3991, 6: 27157, 8: 11641, 10: 26842, 12: 30369}, - ("Category Sum of Evens", 2, 6): {0: 2741, 6: 23123, 10: 35050, 12: 39086}, - ("Category Sum of Evens", 2, 7): {0: 1122, 6: 20538, 10: 30952, 12: 47388}, - ("Category Sum of Evens", 2, 8): {0: 3950, 6: 14006, 10: 27341, 12: 54703}, - ("Category Sum of Evens", 3, 1): {0: 12538, 2: 12516, 4: 16530, 6: 21270, 8: 13745, 10: 11209, 14: 12192}, - ("Category Sum of Evens", 3, 2): {0: 7404, 4: 10459, 6: 15644, 8: 15032, 10: 18955, 12: 15021, 16: 17485}, - ("Category Sum of Evens", 3, 3): {0: 2176, 6: 14148, 8: 12295, 10: 20247, 12: 18001, 14: 15953, 16: 17180}, - ("Category Sum of Evens", 3, 4): {0: 4556, 8: 15062, 10: 17232, 12: 18975, 14: 15832, 16: 18749, 18: 9594}, - ("Category Sum of Evens", 3, 5): {0: 2575, 8: 10825, 10: 13927, 12: 19533, 14: 14402, 16: 21954, 18: 16784}, - ("Category Sum of Evens", 3, 6): {0: 1475, 6: 7528, 10: 10614, 12: 19070, 14: 12940, 16: 23882, 18: 24491}, - ("Category Sum of Evens", 3, 7): {0: 862, 6: 5321, 12: 26291, 14: 10985, 16: 24254, 18: 32287}, - ("Category Sum of Evens", 3, 8): {0: 138, 4: 4086, 12: 22703, 16: 32516, 18: 40557}, - ("Category Sum of Evens", 4, 1): {0: 6214, 4: 20921, 6: 17434, 8: 15427, 10: 14158, 12: 11354, 16: 14492}, - ("Category Sum of Evens", 4, 2): { - 0: 2868, - 6: 13362, - 8: 10702, - 10: 15154, - 12: 15715, - 14: 14104, - 16: 12485, - 20: 15610, - }, - ("Category Sum of Evens", 4, 3): { - 0: 573, - 8: 10496, - 10: 10269, - 12: 12879, - 14: 16224, - 16: 17484, - 18: 13847, - 20: 10518, - 22: 7710, - }, - ("Category Sum of Evens", 4, 4): { - 0: 1119, - 6: 5124, - 12: 17394, - 14: 12763, - 16: 17947, - 18: 16566, - 20: 13338, - 22: 15749, - }, - ("Category Sum of Evens", 4, 5): {0: 3477, 12: 12738, 16: 26184, 18: 18045, 20: 14172, 22: 16111, 24: 9273}, - ("Category Sum of Evens", 4, 6): {0: 991, 12: 10136, 16: 21089, 18: 18805, 20: 13848, 22: 20013, 24: 15118}, - ("Category Sum of Evens", 4, 7): {0: 2931, 16: 21174, 18: 18952, 20: 12601, 22: 21947, 24: 22395}, - ("Category Sum of Evens", 4, 8): {0: 1798, 12: 6781, 18: 27146, 20: 11505, 22: 23056, 24: 29714}, - ("Category Sum of Evens", 5, 1): { - 0: 3192, - 4: 13829, - 6: 13373, - 8: 13964, - 10: 14656, - 12: 13468, - 14: 10245, - 18: 17273, - }, - ("Category Sum of Evens", 5, 2): { - 0: 3217, - 8: 10390, - 12: 22094, - 14: 13824, - 16: 14674, - 18: 12124, - 22: 16619, - 24: 7058, - }, - ("Category Sum of Evens", 5, 3): { - 0: 3904, - 12: 11004, - 14: 10339, - 16: 13128, - 18: 14686, - 20: 15282, - 22: 13294, - 26: 18363, - }, - ("Category Sum of Evens", 5, 4): { - 0: 43, - 4: 4025, - 14: 10648, - 16: 10437, - 18: 12724, - 20: 14710, - 22: 16005, - 24: 12896, - 28: 18512, - }, - ("Category Sum of Evens", 5, 5): { - 0: 350, - 8: 4392, - 16: 11641, - 18: 10297, - 20: 12344, - 22: 16826, - 24: 15490, - 26: 12235, - 28: 16425, - }, - ("Category Sum of Evens", 5, 6): { - 0: 374, - 10: 4670, - 18: 13498, - 22: 25729, - 24: 17286, - 26: 13565, - 28: 15274, - 30: 9604, - }, - ("Category Sum of Evens", 5, 7): {0: 1473, 18: 11310, 22: 21341, 24: 18114, 26: 13349, 28: 19048, 30: 15365}, - ("Category Sum of Evens", 5, 8): {0: 1, 4: 3753, 20: 10318, 22: 11699, 24: 18376, 26: 12500, 28: 21211, 30: 22142}, - ("Category Sum of Evens", 6, 1): { - 0: 4767, - 6: 15250, - 8: 11527, - 10: 13220, - 12: 13855, - 14: 12217, - 16: 10036, - 20: 19128, - }, - ("Category Sum of Evens", 6, 2): { - 0: 1380, - 6: 5285, - 12: 13888, - 14: 10495, - 16: 12112, - 18: 12962, - 20: 12458, - 22: 10842, - 26: 14076, - 28: 6502, - }, - ("Category Sum of Evens", 6, 3): { - 0: 1230, - 16: 17521, - 18: 10098, - 20: 12628, - 22: 13809, - 24: 13594, - 26: 11930, - 30: 19190, - }, - ("Category Sum of Evens", 6, 4): {0: 1235, 18: 15534, 22: 22081, 24: 13471, 26: 13991, 28: 12906, 32: 20782}, - ("Category Sum of Evens", 6, 5): {0: 1241, 20: 15114, 24: 21726, 26: 13874, 28: 15232, 30: 12927, 34: 19886}, - ("Category Sum of Evens", 6, 6): {0: 1224, 22: 15886, 26: 21708, 28: 15982, 30: 15534, 32: 12014, 34: 17652}, - ("Category Sum of Evens", 6, 7): {4: 1437, 24: 17624, 28: 24727, 30: 17083, 32: 13001, 34: 15604, 36: 10524}, - ("Category Sum of Evens", 6, 8): {4: 1707, 24: 11310, 28: 20871, 30: 18101, 32: 12842, 34: 18840, 36: 16329}, - ("Category Sum of Evens", 7, 1): { - 0: 6237, - 8: 15390, - 10: 11183, - 12: 12690, - 14: 12463, - 16: 11578, - 20: 17339, - 22: 8870, - 26: 4250, - }, - ("Category Sum of Evens", 7, 2): { - 0: 1433, - 14: 16705, - 18: 19797, - 20: 11747, - 22: 12101, - 24: 10947, - 28: 16547, - 32: 10723, - }, - ("Category Sum of Evens", 7, 3): { - 0: 2135, - 14: 5836, - 20: 13766, - 22: 10305, - 24: 12043, - 26: 13153, - 28: 12644, - 30: 10884, - 34: 19234, - }, - ("Category Sum of Evens", 7, 4): { - 0: 1762, - 22: 16471, - 26: 20839, - 28: 12907, - 30: 13018, - 32: 11907, - 34: 10022, - 38: 13074, - }, - ("Category Sum of Evens", 7, 5): { - 4: 1630, - 24: 14719, - 28: 20377, - 30: 12713, - 32: 13273, - 34: 13412, - 36: 10366, - 40: 13510, - }, - ("Category Sum of Evens", 7, 6): { - 4: 1436, - 26: 14275, - 30: 20680, - 32: 12798, - 34: 15385, - 36: 13346, - 38: 10011, - 40: 12069, - }, - ("Category Sum of Evens", 7, 7): { - 6: 2815, - 24: 6584, - 30: 16532, - 32: 11106, - 34: 15613, - 36: 15702, - 38: 12021, - 40: 12478, - 42: 7149, - }, - ("Category Sum of Evens", 7, 8): {10: 1490, 30: 16831, 34: 23888, 36: 16970, 38: 12599, 40: 16137, 42: 12085}, - ("Category Sum of Evens", 8, 1): { - 0: 3709, - 8: 10876, - 12: 19246, - 14: 11696, - 16: 11862, - 18: 11145, - 22: 16877, - 24: 9272, - 28: 5317, - }, - ("Category Sum of Evens", 8, 2): { - 0: 1361, - 16: 14530, - 20: 17637, - 22: 10922, - 24: 11148, - 26: 10879, - 30: 17754, - 34: 15769, - }, - ("Category Sum of Evens", 8, 3): { - 2: 1601, - 22: 14895, - 26: 18464, - 28: 11561, - 30: 12249, - 32: 11747, - 34: 10070, - 38: 19413, - }, - ("Category Sum of Evens", 8, 4): { - 0: 2339, - 20: 5286, - 26: 11746, - 30: 19858, - 32: 12344, - 34: 12243, - 36: 11307, - 40: 16632, - 42: 8245, - }, - ("Category Sum of Evens", 8, 5): { - 4: 1798, - 28: 14824, - 32: 18663, - 34: 12180, - 36: 12458, - 38: 12260, - 40: 10958, - 44: 16859, - }, - ("Category Sum of Evens", 8, 6): { - 6: 2908, - 26: 6292, - 32: 13573, - 34: 10367, - 36: 12064, - 38: 12862, - 40: 13920, - 42: 11359, - 46: 16655, - }, - ("Category Sum of Evens", 8, 7): { - 8: 2652, - 28: 6168, - 34: 13922, - 36: 10651, - 38: 12089, - 40: 14999, - 42: 13899, - 44: 10574, - 46: 15046, - }, - ("Category Sum of Evens", 8, 8): { - 10: 2547, - 30: 6023, - 36: 15354, - 38: 10354, - 40: 14996, - 42: 16214, - 44: 11803, - 46: 13670, - 48: 9039, - }, - ("Category Double Threes and Fours", 1, 1): {0: 66749, 6: 16591, 8: 16660}, - ("Category Double Threes and Fours", 1, 2): {0: 44675, 6: 27694, 8: 27631}, - ("Category Double Threes and Fours", 1, 3): {0: 29592, 6: 35261, 8: 35147}, - ("Category Double Threes and Fours", 1, 4): {0: 24601, 6: 29406, 8: 45993}, - ("Category Double Threes and Fours", 1, 5): {0: 20499, 6: 24420, 8: 55081}, - ("Category Double Threes and Fours", 1, 6): {0: 17116, 6: 20227, 8: 62657}, - ("Category Double Threes and Fours", 1, 7): {0: 14193, 6: 17060, 8: 68747}, - ("Category Double Threes and Fours", 1, 8): {0: 11977, 6: 13924, 8: 74099}, - ("Category Double Threes and Fours", 2, 1): {0: 44382, 6: 22191, 8: 22251, 14: 11176}, - ("Category Double Threes and Fours", 2, 2): {0: 19720, 6: 24652, 8: 24891, 14: 23096, 16: 7641}, - ("Category Double Threes and Fours", 2, 3): {0: 8765, 6: 21008, 8: 20929, 12: 12201, 14: 24721, 16: 12376}, - ("Category Double Threes and Fours", 2, 4): {0: 6164, 6: 14466, 8: 22828, 14: 35406, 16: 21136}, - ("Category Double Threes and Fours", 2, 5): {0: 4307, 6: 10005, 8: 22620, 14: 32879, 16: 30189}, - ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 28513, 14: 29530, 16: 39078}, - ("Category Double Threes and Fours", 2, 7): {0: 2042, 8: 24335, 14: 26250, 16: 47373}, - ("Category Double Threes and Fours", 2, 8): {0: 1385, 8: 23166, 14: 20907, 16: 54542}, - ("Category Double Threes and Fours", 3, 1): {0: 29378, 6: 22335, 8: 22138, 14: 16783, 16: 9366}, - ("Category Double Threes and Fours", 3, 2): { - 0: 8894, - 6: 16518, - 8: 16277, - 12: 10334, - 14: 20757, - 16: 12265, - 22: 14955, - }, - ("Category Double Threes and Fours", 3, 3): { - 0: 2643, - 8: 18522, - 12: 11066, - 14: 21922, - 16: 11045, - 20: 17235, - 22: 17567, - }, - ("Category Double Threes and Fours", 3, 4): { - 0: 1523, - 8: 13773, - 14: 26533, - 16: 18276, - 20: 11695, - 22: 18521, - 24: 9679, - }, - ("Category Double Threes and Fours", 3, 5): {0: 845, 8: 10218, 14: 20245, 16: 20293, 22: 31908, 24: 16491}, - ("Category Double Threes and Fours", 3, 6): {0: 499, 8: 7230, 14: 15028, 16: 20914, 22: 31835, 24: 24494}, - ("Category Double Threes and Fours", 3, 7): {0: 1298, 8: 5434, 16: 30595, 22: 29980, 24: 32693}, - ("Category Double Threes and Fours", 3, 8): {0: 178, 6: 4363, 16: 27419, 22: 27614, 24: 40426}, - ("Category Double Threes and Fours", 4, 1): {0: 19809, 6: 19538, 8: 19765, 14: 22348, 18: 12403, 22: 6137}, - ("Category Double Threes and Fours", 4, 2): { - 0: 3972, - 8: 19440, - 14: 27646, - 16: 12978, - 20: 11442, - 22: 11245, - 24: 6728, - 28: 6549, - }, - ("Category Double Threes and Fours", 4, 3): { - 0: 745, - 6: 7209, - 14: 19403, - 18: 11744, - 20: 15371, - 22: 15441, - 26: 13062, - 30: 17025, - }, - ("Category Double Threes and Fours", 4, 4): { - 0: 371, - 6: 4491, - 14: 13120, - 16: 10176, - 20: 11583, - 22: 18508, - 24: 10280, - 28: 15624, - 30: 15847, - }, - ("Category Double Threes and Fours", 4, 5): { - 0: 163, - 6: 4251, - 16: 15796, - 22: 26145, - 24: 17306, - 28: 10930, - 30: 16244, - 32: 9165, - }, - ("Category Double Threes and Fours", 4, 6): {0: 79, 16: 14439, 22: 21763, 24: 18861, 30: 29518, 32: 15340}, - ("Category Double Threes and Fours", 4, 7): {0: 1042, 16: 12543, 22: 13634, 24: 20162, 30: 30259, 32: 22360}, - ("Category Double Threes and Fours", 4, 8): {0: 20, 6: 2490, 16: 6901, 22: 10960, 24: 20269, 30: 29442, 32: 29918}, - ("Category Double Threes and Fours", 5, 1): { - 0: 13122, - 6: 16411, - 8: 16451, - 14: 24768, - 16: 10392, - 22: 14528, - 26: 4328, - }, - ("Category Double Threes and Fours", 5, 2): { - 0: 1676, - 8: 10787, - 14: 20218, - 18: 11102, - 20: 12668, - 22: 12832, - 26: 10994, - 30: 15390, - 34: 4333, - }, - ("Category Double Threes and Fours", 5, 3): { - 0: 223, - 14: 12365, - 16: 7165, - 20: 11385, - 22: 11613, - 26: 15182, - 28: 13665, - 32: 14400, - 36: 14002, - }, - ("Category Double Threes and Fours", 5, 4): { - 0: 95, - 6: 2712, - 16: 8862, - 22: 18696, - 26: 12373, - 28: 13488, - 30: 14319, - 34: 12414, - 38: 17041, - }, - ("Category Double Threes and Fours", 5, 5): { - 0: 1333, - 14: 5458, - 22: 13613, - 24: 10772, - 28: 11201, - 30: 16810, - 32: 10248, - 36: 14426, - 38: 16139, - }, - ("Category Double Threes and Fours", 5, 6): { - 0: 16, - 16: 6354, - 24: 16213, - 30: 25369, - 32: 16845, - 36: 10243, - 38: 15569, - 40: 9391, - }, - ("Category Double Threes and Fours", 5, 7): { - 0: 161, - 12: 3457, - 24: 12437, - 30: 21495, - 32: 18636, - 38: 28581, - 40: 15233, - }, - ("Category Double Threes and Fours", 5, 8): { - 0: 478, - 16: 4861, - 26: 10119, - 30: 13694, - 32: 19681, - 38: 29177, - 40: 21990, - }, - ("Category Double Threes and Fours", 6, 1): { - 0: 8738, - 6: 13463, - 8: 12988, - 14: 24653, - 16: 11068, - 22: 19621, - 26: 5157, - 30: 4312, - }, - ("Category Double Threes and Fours", 6, 2): { - 0: 784, - 6: 5735, - 14: 13407, - 16: 8170, - 20: 11349, - 22: 11356, - 26: 12465, - 28: 10790, - 30: 11527, - 38: 14417, - }, - ("Category Double Threes and Fours", 6, 3): { - 0: 72, - 14: 8986, - 22: 13700, - 26: 12357, - 28: 12114, - 32: 15882, - 36: 19286, - 40: 13540, - 44: 4063, - }, - ("Category Double Threes and Fours", 6, 4): { - 0: 439, - 18: 7427, - 22: 9284, - 28: 14203, - 30: 10836, - 34: 14646, - 36: 12511, - 38: 10194, - 42: 10202, - 46: 10258, - }, - ("Category Double Threes and Fours", 6, 5): { - 0: 166, - 20: 7618, - 24: 5198, - 30: 17479, - 34: 12496, - 36: 12190, - 38: 14163, - 42: 12571, - 46: 18119, - }, - ("Category Double Threes and Fours", 6, 6): { - 0: 1843, - 22: 5905, - 30: 12997, - 32: 10631, - 36: 10342, - 38: 16439, - 40: 10795, - 44: 13485, - 46: 17563, - }, - ("Category Double Threes and Fours", 6, 7): { - 0: 31, - 12: 2221, - 24: 5004, - 32: 15743, - 38: 24402, - 40: 17005, - 46: 25241, - 48: 10353, - }, - ("Category Double Threes and Fours", 6, 8): { - 8: 79, - 16: 4037, - 32: 12559, - 38: 20863, - 40: 18347, - 46: 27683, - 48: 16432, - }, - ("Category Double Threes and Fours", 7, 1): { - 0: 5803, - 6: 10242, - 8: 10404, - 14: 22886, - 16: 10934, - 22: 19133, - 24: 7193, - 28: 8167, - 32: 5238, - }, - ("Category Double Threes and Fours", 7, 2): { - 0: 357, - 14: 17082, - 22: 17524, - 26: 11974, - 28: 11132, - 32: 13186, - 36: 13959, - 40: 10028, - 44: 4758, - }, - ("Category Double Threes and Fours", 7, 3): { - 0: 361, - 18: 7136, - 22: 5983, - 28: 13899, - 32: 12974, - 34: 10088, - 36: 10081, - 40: 14481, - 44: 14127, - 46: 6547, - 50: 4323, - }, - ("Category Double Threes and Fours", 7, 4): { - 0: 1182, - 18: 4299, - 30: 16331, - 34: 11316, - 36: 10741, - 40: 16028, - 44: 18815, - 48: 15225, - 52: 6063, - }, - ("Category Double Threes and Fours", 7, 5): { - 0: 45, - 12: 3763, - 32: 17140, - 38: 19112, - 42: 13655, - 44: 11990, - 46: 11137, - 50: 10646, - 54: 12512, - }, - ("Category Double Threes and Fours", 7, 6): { - 8: 2400, - 28: 5277, - 32: 5084, - 38: 16047, - 42: 12133, - 44: 11451, - 46: 14027, - 50: 13198, - 54: 20383, - }, - ("Category Double Threes and Fours", 7, 7): { - 6: 1968, - 30: 5585, - 38: 12210, - 40: 10376, - 46: 25548, - 48: 15392, - 54: 21666, - 56: 7255, - }, - ("Category Double Threes and Fours", 7, 8): { - 8: 42, - 20: 2293, - 32: 4653, - 40: 15068, - 46: 23170, - 48: 17057, - 54: 25601, - 56: 12116, - }, - ("Category Double Threes and Fours", 8, 1): { - 0: 3982, - 8: 15658, - 14: 20388, - 16: 10234, - 20: 10167, - 22: 10162, - 28: 15330, - 32: 8758, - 36: 5321, - }, - ("Category Double Threes and Fours", 8, 2): { - 0: 161, - 6: 3169, - 14: 7106, - 22: 16559, - 28: 16400, - 32: 12950, - 36: 16399, - 40: 10090, - 44: 11474, - 48: 5692, - }, - ("Category Double Threes and Fours", 8, 3): { - 0: 856, - 16: 4092, - 30: 13686, - 34: 12838, - 38: 15010, - 42: 17085, - 46: 14067, - 50: 11844, - 52: 6500, - 56: 4022, - }, - ("Category Double Threes and Fours", 8, 4): { - 0: 36, - 12: 2795, - 30: 9742, - 36: 11726, - 40: 12404, - 44: 18791, - 48: 14662, - 52: 15518, - 54: 8066, - 58: 6260, - }, - ("Category Double Threes and Fours", 8, 5): { - 6: 8, - 12: 2948, - 30: 5791, - 38: 10658, - 42: 10175, - 46: 19359, - 50: 14449, - 52: 10531, - 56: 13257, - 60: 12824, - }, - ("Category Double Threes and Fours", 8, 6): { - 0: 2, - 12: 2528, - 32: 4832, - 40: 11436, - 46: 17832, - 50: 13016, - 52: 11631, - 54: 12058, - 58: 11458, - 62: 15207, - }, - ("Category Double Threes and Fours", 8, 7): { - 6: 2, - 12: 2204, - 40: 9320, - 46: 14688, - 50: 11494, - 52: 10602, - 54: 14541, - 58: 13849, - 62: 23300, - }, - ("Category Double Threes and Fours", 8, 8): { - 8: 1, - 16: 1773, - 42: 8766, - 48: 17452, - 54: 24338, - 56: 15722, - 62: 22745, - 64: 9203, - }, - ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 4: 16803, 8: 16630}, - ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 4: 27448, 8: 27743}, - ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 4: 23184, 8: 39716}, - ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 4: 19221, 8: 49816}, - ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 4: 16079, 8: 58605}, - ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 4: 13237, 8: 65258}, - ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 4: 11100, 8: 71224}, - ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 4: 9323, 8: 75706}, - ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 4: 22273, 8: 24842, 12: 8319}, - ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 4: 24890, 8: 32262, 12: 15172, 16: 7713}, - ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 4: 17158, 8: 34907, 12: 18539, 16: 15630}, - ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 4: 11981, 8: 34465, 12: 19108, 16: 24903}, - ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 4: 8302, 8: 32470, 12: 18612, 16: 34144}, - ("Category Quadruple Ones and Twos", 2, 6): {0: 4569, 4: 5737, 8: 29716, 12: 17216, 16: 42762}, - ("Category Quadruple Ones and Twos", 2, 7): {0: 3146, 8: 30463, 12: 15756, 16: 50635}, - ("Category Quadruple Ones and Twos", 2, 8): {0: 2265, 8: 26302, 12: 14167, 16: 57266}, - ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 4: 22574, 8: 27747, 12: 11557, 16: 8682}, - ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 4: 16295, 8: 26434, 12: 22986, 16: 16799, 20: 8629}, - ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 4: 9447, 8: 22255, 12: 21685, 16: 24084, 20: 11167, 24: 6299}, - ("Category Quadruple Ones and Twos", 3, 4): { - 0: 2864, - 4: 5531, - 8: 17681, - 12: 18400, - 16: 28524, - 20: 14552, - 24: 12448, - }, - ("Category Quadruple Ones and Twos", 3, 5): {0: 1676, 8: 16697, 12: 14755, 16: 30427, 20: 16602, 24: 19843}, - ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 8: 10259, 12: 11326, 16: 31125, 20: 16984, 24: 27625}, - ("Category Quadruple Ones and Twos", 3, 7): {0: 1688, 8: 7543, 12: 8769, 16: 29367, 20: 17085, 24: 35548}, - ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 8: 5277, 12: 6388, 16: 27741, 20: 16170, 24: 43483}, - ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 4: 19657, 8: 27288, 12: 16126, 16: 11167, 24: 6071}, - ("Category Quadruple Ones and Twos", 4, 2): { - 0: 4023, - 4: 9776, - 8: 19015, - 12: 22094, - 16: 20986, - 20: 13805, - 24: 10301, - }, - ("Category Quadruple Ones and Twos", 4, 3): { - 0: 1848, - 8: 17116, - 12: 16853, - 16: 22831, - 20: 18400, - 24: 14480, - 28: 8472, - }, - ("Category Quadruple Ones and Twos", 4, 4): { - 0: 930, - 8: 10375, - 12: 12063, - 16: 21220, - 20: 19266, - 24: 20615, - 28: 9443, - 32: 6088, - }, - ("Category Quadruple Ones and Twos", 4, 5): { - 0: 1561, - 12: 12612, - 16: 18209, - 20: 17910, - 24: 25474, - 28: 12864, - 32: 11370, - }, - ("Category Quadruple Ones and Twos", 4, 6): { - 0: 722, - 12: 7979, - 16: 14796, - 20: 15416, - 24: 28256, - 28: 14675, - 32: 18156, - }, - ("Category Quadruple Ones and Twos", 4, 7): { - 0: 115, - 12: 5304, - 16: 11547, - 20: 12289, - 24: 29181, - 28: 16052, - 32: 25512, - }, - ("Category Quadruple Ones and Twos", 4, 8): {0: 164, 8: 2971, 16: 8888, 20: 9679, 24: 28785, 28: 16180, 32: 33333}, - ("Category Quadruple Ones and Twos", 5, 1): { - 0: 13112, - 4: 16534, - 8: 24718, - 12: 18558, - 16: 14547, - 20: 7055, - 24: 5476, - }, - ("Category Quadruple Ones and Twos", 5, 2): { - 0: 1764, - 4: 5529, - 8: 12216, - 12: 17687, - 16: 20808, - 20: 18149, - 24: 12849, - 28: 6991, - 32: 4007, - }, - ("Category Quadruple Ones and Twos", 5, 3): { - 0: 719, - 8: 8523, - 12: 11074, - 16: 17322, - 20: 19002, - 24: 18643, - 28: 12827, - 32: 7960, - 36: 3930, - }, - ("Category Quadruple Ones and Twos", 5, 4): { - 0: 1152, - 12: 9790, - 16: 12913, - 20: 15867, - 24: 20749, - 28: 16398, - 32: 14218, - 36: 8913, - }, - ("Category Quadruple Ones and Twos", 5, 5): { - 0: 98, - 12: 5549, - 16: 8863, - 20: 12037, - 24: 20010, - 28: 17568, - 32: 19789, - 36: 9319, - 40: 6767, - }, - ("Category Quadruple Ones and Twos", 5, 6): { - 0: 194, - 8: 2663, - 16: 5734, - 20: 8436, - 24: 17830, - 28: 16864, - 32: 24246, - 36: 12115, - 40: 11918, - }, - ("Category Quadruple Ones and Twos", 5, 7): { - 0: 1449, - 20: 9396, - 24: 14936, - 28: 14969, - 32: 27238, - 36: 14094, - 40: 17918, - }, - ("Category Quadruple Ones and Twos", 5, 8): { - 0: 747, - 20: 6034, - 24: 11929, - 28: 12517, - 32: 28388, - 36: 15339, - 40: 25046, - }, - ("Category Quadruple Ones and Twos", 6, 1): { - 0: 8646, - 4: 13011, - 8: 21357, - 12: 19385, - 16: 17008, - 20: 10409, - 24: 6249, - 28: 3935, - }, - ("Category Quadruple Ones and Twos", 6, 2): { - 0: 844, - 8: 10311, - 12: 12792, - 16: 17480, - 20: 18814, - 24: 16492, - 28: 11889, - 32: 6893, - 36: 4485, - }, - ("Category Quadruple Ones and Twos", 6, 3): { - 0: 1241, - 12: 9634, - 16: 11685, - 20: 15584, - 24: 17967, - 28: 16506, - 32: 13314, - 36: 8034, - 40: 6035, - }, - ("Category Quadruple Ones and Twos", 6, 4): { - 0: 1745, - 16: 9804, - 20: 10562, - 24: 15746, - 28: 17174, - 32: 17787, - 36: 12820, - 40: 9289, - 44: 5073, - }, - ("Category Quadruple Ones and Twos", 6, 5): { - 0: 2076, - 20: 10247, - 24: 12264, - 28: 14810, - 32: 19588, - 36: 16002, - 40: 14682, - 44: 6410, - 48: 3921, - }, - ("Category Quadruple Ones and Twos", 6, 6): { - 0: 884, - 20: 5943, - 24: 8774, - 28: 11481, - 32: 19145, - 36: 16864, - 40: 19906, - 44: 9386, - 48: 7617, - }, - ("Category Quadruple Ones and Twos", 6, 7): { - 0: 1386, - 24: 8138, - 28: 8372, - 32: 17207, - 36: 16148, - 40: 24051, - 44: 11862, - 48: 12836, - }, - ("Category Quadruple Ones and Twos", 6, 8): { - 0: 1841, - 28: 9606, - 32: 14489, - 36: 14585, - 40: 26779, - 44: 13821, - 48: 18879, - }, - ("Category Quadruple Ones and Twos", 7, 1): { - 0: 5780, - 4: 10185, - 8: 17905, - 12: 18364, - 16: 18160, - 20: 13115, - 24: 8617, - 32: 7874, - }, - ("Category Quadruple Ones and Twos", 7, 2): { - 0: 1795, - 12: 12828, - 16: 13204, - 20: 16895, - 24: 17562, - 28: 15061, - 32: 11122, - 36: 6507, - 40: 5026, - }, - ("Category Quadruple Ones and Twos", 7, 3): { - 0: 2065, - 16: 10495, - 20: 11008, - 24: 14839, - 28: 16393, - 32: 16118, - 36: 12681, - 40: 8773, - 48: 7628, - }, - ("Category Quadruple Ones and Twos", 7, 4): { - 0: 1950, - 20: 9612, - 24: 10535, - 28: 13596, - 32: 16527, - 36: 15938, - 40: 14071, - 44: 9192, - 48: 8579, - }, - ("Category Quadruple Ones and Twos", 7, 5): { - 0: 223, - 20: 5144, - 24: 6337, - 28: 9400, - 32: 14443, - 36: 15955, - 40: 17820, - 44: 13369, - 48: 10702, - 56: 6607, - }, - ("Category Quadruple Ones and Twos", 7, 6): { - 0: 271, - 24: 5976, - 28: 5988, - 32: 11398, - 36: 13738, - 40: 19063, - 44: 15587, - 48: 15867, - 52: 7202, - 56: 4910, - }, - ("Category Quadruple Ones and Twos", 7, 7): { - 0: 1032, - 28: 5724, - 32: 8275, - 36: 10801, - 40: 18184, - 44: 16470, - 48: 20467, - 52: 9969, - 56: 9078, - }, - ("Category Quadruple Ones and Twos", 7, 8): { - 0: 1508, - 32: 7832, - 36: 7770, - 40: 16197, - 44: 15477, - 48: 24388, - 52: 12403, - 56: 14425, - }, - ("Category Quadruple Ones and Twos", 8, 1): { - 0: 3811, - 4: 7682, - 8: 14638, - 12: 17214, - 16: 18191, - 20: 14651, - 24: 10976, - 28: 6591, - 36: 6246, - }, - ("Category Quadruple Ones and Twos", 8, 2): { - 0: 906, - 12: 7768, - 16: 9421, - 20: 13623, - 24: 16213, - 28: 16246, - 32: 14131, - 36: 10076, - 40: 6198, - 48: 5418, - }, - ("Category Quadruple Ones and Twos", 8, 3): { - 0: 224, - 8: 2520, - 20: 11222, - 24: 10733, - 28: 13934, - 32: 15751, - 36: 14882, - 40: 12409, - 44: 8920, - 48: 5462, - 52: 3943, - }, - ("Category Quadruple Ones and Twos", 8, 4): { - 0: 233, - 20: 5163, - 24: 6057, - 28: 9073, - 32: 12990, - 36: 14756, - 40: 15851, - 44: 13795, - 48: 10706, - 52: 6310, - 56: 5066, - }, - ("Category Quadruple Ones and Twos", 8, 5): { - 0: 76, - 12: 2105, - 28: 8316, - 32: 8993, - 36: 12039, - 40: 15561, - 44: 15382, - 48: 15278, - 52: 10629, - 56: 7377, - 60: 4244, - }, - ("Category Quadruple Ones and Twos", 8, 6): { - 4: 262, - 32: 10321, - 36: 8463, - 40: 13177, - 44: 14818, - 48: 17731, - 52: 14024, - 56: 12425, - 60: 5446, - 64: 3333, - }, - ("Category Quadruple Ones and Twos", 8, 7): { - 8: 300, - 32: 5443, - 36: 5454, - 40: 10276, - 44: 12582, - 48: 18487, - 52: 15549, - 56: 17187, - 60: 8149, - 64: 6573, - }, - ("Category Quadruple Ones and Twos", 8, 8): { - 8: 354, - 36: 5678, - 40: 7484, - 44: 9727, - 48: 17080, - 52: 15898, - 56: 21877, - 60: 10773, - 64: 11129, - }, + ("Category Half of Sixes", 8, 1): {0: 23337, 6: 76663}, + ("Category Half of Sixes", 8, 2): {0: 5310, 9: 74178, 12: 20512}, + ("Category Half of Sixes", 8, 3): {0: 8656, 12: 70598, 15: 20746}, + ("Category Half of Sixes", 8, 4): {0: 291, 12: 59487, 18: 40222}, + ("Category Half of Sixes", 8, 5): {0: 5145, 15: 63787, 18: 31068}, + ("Category Half of Sixes", 8, 6): {0: 8804, 18: 91196}, + ("Category Half of Sixes", 8, 7): {0: 4347, 18: 65663, 21: 29990}, + ("Category Half of Sixes", 8, 8): {0: 9252, 21: 90748}, + ("Category Twos and Threes", 1, 1): {0: 66466, 2: 33534}, + ("Category Twos and Threes", 1, 2): {0: 55640, 2: 44360}, + ("Category Twos and Threes", 1, 3): {0: 57822, 3: 42178}, + ("Category Twos and Threes", 1, 4): {0: 48170, 3: 51830}, + ("Category Twos and Threes", 1, 5): {0: 40294, 3: 59706}, + ("Category Twos and Threes", 1, 6): {0: 33417, 3: 66583}, + ("Category Twos and Threes", 1, 7): {0: 27852, 3: 72148}, + ("Category Twos and Threes", 1, 8): {0: 23364, 3: 76636}, + ("Category Twos and Threes", 2, 1): {0: 44565, 3: 55435}, + ("Category Twos and Threes", 2, 2): {0: 46335, 3: 53665}, + ("Category Twos and Threes", 2, 3): {0: 32347, 3: 67653}, + ("Category Twos and Threes", 2, 4): {0: 22424, 5: 77576}, + ("Category Twos and Threes", 2, 5): {0: 15661, 6: 84339}, + ("Category Twos and Threes", 2, 6): {0: 10775, 6: 89225}, + ("Category Twos and Threes", 2, 7): {0: 7375, 6: 92625}, + ("Category Twos and Threes", 2, 8): {0: 5212, 6: 94788}, + ("Category Twos and Threes", 3, 1): {0: 29892, 3: 70108}, + ("Category Twos and Threes", 3, 2): {0: 17285, 5: 82715}, + ("Category Twos and Threes", 3, 3): {0: 17436, 6: 82564}, + ("Category Twos and Threes", 3, 4): {0: 9962, 6: 90038}, + ("Category Twos and Threes", 3, 5): {0: 3347, 6: 96653}, + ("Category Twos and Threes", 3, 6): {0: 1821, 8: 98179}, + ("Category Twos and Threes", 3, 7): {0: 1082, 6: 61417, 9: 37501}, + ("Category Twos and Threes", 3, 8): {0: 13346, 9: 86654}, + ("Category Twos and Threes", 4, 1): {0: 19619, 5: 80381}, + ("Category Twos and Threes", 4, 2): {0: 18914, 6: 81086}, + ("Category Twos and Threes", 4, 3): {0: 4538, 5: 61859, 8: 33603}, + ("Category Twos and Threes", 4, 4): {0: 2183, 6: 62279, 9: 35538}, + ("Category Twos and Threes", 4, 5): {0: 16416, 9: 83584}, + ("Category Twos and Threes", 4, 6): {0: 6285, 9: 93715}, + ("Category Twos and Threes", 4, 7): {0: 30331, 11: 69669}, + ("Category Twos and Threes", 4, 8): {0: 22305, 12: 77695}, + ("Category Twos and Threes", 5, 1): {0: 13070, 5: 86930}, + ("Category Twos and Threes", 5, 2): {0: 5213, 5: 61441, 8: 33346}, + ("Category Twos and Threes", 5, 3): {0: 2126, 6: 58142, 9: 39732}, + ("Category Twos and Threes", 5, 4): {0: 848, 2: 30734, 11: 68418}, + ("Category Twos and Threes", 5, 5): {0: 29502, 12: 70498}, + ("Category Twos and Threes", 5, 6): {0: 123, 9: 52792, 12: 47085}, + ("Category Twos and Threes", 5, 7): {0: 8241, 12: 91759}, + ("Category Twos and Threes", 5, 8): {0: 13, 2: 31670, 14: 68317}, + ("Category Twos and Threes", 6, 1): {0: 22090, 6: 77910}, + ("Category Twos and Threes", 6, 2): {0: 2944, 6: 62394, 9: 34662}, + ("Category Twos and Threes", 6, 3): {0: 977, 2: 30626, 11: 68397}, + ("Category Twos and Threes", 6, 4): {0: 320, 8: 58370, 12: 41310}, + ("Category Twos and Threes", 6, 5): {0: 114, 2: 31718, 14: 68168}, + ("Category Twos and Threes", 6, 6): {0: 29669, 15: 70331}, + ("Category Twos and Threes", 6, 7): {0: 19855, 15: 80145}, + ("Category Twos and Threes", 6, 8): {0: 8524, 15: 91476}, + ("Category Twos and Threes", 7, 1): {0: 5802, 4: 54580, 7: 39618}, + ("Category Twos and Threes", 7, 2): {0: 1605, 6: 62574, 10: 35821}, + ("Category Twos and Threes", 7, 3): {0: 471, 8: 59691, 12: 39838}, + ("Category Twos and Threes", 7, 4): {0: 26620, 14: 73380}, + ("Category Twos and Threes", 7, 5): {0: 17308, 11: 37515, 15: 45177}, + ("Category Twos and Threes", 7, 6): {0: 30281, 17: 69719}, + ("Category Twos and Threes", 7, 7): {0: 28433, 18: 71567}, + ("Category Twos and Threes", 7, 8): {0: 13274, 18: 86726}, + ("Category Twos and Threes", 8, 1): {0: 3799, 5: 56614, 8: 39587}, + ("Category Twos and Threes", 8, 2): {0: 902, 7: 58003, 11: 41095}, + ("Category Twos and Threes", 8, 3): {0: 29391, 14: 70609}, + ("Category Twos and Threes", 8, 4): {0: 26041, 12: 40535, 16: 33424}, + ("Category Twos and Threes", 8, 5): {0: 26328, 14: 38760, 18: 34912}, + ("Category Twos and Threes", 8, 6): {0: 22646, 15: 45218, 19: 32136}, + ("Category Twos and Threes", 8, 7): {0: 25908, 20: 74092}, + ("Category Twos and Threes", 8, 8): {3: 18441, 17: 38826, 21: 42733}, + ("Category Sum of Odds", 1, 1): {0: 66572, 5: 33428}, + ("Category Sum of Odds", 1, 2): {0: 44489, 5: 55511}, + ("Category Sum of Odds", 1, 3): {0: 37185, 5: 62815}, + ("Category Sum of Odds", 1, 4): {0: 30917, 5: 69083}, + ("Category Sum of Odds", 1, 5): {0: 41833, 5: 58167}, + ("Category Sum of Odds", 1, 6): {0: 34902, 5: 65098}, + ("Category Sum of Odds", 1, 7): {0: 29031, 5: 70969}, + ("Category Sum of Odds", 1, 8): {0: 24051, 5: 75949}, + ("Category Sum of Odds", 2, 1): {0: 66460, 5: 33540}, + ("Category Sum of Odds", 2, 2): {0: 11216, 5: 65597, 8: 23187}, + ("Category Sum of Odds", 2, 3): {0: 30785, 8: 69215}, + ("Category Sum of Odds", 2, 4): {0: 21441, 10: 78559}, + ("Category Sum of Odds", 2, 5): {0: 14948, 10: 85052}, + ("Category Sum of Odds", 2, 6): {0: 4657, 3: 35569, 10: 59774}, + ("Category Sum of Odds", 2, 7): {0: 7262, 5: 42684, 10: 50054}, + ("Category Sum of Odds", 2, 8): {0: 4950, 5: 37432, 10: 57618}, + ("Category Sum of Odds", 3, 1): {0: 29203, 6: 70797}, + ("Category Sum of Odds", 3, 2): {0: 34454, 9: 65546}, + ("Category Sum of Odds", 3, 3): {0: 5022, 3: 32067, 8: 45663, 13: 17248}, + ("Category Sum of Odds", 3, 4): {0: 6138, 4: 33396, 13: 60466}, + ("Category Sum of Odds", 3, 5): {0: 29405, 15: 70595}, + ("Category Sum of Odds", 3, 6): {0: 21390, 15: 78610}, + ("Category Sum of Odds", 3, 7): {0: 8991, 8: 38279, 15: 52730}, + ("Category Sum of Odds", 3, 8): {0: 6340, 8: 34003, 15: 59657}, + ("Category Sum of Odds", 4, 1): {0: 28095, 4: 38198, 8: 33707}, + ("Category Sum of Odds", 4, 2): {0: 27003, 11: 72997}, + ("Category Sum of Odds", 4, 3): {0: 18712, 8: 40563, 13: 40725}, + ("Category Sum of Odds", 4, 4): {0: 30691, 15: 69309}, + ("Category Sum of Odds", 4, 5): {0: 433, 3: 32140, 13: 43150, 18: 24277}, + ("Category Sum of Odds", 4, 6): {0: 6549, 9: 32451, 15: 43220, 20: 17780}, + ("Category Sum of Odds", 4, 7): {0: 29215, 15: 45491, 20: 25294}, + ("Category Sum of Odds", 4, 8): {0: 11807, 13: 38927, 20: 49266}, + ("Category Sum of Odds", 5, 1): {0: 25139, 9: 74861}, + ("Category Sum of Odds", 5, 2): {0: 25110, 9: 40175, 14: 34715}, + ("Category Sum of Odds", 5, 3): {0: 23453, 11: 37756, 16: 38791}, + ("Category Sum of Odds", 5, 4): {0: 22993, 13: 37263, 18: 39744}, + ("Category Sum of Odds", 5, 5): {0: 25501, 15: 38407, 20: 36092}, + ("Category Sum of Odds", 5, 6): {0: 2542, 10: 32537, 18: 41122, 23: 23799}, + ("Category Sum of Odds", 5, 7): {0: 8228, 14: 32413, 20: 41289, 25: 18070}, + ("Category Sum of Odds", 5, 8): {0: 2, 2: 31173, 20: 43652, 25: 25173}, + ("Category Sum of Odds", 6, 1): {0: 23822, 6: 40166, 11: 36012}, + ("Category Sum of Odds", 6, 2): {0: 24182, 11: 37137, 16: 38681}, + ("Category Sum of Odds", 6, 3): {0: 27005, 14: 35759, 19: 37236}, + ("Category Sum of Odds", 6, 4): {0: 25133, 16: 35011, 21: 39856}, + ("Category Sum of Odds", 6, 5): {0: 24201, 18: 34934, 23: 40865}, + ("Category Sum of Odds", 6, 6): {0: 12978, 17: 32943, 23: 36836, 28: 17243}, + ("Category Sum of Odds", 6, 7): {0: 2314, 14: 32834, 23: 40134, 28: 24718}, + ("Category Sum of Odds", 6, 8): {0: 5464, 18: 34562, 25: 40735, 30: 19239}, + ("Category Sum of Odds", 7, 1): {0: 29329, 8: 37697, 13: 32974}, + ("Category Sum of Odds", 7, 2): {0: 29935, 14: 34878, 19: 35187}, + ("Category Sum of Odds", 7, 3): {0: 30638, 17: 33733, 22: 35629}, + ("Category Sum of Odds", 7, 4): {0: 163, 6: 32024, 20: 33870, 25: 33943}, + ("Category Sum of Odds", 7, 5): {0: 31200, 22: 35565, 27: 33235}, + ("Category Sum of Odds", 7, 6): {2: 30174, 24: 36670, 29: 33156}, + ("Category Sum of Odds", 7, 7): {4: 8712, 21: 35208, 28: 36799, 33: 19281}, + ("Category Sum of Odds", 7, 8): {0: 1447, 18: 32027, 28: 39941, 33: 26585}, + ("Category Sum of Odds", 8, 1): {0: 26931, 9: 35423, 14: 37646}, + ("Category Sum of Odds", 8, 2): {0: 29521, 16: 32919, 21: 37560}, + ("Category Sum of Odds", 8, 3): {0: 412, 7: 32219, 20: 32055, 25: 35314}, + ("Category Sum of Odds", 8, 4): {1: 27021, 22: 36376, 28: 36603}, + ("Category Sum of Odds", 8, 5): {1: 1069, 14: 32451, 26: 32884, 31: 33596}, + ("Category Sum of Odds", 8, 6): {4: 31598, 28: 33454, 33: 34948}, + ("Category Sum of Odds", 8, 7): {6: 27327, 29: 35647, 34: 37026}, + ("Category Sum of Odds", 8, 8): {4: 1, 26: 40489, 33: 37825, 38: 21685}, + ("Category Sum of Evens", 1, 1): {0: 49585, 6: 50415}, + ("Category Sum of Evens", 1, 2): {0: 44331, 6: 55669}, + ("Category Sum of Evens", 1, 3): {0: 29576, 6: 70424}, + ("Category Sum of Evens", 1, 4): {0: 24744, 6: 75256}, + ("Category Sum of Evens", 1, 5): {0: 20574, 6: 79426}, + ("Category Sum of Evens", 1, 6): {0: 17182, 6: 82818}, + ("Category Sum of Evens", 1, 7): {0: 14152, 6: 85848}, + ("Category Sum of Evens", 1, 8): {0: 8911, 6: 91089}, + ("Category Sum of Evens", 2, 1): {0: 25229, 8: 74771}, + ("Category Sum of Evens", 2, 2): {0: 18682, 6: 58078, 10: 23240}, + ("Category Sum of Evens", 2, 3): {0: 8099, 10: 91901}, + ("Category Sum of Evens", 2, 4): {0: 16906, 12: 83094}, + ("Category Sum of Evens", 2, 5): {0: 11901, 12: 88099}, + ("Category Sum of Evens", 2, 6): {0: 8054, 12: 91946}, + ("Category Sum of Evens", 2, 7): {0: 5695, 12: 94305}, + ("Category Sum of Evens", 2, 8): {0: 3950, 12: 96050}, + ("Category Sum of Evens", 3, 1): {0: 25054, 6: 51545, 10: 23401}, + ("Category Sum of Evens", 3, 2): {0: 17863, 10: 64652, 14: 17485}, + ("Category Sum of Evens", 3, 3): {0: 7748, 12: 75072, 16: 17180}, + ("Category Sum of Evens", 3, 4): {0: 1318, 12: 70339, 16: 28343}, + ("Category Sum of Evens", 3, 5): {0: 7680, 12: 53582, 18: 38738}, + ("Category Sum of Evens", 3, 6): {0: 1475, 12: 50152, 18: 48373}, + ("Category Sum of Evens", 3, 7): {0: 14328, 18: 85672}, + ("Category Sum of Evens", 3, 8): {0: 10001, 18: 89999}, + ("Category Sum of Evens", 4, 1): {0: 6214, 8: 67940, 12: 25846}, + ("Category Sum of Evens", 4, 2): {0: 16230, 12: 55675, 16: 28095}, + ("Category Sum of Evens", 4, 3): {0: 11069, 16: 70703, 20: 18228}, + ("Category Sum of Evens", 4, 4): {0: 13339, 20: 86661}, + ("Category Sum of Evens", 4, 5): {0: 8193, 18: 66423, 22: 25384}, + ("Category Sum of Evens", 4, 6): {0: 11127, 18: 53742, 22: 35131}, + ("Category Sum of Evens", 4, 7): {0: 7585, 18: 48073, 24: 44342}, + ("Category Sum of Evens", 4, 8): {0: 642, 18: 46588, 24: 52770}, + ("Category Sum of Evens", 5, 1): {0: 8373, 8: 50641, 16: 40986}, + ("Category Sum of Evens", 5, 2): {0: 7271, 12: 42254, 20: 50475}, + ("Category Sum of Evens", 5, 3): {0: 8350, 16: 44711, 24: 46939}, + ("Category Sum of Evens", 5, 4): {0: 8161, 18: 44426, 26: 47413}, + ("Category Sum of Evens", 5, 5): {0: 350, 8: 16033, 24: 67192, 28: 16425}, + ("Category Sum of Evens", 5, 6): {0: 10318, 24: 64804, 28: 24878}, + ("Category Sum of Evens", 5, 7): {0: 12783, 24: 52804, 28: 34413}, + ("Category Sum of Evens", 5, 8): {0: 1, 24: 56646, 30: 43353}, + ("Category Sum of Evens", 6, 1): {0: 10482, 10: 48137, 18: 41381}, + ("Category Sum of Evens", 6, 2): {0: 12446, 16: 43676, 24: 43878}, + ("Category Sum of Evens", 6, 3): {0: 11037, 20: 44249, 28: 44714}, + ("Category Sum of Evens", 6, 4): {0: 10005, 22: 42316, 30: 47679}, + ("Category Sum of Evens", 6, 5): {0: 9751, 24: 42204, 32: 48045}, + ("Category Sum of Evens", 6, 6): {0: 9692, 26: 45108, 34: 45200}, + ("Category Sum of Evens", 6, 7): {4: 1437, 26: 42351, 34: 56212}, + ("Category Sum of Evens", 6, 8): {4: 13017, 30: 51814, 36: 35169}, + ("Category Sum of Evens", 7, 1): {0: 12688, 12: 45275, 20: 42037}, + ("Category Sum of Evens", 7, 2): {0: 1433, 20: 60350, 28: 38217}, + ("Category Sum of Evens", 7, 3): {0: 13724, 24: 43514, 32: 42762}, + ("Category Sum of Evens", 7, 4): {0: 11285, 26: 40694, 34: 48021}, + ("Category Sum of Evens", 7, 5): {4: 5699, 28: 43740, 36: 50561}, + ("Category Sum of Evens", 7, 6): {4: 5478, 30: 43711, 38: 50811}, + ("Category Sum of Evens", 7, 7): {6: 9399, 32: 43251, 40: 47350}, + ("Category Sum of Evens", 7, 8): {10: 1490, 32: 40719, 40: 57791}, + ("Category Sum of Evens", 8, 1): {0: 14585, 14: 42804, 22: 42611}, + ("Category Sum of Evens", 8, 2): {0: 15891, 22: 39707, 30: 44402}, + ("Category Sum of Evens", 8, 3): {2: 297, 12: 16199, 28: 42274, 36: 41230}, + ("Category Sum of Evens", 8, 4): {0: 7625, 30: 43948, 38: 48427}, + ("Category Sum of Evens", 8, 5): {4: 413, 18: 16209, 34: 43301, 42: 40077}, + ("Category Sum of Evens", 8, 6): {6: 14927, 36: 43139, 44: 41934}, + ("Category Sum of Evens", 8, 7): {8: 5042, 36: 40440, 44: 54518}, + ("Category Sum of Evens", 8, 8): {10: 5005, 38: 44269, 46: 50726}, + ("Category Double Threes and Fours", 1, 1): {0: 66749, 8: 33251}, + ("Category Double Threes and Fours", 1, 2): {0: 44675, 8: 55325}, + ("Category Double Threes and Fours", 1, 3): {0: 29592, 8: 70408}, + ("Category Double Threes and Fours", 1, 4): {0: 24601, 8: 75399}, + ("Category Double Threes and Fours", 1, 5): {0: 20499, 8: 79501}, + ("Category Double Threes and Fours", 1, 6): {0: 17116, 8: 82884}, + ("Category Double Threes and Fours", 1, 7): {0: 14193, 8: 85807}, + ("Category Double Threes and Fours", 1, 8): {0: 11977, 8: 88023}, + ("Category Double Threes and Fours", 2, 1): {0: 44382, 8: 55618}, + ("Category Double Threes and Fours", 2, 2): {0: 19720, 8: 57236, 14: 23044}, + ("Category Double Threes and Fours", 2, 3): {0: 8765, 8: 41937, 14: 49298}, + ("Category Double Threes and Fours", 2, 4): {0: 6164, 16: 93836}, + ("Category Double Threes and Fours", 2, 5): {0: 4307, 8: 38682, 16: 57011}, + ("Category Double Threes and Fours", 2, 6): {0: 2879, 8: 32717, 16: 64404}, + ("Category Double Threes and Fours", 2, 7): {0: 6679, 16: 93321}, + ("Category Double Threes and Fours", 2, 8): {0: 4758, 16: 95242}, + ("Category Double Threes and Fours", 3, 1): {0: 29378, 8: 50024, 14: 20598}, + ("Category Double Threes and Fours", 3, 2): {0: 8894, 14: 74049, 18: 17057}, + ("Category Double Threes and Fours", 3, 3): {0: 2643, 14: 62555, 22: 34802}, + ("Category Double Threes and Fours", 3, 4): {0: 1523, 6: 19996, 16: 50281, 22: 28200}, + ("Category Double Threes and Fours", 3, 5): {0: 845, 16: 60496, 24: 38659}, + ("Category Double Threes and Fours", 3, 6): {0: 499, 16: 51131, 24: 48370}, + ("Category Double Threes and Fours", 3, 7): {0: 5542, 16: 37755, 24: 56703}, + ("Category Double Threes and Fours", 3, 8): {0: 3805, 16: 32611, 24: 63584}, + ("Category Double Threes and Fours", 4, 1): {0: 19809, 8: 39303, 16: 40888}, + ("Category Double Threes and Fours", 4, 2): {0: 3972, 16: 71506, 22: 24522}, + ("Category Double Threes and Fours", 4, 3): {0: 745, 18: 53727, 22: 28503, 28: 17025}, + ("Category Double Threes and Fours", 4, 4): {0: 4862, 16: 34879, 22: 33529, 28: 26730}, + ("Category Double Threes and Fours", 4, 5): {0: 2891, 16: 25367, 24: 46333, 30: 25409}, + ("Category Double Threes and Fours", 4, 6): {0: 2525, 24: 62353, 30: 35122}, + ("Category Double Threes and Fours", 4, 7): {0: 1042, 24: 54543, 32: 44415}, + ("Category Double Threes and Fours", 4, 8): {0: 2510, 24: 44681, 32: 52809}, + ("Category Double Threes and Fours", 5, 1): {0: 13122, 14: 68022, 20: 18856}, + ("Category Double Threes and Fours", 5, 2): {0: 1676, 14: 37791, 22: 40810, 28: 19723}, + ("Category Double Threes and Fours", 5, 3): {0: 2945, 16: 28193, 22: 26795, 32: 42067}, + ("Category Double Threes and Fours", 5, 4): {0: 2807, 26: 53419, 30: 26733, 36: 17041}, + ("Category Double Threes and Fours", 5, 5): {0: 3651, 24: 38726, 32: 41484, 38: 16139}, + ("Category Double Threes and Fours", 5, 6): {0: 362, 12: 13070, 32: 61608, 38: 24960}, + ("Category Double Threes and Fours", 5, 7): {0: 161, 12: 15894, 32: 49464, 38: 34481}, + ("Category Double Threes and Fours", 5, 8): {0: 82, 12: 11438, 32: 45426, 40: 43054}, + ("Category Double Threes and Fours", 6, 1): {0: 8738, 6: 26451, 16: 43879, 22: 20932}, + ("Category Double Threes and Fours", 6, 2): {0: 784, 16: 38661, 28: 42164, 32: 18391}, + ("Category Double Threes and Fours", 6, 3): {0: 1062, 22: 34053, 28: 27996, 38: 36889}, + ("Category Double Threes and Fours", 6, 4): {0: 439, 12: 13100, 30: 43296, 40: 43165}, + ("Category Double Threes and Fours", 6, 5): {0: 3957, 34: 51190, 38: 26734, 44: 18119}, + ("Category Double Threes and Fours", 6, 6): {0: 4226, 32: 37492, 40: 40719, 46: 17563}, + ("Category Double Threes and Fours", 6, 7): {0: 31, 12: 13933, 40: 60102, 46: 25934}, + ("Category Double Threes and Fours", 6, 8): {8: 388, 22: 16287, 40: 48255, 48: 35070}, + ("Category Double Threes and Fours", 7, 1): {0: 5803, 8: 28280, 14: 26186, 26: 39731}, + ("Category Double Threes and Fours", 7, 2): {0: 3319, 20: 36331, 30: 38564, 36: 21786}, + ("Category Double Threes and Fours", 7, 3): {0: 2666, 18: 16444, 34: 41412, 44: 39478}, + ("Category Double Threes and Fours", 7, 4): {0: 99, 12: 9496, 38: 50302, 46: 40103}, + ("Category Double Threes and Fours", 7, 5): {0: 45, 12: 13200, 42: 52460, 50: 34295}, + ("Category Double Threes and Fours", 7, 6): {8: 2400, 28: 16653, 46: 60564, 52: 20383}, + ("Category Double Threes and Fours", 7, 7): {6: 7, 12: 11561, 44: 44119, 54: 44313}, + ("Category Double Threes and Fours", 7, 8): {8: 4625, 44: 40601, 48: 26475, 54: 28299}, + ("Category Double Threes and Fours", 8, 1): {0: 3982, 16: 56447, 28: 39571}, + ("Category Double Threes and Fours", 8, 2): {0: 1645, 20: 25350, 30: 37385, 42: 35620}, + ("Category Double Threes and Fours", 8, 3): {0: 6, 26: 23380, 40: 40181, 50: 36433}, + ("Category Double Threes and Fours", 8, 4): {0: 541, 20: 16547, 42: 38406, 52: 44506}, + ("Category Double Threes and Fours", 8, 5): {6: 2956, 30: 16449, 46: 43983, 56: 36612}, + ("Category Double Threes and Fours", 8, 6): {0: 2, 12: 7360, 38: 19332, 54: 53627, 58: 19679}, + ("Category Double Threes and Fours", 8, 7): {6: 9699, 48: 38611, 54: 28390, 60: 23300}, + ("Category Double Threes and Fours", 8, 8): {8: 5, 20: 10535, 52: 41790, 62: 47670}, + ("Category Quadruple Ones and Twos", 1, 1): {0: 66567, 8: 33433}, + ("Category Quadruple Ones and Twos", 1, 2): {0: 44809, 8: 55191}, + ("Category Quadruple Ones and Twos", 1, 3): {0: 37100, 8: 62900}, + ("Category Quadruple Ones and Twos", 1, 4): {0: 30963, 8: 69037}, + ("Category Quadruple Ones and Twos", 1, 5): {0: 25316, 8: 74684}, + ("Category Quadruple Ones and Twos", 1, 6): {0: 21505, 8: 78495}, + ("Category Quadruple Ones and Twos", 1, 7): {0: 17676, 8: 82324}, + ("Category Quadruple Ones and Twos", 1, 8): {0: 14971, 8: 85029}, + ("Category Quadruple Ones and Twos", 2, 1): {0: 44566, 8: 55434}, + ("Category Quadruple Ones and Twos", 2, 2): {0: 19963, 8: 57152, 12: 22885}, + ("Category Quadruple Ones and Twos", 2, 3): {0: 13766, 8: 52065, 16: 34169}, + ("Category Quadruple Ones and Twos", 2, 4): {0: 9543, 8: 46446, 16: 44011}, + ("Category Quadruple Ones and Twos", 2, 5): {0: 6472, 8: 40772, 16: 52756}, + ("Category Quadruple Ones and Twos", 2, 6): {0: 10306, 12: 46932, 16: 42762}, + ("Category Quadruple Ones and Twos", 2, 7): {0: 7120, 12: 42245, 16: 50635}, + ("Category Quadruple Ones and Twos", 2, 8): {0: 4989, 12: 37745, 16: 57266}, + ("Category Quadruple Ones and Twos", 3, 1): {0: 29440, 8: 50321, 16: 20239}, + ("Category Quadruple Ones and Twos", 3, 2): {0: 8857, 8: 42729, 16: 48414}, + ("Category Quadruple Ones and Twos", 3, 3): {0: 5063, 12: 53387, 20: 41550}, + ("Category Quadruple Ones and Twos", 3, 4): {0: 8395, 16: 64605, 24: 27000}, + ("Category Quadruple Ones and Twos", 3, 5): {0: 4895, 16: 58660, 24: 36445}, + ("Category Quadruple Ones and Twos", 3, 6): {0: 2681, 16: 52710, 24: 44609}, + ("Category Quadruple Ones and Twos", 3, 7): {0: 586, 16: 46781, 24: 52633}, + ("Category Quadruple Ones and Twos", 3, 8): {0: 941, 16: 39406, 24: 59653}, + ("Category Quadruple Ones and Twos", 4, 1): {0: 19691, 8: 46945, 16: 33364}, + ("Category Quadruple Ones and Twos", 4, 2): {0: 4023, 12: 50885, 24: 45092}, + ("Category Quadruple Ones and Twos", 4, 3): {0: 6553, 16: 52095, 28: 41352}, + ("Category Quadruple Ones and Twos", 4, 4): {0: 3221, 16: 41367, 24: 39881, 28: 15531}, + ("Category Quadruple Ones and Twos", 4, 5): {0: 1561, 20: 48731, 28: 49708}, + ("Category Quadruple Ones and Twos", 4, 6): {0: 190, 20: 38723, 28: 42931, 32: 18156}, + ("Category Quadruple Ones and Twos", 4, 7): {0: 5419, 24: 53017, 32: 41564}, + ("Category Quadruple Ones and Twos", 4, 8): {0: 3135, 24: 47352, 32: 49513}, + ("Category Quadruple Ones and Twos", 5, 1): {0: 13112, 8: 41252, 20: 45636}, + ("Category Quadruple Ones and Twos", 5, 2): {0: 7293, 16: 50711, 28: 41996}, + ("Category Quadruple Ones and Twos", 5, 3): {0: 719, 20: 55921, 32: 43360}, + ("Category Quadruple Ones and Twos", 5, 4): {0: 1152, 20: 38570, 32: 60278}, + ("Category Quadruple Ones and Twos", 5, 5): {0: 5647, 24: 40910, 36: 53443}, + ("Category Quadruple Ones and Twos", 5, 6): {0: 194, 28: 51527, 40: 48279}, + ("Category Quadruple Ones and Twos", 5, 7): {0: 1449, 28: 39301, 36: 41332, 40: 17918}, + ("Category Quadruple Ones and Twos", 5, 8): {0: 6781, 32: 52834, 40: 40385}, + ("Category Quadruple Ones and Twos", 6, 1): {0: 8646, 12: 53753, 24: 37601}, + ("Category Quadruple Ones and Twos", 6, 2): {0: 844, 16: 40583, 28: 58573}, + ("Category Quadruple Ones and Twos", 6, 3): {0: 1241, 24: 54870, 36: 43889}, + ("Category Quadruple Ones and Twos", 6, 4): {0: 1745, 28: 53286, 40: 44969}, + ("Category Quadruple Ones and Twos", 6, 5): {0: 2076, 32: 56909, 44: 41015}, + ("Category Quadruple Ones and Twos", 6, 6): {0: 6827, 32: 39400, 44: 53773}, + ("Category Quadruple Ones and Twos", 6, 7): {0: 1386, 36: 49865, 48: 48749}, + ("Category Quadruple Ones and Twos", 6, 8): {0: 1841, 36: 38680, 44: 40600, 48: 18879}, + ("Category Quadruple Ones and Twos", 7, 1): {0: 5780, 12: 46454, 24: 47766}, + ("Category Quadruple Ones and Twos", 7, 2): {0: 6122, 20: 38600, 32: 55278}, + ("Category Quadruple Ones and Twos", 7, 3): {0: 2065, 28: 52735, 40: 45200}, + ("Category Quadruple Ones and Twos", 7, 4): {0: 1950, 32: 50270, 44: 47780}, + ("Category Quadruple Ones and Twos", 7, 5): {0: 2267, 36: 49235, 48: 48498}, + ("Category Quadruple Ones and Twos", 7, 6): {0: 2500, 40: 53934, 52: 43566}, + ("Category Quadruple Ones and Twos", 7, 7): {0: 6756, 44: 53730, 56: 39514}, + ("Category Quadruple Ones and Twos", 7, 8): {0: 3625, 44: 45159, 56: 51216}, + ("Category Quadruple Ones and Twos", 8, 1): {0: 11493, 16: 50043, 28: 38464}, + ("Category Quadruple Ones and Twos", 8, 2): {0: 136, 24: 47795, 36: 52069}, + ("Category Quadruple Ones and Twos", 8, 3): {0: 2744, 32: 51640, 48: 45616}, + ("Category Quadruple Ones and Twos", 8, 4): {0: 2293, 36: 45979, 48: 51728}, + ("Category Quadruple Ones and Twos", 8, 5): {0: 2181, 40: 44909, 52: 52910}, + ("Category Quadruple Ones and Twos", 8, 6): {4: 2266, 44: 44775, 56: 52959}, + ("Category Quadruple Ones and Twos", 8, 7): {8: 2344, 48: 50198, 60: 47458}, + ("Category Quadruple Ones and Twos", 8, 8): {8: 2808, 48: 37515, 56: 37775, 64: 21902}, ("Category Micro Straight", 1, 1): {0: 100000}, ("Category Micro Straight", 1, 2): {0: 100000}, ("Category Micro Straight", 1, 3): {0: 100000}, @@ -3527,7 +2329,7 @@ yacht_weights = { ("Category 4&5 Full House", 4, 6): {0: 100000}, ("Category 4&5 Full House", 4, 7): {0: 100000}, ("Category 4&5 Full House", 4, 8): {0: 100000}, - ("Category 4&5 Full House", 5, 1): {0: 99724, 50: 276}, + ("Category 4&5 Full House", 5, 1): {0: 100000}, ("Category 4&5 Full House", 5, 2): {0: 96607, 50: 3393}, ("Category 4&5 Full House", 5, 3): {0: 88788, 50: 11212}, ("Category 4&5 Full House", 5, 4): {0: 77799, 50: 22201}, @@ -3535,7 +2337,7 @@ yacht_weights = { ("Category 4&5 Full House", 5, 6): {0: 54548, 50: 45452}, ("Category 4&5 Full House", 5, 7): {0: 44898, 50: 55102}, ("Category 4&5 Full House", 5, 8): {0: 36881, 50: 63119}, - ("Category 4&5 Full House", 6, 1): {0: 98841, 50: 1159}, + ("Category 4&5 Full House", 6, 1): {0: 100000}, ("Category 4&5 Full House", 6, 2): {0: 88680, 50: 11320}, ("Category 4&5 Full House", 6, 3): {0: 70215, 50: 29785}, ("Category 4&5 Full House", 6, 4): {0: 50801, 50: 49199}, diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index c36c59544f..3a79eff040 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -56,7 +56,7 @@ class YachtDiceWorld(World): item_name_groups = item_groups - ap_world_version = "2.1.1" + ap_world_version = "2.1.2" def _get_yachtdice_data(self): return { @@ -190,7 +190,6 @@ class YachtDiceWorld(World): if self.frags_per_roll == 1: self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory else: - self.itempool.append("Roll") # always add a full roll to make generation easier (will be early) self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add) already_items = len(self.itempool) @@ -231,13 +230,10 @@ class YachtDiceWorld(World): weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll - extra_points_added = 0 - multipliers_added = 0 - items_added = 0 - - def get_item_to_add(weights, extra_points_added, multipliers_added, items_added): - items_added += 1 + extra_points_added = [0] # make it a mutible type so we can change the value in the function + step_score_multipliers_added = [0] + def get_item_to_add(weights, extra_points_added, step_score_multipliers_added): all_items = self.itempool + self.precollected dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment") if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice: @@ -246,21 +242,18 @@ class YachtDiceWorld(World): if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll: weights["Roll"] = 0 # don't allow >= 6 rolls - # Don't allow too many multipliers - if multipliers_added > 50: - weights["Fixed Score Multiplier"] = 0 - weights["Step Score Multiplier"] = 0 - # Don't allow too many extra points - if extra_points_added > 300: + if extra_points_added[0] > 400: weights["Points"] = 0 + if step_score_multipliers_added[0] > 10: + weights["Step Score Multiplier"] = 0 + # if all weights are zero, allow to add fixed score multiplier, double category, points. if sum(weights.values()) == 0: - if multipliers_added <= 50: - weights["Fixed Score Multiplier"] = 1 + weights["Fixed Score Multiplier"] = 1 weights["Double category"] = 1 - if extra_points_added <= 300: + if extra_points_added[0] <= 400: weights["Points"] = 1 # Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item @@ -274,11 +267,10 @@ class YachtDiceWorld(World): return "Roll" if self.frags_per_roll == 1 else "Roll Fragment" elif which_item_to_add == "Fixed Score Multiplier": weights["Fixed Score Multiplier"] /= 1.05 - multipliers_added += 1 return "Fixed Score Multiplier" elif which_item_to_add == "Step Score Multiplier": weights["Step Score Multiplier"] /= 1.1 - multipliers_added += 1 + step_score_multipliers_added[0] += 1 return "Step Score Multiplier" elif which_item_to_add == "Double category": # Below entries are the weights to add each category. @@ -303,15 +295,15 @@ class YachtDiceWorld(World): choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0] if choice == "1 Point": weights["Points"] /= 1.01 - extra_points_added += 1 + extra_points_added[0] += 1 return "1 Point" elif choice == "10 Points": weights["Points"] /= 1.1 - extra_points_added += 10 + extra_points_added[0] += 10 return "10 Points" elif choice == "100 Points": weights["Points"] /= 2 - extra_points_added += 100 + extra_points_added[0] += 100 return "100 Points" else: raise Exception("Unknown point value (Yacht Dice)") @@ -320,7 +312,7 @@ class YachtDiceWorld(World): # adding 17 items as a start seems like the smartest way to get close to 1000 points for _ in range(17): - self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added)) + self.itempool.append(get_item_to_add(weights, extra_points_added, step_score_multipliers_added)) score_in_logic = dice_simulation_fill_pool( self.itempool + self.precollected, @@ -348,7 +340,7 @@ class YachtDiceWorld(World): else: # Keep adding items until a score of 1000 is in logic while score_in_logic < 1000: - item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added) + item_to_add = get_item_to_add(weights, extra_points_added, step_score_multipliers_added) self.itempool.append(item_to_add) if item_to_add == "1 Point": score_in_logic += 1 From 430b71a092b9bff8aa8f6a61dd4266a14cd056ec Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 7 Sep 2024 17:03:04 -0500 Subject: [PATCH 202/222] Core: have webhost slot name links go through the launcher (#2779) * Core: have webhost slot name links go through the launcher so that components can use them * fix query handling, remove debug prints, and change mousover text for new behavior * remove a missed debug and unused function * filter room id to suuid since that's what everything else uses * pass args to common client correctly * add GUI to select which client to open * remove args parsing and "require" components to parse it themselves * support for messenger since it was basically already done * use "proper" args argparsing and clean up uri handling * use a timer and auto launch text client if no component is found * change the timer to be a bit more appealing. also found a bug lmao * don't hold 5 hostage and capitalize URI ig --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 4 +- Launcher.py | 98 +++++++++++++++++++++++++++++--- WebHostLib/templates/macros.html | 2 +- inno_setup.iss | 4 +- worlds/LauncherComponents.py | 15 +++-- worlds/messenger/__init__.py | 2 +- worlds/messenger/client_setup.py | 15 +++-- 7 files changed, 115 insertions(+), 25 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 750bee80bd..fe9df38dbd 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -994,7 +994,7 @@ def get_base_parser(description: typing.Optional[str] = None): return parser -def run_as_textclient(): +def run_as_textclient(*args): class TextContext(CommonContext): # Text Mode to use !hint and such with games that have no text entry tags = CommonContext.tags | {"TextOnly"} @@ -1033,7 +1033,7 @@ def run_as_textclient(): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args() + args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable if args.url: url = urllib.parse.urlparse(args.url) diff --git a/Launcher.py b/Launcher.py index 6b66b2a3a6..97903e2ad1 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,10 +16,11 @@ import multiprocessing import shlex import subprocess import sys +import urllib.parse import webbrowser from os.path import isfile from shutil import which -from typing import Callable, Sequence, Union, Optional +from typing import Callable, Optional, Sequence, Tuple, Union import Utils import settings @@ -107,7 +108,81 @@ components.extend([ ]) -def identify(path: Union[None, str]): +def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: + url = urllib.parse.urlparse(path) + queries = urllib.parse.parse_qs(url.query) + launch_args = (path, *launch_args) + client_component = None + text_client_component = None + if "game" in queries: + game = queries["game"][0] + else: # TODO around 0.6.0 - this is for pre this change webhost uri's + game = "Archipelago" + for component in components: + if component.supports_uri and component.game_name == game: + client_component = component + elif component.display_name == "Text Client": + text_client_component = component + + from kvui import App, Button, BoxLayout, Label, Clock, Window + + class Popup(App): + timer_label: Label + remaining_time: Optional[int] + + def __init__(self): + self.title = "Connect to Multiworld" + self.icon = r"data/icon.png" + super().__init__() + + def build(self): + layout = BoxLayout(orientation="vertical") + + if client_component is None: + self.remaining_time = 7 + label_text = (f"A game client able to parse URIs was not detected for {game}.\n" + f"Launching Text Client in 7 seconds...") + self.timer_label = Label(text=label_text) + layout.add_widget(self.timer_label) + Clock.schedule_interval(self.update_label, 1) + else: + layout.add_widget(Label(text="Select client to open and connect with.")) + button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4)) + + text_client_button = Button( + text=text_client_component.display_name, + on_release=lambda *args: run_component(text_client_component, *launch_args) + ) + button_row.add_widget(text_client_button) + + game_client_button = Button( + text=client_component.display_name, + on_release=lambda *args: run_component(client_component, *launch_args) + ) + button_row.add_widget(game_client_button) + + layout.add_widget(button_row) + + return layout + + def update_label(self, dt): + if self.remaining_time > 1: + # countdown the timer and string replace the number + self.remaining_time -= 1 + self.timer_label.text = self.timer_label.text.replace( + str(self.remaining_time + 1), str(self.remaining_time) + ) + else: + # our timer is finished so launch text client and close down + run_component(text_client_component, *launch_args) + Clock.unschedule(self.update_label) + App.get_running_app().stop() + Window.close() + + Popup().run() + + +def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]: if path is None: return None, None for component in components: @@ -299,20 +374,24 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): elif not args: args = {} - if args.get("Patch|Game|Component", None) is not None: - file, component = identify(args["Patch|Game|Component"]) + path = args.get("Patch|Game|Component|url", None) + if path is not None: + if path.startswith("archipelago://"): + handle_uri(path, args.get("args", ())) + return + file, component = identify(path) if file: args['file'] = file if component: args['component'] = component if not component: - logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}") + logging.warning(f"Could not identify Component responsible for {path}") if args["update_settings"]: update_settings() - if 'file' in args: + if "file" in args: run_component(args["component"], args["file"], *args["args"]) - elif 'component' in args: + elif "component" in args: run_component(args["component"], *args["args"]) elif not args["update_settings"]: run_gui() @@ -326,8 +405,9 @@ if __name__ == '__main__': run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") - run_group.add_argument("Patch|Game|Component", type=str, nargs="?", - help="Pass either a patch file, a generated game or the name of a component to run.") + run_group.add_argument("Patch|Game|Component|url", type=str, nargs="?", + help="Pass either a patch file, a generated game, the component name to run, or a url to " + "connect with.") run_group.add_argument("args", nargs="*", help="Arguments to pass to component.") main(parser.parse_args()) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 7bbb894de0..6b2a4b0ed7 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -22,7 +22,7 @@ {% for patch in room.seed.slots|list|sort(attribute="player_id") %} {{ patch.player_id }} -
{{ patch.player_name }} + {{ patch.player_name }} {{ patch.game }} {% if patch.data %} diff --git a/inno_setup.iss b/inno_setup.iss index 3bb76fc40a..38e655d917 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -228,8 +228,8 @@ Root: HKCR; Subkey: "{#MyAppName}worlddata\shell\open\command"; ValueData: """{a Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueData: "Archipegalo Protocol"; Flags: uninsdeletekey; Root: HKCR; Subkey: "archipelago"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: ""; -Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoTextClient.exe,0"; -Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoTextClient.exe"" ""%1"""; +Root: HKCR; Subkey: "archipelago\DefaultIcon"; ValueType: "string"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; +Root: HKCR; Subkey: "archipelago\shell\open\command"; ValueType: "string"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; [Code] // See: https://stackoverflow.com/a/51614652/2287576 diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index d127bbea36..4c64642aba 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -26,10 +26,13 @@ class Component: cli: bool func: Optional[Callable] file_identifier: Optional[Callable[[str], bool]] + game_name: Optional[str] + supports_uri: Optional[bool] def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None, cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None, - func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None): + func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None, + game_name: Optional[str] = None, supports_uri: Optional[bool] = False): self.display_name = display_name self.script_name = script_name self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None @@ -45,6 +48,8 @@ class Component: Type.ADJUSTER if "Adjuster" in display_name else Type.MISC) self.func = func self.file_identifier = file_identifier + self.game_name = game_name + self.supports_uri = supports_uri def handles_file(self, path: str): return self.file_identifier(path) if self.file_identifier else False @@ -56,10 +61,10 @@ class Component: processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()): global processes import multiprocessing - process = multiprocessing.Process(target=func, name=name) + process = multiprocessing.Process(target=func, name=name, args=args) process.start() processes.add(process) @@ -78,9 +83,9 @@ class SuffixIdentifier: return False -def launch_textclient(): +def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, name="TextClient") + launch_subprocess(CommonClient.run_as_textclient, "TextClient", args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index a03c33c2f7..1bca3a37ad 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -19,7 +19,7 @@ from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shu from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation components.append( - Component("The Messenger", component_type=Type.CLIENT, func=launch_game)#, game_name="The Messenger", supports_uri=True) + Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True) ) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 9fd08e52d8..6bff78df36 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -1,3 +1,4 @@ +import argparse import io import logging import os.path @@ -17,7 +18,7 @@ from Utils import is_windows, messagebox, tuplize_version MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" -def launch_game(url: Optional[str] = None) -> None: +def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" @@ -150,15 +151,19 @@ def launch_game(url: Optional[str] = None) -> None: install_mod() elif should_update is None: return + + parser = argparse.ArgumentParser(description="Messenger Client Launcher") + parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") + args = parser.parse_args(args) if not is_windows: - if url: - open_file(f"steam://rungameid/764790//{url}/") + if args.url: + open_file(f"steam://rungameid/764790//{args.url}/") else: open_file("steam://rungameid/764790") else: os.chdir(game_folder) - if url: - subprocess.Popen([MessengerWorld.settings.game_path, str(url)]) + if args.url: + subprocess.Popen([MessengerWorld.settings.game_path, str(args.url)]) else: subprocess.Popen(MessengerWorld.settings.game_path) os.chdir(working_directory) From b8c2e14e8b0b1f7837b7cefc1aaeb94ce87bf93f Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 07:17:20 -0500 Subject: [PATCH 203/222] CommonClient: allow worlds to change title of run_gui without rewriting it (#3297) * moves the title name in CommonContext.run_gui into a parameter defaulted to the normal default so others using it don't have to rewrite everything * Change to using a GameManager attribute instead of a default param * Update CommonClient.py treble suggestion 1 Co-authored-by: Aaron Wagener * Update CommonClient.py treble suggestion 2 Co-authored-by: Aaron Wagener * Update CommonClient.py treble suggestion 3 Co-authored-by: Doug Hoskisson * Use make_gui() instead of a property to push kivy importing back to lazy loading regardless of gui_enabled status * cleanup * almost forgot to type it * change make_gui to be a class so clients can subclass it * clean up code readability --------- Co-authored-by: Aaron Wagener Co-authored-by: Doug Hoskisson Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index fe9df38dbd..7f91172acf 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,17 +662,19 @@ class CommonContext: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def run_gui(self): - """Import kivy UI system and start running it as self.ui_task.""" + def make_gui(self) -> type: + """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager class TextManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] base_title = "Archipelago Text Client" - self.ui = TextManager(self) + return TextManager + + def run_gui(self): + """Import kivy UI system from make_gui() and start running it as self.ui_task.""" + ui_class = self.make_gui() + self.ui = ui_class(self) self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") def run_cli(self): From 5348f693fe9edd4756b91969a0ac66f5877fc4be Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Sep 2024 05:19:37 -0700 Subject: [PATCH 204/222] Pokemon Emerald: Use some new state functions, improve rule reuse (#3383) * Pokemon Emerald: Use some new state functions, improve rule reuse * Pokemon Emerald: Remove a couple more extra lambdas * Pokemon Emerald: Swap some rules to use exclusive groups/lists * Pokemon Emerald: Linting We're not gonna keep both me and the linter happy here, but this at least gets things more consistent * Pokemon Emerald: Update _exclusive to _unique --- worlds/pokemon_emerald/rules.py | 159 ++++++++++++++++---------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index 5b2aaa1ffc..5f83686ebe 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -19,20 +19,20 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: hm_rules: Dict[str, Callable[[CollectionState], bool]] = {} for hm, badges in world.hm_requirements.items(): if isinstance(badges, list): - hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \ - and state.has_all(badges, world.player) + hm_rules[hm] = lambda state, hm=hm, badges=badges: \ + state.has(hm, world.player) and state.has_all(badges, world.player) else: - hm_rules[hm] = lambda state, hm=hm, badges=badges: state.has(hm, world.player) \ - and state.has_group("Badges", world.player, badges) + hm_rules[hm] = lambda state, hm=hm, badges=badges: \ + state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges) def has_acro_bike(state: CollectionState): return state.has("Acro Bike", world.player) def has_mach_bike(state: CollectionState): return state.has("Mach Bike", world.player) - + def defeated_n_gym_leaders(state: CollectionState, n: int) -> bool: - return sum([state.has(event, world.player) for event in [ + return state.has_from_list_unique([ "EVENT_DEFEAT_ROXANNE", "EVENT_DEFEAT_BRAWLY", "EVENT_DEFEAT_WATTSON", @@ -41,7 +41,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: "EVENT_DEFEAT_WINONA", "EVENT_DEFEAT_TATE_AND_LIZA", "EVENT_DEFEAT_JUAN", - ]]) >= n + ], world.player, n) huntable_legendary_events = [ f"EVENT_ENCOUNTER_{key}" @@ -61,8 +61,9 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: }.items() if name in world.options.allowed_legendary_hunt_encounters.value ] + def encountered_n_legendaries(state: CollectionState, n: int) -> bool: - return sum(int(state.has(event, world.player)) for event in huntable_legendary_events) >= n + return state.has_from_list_unique(huntable_legendary_events, world.player, n) def get_entrance(entrance: str): return world.multiworld.get_entrance(entrance, world.player) @@ -235,11 +236,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: if world.options.norman_requirement == NormanRequirement.option_badges: set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"), - lambda state: state.has_group("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) ) set_rule( get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"), - lambda state: state.has_group("Badges", world.player, world.options.norman_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value) ) else: set_rule( @@ -299,15 +300,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE116/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_116_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_116_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE116/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_116_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_116_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Rusturf Tunnel @@ -347,19 +348,19 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_ROUTE115/NORTH_ABOVE_SLOPE"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_ROUTE115/NORTH_BELOW_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_115_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_115_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE115/NORTH_ABOVE_SLOPE -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_115_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_115_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) if world.options.extra_boulders: @@ -375,7 +376,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: if world.options.extra_bumpy_slope: set_rule( get_entrance("REGION_ROUTE115/SOUTH_BELOW_LEDGE -> REGION_ROUTE115/SOUTH_ABOVE_LEDGE"), - lambda state: has_acro_bike(state) + has_acro_bike ) else: set_rule( @@ -386,17 +387,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: # Route 105 set_rule( get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_105_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_105_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE105/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_105_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_105_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("MAP_ROUTE105:0/MAP_ISLAND_CAVE:0"), @@ -439,7 +440,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_GRANITE_CAVE_B1F/LOWER -> REGION_GRANITE_CAVE_B1F/UPPER"), - lambda state: has_mach_bike(state) + has_mach_bike ) # Route 107 @@ -643,15 +644,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE114/ABOVE_WATERFALL -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_114_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_114_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE114/MAIN -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_114_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_114_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Meteor Falls @@ -699,11 +700,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: # Jagged Pass set_rule( get_entrance("REGION_JAGGED_PASS/BOTTOM -> REGION_JAGGED_PASS/MIDDLE"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_JAGGED_PASS/MIDDLE -> REGION_JAGGED_PASS/TOP"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("MAP_JAGGED_PASS:4/MAP_MAGMA_HIDEOUT_1F:0"), @@ -719,11 +720,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: # Mirage Tower set_rule( get_entrance("REGION_MIRAGE_TOWER_2F/TOP -> REGION_MIRAGE_TOWER_2F/BOTTOM"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_MIRAGE_TOWER_2F/BOTTOM -> REGION_MIRAGE_TOWER_2F/TOP"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_MIRAGE_TOWER_3F/TOP -> REGION_MIRAGE_TOWER_3F/BOTTOM"), @@ -812,15 +813,15 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE118/EAST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_118_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_118_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_ROUTE118/WEST -> REGION_TERRA_CAVE_ENTRANCE/MAIN"), - lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("TERRA_CAVE_ROUTE_118_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("TERRA_CAVE_ROUTE_118_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Route 119 @@ -830,11 +831,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE119/LOWER -> REGION_ROUTE119/LOWER_ACROSS_RAILS"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_ROUTE119/LOWER_ACROSS_RAILS -> REGION_ROUTE119/LOWER"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_ROUTE119/UPPER -> REGION_ROUTE119/MIDDLE_RIVER"), @@ -850,7 +851,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_ROUTE119/ABOVE_WATERFALL -> REGION_ROUTE119/ABOVE_WATERFALL_ACROSS_RAILS"), - lambda state: has_acro_bike(state) + has_acro_bike ) if "Route 119 Aqua Grunts" not in world.options.remove_roadblocks.value: set_rule( @@ -927,11 +928,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTH/MAIN -> REGION_SAFARI_ZONE_NORTH/MAIN"), - lambda state: has_acro_bike(state) + has_acro_bike ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_NORTHWEST/MAIN"), - lambda state: has_mach_bike(state) + has_mach_bike ) set_rule( get_entrance("REGION_SAFARI_ZONE_SOUTHWEST/MAIN -> REGION_SAFARI_ZONE_SOUTHWEST/POND"), @@ -1115,17 +1116,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: # Route 125 set_rule( get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_125_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_125_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE125/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_125_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_125_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Shoal Cave @@ -1257,17 +1258,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_127_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_127_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE127/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_127_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_127_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Route 128 @@ -1374,17 +1375,17 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: # Route 129 set_rule( get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_1 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_129_1", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_129_1", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) set_rule( get_entrance("REGION_UNDERWATER_ROUTE129/MARINE_CAVE_ENTRANCE_2 -> REGION_UNDERWATER_MARINE_CAVE/MAIN"), - lambda state: hm_rules["HM08 Dive"](state) and \ - state.has("EVENT_DEFEAT_CHAMPION", world.player) and \ - state.has("MARINE_CAVE_ROUTE_129_2", world.player) and \ - state.has("EVENT_DEFEAT_SHELLY", world.player) + lambda state: hm_rules["HM08 Dive"](state) + and state.has("EVENT_DEFEAT_CHAMPION", world.player) + and state.has("MARINE_CAVE_ROUTE_129_2", world.player) + and state.has("EVENT_DEFEAT_SHELLY", world.player) ) # Pacifidlog Town @@ -1505,7 +1506,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None: if world.options.elite_four_requirement == EliteFourRequirement.option_badges: set_rule( get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"), - lambda state: state.has_group("Badges", world.player, world.options.elite_four_count.value) + lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value) ) else: set_rule( From a6521084723c2b9702961d7cee97dcef96165918 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:21:26 -0400 Subject: [PATCH 205/222] Docs: Update Trap classification comment #3485 --- BaseClasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 715732589b..b40b872f0c 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1207,7 +1207,7 @@ class ItemClassification(IntFlag): filler = 0b0000 # aka trash, as in filler items like ammo, currency etc, progression = 0b0001 # Item that is logically relevant useful = 0b0010 # Item that is generally quite useful, but not required for anything logical - trap = 0b0100 # detrimental or entirely useless (nothing) item + trap = 0b0100 # detrimental item skip_balancing = 0b1000 # should technically never occur on its own # Item that is logically relevant, but progression balancing should not touch. # Typically currency or other counted items. From dad228cd4a760d2d49706d4026791bc3d0e4f377 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 8 Sep 2024 08:42:59 -0400 Subject: [PATCH 206/222] TUNIC: Logic Rules Redux (#3544) * 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 * Create new options * Slightly revise ls rule * Update options.py * Update options.py * Add tedious option for ls * Update laurels zips description * Create new options * Slightly revise ls rule * Update options.py * Update options.py * Add tedious option for ls * Update laurels zips description * Creating structures to redo ladder storage rules * Put together overworld ladder groups, remove tedious * Write up the rules for the regular rules * Update slot data and UT stuff * Put new ice grapple stuff in er rules * Ice grapple hard to get to fountain cross room * More ladder data * Wrote majority of overworld ladder rules * Finish the ladder storage rules * Update notes * Add note * Add well rail to the rules * More rules * Comment out logically irrelevant entrances * Update with laurels_zip helper * Add parameter to has_ice_grapple_logic for difficulty * Add new parameter to has_ice_grapple_logic * Move ice grapple chest to lower forest in ER/ladders * Fix rule * Finishing out hooking the new rules into the code * Fix bugs * Add more hard ice grapples * Fix more bugs * Shops my beloved * Change victory condition back * Remove debug stuff * Update plando connections description * Fix extremely rare bug * Add well front -> back hard ladder storages * Note in ls rules about knocking yourself down with bombs being out of logic * Add atoll fuse with wand + hard ls * Add some nonsense that boils down to activating the fuse in overworld * Further update LS description * Fix missing logic on bridge switch chest in upper zig * Revise upper zig rule change to account for ER * Fix merge conflict * Fix formatting, fix rule for heir access after merge * Add the shop sword logic stuff in * Remove todo that was already done * Fill out a to-do with some cursed nonsense * Fix event in wrong region * Fix missing cathedral -> elevator connection * Fix missing cathedral -> elevator connection * Add ER exception to cathedral -> elevator * Fix secret gathering place issue * Fix incorrect ls rule * Move 3 locations to Quarry Back since they're easily accessible from the back * Also update non-er region * Remove redundant parentheses * Add new test for a weird edge case in ER * Slight option description updates * Use has_ladder in spots where it wasn't used for some reason, add a comment * Fix unit test for ER * Update per exempt's suggestion * Add back LogicRules as an invisible option, to not break old yamls * Remove unused elevation from portal class * Update ladder storage without items description * Remove shop_scene stuff since it's no longer relevant in the mod by the time this version comes out * Remove shop scene stuff from game info since it's no longer relevant in the mod by the time this comes out * Update portal list to match main * god I love github merging things * Remove note * Add ice grapple hard path from upper overworld to temple rafters entrance * Actually that should be medium * Remove outdated note * Add ice grapple hard for swamp mid to the ledge * Add missing laurels zip in swamp * Some fixes to the ladder storage data while reviewing it * Add unit test for weird edge case * Backport outlet region system to fix ls bug * Fix incorrect ls, add todo * Add missing swamp ladder storage connections * Add swamp zip to er data * Add swamp zip to er rules * Add hard ice grapple for forest grave path main to upper * Add ice grapple logic for all bomb walls except the east quarry one * Add ice grapple logic for frog stairs eye to mouth without the ladder * Add hard ice grapple for overworld to the stairs to west garden * Add the ice grapple boss quick kills to medium ice grappling * Add the reverse connection for the ice grapple kill on Garden Knight * Add atoll house ice grapple push, and add west garden ice grapple entry to the regular rules --- worlds/tunic/__init__.py | 55 +- worlds/tunic/docs/en_TUNIC.md | 2 - worlds/tunic/er_data.py | 337 ++++++++----- worlds/tunic/er_rules.py | 743 ++++++++++++---------------- worlds/tunic/er_scripts.py | 114 +++-- worlds/tunic/items.py | 2 + worlds/tunic/ladder_storage_data.py | 186 +++++++ worlds/tunic/locations.py | 6 +- worlds/tunic/options.py | 124 +++-- worlds/tunic/regions.py | 3 +- worlds/tunic/rules.py | 114 +++-- worlds/tunic/test/test_access.py | 54 ++ 12 files changed, 1042 insertions(+), 698 deletions(-) create mode 100644 worlds/tunic/ladder_storage_data.py diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index bbffd9c144..cdd968acce 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -7,8 +7,9 @@ from .rules import set_location_rules, set_region_rules, randomize_ability_unloc from .er_rules import set_er_location_rules from .regions import tunic_regions from .er_scripts import create_er_regions -from .er_data import portal_mapping -from .options import TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections +from .er_data import portal_mapping, RegionInfo, tunic_er_regions +from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, + LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage) from worlds.AutoWorld import WebWorld, World from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP @@ -48,10 +49,12 @@ class TunicLocation(Location): class SeedGroup(TypedDict): - logic_rules: int # logic rules value + laurels_zips: bool # laurels_zips value + ice_grappling: int # ice_grappling value + ladder_storage: int # ls value laurels_at_10_fairies: bool # laurels location value fixed_shop: bool # fixed shop value - plando: TunicPlandoConnections # consolidated of plando connections for the seed group + plando: TunicPlandoConnections # consolidated plando connections for the seed group class TunicWorld(World): @@ -77,8 +80,17 @@ class TunicWorld(World): tunic_portal_pairs: Dict[str, str] er_portal_hints: Dict[int, str] seed_groups: Dict[str, SeedGroup] = {} + shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected + er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work def generate_early(self) -> None: + if self.options.logic_rules >= LogicRules.option_no_major_glitches: + self.options.laurels_zips.value = LaurelsZips.option_true + self.options.ice_grappling.value = IceGrappling.option_medium + if self.options.logic_rules.value == LogicRules.option_unrestricted: + self.options.ladder_storage.value = LadderStorage.option_medium + + self.er_regions = tunic_er_regions.copy() if self.options.plando_connections: for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later @@ -99,7 +111,10 @@ class TunicWorld(World): self.options.keys_behind_bosses.value = passthrough["keys_behind_bosses"] self.options.sword_progression.value = passthrough["sword_progression"] self.options.ability_shuffling.value = passthrough["ability_shuffling"] - self.options.logic_rules.value = passthrough["logic_rules"] + self.options.laurels_zips.value = passthrough["laurels_zips"] + self.options.ice_grappling.value = passthrough["ice_grappling"] + self.options.ladder_storage.value = passthrough["ladder_storage"] + self.options.ladder_storage_without_items = passthrough["ladder_storage_without_items"] self.options.lanternless.value = passthrough["lanternless"] self.options.maskless.value = passthrough["maskless"] self.options.hexagon_quest.value = passthrough["hexagon_quest"] @@ -118,19 +133,28 @@ class TunicWorld(World): group = tunic.options.entrance_rando.value # if this is the first world in the group, set the rules equal to its rules if group not in cls.seed_groups: - cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value, - laurels_at_10_fairies=tunic.options.laurels_location == 3, - fixed_shop=bool(tunic.options.fixed_shop), - plando=tunic.options.plando_connections) + cls.seed_groups[group] = \ + SeedGroup(laurels_zips=bool(tunic.options.laurels_zips), + ice_grappling=tunic.options.ice_grappling.value, + ladder_storage=tunic.options.ladder_storage.value, + laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, + fixed_shop=bool(tunic.options.fixed_shop), + plando=tunic.options.plando_connections) continue - + + # off is more restrictive + if not tunic.options.laurels_zips: + cls.seed_groups[group]["laurels_zips"] = False # lower value is more restrictive - if tunic.options.logic_rules.value < cls.seed_groups[group]["logic_rules"]: - cls.seed_groups[group]["logic_rules"] = tunic.options.logic_rules.value + if tunic.options.ice_grappling < cls.seed_groups[group]["ice_grappling"]: + cls.seed_groups[group]["ice_grappling"] = tunic.options.ice_grappling.value + # lower value is more restrictive + if tunic.options.ladder_storage.value < cls.seed_groups[group]["ladder_storage"]: + cls.seed_groups[group]["ladder_storage"] = tunic.options.ladder_storage.value # laurels at 10 fairies changes logic for secret gathering place placement if tunic.options.laurels_location == 3: cls.seed_groups[group]["laurels_at_10_fairies"] = True - # fewer shops, one at windmill + # more restrictive, overrides the option for others in the same group, which is better than failing imo if tunic.options.fixed_shop: cls.seed_groups[group]["fixed_shop"] = True @@ -366,7 +390,10 @@ class TunicWorld(World): "ability_shuffling": self.options.ability_shuffling.value, "hexagon_quest": self.options.hexagon_quest.value, "fool_traps": self.options.fool_traps.value, - "logic_rules": self.options.logic_rules.value, + "laurels_zips": self.options.laurels_zips.value, + "ice_grappling": self.options.ice_grappling.value, + "ladder_storage": self.options.ladder_storage.value, + "ladder_storage_without_items": self.options.ladder_storage_without_items.value, "lanternless": self.options.lanternless.value, "maskless": self.options.maskless.value, "entrance_rando": int(bool(self.options.entrance_rando.value)), diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 27df4ce38b..b2e1a71897 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -83,8 +83,6 @@ Notes: - The `direction` field is not supported. Connections are always coupled. - For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log. - There is no limit to the number of Shops you can plando. -- If you have more than one shop in a scene, you may be wrong warped when exiting a shop. -- If you have a shop in every scene, and you have an odd number of shops, it will error out. See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando. diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 6316292e56..343bf30553 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,6 +1,9 @@ -from typing import Dict, NamedTuple, List +from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional from enum import IntEnum +if TYPE_CHECKING: + from . import TunicWorld + class Portal(NamedTuple): name: str # human-readable name @@ -9,6 +12,8 @@ class Portal(NamedTuple): tag: str # vanilla tag def scene(self) -> str: # the actual scene name in Tunic + if self.region.startswith("Shop"): + return tunic_er_regions["Shop"].game_scene return tunic_er_regions[self.region].game_scene def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod @@ -458,7 +463,7 @@ portal_mapping: List[Portal] = [ Portal(name="Cathedral Main Exit", region="Cathedral", destination="Swamp Redux 2", tag="_main"), - Portal(name="Cathedral Elevator", region="Cathedral", + Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet", destination="Cathedral Arena", tag="_"), Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", destination="Swamp Redux 2", tag="_secret"), @@ -517,6 +522,13 @@ portal_mapping: List[Portal] = [ class RegionInfo(NamedTuple): game_scene: str # the name of the scene in the actual game dead_end: int = 0 # if a region has only one exit + outlet_region: Optional[str] = None + is_fake_region: bool = False + + +# gets the outlet region name if it exists, the region if it doesn't +def get_portal_outlet_region(portal: Portal, world: "TunicWorld") -> str: + return world.er_regions[portal.region].outlet_region or portal.region class DeadEnd(IntEnum): @@ -558,11 +570,11 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux", outlet_region="Overworld"), "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal - "Cube Cave Entrance Region": RegionInfo("Overworld Redux"), # other side of the bomb wall + "Overworld Town Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Overworld Spawn Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"), + "Cube Cave Entrance Region": RegionInfo("Overworld Redux", outlet_region="Overworld"), # other side of the bomb wall "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats), "Windmill": RegionInfo("Windmill"), "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door @@ -591,7 +603,7 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Forest Belltower Lower": RegionInfo("Forest Belltower"), "East Forest": RegionInfo("East Forest Redux"), "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), - "East Forest Portal": RegionInfo("East Forest Redux"), + "East Forest Portal": RegionInfo("East Forest Redux", outlet_region="East Forest"), "Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), @@ -601,7 +613,7 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Forest Grave Path Main": RegionInfo("Sword Access"), "Forest Grave Path Upper": RegionInfo("Sword Access"), "Forest Grave Path by Grave": RegionInfo("Sword Access"), - "Forest Hero's Grave": RegionInfo("Sword Access"), + "Forest Hero's Grave": RegionInfo("Sword Access", outlet_region="Forest Grave Path by Grave"), "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits "Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder "Dark Tomb Main": RegionInfo("Crypt Redux"), @@ -614,18 +626,19 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests "West Garden": RegionInfo("Archipelagos Redux"), "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats), - "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"), + "West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), "West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"), "West Garden after Boss": RegionInfo("Archipelagos Redux"), - "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"), "Ruined Atoll": RegionInfo("Atoll Redux"), "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), "Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), "Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"), - "Ruined Atoll Portal": RegionInfo("Atoll Redux"), - "Ruined Atoll Statue": RegionInfo("Atoll Redux"), + "Ruined Atoll Portal": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), + "Ruined Atoll Statue": RegionInfo("Atoll Redux", outlet_region="Ruined Atoll"), "Frog Stairs Eye Exit": RegionInfo("Frog Stairs"), "Frog Stairs Upper": RegionInfo("Frog Stairs"), "Frog Stairs Lower": RegionInfo("Frog Stairs"), @@ -633,18 +646,20 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Frog's Domain Entry": RegionInfo("frog cave main"), "Frog's Domain": RegionInfo("frog cave main"), "Frog's Domain Back": RegionInfo("frog cave main"), - "Library Exterior Tree Region": RegionInfo("Library Exterior"), + "Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"), + "Library Exterior by Tree": RegionInfo("Library Exterior"), "Library Exterior Ladder Region": RegionInfo("Library Exterior"), "Library Hall Bookshelf": RegionInfo("Library Hall"), "Library Hall": RegionInfo("Library Hall"), - "Library Hero's Grave Region": RegionInfo("Library Hall"), + "Library Hero's Grave Region": RegionInfo("Library Hall", outlet_region="Library Hall"), "Library Hall to Rotunda": RegionInfo("Library Hall"), "Library Rotunda to Hall": RegionInfo("Library Rotunda"), "Library Rotunda": RegionInfo("Library Rotunda"), "Library Rotunda to Lab": RegionInfo("Library Rotunda"), "Library Lab": RegionInfo("Library Lab"), "Library Lab Lower": RegionInfo("Library Lab"), - "Library Portal": RegionInfo("Library Lab"), + "Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"), + "Library Lab on Portal Pad": RegionInfo("Library Lab"), "Library Lab to Librarian": RegionInfo("Library Lab"), "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats), "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), @@ -663,22 +678,22 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Fortress Grave Path": RegionInfo("Fortress Reliquary"), "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted), "Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"), - "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"), "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats), "Fortress Arena": RegionInfo("Fortress Arena"), - "Fortress Arena Portal": RegionInfo("Fortress Arena"), + "Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"), "Lower Mountain": RegionInfo("Mountain"), "Lower Mountain Stairs": RegionInfo("Mountain"), "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats), "Quarry Connector": RegionInfo("Darkwoods Tunnel"), "Quarry Entry": RegionInfo("Quarry Redux"), "Quarry": RegionInfo("Quarry Redux"), - "Quarry Portal": RegionInfo("Quarry Redux"), + "Quarry Portal": RegionInfo("Quarry Redux", outlet_region="Quarry Entry"), "Quarry Back": RegionInfo("Quarry Redux"), "Quarry Monastery Entry": RegionInfo("Quarry Redux"), "Monastery Front": RegionInfo("Monastery"), "Monastery Back": RegionInfo("Monastery"), - "Monastery Hero's Grave Region": RegionInfo("Monastery"), + "Monastery Hero's Grave Region": RegionInfo("Monastery", outlet_region="Monastery Back"), "Monastery Rope": RegionInfo("Quarry Redux"), "Lower Quarry": RegionInfo("Quarry Redux"), "Even Lower Quarry": RegionInfo("Quarry Redux"), @@ -691,19 +706,21 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side - "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special), # the exit from zig skip, for use with fixed shop on - "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side - "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), + "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, for use with fixed shop on + "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side + "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"), + "Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"), "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), "Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south "Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door "Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door - "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2", outlet_region="Swamp Ledge under Cathedral Door"), # just the door "Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance - "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"), + "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"), "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse "Cathedral": RegionInfo("Cathedral Redux"), + "Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats), "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), @@ -711,10 +728,10 @@ tunic_er_regions: Dict[str, RegionInfo] = { "Far Shore": RegionInfo("Transit"), "Far Shore to Spawn Region": RegionInfo("Transit"), "Far Shore to East Forest Region": RegionInfo("Transit"), - "Far Shore to Quarry Region": RegionInfo("Transit"), - "Far Shore to Fortress Region": RegionInfo("Transit"), - "Far Shore to Library Region": RegionInfo("Transit"), - "Far Shore to West Garden Region": RegionInfo("Transit"), + "Far Shore to Quarry Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Fortress Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to Library Region": RegionInfo("Transit", outlet_region="Far Shore"), + "Far Shore to West Garden Region": RegionInfo("Transit", outlet_region="Far Shore"), "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), @@ -728,6 +745,16 @@ tunic_er_regions: Dict[str, RegionInfo] = { } +# this is essentially a pared down version of the region connections in rules.py, with some minor differences +# the main purpose of this is to make it so that you can access every region +# most items are excluded from the rules here, since we can assume Archipelago will properly place them +# laurels (hyperdash) can be locked at 10 fairies, requiring access to secret gathering place +# so until secret gathering place has been paired, you do not have hyperdash, so you cannot use hyperdash entrances +# Zip means you need the laurels zips option enabled +# IG# refers to ice grappling difficulties +# LS# refers to ladder storage difficulties +# LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic +# this is because it just means you can reach the entrances in that region via ladder storage traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld": { "Overworld Beach": @@ -735,13 +762,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld to Atoll Upper": [["Hyperdash"]], "Overworld Belltower": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Upper Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Swamp Lower Entry": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Overworld Well Ladder": [], "Overworld Ruined Passage Door": @@ -759,11 +786,11 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld after Envoy": [], "Overworld Quarry Entry": - [["NMG"]], + [["IG2"], ["LS1"]], "Overworld Tunnel Turret": - [["NMG"], ["Hyperdash"]], + [["IG1"], ["LS1"], ["Hyperdash"]], "Overworld Temple Door": - [["NMG"], ["Forest Belltower Upper", "Overworld Belltower"]], + [["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]], "Overworld Southeast Cross Door": [], "Overworld Fountain Cross Door": @@ -773,25 +800,28 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld Spawn Portal": [], "Overworld Well to Furnace Rail": - [["UR"]], + [["LS2"]], "Overworld Old House Door": [], "Cube Cave Entrance Region": [], + # drop a rudeling, icebolt or ice bomb + "Overworld to West Garden from Furnace": + [["IG3"]], }, "East Overworld": { "Above Ruined Passage": [], "After Ruined Passage": - [["NMG"]], - "Overworld": - [], + [["IG1"], ["LS1"]], + # "Overworld": + # [], "Overworld at Patrol Cave": [], "Overworld above Patrol Cave": [], "Overworld Special Shop Entry": - [["Hyperdash"], ["UR"]] + [["Hyperdash"], ["LS1"]] }, "Overworld Special Shop Entry": { "East Overworld": @@ -800,8 +830,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld Belltower": { "Overworld Belltower at Bell": [], - "Overworld": - [], + # "Overworld": + # [], "Overworld to West Garden Upper": [], }, @@ -809,19 +839,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Overworld Belltower": [], }, - "Overworld Swamp Upper Entry": { - "Overworld": - [], - }, - "Overworld Swamp Lower Entry": { - "Overworld": - [], - }, + # "Overworld Swamp Upper Entry": { + # "Overworld": + # [], + # }, + # "Overworld Swamp Lower Entry": { + # "Overworld": + # [], + # }, "Overworld Beach": { - "Overworld": - [], + # "Overworld": + # [], "Overworld West Garden Laurels Entry": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"]], "Overworld to Atoll Upper": [], "Overworld Tunnel Turret": @@ -832,38 +862,37 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [["Hyperdash"]], }, "Overworld to Atoll Upper": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Tunnel Turret": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Beach": [], }, "Overworld Well Ladder": { - "Overworld": - [], + # "Overworld": + # [], }, "Overworld at Patrol Cave": { "East Overworld": - [["Hyperdash"]], + [["Hyperdash"], ["LS1"], ["IG1"]], "Overworld above Patrol Cave": [], }, "Overworld above Patrol Cave": { - "Overworld": - [], + # "Overworld": + # [], "East Overworld": [], "Upper Overworld": [], "Overworld at Patrol Cave": [], - "Overworld Belltower at Bell": - [["NMG"]], + # readd long dong if we ever do a misc tricks option }, "Upper Overworld": { "Overworld above Patrol Cave": @@ -878,51 +907,49 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], }, "Overworld above Quarry Entrance": { - "Overworld": - [], + # "Overworld": + # [], "Upper Overworld": [], }, "Overworld Quarry Entry": { "Overworld after Envoy": [], - "Overworld": - [["NMG"]], + # "Overworld": + # [["IG1"]], }, "Overworld after Envoy": { - "Overworld": - [], + # "Overworld": + # [], "Overworld Quarry Entry": [], }, "After Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "Above Ruined Passage": [], - "East Overworld": - [["NMG"]], }, "Above Ruined Passage": { - "Overworld": - [], + # "Overworld": + # [], "After Ruined Passage": [], "East Overworld": [], }, - "Overworld Ruined Passage Door": { - "Overworld": - [["Hyperdash", "NMG"]], - }, - "Overworld Town Portal": { - "Overworld": - [], - }, - "Overworld Spawn Portal": { - "Overworld": - [], - }, + # "Overworld Ruined Passage Door": { + # "Overworld": + # [["Hyperdash", "Zip"]], + # }, + # "Overworld Town Portal": { + # "Overworld": + # [], + # }, + # "Overworld Spawn Portal": { + # "Overworld": + # [], + # }, "Cube Cave Entrance Region": { "Overworld": [], @@ -933,7 +960,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Old House Back": { "Old House Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Furnace Fuse": { "Furnace Ladder Area": @@ -941,9 +968,9 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Furnace Ladder Area": { "Furnace Fuse": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Furnace Walking Path": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Furnace Walking Path": { "Furnace Ladder Area": @@ -971,7 +998,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "East Forest": { "East Forest Dance Fox Spot": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], "East Forest Portal": [], "Lower Forest": @@ -979,7 +1006,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "East Forest Dance Fox Spot": { "East Forest": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "East Forest Portal": { "East Forest": @@ -995,7 +1022,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Guard House 1 West": { "Guard House 1 East": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], }, "Guard House 2 Upper": { "Guard House 2 Lower": @@ -1007,19 +1034,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Forest Grave Path Main": { "Forest Grave Path Upper": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG3"]], "Forest Grave Path by Grave": [], }, "Forest Grave Path Upper": { "Forest Grave Path Main": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"]], }, "Forest Grave Path by Grave": { "Forest Hero's Grave": [], "Forest Grave Path Main": - [["NMG"]], + [["IG1"]], }, "Forest Hero's Grave": { "Forest Grave Path by Grave": @@ -1051,7 +1078,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Dark Tomb Checkpoint": { "Well Boss": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], }, "Dark Tomb Entry Point": { "Dark Tomb Upper": @@ -1075,13 +1102,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "West Garden": { "West Garden Laurels Exit Region": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "West Garden after Boss": [], "West Garden Hero's Grave Region": [], "West Garden Portal Item": - [["NMG"]], + [["IG2"]], }, "West Garden Laurels Exit Region": { "West Garden": @@ -1093,13 +1120,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "West Garden Portal Item": { "West Garden": - [["NMG"]], - "West Garden Portal": - [["Hyperdash", "West Garden"]], + [["IG1"]], + "West Garden by Portal": + [["Hyperdash"]], }, - "West Garden Portal": { + "West Garden by Portal": { "West Garden Portal Item": [["Hyperdash"]], + "West Garden Portal": + [["West Garden"]], + }, + "West Garden Portal": { + "West Garden by Portal": + [], }, "West Garden Hero's Grave Region": { "West Garden": @@ -1107,7 +1140,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Ruined Atoll": { "Ruined Atoll Lower Entry Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS1"]], "Ruined Atoll Ladder Tops": [], "Ruined Atoll Frog Mouth": @@ -1174,11 +1207,17 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], }, "Library Exterior Ladder Region": { + "Library Exterior by Tree": + [], + }, + "Library Exterior by Tree": { "Library Exterior Tree Region": [], + "Library Exterior Ladder Region": + [], }, "Library Exterior Tree Region": { - "Library Exterior Ladder Region": + "Library Exterior by Tree": [], }, "Library Hall Bookshelf": { @@ -1223,15 +1262,21 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Library Lab": { "Library Lab Lower": [["Hyperdash"]], - "Library Portal": + "Library Lab on Portal Pad": [], "Library Lab to Librarian": [], }, - "Library Portal": { + "Library Lab on Portal Pad": { + "Library Portal": + [], "Library Lab": [], }, + "Library Portal": { + "Library Lab on Portal Pad": + [], + }, "Library Lab to Librarian": { "Library Lab": [], @@ -1240,11 +1285,9 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Fortress Exterior from Overworld": [], "Fortress Courtyard Upper": - [["UR"]], - "Fortress Exterior near cave": - [["UR"]], + [["LS2"]], "Fortress Courtyard": - [["UR"]], + [["LS1"]], }, "Fortress Exterior from Overworld": { "Fortress Exterior from East Forest": @@ -1252,15 +1295,15 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Fortress Exterior near cave": [], "Fortress Courtyard": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG1"], ["LS1"]], }, "Fortress Exterior near cave": { "Fortress Exterior from Overworld": - [["Hyperdash"], ["UR"]], - "Fortress Courtyard": - [["UR"]], + [["Hyperdash"], ["LS1"]], + "Fortress Courtyard": # ice grapple hard: shoot far fire pot, it aggros one of the enemies over to you + [["IG3"], ["LS1"]], "Fortress Courtyard Upper": - [["UR"]], + [["LS2"]], "Beneath the Vault Entry": [], }, @@ -1270,7 +1313,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Fortress Courtyard": { "Fortress Courtyard Upper": - [["NMG"]], + [["IG1"]], "Fortress Exterior from Overworld": [["Hyperdash"]], }, @@ -1296,7 +1339,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Fortress East Shortcut Lower": { "Fortress East Shortcut Upper": - [["NMG"]], + [["IG1"]], }, "Fortress East Shortcut Upper": { "Fortress East Shortcut Lower": @@ -1304,11 +1347,11 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Eastern Vault Fortress": { "Eastern Vault Fortress Gold Door": - [["NMG"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], + [["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], }, "Eastern Vault Fortress Gold Door": { "Eastern Vault Fortress": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path": { "Fortress Hero's Grave Region": @@ -1318,7 +1361,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Fortress Grave Path Upper": { "Fortress Grave Path": - [["NMG"]], + [["IG1"]], }, "Fortress Grave Path Dusty Entrance Region": { "Fortress Grave Path": @@ -1346,7 +1389,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Monastery Back": { "Monastery Front": - [["Hyperdash", "NMG"]], + [["Hyperdash", "Zip"]], "Monastery Hero's Grave Region": [], }, @@ -1363,6 +1406,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [["Quarry Connector"]], "Quarry": [], + "Monastery Rope": + [["LS2"]], }, "Quarry Portal": { "Quarry Entry": @@ -1374,7 +1419,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Quarry Back": [["Hyperdash"]], "Monastery Rope": - [["UR"]], + [["LS2"]], }, "Quarry Back": { "Quarry": @@ -1392,7 +1437,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Quarry Monastery Entry": [], "Lower Quarry Zig Door": - [["NMG"]], + [["IG3"]], }, "Lower Quarry": { "Even Lower Quarry": @@ -1402,7 +1447,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { "Lower Quarry": [], "Lower Quarry Zig Door": - [["Quarry", "Quarry Connector"], ["NMG"]], + [["Quarry", "Quarry Connector"], ["IG3"]], }, "Monastery Rope": { "Quarry Back": @@ -1430,7 +1475,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Rooted Ziggurat Lower Back": { "Rooted Ziggurat Lower Front": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"], ["IG1"]], "Rooted Ziggurat Portal Room Entrance": [], }, @@ -1443,26 +1488,35 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], }, "Rooted Ziggurat Portal Room Exit": { - "Rooted Ziggurat Portal": + "Rooted Ziggurat Portal Room": [], }, - "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": { + "Rooted Ziggurat Portal": + [], "Rooted Ziggurat Portal Room Exit": [["Rooted Ziggurat Lower Back"]], }, + "Rooted Ziggurat Portal": { + "Rooted Ziggurat Portal Room": + [], + }, "Swamp Front": { "Swamp Mid": [], + # get one pillar from the gate, then dash onto the gate, very tricky + "Back of Swamp Laurels Area": + [["Hyperdash", "Zip"]], }, "Swamp Mid": { "Swamp Front": [], "Swamp to Cathedral Main Entrance Region": - [["Hyperdash"], ["NMG"]], + [["Hyperdash"], ["IG2"], ["LS3"]], "Swamp Ledge under Cathedral Door": [], "Back of Swamp": - [["UR"]], + [["LS1"]], # ig3 later? }, "Swamp Ledge under Cathedral Door": { "Swamp Mid": @@ -1476,24 +1530,41 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { }, "Swamp to Cathedral Main Entrance Region": { "Swamp Mid": - [["NMG"]], + [["IG1"]], }, "Back of Swamp": { "Back of Swamp Laurels Area": - [["Hyperdash"], ["UR"]], + [["Hyperdash"], ["LS2"]], "Swamp Hero's Grave Region": [], + "Swamp Mid": + [["LS2"]], + "Swamp Front": + [["LS1"]], + "Swamp to Cathedral Main Entrance Region": + [["LS3"]], + "Swamp to Cathedral Treasure Room": + [["LS3"]] }, "Back of Swamp Laurels Area": { "Back of Swamp": [["Hyperdash"]], + # get one pillar from the gate, then dash onto the gate, very tricky "Swamp Mid": - [["NMG", "Hyperdash"]], + [["IG1", "Hyperdash"], ["Hyperdash", "Zip"]], }, "Swamp Hero's Grave Region": { "Back of Swamp": [], }, + "Cathedral": { + "Cathedral to Gauntlet": + [], + }, + "Cathedral to Gauntlet": { + "Cathedral": + [], + }, "Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet": [], diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 3d1973beb3..65175e41ca 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,8 +1,10 @@ -from typing import Dict, Set, List, Tuple, TYPE_CHECKING +from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item +from .options import IceGrappling, LadderStorage from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, - bomb_walls) -from .er_data import Portal + laurels_zip, bomb_walls) +from .er_data import Portal, get_portal_outlet_region +from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -82,13 +84,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld"]) + # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, world)) + rule=lambda state: has_ladder("Ladders to West Bell", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], rule=lambda state: has_ladder("Ladders to West Bell", state, world)) @@ -97,32 +102,35 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Belltower at Bell"], 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( - connecting_region=regions["Overworld Belltower at Bell"], - rule=lambda state: options.logic_rules and state.has(fire_wand, player)) + # long dong, do not make a reverse connection here or to belltower, maybe readd later + # regions["Overworld above Patrol Cave"].connect( + # connecting_region=regions["Overworld Belltower at Bell"], + # rule=lambda state: options.logic_rules and state.has(fire_wand, player)) - # nmg: can laurels through the ruined passage door + # can laurels through the ruined passage door at either corner regions["Overworld"].connect( connecting_region=regions["Overworld Ruined Passage Door"], rule=lambda state: state.has(key, player, 2) - or (state.has(laurels, player) and options.logic_rules)) + or laurels_zip(state, world)) regions["Overworld Ruined Passage Door"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Overworld"].connect( connecting_region=regions["After Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world)) + # for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down + # you can ice grapple through the bomb wall, so no need for shop logic checking regions["Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or state.has(laurels, player)) + or state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Above Ruined Passage"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -138,7 +146,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Above Ruined Passage"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Above Ruined Passage"], rule=lambda state: has_ladder("Ladders near Weathervane", state, world) @@ -147,15 +155,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["After Ruined Passage"].connect( connecting_region=regions["East Overworld"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Overworld"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -169,7 +177,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld at Patrol Cave"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) @@ -185,7 +193,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Patrol Cave"].connect( connecting_region=regions["East Overworld"], rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)) @@ -193,7 +201,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld above Patrol Cave"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Upper Overworld"].connect( connecting_region=regions["Overworld above Patrol Cave"], rule=lambda state: has_ladder("Ladders near Patrol Cave", state, world) @@ -206,13 +214,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Upper Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) + # ice grapple push guard captain down the ledge regions["Upper Overworld"].connect( connecting_region=regions["Overworld after Temple Rafters"], - rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world)) + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) regions["Overworld after Temple Rafters"].connect( connecting_region=regions["Upper Overworld"], rule=lambda state: has_ladder("Ladder near Temple Rafters", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Overworld above Quarry Entrance"].connect( connecting_region=regions["Overworld"], @@ -224,13 +234,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({laurels, grapple, gun}, player) - or state.has("Sword Upgrade", player, 4) - or options.logic_rules) + or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld Quarry Entry"], @@ -242,10 +250,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # ice grapple through the gate regions["Overworld"].connect( connecting_region=regions["Overworld Quarry Entry"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Quarry Entry"].connect( connecting_region=regions["Overworld"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -256,7 +264,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Lower Entry"], - rule=lambda state: has_ladder("Ladder to Swamp", state, world)) + rule=lambda state: has_ladder("Ladder to Swamp", state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Swamp Lower Entry"].connect( connecting_region=regions["Overworld"], rule=lambda state: has_ladder("Ladder to Swamp", state, world)) @@ -279,20 +288,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - # not including ice grapple through this because it's very tedious to get an enemy here + # lure enemy over and ice grapple through regions["Overworld"].connect( connecting_region=regions["Overworld Southeast Cross Door"], - rule=lambda state: has_ability(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld Southeast Cross Door"].connect( connecting_region=regions["Overworld"], 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(holy_cross, state, world)) + rule=lambda state: has_ability(holy_cross, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) @@ -312,7 +322,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( connecting_region=regions["Overworld above Patrol Cave"], @@ -325,12 +335,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld Beach"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + rule=lambda state: state.has(laurels, player)) regions["Overworld Tunnel Turret"].connect( connecting_region=regions["Overworld"], rule=lambda state: state.has_any({grapple, laurels}, player)) @@ -341,13 +350,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Cube Cave Entrance Region"].connect( connecting_region=regions["Overworld"]) + # drop a rudeling down, icebolt or ice bomb + regions["Overworld"].connect( + connecting_region=regions["Overworld to West Garden from Furnace"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # Overworld side areas regions["Old House Front"].connect( connecting_region=regions["Old House Back"]) - # nmg: laurels through the gate + # laurels through the gate, use left wall to space yourself regions["Old House Back"].connect( connecting_region=regions["Old House Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Sealed Temple"].connect( connecting_region=regions["Sealed Temple Rafters"]) @@ -388,15 +402,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Forest Belltower Lower"], rule=lambda state: has_ladder("Ladder to East Forest", state, world)) - # nmg: ice grapple up to dance fox spot, and vice versa + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["East Forest"].connect( connecting_region=regions["East Forest Portal"], @@ -407,7 +421,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["East Forest"].connect( connecting_region=regions["Lower Forest"], 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))) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Lower Forest"].connect( connecting_region=regions["East Forest"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) @@ -425,22 +439,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Guard House 2 Upper"], rule=lambda state: has_ladder("Ladders to Lower Forest", state, world)) - # nmg: ice grapple from upper grave path exit to the rest of it + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + # for the ice grapple, lure a rudeling up top, then grapple push it across regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path Upper"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Forest Grave Path Main"].connect( connecting_region=regions["Forest Grave Path by Grave"]) - # nmg: ice grapple or laurels through the gate + # 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, world) - or (state.has(laurels, player) and options.logic_rules)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + or laurels_zip(state, world)) regions["Forest Grave Path by Grave"].connect( connecting_region=regions["Forest Hero's Grave"], @@ -473,10 +489,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Well Boss"].connect( connecting_region=regions["Dark Tomb Checkpoint"]) - # nmg: can laurels through the gate + # can laurels through the gate, no setup needed regions["Dark Tomb Checkpoint"].connect( connecting_region=regions["Well Boss"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], @@ -505,12 +521,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) + # you can grapple Garden Knight to aggro it, then ledge it regions["West Garden after Boss"].connect( connecting_region=regions["West Garden"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + # ice grapple push Garden Knight off the side regions["West Garden"].connect( connecting_region=regions["West Garden after Boss"], - rule=lambda state: state.has(laurels, player) or has_sword(state, player)) + rule=lambda state: state.has(laurels, player) or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Hero's Grave Region"], @@ -519,26 +539,32 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["West Garden"]) regions["West Garden Portal"].connect( + connecting_region=regions["West Garden by Portal"]) + regions["West Garden by Portal"].connect( + connecting_region=regions["West Garden Portal"], + rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player)) + + regions["West Garden by Portal"].connect( connecting_region=regions["West Garden Portal Item"], 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(prayer, state, world)) + connecting_region=regions["West Garden by Portal"], + rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple to and from the item behind the magic dagger house + # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["West Garden"].connect( connecting_region=regions["West Garden Portal Item"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) # Atoll and Frog's Domain - # nmg: ice grapple the bird below the portal + # 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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)) @@ -570,13 +596,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], rule=lambda state: has_ability(prayer, state, world) - and has_ladder("Ladders in South Atoll", state, world)) + and (has_ladder("Ladders in South Atoll", state, world) + # shoot fuse and have the shot hit you mid-LS + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard))) 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, world)) + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) @@ -605,14 +635,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # Library regions["Library Exterior Tree Region"].connect( + connecting_region=regions["Library Exterior by Tree"]) + regions["Library Exterior by Tree"].connect( + connecting_region=regions["Library Exterior Tree Region"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Library Exterior by Tree"].connect( connecting_region=regions["Library Exterior Ladder Region"], rule=lambda state: state.has_any({grapple, laurels}, player) 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(prayer, state, world) - and ((state.has(laurels, player) and has_ladder("Ladders in Library", state, world)) - or state.has(grapple, player))) + connecting_region=regions["Library Exterior by Tree"], + rule=lambda state: state.has(grapple, player) + or (state.has(laurels, player) and has_ladder("Ladders in Library", state, world))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], @@ -658,14 +693,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ and has_ladder("Ladders in Library", state, world)) regions["Library Lab"].connect( - connecting_region=regions["Library Portal"], - 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 on Portal Pad"], + rule=lambda state: has_ladder("Ladders in Library", state, world)) + regions["Library Lab on Portal Pad"].connect( connecting_region=regions["Library Lab"], rule=lambda state: has_ladder("Ladders in Library", state, world) or state.has(laurels, player)) + regions["Library Lab on Portal Pad"].connect( + connecting_region=regions["Library Portal"], + rule=lambda state: has_ability(prayer, state, world)) + regions["Library Portal"].connect( + connecting_region=regions["Library Lab on Portal Pad"]) + regions["Library Lab"].connect( connecting_region=regions["Library Lab to Librarian"], rule=lambda state: has_ladder("Ladders in Library", state, world)) @@ -688,6 +728,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Fortress Exterior near cave"], rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) + # shoot far fire pot, enemy gets aggro'd + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Fortress Courtyard"], + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, 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, world)) @@ -702,14 +747,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) @@ -733,17 +778,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - rule=lambda state: has_ice_grapple_logic(True, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) regions["Fortress Grave Path"].connect( connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], @@ -761,7 +806,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # 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, world)) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], @@ -819,25 +864,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Lower Quarry"].connect( connecting_region=regions["Even Lower Quarry"], rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, 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, world) and options.entrance_rando)) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) # 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, world) and options.entrance_rando) + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) - # nmg: can laurels through the gate + # laurels through the gate, no setup needed regions["Monastery Back"].connect( connecting_region=regions["Monastery Front"], - rule=lambda state: state.has(laurels, player) and options.logic_rules) + rule=lambda state: laurels_zip(state, world)) regions["Monastery Back"].connect( connecting_region=regions["Monastery Hero's Grave Region"], @@ -863,14 +908,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) 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, world)) - and has_ability(prayer, state, world) - and has_sword(state, player)) - or can_ladder_storage(state, world)) + rule=lambda state: (state.has(laurels, player) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + and has_ability(prayer, state, world) + and has_sword(state, player)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], @@ -882,40 +926,62 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Front"]) regions["Rooted Ziggurat Portal"].connect( + connecting_region=regions["Rooted Ziggurat Portal Room"]) + regions["Rooted Ziggurat Portal Room"].connect( + connecting_region=regions["Rooted Ziggurat Portal"], + rule=lambda state: has_ability(prayer, state, world)) + + regions["Rooted Ziggurat Portal Room"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], 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(prayer, state, world)) + connecting_region=regions["Rooted Ziggurat Portal Room"]) # Swamp and Cathedral regions["Swamp Front"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Swamp Mid"].connect( connecting_region=regions["Swamp Front"], rule=lambda state: has_ladder("Ladders in Swamp", state, world) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, world)) # nmg: ice grapple through gate + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # nmg: ice grapple through cathedral door, can do it both ways - regions["Swamp Mid"].connect( + # a whole lot of stuff to basically say "you need to pray at the overworld fuse" + swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], - rule=lambda state: (has_ability(prayer, state, world) and state.has(laurels, player)) - or has_ice_grapple_logic(False, state, world)) + rule=lambda state: (has_ability(prayer, state, world) + and (state.has(laurels, player) + # blam yourself in the face with a wand shot off the fuse + or (can_ladder_storage(state, world) and state.has(fire_wand, player) + and options.ladder_storage >= LadderStorage.option_hard + and (not options.shuffle_ladders + or state.has_any({"Ladders in Overworld Town", + "Ladder to Swamp", + "Ladders near Weathervane"}, player) + or (state.has("Ladder to Ruined Atoll", player) + and state.can_reach_region("Overworld Beach", player)))))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + + if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: + world.multiworld.register_indirect_condition(regions["Overworld Beach"], swamp_mid_to_cath) + regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], - rule=lambda state: has_ice_grapple_logic(False, state, world)) + rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) + # grapple push the enemy by the door down, then grapple to it. Really jank regions["Swamp Mid"].connect( connecting_region=regions["Swamp Ledge under Cathedral Door"], - rule=lambda state: has_ladder("Ladders in Swamp", state, world)) + rule=lambda state: has_ladder("Ladders in Swamp", state, world) + or has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) + # ice grapple enemy standing at the door regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp Mid"], 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 + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], @@ -930,11 +996,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Back of Swamp"], rule=lambda state: state.has(laurels, player)) - # nmg: can ice grapple down while you're on the pillars + # ice grapple down from the pillar, or do that really annoying laurels zip + # the zip goes to front or mid, just doing mid since mid -> front can be done with laurels alone 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, world)) + rule=lambda state: laurels_zip(state, world) + or (state.has(laurels, player) + and has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))) + # get one pillar from the gate, then dash onto the gate, very tricky + regions["Swamp Front"].connect( + connecting_region=regions["Back of Swamp Laurels Area"], + rule=lambda state: laurels_zip(state, world)) regions["Back of Swamp"].connect( connecting_region=regions["Swamp Hero's Grave Region"], @@ -942,6 +1014,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) + regions["Cathedral"].connect( + connecting_region=regions["Cathedral to Gauntlet"], + rule=lambda state: (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + or options.entrance_rando) # elevator is always there in ER + regions["Cathedral to Gauntlet"].connect( + connecting_region=regions["Cathedral"]) + regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1000,337 +1080,141 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ 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 - if options.logic_rules == "unrestricted": + if options.ladder_storage: def get_portal_info(portal_sd: str) -> Tuple[str, str]: for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: - return portal1.name, portal2.region + return portal1.name, get_portal_outlet_region(portal2, world) if portal2.scene_destination() == portal_sd: - return portal2.name, portal1.region + return portal2.name, get_portal_outlet_region(portal1, world) raise Exception("no matches found in get_paired_region") - ladder_storages: List[Tuple[str, str, Set[str]]] = [ - # LS from Overworld main - # The upper Swamp entrance - ("Overworld", "Overworld Redux, Swamp Redux 2_wall", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper atoll entrance - ("Overworld", "Overworld Redux, Atoll Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld", "Overworld Redux, Furnace_gyro_west", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Upper West Garden entry, by the belltower - ("Overworld", "Overworld Redux, Archipelagos Redux_upper", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Ruined Passage - ("Overworld", "Overworld Redux, Ruins Passage_east", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), - # Quarry entry - ("Overworld", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}), - # East Forest entry - ("Overworld", "Overworld Redux, Forest Belltower_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Fortress entry - ("Overworld", "Overworld Redux, Fortress Courtyard_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Patrol Cave entry - ("Overworld", "Overworld Redux, PatrolCave_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld", "Overworld Redux, ShopSpecial_", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential - ("Overworld", "Overworld Redux, Temple_rafters", - {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", - "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", - "Ladders near Dark Tomb"}), - # Spot above the Quarry entrance, - # only gets you to the mountain stairs - ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_", - {"Ladders near Dark Tomb"}), + # connect ls elevation regions to their destinations + def ls_connect(origin_name: str, portal_sdt: str) -> None: + p_name, paired_region_name = get_portal_info(portal_sdt) + ladder_regions[origin_name].connect( + regions[paired_region_name], + name=p_name + " (LS) " + origin_name) - # LS from the Overworld Beach - # West Garden entry by the Furnace - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # West Garden laurels entry - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp lower entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Rotating Lights entrance - ("Overworld Beach", "Overworld Redux, Overworld Cave_", - {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), - # Swamp upper entrance - ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall", - {"Ladder to Ruined Atoll"}), - # Furnace entrance, next to the sign that leads to West Garden - ("Overworld Beach", "Overworld Redux, Furnace_gyro_west", - {"Ladder to Ruined Atoll"}), - # Upper West Garden entry, by the belltower - ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper", - {"Ladder to Ruined Atoll"}), - # Ruined Passage - ("Overworld Beach", "Overworld Redux, Ruins Passage_east", - {"Ladder to Ruined Atoll"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct", - {"Ladder to Ruined Atoll"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladder to Ruined Atoll"}), - # Quarry entry - ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_", - {"Ladder to Ruined Atoll"}), + # get what non-overworld ladder storage connections we want together + non_ow_ls_list = [] + non_ow_ls_list.extend(easy_ls) + if options.ladder_storage >= LadderStorage.option_medium: + non_ow_ls_list.extend(medium_ls) + if options.ladder_storage >= LadderStorage.option_hard: + non_ow_ls_list.extend(hard_ls) - # LS from that low spot where you normally walk to swamp - # Only has low ones you can't get to from main Overworld - # West Garden main entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower", - {"Ladder to Swamp"}), - # Maze Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_", - {"Ladder to Swamp"}), - # Hourglass Cave entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach", - {"Ladder to Swamp"}), - # Lower Atoll entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower", - {"Ladder to Swamp"}), - # Lowest West Garden entry from swamp ladder - ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest", - {"Ladder to Swamp"}), + # create the ls elevation regions + ladder_regions: Dict[str, Region] = {} + for name in ow_ladder_groups.keys(): + ladder_regions[name] = Region(name, player, world.multiworld) - # from the ladders by the belltower - # Ruined Passage - ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east", - {"Ladders to West Bell"}), - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct", - {"Ladders to West Bell"}), - # Well rail, east side. Need some height from the temple stairs - ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north", - {"Ladders to West Bell"}), - # Quarry entry - ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_", - {"Ladders to West Bell"}), - # East Forest entry - ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_", - {"Ladders to West Bell"}), - # Fortress entry - ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_", - {"Ladders to West Bell"}), - # Patrol Cave entry - ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_", - {"Ladders to West Bell"}), - # Special Shop entry, excluded in non-ER due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_", - {"Ladders to West Bell"}), - # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential - ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters", - {"Ladders to West Bell"}), + # connect the ls elevations to each other where applicable + if options.ladder_storage >= LadderStorage.option_medium: + for i in range(len(ow_ladder_groups) - 1): + ladder_regions[f"LS Elev {i}"].connect(ladder_regions[f"LS Elev {i + 1}"]) - # In the furnace - # Furnace ladder to the fuse entrance - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()), - # Furnace ladder to Dark Tomb - ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()), - # Furnace ladder to the West Garden connector - ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()), + # connect the applicable overworld regions to the ls elevation regions + for origin_region, ladders in region_ladders.items(): + for ladder_region, region_info in ow_ladder_groups.items(): + # checking if that region has a ladder or ladders for that elevation + common_ladders: FrozenSet[str] = frozenset(ladders.intersection(region_info.ladders)) + if common_ladders: + if options.shuffle_ladders: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state, lads=common_ladders: state.has_any(lads, player) + and can_ladder_storage(state, world)) + else: + regions[origin_region].connect( + connecting_region=ladder_regions[ladder_region], + rule=lambda state: can_ladder_storage(state, world)) - # West Garden - # exit after Garden Knight - ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()), - # West Garden laurels exit - ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()), + # connect ls elevation regions to the region on the other side of the portals + for ladder_region, region_info in ow_ladder_groups.items(): + for portal_dest in region_info.portals: + ls_connect(ladder_region, "Overworld Redux, " + portal_dest) - # Atoll, use the little ladder you fix at the beginning - ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()), - ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()), + # connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail + if options.ladder_storage >= LadderStorage.option_medium: + for ladder_region, region_info in ow_ladder_groups.items(): + for dest_region in region_info.regions: + ladder_regions[ladder_region].connect( + connecting_region=regions[dest_region], + name=ladder_region + " (LS) " + dest_region) + # well rail, need height off portal pad for one side, and a tiny extra from stairs on the other + ls_connect("LS Elev 3", "Overworld Redux, Sewer_west_aqueduct") + ls_connect("LS Elev 3", "Overworld Redux, Furnace_gyro_upper_north") - # East Forest - # Entrance by the dancing fox holy cross spot - ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()), + # connect ls elevation regions to portals where you need to get behind the map to enter it + if options.ladder_storage >= LadderStorage.option_hard: + ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_") + ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_") + ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house") + ls_connect("LS Elev 5", "Overworld Redux, Temple_main") - # From the west side of Guard House 1 to the east side - ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()), - ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()), - - # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch - ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()), - - # Fortress Exterior - # shop, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()), - # Fortress main entry and grave path lower entry, ls at the ladder by the telescope - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the east side of the area - ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()), - ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()), - - # same as above, except from the Beneath the Vault entrance ladder - ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", - {"Ladder to Beneath the Vault"}), - ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", - {"Ladder to Beneath the Vault"}), - - # ls at the ladder, need to gain a little height to get up the stairs - # excluded in non-ER due to soft lock potential - ("Lower Mountain", "Mountain, Mountaintop_", set()), - - # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword - ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()), - - # Swamp to Gauntlet - ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", - {"Ladders in Swamp"}), - # Swamp to Overworld upper - ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", - {"Ladders in Swamp"}), - # Ladder by the hero grave - ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()), - ("Back of Swamp", "Swamp Redux 2, Shop_", set()), - # Need to put the cathedral HC code mid-flight - ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()), - ] - - for region_name, scene_dest, ladders in ladder_storages: - portal_name, paired_region = get_portal_info(scene_dest) - # this is the only exception, requiring holy cross as well - if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp": - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - 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: - regions[region_name].connect( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player) - and (state.has("Ladders to West Bell", player))) - # soft locked unless you have either ladder. if you have laurels, you use the other Entrance - elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \ - 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({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) - # soft locked for the same reasons as above - elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \ - 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_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: - 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_any({"Ladders to West Bell", laurels}, player)) - # soft locked if you can't get back out - elif portal_name == "Fortress Courtyard to Beneath the Vault" 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("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) - and (state.has_any({"Ladders in Overworld Town", grapple}, player) - 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(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: + # connect the non-overworld ones + for ls_info in non_ow_ls_list: + # for places where the destination is a region (so you have to get knocked down) + if ls_info.dest_is_region: + # none of the non-ow ones have multiple ladders that can be used, so don't need has_any + if options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[ls_info.destination], + name=ls_info.destination + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) continue - # soft lock if you don't have the ladder, I regret writing unrestricted logic - elif portal_name == "Temple Rafters 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("Ladder near Temple Rafters", player) - or (state.has_all({laurels, grapple}, player) - and ((state.has("Ladders near Patrol Cave", player) - and (state.has("Ladders near Dark Tomb", player) - 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, 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( - regions[paired_region], - name=portal_name + " (LS) " + region_name, - rule=lambda state: has_stick(state, player)) - # one ladder required - elif len(ladders) == 1: - ladder = ladders.pop() - 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)) - # if multiple ladders can be used + + portal_name, dest_region = get_portal_info(ls_info.destination) + # these two are special cases + if ls_info.destination == "Atoll Redux, Frog Stairs_mouth": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) + and (has_ladder("Ladders in South Atoll", state, world) + or state.has(key, player, 2) # can do it from the rope + # ice grapple push a crab into the door + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or options.ladder_storage >= LadderStorage.option_medium)) # use the little ladder + # holy cross mid-ls to get in here + elif ls_info.destination == "Swamp Redux 2, Cathedral Redux_secret": + if ls_info.origin == "Swamp Mid": + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world) + and has_ladder("Ladders in Swamp", state, world)) + else: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world) and has_ability(holy_cross, state, world)) + + elif options.shuffle_ladders and ls_info.ladders_req: + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state, lad=ls_info.ladders_req: can_ladder_storage(state, world) + and state.has(lad, player)) 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)) + regions[ls_info.origin].connect( + connecting_region=regions[dest_region], + name=portal_name + " (LS) " + ls_info.origin, + rule=lambda state: can_ladder_storage(state, world)) + + for region in ladder_regions.values(): + world.multiworld.regions.append(region) def set_er_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -1439,10 +1323,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -1465,23 +1352,25 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) # Ziggurat - # if ER is off, you still need to get past the Admin or you'll get stuck in lower zig + # if ER is off, while you can get the chest, you won't be able to actually get through zig set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), - lambda state: has_sword(state, player) or (state.has(fire_wand, player) and (state.has(laurels, player) - or options.entrance_rando))) + lambda state: has_sword(state, player) or (state.has(fire_wand, player) + and (state.has(laurels, player) + or world.options.entrance_rando))) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), lambda state: has_sword(state, player) and has_ability(prayer, state, world)) # Bosses set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player)) - # nmg - kill Librarian with a lure, or gun I guess set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: (has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player) and has_ladder("Ladders in Library", state, world)) - # nmg - kill boss scav with orb + firecracker, or similar + # can ice grapple boss scav off the side + # the grapple from the other side of the bridge isn't in logic 'cause we don't have a misc tricks option set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), @@ -1490,7 +1379,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), lambda state: state.has(laurels, player)) - # these two swamp checks really want you to kill the big skeleton first + # really hard to do 4 skulls with a big skeleton chasing you around set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"), lambda state: has_sword(state, player)) @@ -1541,7 +1430,13 @@ def set_er_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or can_shop(state, world)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or can_shop(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # not enough space to ice grapple into here + set_rule(world.get_location("Quarry - [East] Bombable Wall"), + lambda state: state.has(gun, player) or can_shop(state, world)) # Shop set_rule(world.get_location("Shop - Potion 1"), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index e7c8fd58d0..05f6177aa5 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,7 @@ -from typing import Dict, List, Set, TYPE_CHECKING +from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd +from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo from .er_rules import set_er_region_rules from Options import PlandoConnection from .options import EntranceRando @@ -22,17 +22,18 @@ class TunicERLocation(Location): def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} + for region_name, region_data in world.er_regions.items(): + regions[region_name] = Region(region_name, world.player, world.multiworld) + if world.options.entrance_rando: - portal_pairs = pair_portals(world) + portal_pairs = pair_portals(world, regions) + # output the entrances to the spoiler log here for convenience sorted_portal_pairs = sort_portals(portal_pairs) for portal1, portal2 in sorted_portal_pairs.items(): world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) else: - portal_pairs = vanilla_portals() - - for region_name, region_data in tunic_er_regions.items(): - regions[region_name] = Region(region_name, world.player, world.multiworld) + portal_pairs = vanilla_portals(world, regions) set_er_region_rules(world, regions, portal_pairs) @@ -93,7 +94,18 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: region.locations.append(location) -def vanilla_portals() -> Dict[Portal, Portal]: +# all shops are the same shop. however, you cannot get to all shops from the same shop entrance. +# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back +def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None: + new_shop_name = f"Shop {world.shop_num}" + world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats) + new_shop_region = Region(new_shop_name, world.player, world.multiworld) + new_shop_region.connect(regions["Shop"]) + regions[new_shop_name] = new_shop_region + world.shop_num += 1 + + +def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} # we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"] @@ -105,8 +117,9 @@ def vanilla_portals() -> Dict[Portal, Portal]: portal2_sdt = portal1.destination_scene() if portal2_sdt.startswith("Shop,"): - portal2 = Portal(name="Shop", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) elif portal2_sdt == "Purgatory, Purgatory_bottom": portal2_sdt = "Purgatory, Purgatory_top" @@ -125,14 +138,15 @@ def vanilla_portals() -> Dict[Portal, Portal]: # pairing off portals, starting with dead ends -def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: - # separate the portals into dead ends and non-dead ends +def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} dead_ends: List[Portal] = [] two_plus: List[Portal] = [] player_name = world.player_name portal_map = portal_mapping.copy() - logic_rules = world.options.logic_rules.value + laurels_zips = world.options.laurels_zips.value + ice_grappling = world.options.ice_grappling.value + ladder_storage = world.options.ladder_storage.value fixed_shop = world.options.fixed_shop laurels_location = world.options.laurels_location traversal_reqs = deepcopy(traversal_requirements) @@ -142,19 +156,21 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # if it's not one of the EntranceRando options, it's a custom seed if world.options.entrance_rando.value not in EntranceRando.options.values(): seed_group = world.seed_groups[world.options.entrance_rando.value] - logic_rules = seed_group["logic_rules"] + laurels_zips = seed_group["laurels_zips"] + ice_grappling = seed_group["ice_grappling"] + ladder_storage = seed_group["ladder_storage"] fixed_shop = seed_group["fixed_shop"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False + logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) + # marking that you don't immediately have laurels if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"): has_laurels = False - shop_scenes: Set[str] = set() shop_count = 6 if fixed_shop: shop_count = 0 - shop_scenes.add("Overworld Redux") else: # if fixed shop is off, remove this portal for portal in portal_map: @@ -169,13 +185,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # create separate lists for dead ends and non-dead ends for portal in portal_map: - dead_end_status = tunic_er_regions[portal.region].dead_end + dead_end_status = world.er_regions[portal.region].dead_end if dead_end_status == DeadEnd.free: two_plus.append(portal) elif dead_end_status == DeadEnd.all_cats: dead_ends.append(portal) elif dead_end_status == DeadEnd.restricted: - if logic_rules: + if ice_grappling: two_plus.append(portal) else: dead_ends.append(portal) @@ -196,7 +212,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # make better start region stuff when/if implementing random start start_region = "Overworld" connected_regions.add(start_region) - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if world.options.entrance_rando.value in EntranceRando.options.values(): plando_connections = world.options.plando_connections.value @@ -225,12 +241,14 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) non_dead_end_regions = set() - for region_name, region_info in tunic_er_regions.items(): + for region_name, region_info in world.er_regions.items(): if not region_info.dead_end: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 2 and logic_rules: + # if ice grappling to places is in logic, both places stop being dead ends + elif region_info.dead_end == DeadEnd.restricted and ice_grappling: non_dead_end_regions.add(region_name) - elif region_info.dead_end == 3: + # secret gathering place and zig skip get weird, special handling + elif region_info.dead_end == DeadEnd.special: if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \ or (region_name == "Zig Skip Exit" and fixed_shop): non_dead_end_regions.add(region_name) @@ -239,6 +257,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: for connection in plando_connections: p_entrance = connection.entrance p_exit = connection.exit + # if you plando secret gathering place, need to know that during portal pairing + if "Secret Gathering Place Exit" in [p_entrance, p_exit]: + waterfall_plando = True portal1_dead_end = True portal2_dead_end = True @@ -285,16 +306,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: break # if it's not a dead end, it might be a shop if p_exit == "Shop Portal": - portal2 = Portal(name="Shop Portal", region="Shop", + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", destination="Previous Region", tag="_") + create_shop_region(world, regions) shop_count -= 1 # need to maintain an even number of portals total if shop_count < 0: shop_count += 2 - for p in portal_mapping: - if p.name == p_entrance: - shop_scenes.add(p.scene()) - break # and if it's neither shop nor dead end, it just isn't correct else: if not portal2: @@ -327,11 +345,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: else: raise Exception(f"{player_name} paired a dead end to a dead end in their " "plando connections.") - waterfall_plando = True portal_pairs[portal1] = portal2 # if we have plando connections, our connected regions may change somewhat - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"): portal1 = None @@ -343,7 +360,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: raise Exception(f"Failed to do Fixed Shop option. " f"Did {player_name} plando connection the Windmill Shop entrance?") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 two_plus.remove(portal1) @@ -393,7 +412,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: if waterfall_plando: cr = connected_regions.copy() cr.add(portal.region) - if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules): + if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks): continue elif portal.region != "Secret Gathering Place": continue @@ -405,9 +424,9 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # once we have both portals, connect them and add the new region(s) to connected_regions if check_success == 2: - connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules) if "Secret Gathering Place" in connected_regions: has_laurels = True + connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) portal_pairs[portal1] = portal2 check_success = 0 random_object.shuffle(two_plus) @@ -418,16 +437,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: shop_count = 0 for i in range(shop_count): - portal1 = None - for portal in two_plus: - if portal.scene() not in shop_scenes: - shop_scenes.add(portal.scene()) - portal1 = portal - two_plus.remove(portal) - break + portal1 = two_plus.pop() if portal1 is None: - raise Exception("Too many shops in the pool, or something else went wrong.") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + raise Exception("TUNIC: Too many shops in the pool, or something else went wrong.") + portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}", + destination="Previous Region", tag="_") + create_shop_region(world, regions) portal_pairs[portal1] = portal2 @@ -460,13 +475,12 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic region1 = regions[portal1.region] region2 = regions[portal2.region] region1.connect(connecting_region=region2, name=portal1.name) - # prevent the logic from thinking you can get to any shop-connected region from the shop - if portal2.name not in {"Shop", "Shop Portal"}: - region2.connect(connecting_region=region1, name=portal2.name) + region2.connect(connecting_region=region1, name=portal2.name) def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]], - has_laurels: bool, logic: int) -> Set[str]: + has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]: + zips, ice_grapples, ls = logic # starting count, so we can run it again if this changes region_count = len(connected_regions) for origin, destinations in traversal_reqs.items(): @@ -485,11 +499,15 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s if req == "Hyperdash": if not has_laurels: break - elif req == "NMG": - if not logic: + elif req == "Zip": + if not zips: break - elif req == "UR": - if logic < 2: + # if req is higher than logic option, then it breaks since it's not a valid connection + elif req.startswith("IG"): + if int(req[-1]) > ice_grapples: + break + elif req.startswith("LS"): + if int(req[-1]) > ls: break elif req not in connected_regions: break diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index e0ee17831a..55aa3468fc 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -166,6 +166,7 @@ item_table: Dict[str, TunicItemData] = { "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "Ladders"), } +# items to be replaced by fool traps fool_tiers: List[List[str]] = [ [], ["Money x1", "Money x10", "Money x15", "Money x16"], @@ -173,6 +174,7 @@ fool_tiers: List[List[str]] = [ ["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"], ] +# items we'll want the location of in slot data, for generating in-game hints slot_data_item_names = [ "Stick", "Sword", diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py new file mode 100644 index 0000000000..a29d50b4f4 --- /dev/null +++ b/worlds/tunic/ladder_storage_data.py @@ -0,0 +1,186 @@ +from typing import Dict, List, Set, NamedTuple, Optional + + +# ladders in overworld, since it is the most complex area for ladder storage +class OWLadderInfo(NamedTuple): + ladders: Set[str] # ladders where the top or bottom is at the same elevation + portals: List[str] # portals at the same elevation, only those without doors + regions: List[str] # regions where a melee enemy can hit you out of ladder storage + + +# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets +ow_ladder_groups: Dict[str, OWLadderInfo] = { + # lowest elevation + "LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"}, + ["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_", + "Town Basement_beach", "Archipelagos Redux_lower", "Archipelagos Redux_lowest"], + ["Overworld Beach"]), + # also the east filigree room + "LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"}, + ["Furnace_gyro_lower", "Swamp Redux 2_wall"], + ["Overworld Tunnel Turret"]), + # also the fountain filigree room and ruined passage door + "LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"}, + ["Archipelagos Redux_upper", "Ruins Passage_east"], + ["After Ruined Passage"]), + # also old house door + "LS Elev 3": OWLadderInfo({"Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell", + "Ladders in Overworld Town"}, + [], + ["Overworld after Envoy", "East Overworld"]), + # skip top of top ladder next to weathervane level, does not provide logical access to anything + "LS Elev 4": OWLadderInfo({"Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Well", + "Ladders in Overworld Town"}, + ["Darkwoods Tunnel_"], + []), + "LS Elev 5": OWLadderInfo({"Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"}, + ["PatrolCave_", "Forest Belltower_", "Fortress Courtyard_", "ShopSpecial_"], + ["East Overworld"]), + # skip top of belltower, middle of dark tomb ladders, and top of checkpoint, does not grant access to anything + "LS Elev 6": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters"}, + ["Temple_rafters"], + ["Overworld above Patrol Cave"]), + # in-line with the chest above dark tomb, gets you up the mountain stairs + "LS Elev 7": OWLadderInfo({"Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"}, + ["Mountain_"], + ["Upper Overworld"]), +} + + +# ladders accessible within different regions of overworld, only those that are relevant +# other scenes will just have them hardcoded since this type of structure is not necessary there +region_ladders: Dict[str, Set[str]] = { + "Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", + "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"}, + "Overworld Beach": {"Ladder to Ruined Atoll"}, + "Overworld at Patrol Cave": {"Ladders near Patrol Cave"}, + "Overworld Quarry Entry": {"Ladder to Quarry"}, + "Overworld Belltower": {"Ladders to West Bell"}, + "Overworld after Temple Rafters": {"Ladders near Temple Rafters"}, +} + + +class LadderInfo(NamedTuple): + origin: str # origin region + destination: str # destination portal + ladders_req: Optional[str] = None # ladders required to do this + dest_is_region: bool = False # whether it is a region that you are going to + + +easy_ls: List[LadderInfo] = [ + # In the furnace + # Furnace ladder to the fuse entrance + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"), + # Furnace ladder to Dark Tomb + LadderInfo("Furnace Ladder Area", "Furnace, Crypt Redux_"), + # Furnace ladder to the West Garden connector + LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west"), + + # West Garden + # exit after Garden Knight + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"), + # West Garden laurels exit + LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"), + + # Atoll, use the little ladder you fix at the beginning + LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"), + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth"), # special case + + # East Forest + # Entrance by the dancing fox holy cross spot + LadderInfo("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper"), + + # From the west side of Guard House 1 to the east side + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate"), + LadderInfo("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_"), + + # Fortress Exterior + # shop, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_"), + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower"), + # Use the top of the ladder by the telescope + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_"), + + # same as above, except from the east side of the area + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower"), + + # same as above, except from the Beneath the Vault entrance ladder + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", + "Ladder to Beneath the Vault"), + + # Swamp to Gauntlet + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", "Ladders in Swamp"), + + # Ladder by the hero grave + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Shop_"), +] + +# if we can gain elevation or get knocked down, add the harder ones +medium_ls: List[LadderInfo] = [ + # region-destination versions of easy ls spots + LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True), + # fortress courtyard knockdowns are never logically relevant, the fuse requires upper + LadderInfo("Back of Swamp", "Swamp Mid", dest_is_region=True), + LadderInfo("Back of Swamp", "Swamp Front", dest_is_region=True), + + # gain height off the northeast fuse ramp + LadderInfo("Ruined Atoll", "Atoll Redux, Frog Stairs_eye"), + + # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch + LadderInfo("Forest Grave Path Main", "Sword Access, East Forest Redux_upper"), + + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + LadderInfo("Fortress Exterior from Overworld", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_"), + LadderInfo("Fortress Exterior from East Forest", "Fortress Courtyard Upper", dest_is_region=True), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", + "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", "Ladder to Beneath the Vault"), + LadderInfo("Fortress Exterior near cave", "Fortress Courtyard Upper", "Ladder to Beneath the Vault", + dest_is_region=True), + + # need to gain height to get up the stairs + LadderInfo("Lower Mountain", "Mountain, Mountaintop_"), + + # Where the rope is behind Monastery + LadderInfo("Quarry Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Monastery Entry", "Quarry Redux, Monastery_back"), + LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"), + + LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"), + LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True), + + # Swamp to Overworld upper + LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"), +] + +hard_ls: List[LadderInfo] = [ + # lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall + LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"), + LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True), + # go through the hexagon engraving above the vault door + LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"), + # the turret at the end here is not affected by enemy rando + LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True), + # todo: see if we can use that new laurels strat here + # LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"), + # go behind the cathedral to reach the door, pretty easily doable + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_main", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_main"), + # need to do hc midair, probably cannot get into this without hc + LadderInfo("Swamp Mid", "Swamp Redux 2, Cathedral Redux_secret", "Ladders in Swamp"), + LadderInfo("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret"), +] diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 0991622816..442e0c0144 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -47,7 +47,7 @@ location_table: Dict[str, TunicLocationData] = { "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="Holy Cross"), "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="Holy Cross"), - "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "Lower Forest"), "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), @@ -205,7 +205,7 @@ location_table: Dict[str, TunicLocationData] = { "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"), "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"), "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"), - "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Monastery - Monastery Chest": TunicLocationData("Monastery", "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 Back", "Quarry Back"), @@ -224,7 +224,7 @@ location_table: Dict[str, TunicLocationData] = { "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 92cbafba23..1683b3ca5a 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Dict, Any from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, - PerGameCommonOptions, OptionGroup) + PerGameCommonOptions, OptionGroup, Visibility) from .er_data import portal_mapping @@ -39,27 +39,6 @@ class AbilityShuffling(Toggle): display_name = "Shuffle Abilities" -class LogicRules(Choice): - """ - Set which logic rules to use for your world. - Restricted: Standard logic, no glitches. - No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. - * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer. - Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. - * Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. - * Using Ladder Storage to get to individual chests is not in logic to avoid tedium. - * Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. - """ - internal_name = "logic_rules" - display_name = "Logic Rules" - option_restricted = 0 - option_no_major_glitches = 1 - alias_nmg = 1 - option_unrestricted = 2 - alias_ur = 2 - default = 0 - - class Lanternless(Toggle): """ Choose whether you require the Lantern for dark areas. @@ -173,8 +152,8 @@ class ShuffleLadders(Toggle): """ internal_name = "shuffle_ladders" display_name = "Shuffle Ladders" - - + + class TunicPlandoConnections(PlandoConnections): """ Generic connection plando. Format is: @@ -189,6 +168,82 @@ class TunicPlandoConnections(PlandoConnections): duplicate_exits = True +class LaurelsZips(Toggle): + """ + Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots. + Notable inclusions are the Monastery gate, Ruined Passage door, Old House gate, Forest Grave Path gate, and getting from the Back of Swamp to the Middle of Swamp. + """ + internal_name = "laurels_zips" + display_name = "Laurels Zips Logic" + + +class IceGrappling(Choice): + """ + Choose whether grappling frozen enemies is in logic. + Easy includes ice grappling enemies that are in range without luring them. May include clips through terrain. + Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. + Hard includes luring or grappling enemies to get to where you want to go. + The Medium and Hard options will give the player the Torch to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Note: You will still be expected to ice grapple to the slime in East Forest from below with this option off. + """ + internal_name = "ice_grappling" + display_name = "Ice Grapple Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorage(Choice): + """ + Choose whether Ladder Storage is in logic. + Easy includes uses of Ladder Storage to get to open doors over a long distance without too much difficulty. May include convenient elevation changes (going up Mountain stairs, stairs in front of Special Shop, etc.). + Medium includes the above as well as changing your elevation using the environment and getting knocked down by melee enemies mid-LS. + Hard includes the above as well as going behind the map to enter closed doors from behind, shooting a fuse with the magic wand to knock yourself down at close range, and getting into the Cathedral Secret Legend room mid-LS. + Enabling any of these difficulty options will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Using the Torch is considered in logic. + Opening individual chests while doing ladder storage is excluded due to tedium. + Knocking yourself out of LS with a bomb is excluded due to the problematic nature of consumables in logic. + """ + internal_name = "ladder_storage" + display_name = "Ladder Storage Logic" + option_off = 0 + option_easy = 1 + option_medium = 2 + option_hard = 3 + default = 0 + + +class LadderStorageWithoutItems(Toggle): + """ + If disabled, you logically require Stick, Sword, or Magic Orb to perform Ladder Storage. + If enabled, you will be expected to perform Ladder Storage without progression items. + This can be done with the plushie code, a Golden Coin, Prayer, and many other options. + + This option has no effect if you do not have Ladder Storage Logic enabled. + """ + internal_name = "ladder_storage_without_items" + display_name = "Ladder Storage without Items" + + +class LogicRules(Choice): + """ + This option has been superseded by the individual trick options. + If set to nmg, it will set Ice Grappling to medium and Laurels Zips on. + If set to ur, it will do nmg as well as set Ladder Storage to medium. + It is here to avoid breaking old yamls, and will be removed at a later date. + """ + visibility = Visibility.none + internal_name = "logic_rules" + display_name = "Logic Rules" + option_restricted = 0 + option_no_major_glitches = 1 + alias_nmg = 1 + option_unrestricted = 2 + alias_ur = 2 + default = 0 + + @dataclass class TunicOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool @@ -199,22 +254,30 @@ class TunicOptions(PerGameCommonOptions): shuffle_ladders: ShuffleLadders entrance_rando: EntranceRando fixed_shop: FixedShop - logic_rules: LogicRules fool_traps: FoolTraps hexagon_quest: HexagonQuest hexagon_goal: HexagonGoal extra_hexagon_percentage: ExtraHexagonPercentage + laurels_location: LaurelsLocation lanternless: Lanternless maskless: Maskless - laurels_location: LaurelsLocation + laurels_zips: LaurelsZips + ice_grappling: IceGrappling + ladder_storage: LadderStorage + ladder_storage_without_items: LadderStorageWithoutItems plando_connections: TunicPlandoConnections + + logic_rules: LogicRules tunic_option_groups = [ OptionGroup("Logic Options", [ - LogicRules, Lanternless, Maskless, + LaurelsZips, + IceGrappling, + LadderStorage, + LadderStorageWithoutItems ]) ] @@ -231,9 +294,12 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = { "Glace Mode": { "accessibility": "minimal", "ability_shuffling": True, - "entrance_rando": "yes", + "entrance_rando": True, "fool_traps": "onslaught", - "logic_rules": "unrestricted", + "laurels_zips": True, + "ice_grappling": "hard", + "ladder_storage": "hard", + "ladder_storage_without_items": True, "maskless": True, "lanternless": True, }, diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index c30a44bb8f..93ec5640e0 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -16,7 +16,8 @@ tunic_regions: Dict[str, Set[str]] = { "Eastern Vault Fortress": {"Beneath the Vault"}, "Beneath the Vault": {"Eastern Vault Fortress"}, "Quarry Back": {"Quarry"}, - "Quarry": {"Lower Quarry"}, + "Quarry": {"Monastery", "Lower Quarry"}, + "Monastery": set(), "Lower Quarry": {"Rooted Ziggurat"}, "Rooted Ziggurat": set(), "Swamp": {"Cathedral"}, diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 6875686903..942bbc773a 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -3,7 +3,7 @@ from typing import Dict, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item, add_rule from BaseClasses import CollectionState -from .options import TunicOptions +from .options import TunicOptions, LadderStorage, IceGrappling if TYPE_CHECKING: from . import TunicWorld @@ -27,10 +27,10 @@ green_hexagon = "Green Questagon" blue_hexagon = "Blue Questagon" gold_hexagon = "Gold Questagon" +# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", - "Quarry - [West] Upper Area Bombable Wall", "Quarry - [East] Bombable Wall", - "Ruined Atoll - [Northwest] Bombable Wall"] + "Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"] def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: @@ -64,32 +64,33 @@ 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, world: "TunicWorld") -> bool: - player = world.player - if not world.options.logic_rules: +def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool: + return world.options.laurels_zips and state.has(laurels, world.player) + + +def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool: + if world.options.ice_grappling < difficulty: return False if not long_range: - return state.has_all({ice_dagger, grapple}, player) + return state.has_all({ice_dagger, grapple}, world.player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) + return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) + if not world.options.ladder_storage: + return False + if world.options.ladder_storage_without_items: + return True + return has_stick(state, world.player) or state.has(grapple, world.player) def has_mask(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.maskless: - return True - else: - return state.has(mask, world.player) + return world.options.maskless or state.has(mask, world.player) def has_lantern(state: CollectionState, world: "TunicWorld") -> bool: - if world.options.lanternless: - return True - else: - return state.has(lantern, world.player) + return world.options.lanternless or state.has(lantern, world.player) def set_region_rules(world: "TunicWorld") -> None: @@ -102,12 +103,14 @@ def set_region_rules(world: "TunicWorld") -> None: lambda state: has_stick(state, player) or state.has(fire_wand, player) world.get_entrance("Overworld -> Dark Tomb").access_rule = \ lambda state: has_lantern(state, world) + # laurels in, ladder storage in through the furnace, or ice grapple down the belltower world.get_entrance("Overworld -> West Garden").access_rule = \ - lambda state: state.has(laurels, player) \ - or can_ladder_storage(state, world) + lambda state: (state.has(laurels, player) + or can_ladder_storage(state, world) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \ lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, world) \ + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \ or can_ladder_storage(state, world) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules world.get_entrance("Overworld -> Beneath the Vault").access_rule = \ @@ -124,8 +127,8 @@ def set_region_rules(world: "TunicWorld") -> None: world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) world.get_entrance("Swamp -> Cathedral").access_rule = \ - lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ - or has_ice_grapple_logic(False, state, world) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \ + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) world.get_entrance("Overworld -> Spirit Arena").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) @@ -133,10 +136,18 @@ def set_region_rules(world: "TunicWorld") -> None: and has_ability(prayer, state, world) and has_sword(state, player) and state.has_any({lantern, laurels}, player)) + world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"), + rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world) + and has_ability(prayer, state, world)) + + if options.ladder_storage >= LadderStorage.option_medium: + # ls at any ladder in a safe spot in quarry to get to the monastery rope entrance + world.get_region("Quarry Back").connect(world.get_region("Monastery"), + rule=lambda state: can_ladder_storage(state, world)) + def set_location_rules(world: "TunicWorld") -> None: player = world.player - options = world.options forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -147,11 +158,13 @@ def set_location_rules(world: "TunicWorld") -> None: 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))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("Fortress Courtyard - Page Near Cave"), 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))) + or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) + and has_lantern(state, world))) set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"), lambda state: has_ability(holy_cross, state, world)) set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"), @@ -186,17 +199,17 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Old House - Normal Chest"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Old House - Holy Cross Chest"), 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))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world))) set_rule(world.get_location("Old House - Shield Pickup"), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or laurels_zip(state, world)) set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Overworld - [Southwest] From West Garden"), @@ -206,7 +219,7 @@ def set_location_rules(world: "TunicWorld") -> None: or (has_lantern(state, world) and has_sword(state, player)) or can_ladder_storage(state, world)) set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"), - lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + lambda state: state.has_any({grapple, laurels}, player)) set_rule(world.get_location("Overworld - [East] Grapple Chest"), lambda state: state.has(grapple, player)) set_rule(world.get_location("Special Shop - Secret Page Pickup"), @@ -215,11 +228,11 @@ def set_location_rules(world: "TunicWorld") -> None: 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))) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Sealed Temple - Page Pickup"), lambda state: 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)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("West Furnace - Lantern Pickup"), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -254,7 +267,7 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world)) set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"), lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) - or has_ice_grapple_logic(True, state, world)) + or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"), @@ -265,12 +278,15 @@ def set_location_rules(world: "TunicWorld") -> None: # Ruined Atoll set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"), lambda state: state.has(laurels, player)) + # ice grapple push a crab through the door set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"), - lambda state: state.has(laurels, player) or state.has(key, player, 2)) + lambda state: state.has(laurels, player) or state.has(key, player, 2) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) set_rule(world.get_location("Librarian - Hexagon Green"), - lambda state: has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player)) # Frog's Domain set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"), @@ -285,10 +301,12 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"), lambda state: has_sword(state, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Fortress Arena - Hexagon Red"), lambda state: state.has(vault_key, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) # Beneath the Vault set_rule(world.get_location("Beneath the Fortress - Bridge"), @@ -301,14 +319,14 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player)) - # nmg - kill boss scav with orb + firecracker, or similar set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"), - lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) + lambda state: has_sword(state, player)) # Swamp set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) + and (state.has(laurels, player) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))) set_rule(world.get_location("Swamp - [Entrance] Above Entryway"), lambda state: state.has(laurels, player)) set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"), @@ -335,8 +353,16 @@ def set_location_rules(world: "TunicWorld") -> None: # Bombable Walls for location_name in bomb_walls: # has_sword is there because you can buy bombs in the shop - set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or has_sword(state, player)) + set_rule(world.get_location(location_name), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) add_rule(world.get_location("Cube Cave - Holy Cross Chest"), + lambda state: state.has(gun, player) + or has_sword(state, player) + or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) + # can't ice grapple to this one, not enough space + set_rule(world.get_location("Quarry - [East] Bombable Wall"), lambda state: state.has(gun, player) or has_sword(state, player)) # Shop diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index 72d4a498d1..bbceb7468f 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -68,3 +68,57 @@ class TestER(TunicTestBase): self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) self.collect_by_name(["Pages 42-43 (Holy Cross)"]) self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross")) + + +class TestERSpecial(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.IceGrappling.internal_name: options.IceGrappling.option_easy, + "plando_connections": [ + { + "entrance": "Stick House Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + { + "entrance": "Ziggurat Lower to Ziggurat Tower", + "exit": "Secret Gathering Place Exit" + } + ]} + # with these plando connections, you need to ice grapple from the back of lower zig to the front to get laurels + + +# ensure that ladder storage connections connect to the outlet region, not the portal's region +class TestLadderStorage(TunicTestBase): + options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, + options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, + options.HexagonQuest.internal_name: options.HexagonQuest.option_false, + options.FixedShop.internal_name: options.FixedShop.option_false, + options.LadderStorage.internal_name: options.LadderStorage.option_hard, + options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false, + "plando_connections": [ + { + "entrance": "Fortress Courtyard Shop", + # "exit": "Ziggurat Portal Room Exit" + "exit": "Spawn to Far Shore" + }, + { + "entrance": "Fortress Courtyard to Beneath the Vault", + "exit": "Stick House Exit" + }, + { + "entrance": "Stick House Entrance", + "exit": "Fortress Courtyard to Overworld" + }, + { + "entrance": "Old House Waterfall Entrance", + "exit": "Ziggurat Portal Room Entrance" + }, + ]} + + def test_ls_to_shop_entrance(self) -> None: + self.collect_by_name(["Magic Orb"]) + self.assertFalse(self.can_reach_location("Fortress Courtyard - Page Near Cave")) + self.collect_by_name(["Pages 24-25 (Prayer)"]) + self.assertTrue(self.can_reach_location("Fortress Courtyard - Page Near Cave")) From d90cf0db656830d2f900884b36c2e1e6476a1fc1 Mon Sep 17 00:00:00 2001 From: neocerber <140952826+neocerber@users.noreply.github.com> Date: Sun, 8 Sep 2024 08:46:34 -0400 Subject: [PATCH 207/222] SC2 EN/FR documentation update (#3440) * Draft of SC2 EN documentation update: added hotkey, known issues; enhanced goal and prog balancing description. Added place holder for changes to apply in the French documentation. * Enforced StarCraft over Starcraft, added information on locations in the FR documentation * Removed a mention to a no longer available third link in the required software (since download_data deprecated the need to do it by hand) * First version of FR campaign restriction for sc2; rewriting (FR/EN) of randomizer goal description * Finished description for sc2 AP goal , minor formating * Added, both en/fr, indications that logic is locations wise and not mission wise (i.e. you might need to dip) * Enforced the 120 carac limit to last commit * Removed mention of needing to use the weighted option page to exlcude unit/upgrades since it is not longer the case in AP v0.5.0 * Added mention of /received being different in SC2 client (both language). Added Known issues in the FR version. * Simplified the text a bit and corrected some errors * Enforced, again, Star-C-raft; setting -> option; applied sugg for readability enhancement --- worlds/sc2/docs/en_Starcraft 2.md | 85 +++++++++++++++++------ worlds/sc2/docs/fr_Starcraft 2.md | 34 +++++++++- worlds/sc2/docs/setup_en.md | 109 ++++++++++++++++++++++-------- worlds/sc2/docs/setup_fr.md | 20 +++--- 4 files changed, 188 insertions(+), 60 deletions(-) diff --git a/worlds/sc2/docs/en_Starcraft 2.md b/worlds/sc2/docs/en_Starcraft 2.md index 06464e3cd2..813fdb5f4a 100644 --- a/worlds/sc2/docs/en_Starcraft 2.md +++ b/worlds/sc2/docs/en_Starcraft 2.md @@ -1,4 +1,4 @@ -# Starcraft 2 +# StarCraft 2 ## Game page in other languages: * [Français](/games/Starcraft%202/info/fr) @@ -7,9 +7,11 @@ The following unlocks are randomized as items: 1. Your ability to build any non-worker unit. -2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss! +2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain +choices simultaneously for Zerg and every Spear of Adun upgrade simultaneously for Protoss! 3. Your ability to get the generic unit upgrades, such as attack and armour upgrades. -4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades for Zerg, and Spear of Adun upgrades for Protoss. +4. Other miscellaneous upgrades such as laboratory upgrades and mercenaries for Terran, Kerrigan levels and upgrades +for Zerg, and Spear of Adun upgrades for Protoss. 5. Small boosts to your starting mineral, vespene gas, and supply totals on each mission. You find items by making progress in these categories: @@ -18,50 +20,91 @@ You find items by making progress in these categories: * Reaching milestones in the mission, such as completing part of a main objective * Completing challenges based on achievements in the base game, such as clearing all Zerg on Devil's Playground -Except for mission completion, these categories can be disabled in the game's settings. For instance, you can disable getting items for reaching required milestones. +In Archipelago's nomenclature, these are the locations where items can be found. +Each location, including mission completion, has a set of rules that specify the items required to access it. +These rules were designed assuming that StarCraft 2 is played on the Brutal difficulty. +Since each location has its own rule, it's possible that an item required for progression is in a mission where you +can't reach all of its locations or complete it. +However, mission completion is always required to gain access to new missions. + +Aside from mission completion, the other location categories can be disabled in the player options. +For instance, you can disable getting items for reaching required milestones. When you receive items, they will immediately become available, even during a mission, and you will be -notified via a text box in the top-right corner of the game screen. Item unlocks are also logged in the Archipelago client. +notified via a text box in the top-right corner of the game screen. +Item unlocks are also logged in the Archipelago client. -Missions are launched through the Starcraft 2 Archipelago client, through the Starcraft 2 Launcher tab. The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. Additionally, metaprogression currencies such as credits and Solarite are not used. +Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab. +The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included. +Additionally, metaprogression currencies such as credits and Solarite are not used. ## What is the goal of this game when randomized? -The goal is to beat the final mission in the mission order. The yaml configuration file controls the mission order and how missions are shuffled. +The goal is to beat the final mission in the mission order. +The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four +StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled. +Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the +expected time to complete the world. +Note that the evolution missions from Heart of the Swarm are not included in the randomizer. -## What non-randomized changes are there from vanilla Starcraft 2? +## What non-randomized changes are there from vanilla StarCraft 2? 1. Some missions have more vespene geysers available to allow a wider variety of units. -2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, brood war, and original ideas. -3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer have tech requirements. +2. Many new units and upgrades have been added as items, coming from co-op, melee, later campaigns, later expansions, +brood war, and original ideas. +3. Higher-tech production structures, including Factories, Starports, Robotics Facilities, and Stargates, no longer +have tech requirements. 4. Zerg missions have been adjusted to give the player a starting Lair where they would only have Hatcheries. -5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors taking longer to build. -6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them without getting stuck in odd places. +5. Upgrades with a downside have had the downside removed, such as automated refineries costing more or tech reactors +taking longer to build. +6. Unit collision within the vents in Enemy Within has been adjusted to allow larger units to travel through them +without getting stuck in odd places. 7. Several vanilla bugs have been fixed. ## Which of my items can be in another player's world? -By default, any of StarCraft 2's items (specified above) can be in another player's world. See the -[Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) -for more information on how to change this. +By default, any of StarCraft 2's items (specified above) can be in another player's world. +See the [Advanced YAML Guide](/tutorial/Archipelago/advanced_settings/en) for more information on how to change this. ## Unique Local Commands -The following commands are only available when using the Starcraft 2 Client to play with Archipelago. You can list them any time in the client with `/help`. +The following commands are only available when using the StarCraft 2 Client to play with Archipelago. +You can list them any time in the client with `/help`. -* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. Will overwrite existing files +* `/download_data` Download the most recent release of the necessary files for playing SC2 with Archipelago. +Will overwrite existing files * `/difficulty [difficulty]` Overrides the difficulty set for the world. * Options: casual, normal, hard, brutal * `/game_speed [game_speed]` Overrides the game speed for the world * Options: default, slower, slow, normal, fast, faster * `/color [faction] [color]` Changes your color for one of your playable factions. * Faction options: raynor, kerrigan, primal, protoss, nova - * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, lightgreen, darkgrey, pink, rainbow, random, default + * Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, + brown, lightgreen, darkgrey, pink, rainbow, random, default * `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation. * Run without arguments to list all options. - * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource amounts, controlling AI allies, etc. -* `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing. -* `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided + * Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource + amounts, controlling AI allies, etc. +* `/disable_mission_check` Disables the check to see if a mission is available to play. +Meant for co-op runs where one player can play the next mission in a chain the other player is doing. +* `/play [mission_id]` Starts a StarCraft 2 mission based off of the mission_id provided * `/available` Get what missions are currently available to play * `/unfinished` Get what missions are currently available to play and have not had all locations checked * `/set_path [path]` Manually set the SC2 install directory (if the automatic detection fails) + +Note that the behavior of the command `/received` was modified in the StarCraft 2 client. +In the Common client of Archipelago, the command returns the list of items received in the reverse order they were +received. +In the StarCraft 2 client, the returned list will be divided by races (i.e., Any, Protoss, Terran, and Zerg). +Additionally, upgrades are grouped beneath their corresponding units or buildings. +A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown. +Every item whose name, race, or group name contains the provided parameter will be shown. + +## Known issues + +- StarCraft 2 Archipelago does not support loading a saved game. +For this reason, it is recommended to play on a difficulty level lower than what you are normally comfortable with. +- StarCraft 2 Archipelago does not support the restart of a mission from the StarCraft 2 menu. +To restart a mission, use the StarCraft 2 Client. +- A crash report is often generated when a mission is closed. +This does not affect the game and can be ignored. diff --git a/worlds/sc2/docs/fr_Starcraft 2.md b/worlds/sc2/docs/fr_Starcraft 2.md index 4fcc8e689b..092835c8e3 100644 --- a/worlds/sc2/docs/fr_Starcraft 2.md +++ b/worlds/sc2/docs/fr_Starcraft 2.md @@ -21,6 +21,14 @@ Les *items* sont trouvés en accomplissant du progrès dans les catégories suiv * Réussir des défis basés sur les succès du jeu de base, e.g. éliminer tous les *Zerg* dans la mission *Devil's Playground* +Dans la nomenclature d'Archipelago, il s'agit des *locations* où l'on peut trouver des *items*. +Pour chaque *location*, incluant le fait de terminer une mission, il y a des règles qui définissent les *items* +nécessaires pour y accéder. +Ces règles ont été conçues en assumant que *StarCraft 2* est joué à la difficulté *Brutal*. +Étant donné que chaque *location* a ses propres règles, il est possible qu'un *item* nécessaire à la progression se +trouve dans une mission dont vous ne pouvez pas atteindre toutes les *locations* ou que vous ne pouvez pas terminer. +Cependant, il est toujours nécessaire de terminer une mission pour pouvoir accéder à de nouvelles missions. + Ces catégories, outre la première, peuvent être désactivées dans les options du jeu. Par exemple, vous pouvez désactiver le fait d'obtenir des *items* lorsque des étapes importantes d'une mission sont accomplies. @@ -37,8 +45,13 @@ Archipelago*. ## Quel est le but de ce jeu quand il est *randomized*? -Le but est de réussir la mission finale dans la disposition des missions (e.g. *blitz*, *grid*, etc.). -Les choix faits dans le fichier *yaml* définissent la disposition des missions et comment elles sont mélangées. +Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.). +Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de +*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le +*mission order*. +Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles +peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde. +Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*. ## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2* @@ -93,3 +106,20 @@ mission de la chaîne qu'un autre joueur est en train d'entamer. l'accès à un *item* n'ont pas été accomplis. * `/set_path [path]` Permet de définir manuellement où *StarCraft 2* est installé ce qui est pertinent seulement si la détection automatique de cette dernière échoue. + +Notez que le comportement de la commande `/received` a été modifié dans le client *StarCraft 2*. +Dans le client *Common* d'Archipelago, elle renvoie la liste des *items* reçus dans l'ordre inverse de leur réception. +Dans le client de *StarCraft 2*, la liste est divisée par races (i.e., *Any*, *Protoss*, *Terran*, et *Zerg*). +De plus, les améliorations sont regroupées sous leurs unités/bâtiments correspondants. +Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés. +Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés. + +## Problèmes connus + +- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde. +Pour cette raison, il est recommandé de jouer à un niveau de difficulté inférieur à celui avec lequel vous êtes +normalement à l'aise. +- *StarCraft 2 Archipelago* ne supporte pas le redémarrage d'une mission depuis le menu de *StarCraft 2*. +Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*. +- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée. +Cela n'affecte pas le jeu et peut être ignoré. diff --git a/worlds/sc2/docs/setup_en.md b/worlds/sc2/docs/setup_en.md index 991ed57e87..5b378873f4 100644 --- a/worlds/sc2/docs/setup_en.md +++ b/worlds/sc2/docs/setup_en.md @@ -1,30 +1,39 @@ # StarCraft 2 Randomizer Setup Guide -This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as where -to obtain a config file for StarCraft 2. +This guide contains instructions on how to install and troubleshoot the StarCraft 2 Archipelago client, as well as +where to obtain a config file for StarCraft 2. ## Required Software - [StarCraft 2](https://starcraft2.com/en-us/) + - While StarCraft 2 Archipelago supports all four campaigns, they are not mandatory to play the randomizer. + If you do not own certain campaigns, you only need to exclude them in the configuration file of your world. - [The most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) ## How do I install this randomizer? -1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the Archipelago installer. +1. Install StarCraft 2 and Archipelago using the links above. The StarCraft 2 Archipelago client is downloaded by the +Archipelago installer. - Linux users should also follow the instructions found at the bottom of this page (["Running in Linux"](#running-in-linux)). 2. Run ArchipelagoStarcraft2Client.exe. - - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. -3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step + only. +3. Type the command `/download_data`. +This will automatically install the Maps and Data files needed to play StarCraft 2 Archipelago. ## Where do I get a config file (aka "YAML") for this game? -Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only using default options. +Yaml files are configuration files that tell Archipelago how you'd like your game to be randomized, even if you're only +using default options. When you're setting up a multiworld, every world needs its own yaml file. There are three basic ways to get a yaml: -* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export the yaml. -* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) page or by generating it from the Launcher (ArchipelagoLauncher.exe). The template includes descriptions of each option, you just have to edit it in your text editor of choice. +* You can go to the [Player Options](/games/Starcraft%202/player-options) page, set your options in the GUI, and export +the yaml. +* You can generate a template, either by downloading it from the [Player Options](/games/Starcraft%202/player-options) +page or by generating it from the Launcher (`ArchipelagoLauncher.exe`). +The template includes descriptions of each option, you just have to edit it in your text editor of choice. * You can ask someone else to share their yaml to use it for yourself or adjust it as you wish. Remember the name you enter in the options page or in the yaml file, you'll need it to connect later! @@ -36,15 +45,31 @@ Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for The simplest way to check is to use the website [validator](/check). -You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the Players/ folder within your Archipelago installation and run ArchipelagoGenerate.exe. You should see a new .zip file within the output/ folder of your Archipelago installation if things worked correctly. It's advisable to run ArchipelagoGenerate through a terminal so that you can see the printout, which will include any errors and the precise output file name if it's successful. If you don't like terminals, you can also check the log file in the logs/ folder. +You can also test it by attempting to generate a multiworld with your yaml. Save your yaml to the `Players/` folder +within your Archipelago installation and run `ArchipelagoGenerate.exe`. +You should see a new `.zip` file within the `output/` folder of your Archipelago installation if things worked +correctly. +It's advisable to run `ArchipelagoGenerate.exe` through a terminal so that you can see the printout, which will include +any errors and the precise output file name if it's successful. +If you don't like terminals, you can also check the log file in the `logs/` folder. #### What does Progression Balancing do? -For Starcraft 2, not much. It's an Archipelago-wide option meant to shift required items earlier in the playthrough, but Starcraft 2 tends to be much more open in what items you can use. As such, this adjustment isn't very noticeable. It can also increase generation times, so we generally recommend turning it off. +For StarCraft 2, this option doesn't have much impact. +It is an Archipelago option designed to balance world progression by swapping items in spheres. +If the Progression Balancing of one world is greater than that of others, items in that world are more likely to be +obtained early, and vice versa if its value is smaller. +However, StarCraft 2 is more permissive regarding the items that can be used to progress, so this option has little +influence on progression in a StarCraft 2 world. +StarCraft 2. +Since this option increases the time required to generate a MultiWorld, we recommend deactivating it (i.e., setting it +to zero) for a StarCraft 2 world. #### How do I specify items in a list, like in excluded items? -You can look up the syntax for yaml collections in the [YAML specification](https://yaml.org/spec/1.2.2/#21-collections). For lists, every item goes on its own line, started with a hyphen: +You can look up the syntax for yaml collections in the +[YAML specification](https://yaml.org/spec/1.2.2/#21-collections). +For lists, every item goes on its own line, started with a hyphen: ```yaml excluded_items: @@ -52,11 +77,13 @@ excluded_items: - Drop-Pods (Kerrigan Tier 7) ``` -An empty list is just a matching pair of square brackets: `[]`. That's the default value in the template, which should let you know to use this syntax. +An empty list is just a matching pair of square brackets: `[]`. +That's the default value in the template, which should let you know to use this syntax. #### How do I specify items for the starting inventory? -The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value: +The starting inventory is a YAML mapping rather than a list, which associates an item with the amount you start with. +The syntax looks like the item name, followed by a colon, then a whitespace character, and then the value: ```yaml start_inventory: @@ -64,37 +91,61 @@ start_inventory: Additional Starting Vespene: 5 ``` -An empty mapping is just a matching pair of curly braces: `{}`. That's the default value in the template, which should let you know to use this syntax. +An empty mapping is just a matching pair of curly braces: `{}`. +That's the default value in the template, which should let you know to use this syntax. #### How do I know the exact names of items and locations? -The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations for each game that it currently supports, including StarCraft 2. +The [*datapackage*](/datapackage) page of the Archipelago website provides a complete list of the items and locations +for each game that it currently supports, including StarCraft 2. -You can also look up a complete list of the item names in the [Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. +You can also look up a complete list of the item names in the +[Icon Repository](https://matthewmarinets.github.io/ap_sc2_icons/) page. This page also contains supplementary information of each item. -However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. +However, the items shown in that page might differ from those shown in the datapackage page of Archipelago since the +former is generated, most of the time, from beta versions of StarCraft 2 Archipelago undergoing development. -As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over the mission in the 'StarCraft 2 Launcher' tab in the client. +As for the locations, you can see all the locations associated to a mission in your world by placing your cursor over +the mission in the 'StarCraft 2 Launcher' tab in the client. ## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. - - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step + only. 2. Type `/connect [server ip]`. - If you're running through the website, the server IP should be displayed near the top of the room page. 3. Type your slot name from your YAML when prompted. 4. If the server has a password, enter that when prompted. -5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your world. Unreachable missions will have greyed-out text. Just click on an available mission to start it! +5. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see all the missions in your +world. +Unreachable missions will have greyed-out text. Just click on an available mission to start it! ## The game isn't launching when I try to start a mission. -First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out -the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a -specific description of what's going wrong and attach your log file to your message. +First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). +If you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel +for help. +Please include a specific description of what's going wrong and attach your log file to your message. + +## My keyboard shortcuts profile is not available when I play *StarCraft 2 Archipelago*. + +For your keyboard shortcuts profile to work in Archipelago, you need to copy your shortcuts file from +`Documents/StarCraft II/Accounts/######/Hotkeys` to `Documents/StarCraft II/Hotkeys`. +If the folder doesn't exist, create it. + +To enable StarCraft 2 Archipelago to use your profile, follow these steps: +1. Launch StarCraft 2 via the Battle.net application. +2. Change your hotkey profile to the standard mode and accept. +3. Select your custom profile and accept. + +You will only need to do this once. ## Running in macOS -To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](/tutorial/Archipelago/mac/en). Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: +[macOS Guide](/tutorial/Archipelago/mac/en). +Note: to launch the client, you will need to run the command `python3 Starcraft2Client.py`. ## Running in Linux @@ -102,9 +153,9 @@ To run StarCraft 2 through Archipelago in Linux, you will need to install the ga of the Archipelago client. Make sure you have StarCraft 2 installed using Wine, and that you have followed the -[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. You will not -need to copy the .dll files. If you're having trouble installing or running StarCraft 2 on Linux, I recommend using the -Lutris installer. +[installation procedures](#how-do-i-install-this-randomizer?) to add the Archipelago maps to the correct location. +You will not need to copy the `.dll` files. +If you're having trouble installing or running StarCraft 2 on Linux, it is recommend to use the Lutris installer. Copy the following into a .sh file, replacing the values of **WINE** and **SC2PATH** variables with the relevant locations, as well as setting **PATH_TO_ARCHIPELAGO** to the directory containing the AppImage if it is not in the same @@ -139,5 +190,5 @@ below, replacing **${ID}** with the numerical ID. lutris lutris:rungameid/${ID} --output-script sc2.sh This will get all of the relevant environment variables Lutris sets to run StarCraft 2 in a script, including the path -to the Wine binary that Lutris uses. You can then remove the line that runs the Battle.Net launcher and copy the code -above into the existing script. +to the Wine binary that Lutris uses. +You can then remove the line that runs the Battle.Net launcher and copy the code above into the existing script. diff --git a/worlds/sc2/docs/setup_fr.md b/worlds/sc2/docs/setup_fr.md index bb6c35bce1..d9b754572a 100644 --- a/worlds/sc2/docs/setup_fr.md +++ b/worlds/sc2/docs/setup_fr.md @@ -6,6 +6,10 @@ indications pour obtenir un fichier de configuration de *StarCraft 2 Archipelago ## Logiciels requis - [*StarCraft 2*](https://starcraft2.com/en-us/) + - Bien que *StarCraft 2 Archipelago* supporte les quatre campagnes, elles ne sont pas obligatoires pour jouer au + *randomizer*. + Si vous ne possédez pas certaines campagnes, il vous suffit de les exclure dans le fichier de configuration de + votre monde. - [La version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) ## Comment est-ce que j'installe ce *randomizer*? @@ -41,10 +45,6 @@ préférences. Prenez soin de vous rappeler du nom de joueur que vous avez inscrit dans la page à options ou dans le fichier *yaml* puisque vous en aurez besoin pour vous connecter à votre monde! -Notez que la page *Player options* ne permet pas de définir certaines des options avancées, e.g., l'exclusion de -certaines unités ou de leurs améliorations. -Utilisez la page [*Weighted Options*](/weighted-options) pour avoir accès à ces dernières. - Si vous désirez des informations et/ou instructions générales sur l'utilisation d'un fichier *yaml* pour Archipelago, veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml). @@ -66,15 +66,15 @@ dans le dossier `logs/`. #### À quoi sert l'option *Progression Balancing*? -Pour *Starcraft 2*, cette option ne fait pas grand-chose. +Pour *StarCraft 2*, cette option ne fait pas grand-chose. Il s'agit d'une option d'Archipelago permettant d'équilibrer la progression des mondes en interchangeant les *items* dans les *spheres*. Si le *Progression Balancing* d'un monde est plus grand que ceux des autres, les *items* de progression de ce monde ont plus de chance d'être obtenus tôt et vice-versa si sa valeur est plus petite que celle des autres mondes. -Cependant, *Starcraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à +Cependant, *StarCraft 2* est beaucoup plus permissif en termes d'*items* qui permettent de progresser, ce réglage à donc peu d'influence sur la progression dans *StarCraft 2*. Vu qu'il augmente le temps de génération d'un *MultiWorld*, nous recommandons de le désactiver, c-à-d le définir à -zéro, pour *Starcraft 2*. +zéro, pour *StarCraft 2*. #### Comment est-ce que je définis une liste d'*items*, e.g. pour l'option *excluded items*? @@ -122,6 +122,10 @@ Cependant, l'information présente dans cette dernière peut différer de celle puisqu'elle est générée, habituellement, à partir de la version en développement de *StarCraft 2 Archipelago* qui n'ont peut-être pas encore été inclus dans le site web d'Archipelago. +Pour ce qui concerne les *locations*, vous pouvez consulter tous les *locations* associés à une mission dans votre +monde en plaçant votre curseur sur la case correspondante dans l'onglet *StarCraft 2 Launcher* du client. + + ## Comment est-ce que je peux joindre un *MultiWorld*? 1. Exécuter `ArchipelagoStarcraft2Client.exe`. @@ -152,7 +156,7 @@ qui se trouve dans `Documents/StarCraft II/Accounts/######/Hotkeys` vers `Docume Si le dossier n'existe pas, créez-le. Pour que *StarCraft 2 Archipelago* utilise votre profil, suivez les étapes suivantes. -Lancez *Starcraft 2* via l'application *Battle.net*. +Lancez *StarCraft 2* via l'application *Battle.net*. Changez votre profil de raccourcis clavier pour le mode standard et acceptez, puis sélectionnez votre profil personnalisé et acceptez. Vous n'aurez besoin de faire ça qu'une seule fois. From 5021997df0997f0bd1151c6e5e523c38c4eafdac Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:13:01 -0500 Subject: [PATCH 208/222] Launcher: explicitly handle cli arguments to be passed to the Component (#3714) * adds handling for the `--` cli arg by having launcher capture, ignore, and pass through all of the values after it, while only processing (and validating) the values before it updates text client and its components to allow for args to be passed through, captured in run_as_textclient, and used in parse_args if present * Update worlds/LauncherComponents.py Co-authored-by: Aaron Wagener * explicitly using default args for parse_args when launched directly * revert manual arg parsing by request * Update CommonClient.py * Update LauncherComponents.py * :) --------- Co-authored-by: Aaron Wagener Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 4 ++-- Launcher.py | 5 ++++- worlds/LauncherComponents.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 7f91172acf..122de476fe 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1035,7 +1035,7 @@ def run_as_textclient(*args): parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") parser.add_argument('--name', default=None, help="Slot Name to connect as.") parser.add_argument("url", nargs="?", help="Archipelago connection url") - args = parser.parse_args(args if args else None) # this is necessary as long as CommonClient itself is launchable + args = parser.parse_args(args) if args.url: url = urllib.parse.urlparse(args.url) @@ -1053,4 +1053,4 @@ def run_as_textclient(*args): if __name__ == '__main__': logging.getLogger().setLevel(logging.INFO) # force log-level to work around log level resetting to WARNING - run_as_textclient() + run_as_textclient(*sys.argv[1:]) # default value for parse_args diff --git a/Launcher.py b/Launcher.py index 97903e2ad1..42f93547cc 100644 --- a/Launcher.py +++ b/Launcher.py @@ -401,7 +401,10 @@ if __name__ == '__main__': init_logging('Launcher') Utils.freeze_support() multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work - parser = argparse.ArgumentParser(description='Archipelago Launcher') + parser = argparse.ArgumentParser( + description='Archipelago Launcher', + usage="[-h] [--update_settings] [Patch|Game|Component] [-- component args here]" + ) run_group = parser.add_argument_group("Run") run_group.add_argument("--update_settings", action="store_true", help="Update host.yaml and exit.") diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 4c64642aba..fe6e44bb30 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -61,7 +61,7 @@ class Component: processes = weakref.WeakSet() -def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()): +def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None: global processes import multiprocessing process = multiprocessing.Process(target=func, name=name, args=args) @@ -85,7 +85,7 @@ class SuffixIdentifier: def launch_textclient(*args): import CommonClient - launch_subprocess(CommonClient.run_as_textclient, "TextClient", args) + launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args) def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]: From e4a5ed1cc45b4d58ba4ebdf095e5a990581bcee3 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 11:40:32 -0500 Subject: [PATCH 209/222] CommonClient: Explicitly parse url arg as an archipelago:// url (#3568) * Launcher "Text Client" --connect archipelago.gg:38281 should work, it doesn't, this fixes that * more explicit handling of expected values * removing launcher updates meaning this pr cannot stand alone but will not have merge issues later * add parser failure when an invalid url is found --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> --- CommonClient.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 122de476fe..911de4226d 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -1039,11 +1039,14 @@ def run_as_textclient(*args): if args.url: url = urllib.parse.urlparse(args.url) - args.connect = url.netloc - if url.username: - args.name = urllib.parse.unquote(url.username) - if url.password: - args.password = urllib.parse.unquote(url.password) + if url.scheme == "archipelago": + args.connect = url.netloc + if url.username: + args.name = urllib.parse.unquote(url.username) + if url.password: + args.password = urllib.parse.unquote(url.password) + else: + parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") colorama.init() From cabfef669a74936000975d911105075b51a79595 Mon Sep 17 00:00:00 2001 From: Jouramie <16137441+Jouramie@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:46:58 -0400 Subject: [PATCH 210/222] Stardew Valley: Fix masteries logic so it requires levels and tools (#3640) * fix and add test * add test to make sure we check xp can be earned * fix python 3.8 test my god I hope it gets removed soon * fixing some review comments * curse you monstersanity * move month rule to has_level vanilla, so next level is in logic once the previous item is received * use progressive masteries to skills in test alsanity * rename reset_collection_state * add more tests around skill and masteries rules * progressive level issue --------- Co-authored-by: agilbert1412 --- worlds/stardew_valley/logic/skill_logic.py | 72 +++++++------- worlds/stardew_valley/rules.py | 12 ++- worlds/stardew_valley/test/__init__.py | 8 +- .../stardew_valley/test/rules/TestSkills.py | 97 ++++++++++++++++--- 4 files changed, 137 insertions(+), 52 deletions(-) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 4d5567302a..17fabca28d 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -15,13 +15,13 @@ from .. import options 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_, False_, true_, And +from ..stardew_rule import StardewRule, true_, True_, False_ from ..strings.craftable_names import Fishing from ..strings.machine_names import Machine from ..strings.performance_names import Performance from ..strings.quality_names import ForageQuality from ..strings.region_names import Region -from ..strings.skill_names import Skill, all_mod_skills +from ..strings.skill_names import Skill, all_mod_skills, all_vanilla_skills from ..strings.tool_names import ToolMaterial, Tool from ..strings.wallet_item_names import Wallet @@ -43,22 +43,17 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): if level <= 0: return True_() - tool_level = (level - 1) // 2 + tool_level = min(4, (level - 1) // 2) tool_material = ToolMaterial.tiers[tool_level] - months = max(1, level - 1) - months_rule = self.logic.time.has_lived_months(months) - 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 = self.logic.skill.has_previous_level(skill, level) if skill == Skill.fishing: xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) elif skill == Skill.farming: 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.can_get_foraging_xp & self.logic.tool.has_tool(Tool.axe, tool_material)) |\ + 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) | \ @@ -70,22 +65,34 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): 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 previous_level_rule & months_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) + return previous_level_rule & self.logic.mod.skill.can_earn_mod_skill_level(skill, level) else: raise Exception(f"Unknown skill: {skill}") - return previous_level_rule & months_rule & xp_rule + return previous_level_rule & xp_rule # Should be cached def has_level(self, skill: str, level: int) -> StardewRule: - if level <= 0: - return True_() + assert level >= 0, f"There is no level before level 0." + if level == 0: + return true_ if self.options.skill_progression == options.SkillProgression.option_vanilla: return self.logic.skill.can_earn_level(skill, level) return self.logic.received(f"{skill} Level", level) + def has_previous_level(self, skill: str, level: int) -> StardewRule: + assert level > 0, f"There is no level before level 0." + if level == 1: + return true_ + + if self.options.skill_progression == options.SkillProgression.option_vanilla: + months = max(1, level - 1) + return self.logic.time.has_lived_months(months) + + return self.logic.received(f"{skill} Level", level - 1) + @cache_self1 def has_farming_level(self, level: int) -> StardewRule: return self.logic.skill.has_level(Skill.farming, level) @@ -108,18 +115,9 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): 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() + def has_any_skills_maxed(self, included_modded_skills: bool = True) -> StardewRule: + skills = self.content.skills.keys() if included_modded_skills else sorted(all_vanilla_skills) + return self.logic.or_(*(self.logic.skill.has_level(skill, 10) for skill in skills)) @cached_property def can_get_farming_xp(self) -> StardewRule: @@ -197,13 +195,19 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]): 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 can_earn_mastery(self, skill: str) -> StardewRule: + # Checking for level 11, so it includes having level 10 and being able to earn xp. + return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) 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") + if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: + return self.logic.received(f"{skill} Mastery") + + return self.logic.skill.can_earn_mastery(skill) + + @cached_property + 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_any_skills_maxed(included_modded_skills=False) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 89b1cf87c3..e9bdd8c25b 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -154,7 +154,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw 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}" + 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) @@ -168,13 +168,16 @@ def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: Sta mods = world_options.mods if world_options.skill_progression == SkillProgression.option_vanilla: return + 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: + + if world_options.skill_progression == SkillProgression.option_progressive: 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) + MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill)) def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int): @@ -256,8 +259,7 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S 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, 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) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 4dee0ebf6d..e7278cba28 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -85,7 +85,7 @@ def allsanity_no_mods_6_x_x(): 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.SkillProgression.internal_name: options.SkillProgression.option_progressive_with_masteries, 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, @@ -310,6 +310,12 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase): self.multiworld.worlds[self.player].total_progression_items -= 1 return created_item + def remove_one_by_name(self, item: str) -> None: + self.remove(self.create_item(item)) + + def reset_collection_state(self): + self.multiworld.state = self.original_state.copy() + pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/rules/TestSkills.py b/worlds/stardew_valley/test/rules/TestSkills.py index 1c6874f315..77adade886 100644 --- a/worlds/stardew_valley/test/rules/TestSkills.py +++ b/worlds/stardew_valley/test/rules/TestSkills.py @@ -1,23 +1,30 @@ -from ... import HasProgressionPercent +from ... import HasProgressionPercent, StardewLogic from ...options import ToolProgression, SkillProgression, Mods -from ...strings.skill_names import all_skills +from ...strings.skill_names import all_skills, all_vanilla_skills, Skill from ...test import SVTestBase -class TestVanillaSkillLogicSimplification(SVTestBase): +class TestSkillProgressionVanilla(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)) + rule = self.multiworld.worlds[1].logic.skill.has_level(Skill.farming, 8) + self.assertEqual(1, sum(1 for i in rule.current_rules if type(i) is HasProgressionPercent)) + + def test_has_mastery_requires_month_equivalent_to_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + time_rule = logic.time.has_lived_months(10) + + self.assertIn(time_rule, rule.current_rules) -class TestAllSkillsRequirePrevious(SVTestBase): +class TestSkillProgressionProgressive(SVTestBase): options = { - SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + SkillProgression.internal_name: SkillProgression.option_progressive, Mods.internal_name: frozenset(Mods.valid_keys), } @@ -25,16 +32,82 @@ class TestAllSkillsRequirePrevious(SVTestBase): 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}" + location = self.multiworld.get_location(location_name, self.player) + with self.subTest(location_name): - can_reach = self.can_reach_location(location_name) if level > 1: - self.assertFalse(can_reach) + self.assert_reach_location_false(location, self.multiworld.state) self.collect(f"{skill} Level") - can_reach = self.can_reach_location(location_name) - self.assertTrue(can_reach) - self.multiworld.state = self.original_state.copy() + + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_has_level_requires_exact_amount_of_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 8) + + self.assertEqual(level_rule, rule) + + def test_has_previous_level_requires_one_less_level_than_requested(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_previous_level(Skill.farming, 8) + level_rule = logic.received("Farming Level", 7) + + self.assertEqual(level_rule, rule) + + def test_has_mastery_requires_10_levels(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + level_rule = logic.received("Farming Level", 10) + + self.assertIn(level_rule, rule.current_rules) +class TestSkillProgressionProgressiveWithMasteryWithoutMods(SVTestBase): + options = { + SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, + ToolProgression.internal_name: ToolProgression.option_progressive, + Mods.internal_name: frozenset(), + } + def test_has_mastery_requires_the_item(self): + logic: StardewLogic = self.multiworld.worlds[1].logic + rule = logic.skill.has_mastery(Skill.farming) + received_mastery = logic.received("Farming Mastery") + + self.assertEqual(received_mastery, rule) + + def test_given_all_levels_when_can_earn_mastery_then_can_earn_mastery(self): + self.collect_everything() + + for skill in all_vanilla_skills: + with self.subTest(skill): + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_true(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_level_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + for skill in all_vanilla_skills: + with self.subTest(skill): + self.collect_everything() + self.remove_one_by_name(f"{skill} Level") + + location = self.multiworld.get_location(f"{skill} Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() + + def test_given_one_tool_missing_when_can_earn_mastery_then_cannot_earn_mastery(self): + self.collect_everything() + + self.remove_one_by_name(f"Progressive Pickaxe") + location = self.multiworld.get_location("Mining Mastery", self.player) + self.assert_reach_location_false(location, self.multiworld.state) + + self.reset_collection_state() From 05b257adf9bd9300acd4ff5584f6087f70716ad1 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 8 Sep 2024 09:48:48 -0700 Subject: [PATCH 211/222] Pokemon Emerald: Make use of `NamedTuple._replace` (#3727) --- worlds/pokemon_emerald/data.py | 6 ++---- worlds/pokemon_emerald/opponents.py | 6 +++--- worlds/pokemon_emerald/pokemon.py | 31 +++++++++++------------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index d89ab5febb..432d593873 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -276,15 +276,13 @@ def _str_to_pokemon_data_type(string: str) -> TrainerPokemonDataTypeEnum: return TrainerPokemonDataTypeEnum.ITEM_CUSTOM_MOVES -@dataclass -class TrainerPokemonData: +class TrainerPokemonData(NamedTuple): species_id: int level: int moves: Optional[Tuple[int, int, int, int]] -@dataclass -class TrainerPartyData: +class TrainerPartyData(NamedTuple): pokemon: List[TrainerPokemonData] pokemon_data_type: TrainerPokemonDataTypeEnum address: int diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index 09e947546d..966d192054 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Dict, List, Set -from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, TrainerPokemonData, data +from .data import NUM_REAL_SPECIES, UNEVOLVED_POKEMON, data from .options import RandomizeTrainerParties from .pokemon import filter_species_by_nearby_bst from .util import int_to_bool_array @@ -111,6 +111,6 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: hm_moves[3] if world.random.random() < 0.25 else level_up_moves[3] ) - new_party.append(TrainerPokemonData(new_species.species_id, pokemon.level, new_moves)) + new_party.append(pokemon._replace(species_id=new_species.species_id, moves=new_moves)) - trainer.party.pokemon = new_party + trainer.party = trainer.party._replace(pokemon=new_party) diff --git a/worlds/pokemon_emerald/pokemon.py b/worlds/pokemon_emerald/pokemon.py index c60e5e9d4f..fec1101dab 100644 --- a/worlds/pokemon_emerald/pokemon.py +++ b/worlds/pokemon_emerald/pokemon.py @@ -4,8 +4,7 @@ Functions related to pokemon species and moves import functools from typing import TYPE_CHECKING, Dict, List, Set, Optional, Tuple -from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, MiscPokemonData, - SpeciesData, data) +from .data import (NUM_REAL_SPECIES, OUT_OF_LOGIC_MAPS, EncounterTableData, LearnsetMove, SpeciesData, data) from .options import (Goal, HmCompatibility, LevelUpMoves, RandomizeAbilities, RandomizeLegendaryEncounters, RandomizeMiscPokemon, RandomizeStarters, RandomizeTypes, RandomizeWildPokemon, TmTutorCompatibility) @@ -461,7 +460,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: type_bias, normal_bias, species.types) else: new_move = 0 - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 # All moves from here onward are actual moves. @@ -473,7 +472,7 @@ def randomize_learnsets(world: "PokemonEmeraldWorld") -> None: new_move = get_random_move(world.random, {move.move_id for move in new_learnset} | world.blacklisted_moves, type_bias, normal_bias, species.types) - new_learnset.append(LearnsetMove(old_learnset[cursor].level, new_move)) + new_learnset.append(old_learnset[cursor]._replace(move_id=new_move)) cursor += 1 species.learnset = new_learnset @@ -581,8 +580,10 @@ def randomize_starters(world: "PokemonEmeraldWorld") -> None: picked_evolution = world.random.choice(potential_evolutions) for trainer_name, starter_position, is_evolved in rival_teams[i]: + new_species_id = picked_evolution if is_evolved else starter.species_id trainer_data = world.modified_trainers[data.constants[trainer_name]] - trainer_data.party.pokemon[starter_position].species_id = picked_evolution if is_evolved else starter.species_id + trainer_data.party.pokemon[starter_position] = \ + trainer_data.party.pokemon[starter_position]._replace(species_id=new_species_id) def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: @@ -594,10 +595,7 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: world.random.shuffle(shuffled_species) for i, encounter in enumerate(data.legendary_encounters): - world.modified_legendary_encounters.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_legendary_encounters.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.legendary_encounters in { RandomizeLegendaryEncounters.option_match_base_stats, @@ -621,9 +619,8 @@ def randomize_legendary_encounters(world: "PokemonEmeraldWorld") -> None: if should_match_bst: candidates = filter_species_by_nearby_bst(candidates, sum(original_species.base_stats)) - world.modified_legendary_encounters.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_legendary_encounters.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) @@ -637,10 +634,7 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: world.modified_misc_pokemon = [] for i, encounter in enumerate(data.misc_pokemon): - world.modified_misc_pokemon.append(MiscPokemonData( - shuffled_species[i], - encounter.address - )) + world.modified_misc_pokemon.append(encounter._replace(species_id=shuffled_species[i])) else: should_match_bst = world.options.misc_pokemon in { RandomizeMiscPokemon.option_match_base_stats, @@ -672,9 +666,8 @@ def randomize_misc_pokemon(world: "PokemonEmeraldWorld") -> None: if len(player_filtered_candidates) > 0: candidates = player_filtered_candidates - world.modified_misc_pokemon.append(MiscPokemonData( - world.random.choice(candidates).species_id, - encounter.address + world.modified_misc_pokemon.append(encounter._replace( + species_id=world.random.choice(candidates).species_id )) From 6d6d35d598dbb984b7b5cccf6567d5dc9e4ddd7d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 8 Sep 2024 11:50:08 -0500 Subject: [PATCH 212/222] Rogue Legacy: Update to Options API (#3755) * fix deprecation * multiworld.random -> world.random * Various small fixes --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Exempt-Medic --- worlds/rogue_legacy/Options.py | 78 ++++++++++--------- worlds/rogue_legacy/Regions.py | 29 +++---- worlds/rogue_legacy/Rules.py | 50 ++++++------ worlds/rogue_legacy/__init__.py | 111 +++++++++++++-------------- worlds/rogue_legacy/test/__init__.py | 2 +- 5 files changed, 138 insertions(+), 132 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index d8298c85c8..9210082f73 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -1,6 +1,6 @@ -from typing import Dict +from Options import Choice, Range, Toggle, DeathLink, DefaultOnToggle, OptionSet, PerGameCommonOptions -from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, OptionSet +from dataclasses import dataclass class StartingGender(Choice): @@ -336,42 +336,44 @@ class AvailableClasses(OptionSet): The upgraded form of your starting class will be available regardless. """ display_name = "Available Classes" - default = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + default = frozenset( + {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} + ) valid_keys = {"Knight", "Mage", "Barbarian", "Knave", "Shinobi", "Miner", "Spellthief", "Lich", "Dragon", "Traitor"} -rl_options: Dict[str, type(Option)] = { - "starting_gender": StartingGender, - "starting_class": StartingClass, - "available_classes": AvailableClasses, - "new_game_plus": NewGamePlus, - "fairy_chests_per_zone": FairyChestsPerZone, - "chests_per_zone": ChestsPerZone, - "universal_fairy_chests": UniversalFairyChests, - "universal_chests": UniversalChests, - "vendors": Vendors, - "architect": Architect, - "architect_fee": ArchitectFee, - "disable_charon": DisableCharon, - "require_purchasing": RequirePurchasing, - "progressive_blueprints": ProgressiveBlueprints, - "gold_gain_multiplier": GoldGainMultiplier, - "number_of_children": NumberOfChildren, - "free_diary_on_generation": FreeDiaryOnGeneration, - "khidr": ChallengeBossKhidr, - "alexander": ChallengeBossAlexander, - "leon": ChallengeBossLeon, - "herodotus": ChallengeBossHerodotus, - "health_pool": HealthUpPool, - "mana_pool": ManaUpPool, - "attack_pool": AttackUpPool, - "magic_damage_pool": MagicDamageUpPool, - "armor_pool": ArmorUpPool, - "equip_pool": EquipUpPool, - "crit_chance_pool": CritChanceUpPool, - "crit_damage_pool": CritDamageUpPool, - "allow_default_names": AllowDefaultNames, - "additional_lady_names": AdditionalNames, - "additional_sir_names": AdditionalNames, - "death_link": DeathLink, -} +@dataclass +class RLOptions(PerGameCommonOptions): + starting_gender: StartingGender + starting_class: StartingClass + available_classes: AvailableClasses + new_game_plus: NewGamePlus + fairy_chests_per_zone: FairyChestsPerZone + chests_per_zone: ChestsPerZone + universal_fairy_chests: UniversalFairyChests + universal_chests: UniversalChests + vendors: Vendors + architect: Architect + architect_fee: ArchitectFee + disable_charon: DisableCharon + require_purchasing: RequirePurchasing + progressive_blueprints: ProgressiveBlueprints + gold_gain_multiplier: GoldGainMultiplier + number_of_children: NumberOfChildren + free_diary_on_generation: FreeDiaryOnGeneration + khidr: ChallengeBossKhidr + alexander: ChallengeBossAlexander + leon: ChallengeBossLeon + herodotus: ChallengeBossHerodotus + health_pool: HealthUpPool + mana_pool: ManaUpPool + attack_pool: AttackUpPool + magic_damage_pool: MagicDamageUpPool + armor_pool: ArmorUpPool + equip_pool: EquipUpPool + crit_chance_pool: CritChanceUpPool + crit_damage_pool: CritDamageUpPool + allow_default_names: AllowDefaultNames + additional_lady_names: AdditionalNames + additional_sir_names: AdditionalNames + death_link: DeathLink diff --git a/worlds/rogue_legacy/Regions.py b/worlds/rogue_legacy/Regions.py index 5d07fccbc4..61b0ef73ec 100644 --- a/worlds/rogue_legacy/Regions.py +++ b/worlds/rogue_legacy/Regions.py @@ -1,15 +1,18 @@ -from typing import Dict, List, NamedTuple, Optional +from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import MultiWorld, Region, Entrance from .Locations import RLLocation, location_table, get_locations_by_category +if TYPE_CHECKING: + from . import RLWorld + class RLRegionData(NamedTuple): locations: Optional[List[str]] region_exits: Optional[List[str]] -def create_regions(multiworld: MultiWorld, player: int): +def create_regions(world: "RLWorld"): regions: Dict[str, RLRegionData] = { "Menu": RLRegionData(None, ["Castle Hamson"]), "The Manor": RLRegionData([], []), @@ -56,9 +59,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["The Fountain Room"].locations.append("Fountain Room") # Chests - chests = int(multiworld.chests_per_zone[player]) + chests = int(world.options.chests_per_zone) for i in range(0, chests): - if multiworld.universal_chests[player]: + if world.options.universal_chests: regions["Castle Hamson"].locations.append(f"Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Chest {i + 1 + (chests * 2)}") @@ -70,9 +73,9 @@ def create_regions(multiworld: MultiWorld, player: int): regions["Land of Darkness"].locations.append(f"Land of Darkness - Chest {i + 1}") # Fairy Chests - chests = int(multiworld.fairy_chests_per_zone[player]) + chests = int(world.options.fairy_chests_per_zone) for i in range(0, chests): - if multiworld.universal_fairy_chests[player]: + if world.options.universal_fairy_chests: regions["Castle Hamson"].locations.append(f"Fairy Chest {i + 1}") regions["Forest Abkhazia"].locations.append(f"Fairy Chest {i + 1 + chests}") regions["The Maya"].locations.append(f"Fairy Chest {i + 1 + (chests * 2)}") @@ -85,14 +88,14 @@ def create_regions(multiworld: MultiWorld, player: int): # Set up the regions correctly. for name, data in regions.items(): - multiworld.regions.append(create_region(multiworld, player, name, data)) + world.multiworld.regions.append(create_region(world.multiworld, world.player, name, data)) - multiworld.get_entrance("Castle Hamson", player).connect(multiworld.get_region("Castle Hamson", player)) - multiworld.get_entrance("The Manor", player).connect(multiworld.get_region("The Manor", player)) - multiworld.get_entrance("Forest Abkhazia", player).connect(multiworld.get_region("Forest Abkhazia", player)) - multiworld.get_entrance("The Maya", player).connect(multiworld.get_region("The Maya", player)) - multiworld.get_entrance("Land of Darkness", player).connect(multiworld.get_region("Land of Darkness", player)) - multiworld.get_entrance("The Fountain Room", player).connect(multiworld.get_region("The Fountain Room", player)) + world.get_entrance("Castle Hamson").connect(world.get_region("Castle Hamson")) + world.get_entrance("The Manor").connect(world.get_region("The Manor")) + world.get_entrance("Forest Abkhazia").connect(world.get_region("Forest Abkhazia")) + world.get_entrance("The Maya").connect(world.get_region("The Maya")) + world.get_entrance("Land of Darkness").connect(world.get_region("Land of Darkness")) + world.get_entrance("The Fountain Room").connect(world.get_region("The Fountain Room")) def create_region(multiworld: MultiWorld, player: int, name: str, data: RLRegionData): diff --git a/worlds/rogue_legacy/Rules.py b/worlds/rogue_legacy/Rules.py index 2fac8d5613..505bbdd635 100644 --- a/worlds/rogue_legacy/Rules.py +++ b/worlds/rogue_legacy/Rules.py @@ -1,9 +1,13 @@ -from BaseClasses import CollectionState, MultiWorld +from BaseClasses import CollectionState +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import RLWorld -def get_upgrade_total(multiworld: MultiWorld, player: int) -> int: - return int(multiworld.health_pool[player]) + int(multiworld.mana_pool[player]) + \ - int(multiworld.attack_pool[player]) + int(multiworld.magic_damage_pool[player]) +def get_upgrade_total(world: "RLWorld") -> int: + return int(world.options.health_pool) + int(world.options.mana_pool) + \ + int(world.options.attack_pool) + int(world.options.magic_damage_pool) def get_upgrade_count(state: CollectionState, player: int) -> int: @@ -19,8 +23,8 @@ def has_upgrade_amount(state: CollectionState, player: int, amount: int) -> bool return get_upgrade_count(state, player) >= amount -def has_upgrades_percentage(state: CollectionState, player: int, percentage: float) -> bool: - return has_upgrade_amount(state, player, round(get_upgrade_total(state.multiworld, player) * (percentage / 100))) +def has_upgrades_percentage(state: CollectionState, world: "RLWorld", percentage: float) -> bool: + return has_upgrade_amount(state, world.player, round(get_upgrade_total(world) * (percentage / 100))) def has_movement_rune(state: CollectionState, player: int) -> bool: @@ -47,15 +51,15 @@ def has_defeated_dungeon(state: CollectionState, player: int) -> bool: return state.has("Defeat Herodotus", player) or state.has("Defeat Astrodotus", player) -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "RLWorld", player: int): # If 'vendors' are 'normal', then expect it to show up in the first half(ish) of the spheres. - if multiworld.vendors[player] == "normal": - multiworld.get_location("Forest Abkhazia Boss Reward", player).access_rule = \ + if world.options.vendors == "normal": + world.get_location("Forest Abkhazia Boss Reward").access_rule = \ lambda state: has_vendors(state, player) # Gate each manor location so everything isn't dumped into sphere 1. manor_rules = { - "Defeat Khidr" if multiworld.khidr[player] == "vanilla" else "Defeat Neo Khidr": [ + "Defeat Khidr" if world.options.khidr == "vanilla" else "Defeat Neo Khidr": [ "Manor - Left Wing Window", "Manor - Left Wing Rooftop", "Manor - Right Wing Window", @@ -66,7 +70,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Left Tree 2", "Manor - Right Tree", ], - "Defeat Alexander" if multiworld.alexander[player] == "vanilla" else "Defeat Alexander IV": [ + "Defeat Alexander" if world.options.alexander == "vanilla" else "Defeat Alexander IV": [ "Manor - Left Big Upper 1", "Manor - Left Big Upper 2", "Manor - Left Big Windows", @@ -78,7 +82,7 @@ def set_rules(multiworld: MultiWorld, player: int): "Manor - Right Big Rooftop", "Manor - Right Extension", ], - "Defeat Ponce de Leon" if multiworld.leon[player] == "vanilla" else "Defeat Ponce de Freon": [ + "Defeat Ponce de Leon" if world.options.leon == "vanilla" else "Defeat Ponce de Freon": [ "Manor - Right High Base", "Manor - Right High Upper", "Manor - Right High Tower", @@ -90,24 +94,24 @@ def set_rules(multiworld: MultiWorld, player: int): # Set rules for manor locations. for event, locations in manor_rules.items(): for location in locations: - multiworld.get_location(location, player).access_rule = lambda state: state.has(event, player) + world.get_location(location).access_rule = lambda state: state.has(event, player) # Set rules for fairy chests to decrease headache of expectation to find non-movement fairy chests. - for fairy_location in [location for location in multiworld.get_locations(player) if "Fairy" in location.name]: + for fairy_location in [location for location in world.multiworld.get_locations(player) if "Fairy" in location.name]: fairy_location.access_rule = lambda state: has_fairy_progression(state, player) # Region rules. - multiworld.get_entrance("Forest Abkhazia", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 12.5) and has_defeated_castle(state, player) + world.get_entrance("Forest Abkhazia").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 12.5) and has_defeated_castle(state, player) - multiworld.get_entrance("The Maya", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 25) and has_defeated_forest(state, player) + world.get_entrance("The Maya").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 25) and has_defeated_forest(state, player) - multiworld.get_entrance("Land of Darkness", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 37.5) and has_defeated_tower(state, player) + world.get_entrance("Land of Darkness").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 37.5) and has_defeated_tower(state, player) - multiworld.get_entrance("The Fountain Room", player).access_rule = \ - lambda state: has_upgrades_percentage(state, player, 50) and has_defeated_dungeon(state, player) + world.get_entrance("The Fountain Room").access_rule = \ + lambda state: has_upgrades_percentage(state, world, 50) and has_defeated_dungeon(state, player) # Win condition. - multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) + world.multiworld.completion_condition[player] = lambda state: state.has("Defeat The Fountain", player) diff --git a/worlds/rogue_legacy/__init__.py b/worlds/rogue_legacy/__init__.py index 78e56a794c..290f4a60ac 100644 --- a/worlds/rogue_legacy/__init__.py +++ b/worlds/rogue_legacy/__init__.py @@ -4,7 +4,7 @@ from BaseClasses import Tutorial from worlds.AutoWorld import WebWorld, World from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table from .Locations import RLLocation, location_table -from .Options import rl_options +from .Options import RLOptions from .Presets import rl_options_presets from .Regions import create_regions from .Rules import set_rules @@ -33,20 +33,17 @@ class RLWorld(World): But that's OK, because no one is perfect, and you don't have to be to succeed. """ game = "Rogue Legacy" - option_definitions = rl_options + options_dataclass = RLOptions + options: RLOptions topology_present = True required_client_version = (0, 3, 5) web = RLWeb() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.code for name, data in location_table.items()} - - # TODO: Replace calls to this function with "options-dict", once that PR is completed and merged. - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + item_name_to_id = {name: data.code for name, data in item_table.items() if data.code is not None} + location_name_to_id = {name: data.code for name, data in location_table.items() if data.code is not None} def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in rl_options} + return self.options.as_dict(*[name for name in self.options_dataclass.type_hints.keys()]) def generate_early(self): location_ids_used_per_game = { @@ -74,18 +71,18 @@ class RLWorld(World): ) # Check validation of names. - additional_lady_names = len(self.get_setting("additional_lady_names").value) - additional_sir_names = len(self.get_setting("additional_sir_names").value) - if not self.get_setting("allow_default_names"): - if additional_lady_names < int(self.get_setting("number_of_children")): + additional_lady_names = len(self.options.additional_lady_names.value) + additional_sir_names = len(self.options.additional_sir_names.value) + if not self.options.allow_default_names: + if additional_lady_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_lady_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_lady_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_lady_names}") - if additional_sir_names < int(self.get_setting("number_of_children")): + if additional_sir_names < int(self.options.number_of_children): raise Exception( f"allow_default_names is off, but not enough names are defined in additional_sir_names. " - f"Expected {int(self.get_setting('number_of_children'))}, Got {additional_sir_names}") + f"Expected {int(self.options.number_of_children)}, Got {additional_sir_names}") def create_items(self): item_pool: List[RLItem] = [] @@ -95,110 +92,110 @@ class RLWorld(World): # Architect if name == "Architect": - if self.get_setting("architect") == "disabled": + if self.options.architect == "disabled": continue - if self.get_setting("architect") == "start_unlocked": + if self.options.architect == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("architect") == "early": + if self.options.architect == "early": self.multiworld.local_early_items[self.player]["Architect"] = 1 # Blacksmith and Enchantress if name == "Blacksmith" or name == "Enchantress": - if self.get_setting("vendors") == "start_unlocked": + if self.options.vendors == "start_unlocked": self.multiworld.push_precollected(self.create_item(name)) continue - if self.get_setting("vendors") == "early": + if self.options.vendors == "early": self.multiworld.local_early_items[self.player]["Blacksmith"] = 1 self.multiworld.local_early_items[self.player]["Enchantress"] = 1 # Haggling - if name == "Haggling" and self.get_setting("disable_charon"): + if name == "Haggling" and self.options.disable_charon: continue # Blueprints if data.category == "Blueprints": # No progressive blueprints if progressive_blueprints are disabled. - if name == "Progressive Blueprints" and not self.get_setting("progressive_blueprints"): + if name == "Progressive Blueprints" and not self.options.progressive_blueprints: continue # No distinct blueprints if progressive_blueprints are enabled. - elif name != "Progressive Blueprints" and self.get_setting("progressive_blueprints"): + elif name != "Progressive Blueprints" and self.options.progressive_blueprints: continue # Classes if data.category == "Classes": if name == "Progressive Knights": - if "Knight" not in self.get_setting("available_classes"): + if "Knight" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knight": + if self.options.starting_class == "knight": quantity = 1 if name == "Progressive Mages": - if "Mage" not in self.get_setting("available_classes"): + if "Mage" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "mage": + if self.options.starting_class == "mage": quantity = 1 if name == "Progressive Barbarians": - if "Barbarian" not in self.get_setting("available_classes"): + if "Barbarian" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "barbarian": + if self.options.starting_class == "barbarian": quantity = 1 if name == "Progressive Knaves": - if "Knave" not in self.get_setting("available_classes"): + if "Knave" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "knave": + if self.options.starting_class == "knave": quantity = 1 if name == "Progressive Miners": - if "Miner" not in self.get_setting("available_classes"): + if "Miner" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "miner": + if self.options.starting_class == "miner": quantity = 1 if name == "Progressive Shinobis": - if "Shinobi" not in self.get_setting("available_classes"): + if "Shinobi" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "shinobi": + if self.options.starting_class == "shinobi": quantity = 1 if name == "Progressive Liches": - if "Lich" not in self.get_setting("available_classes"): + if "Lich" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "lich": + if self.options.starting_class == "lich": quantity = 1 if name == "Progressive Spellthieves": - if "Spellthief" not in self.get_setting("available_classes"): + if "Spellthief" not in self.options.available_classes: continue - if self.get_setting("starting_class") == "spellthief": + if self.options.starting_class == "spellthief": quantity = 1 if name == "Dragons": - if "Dragon" not in self.get_setting("available_classes"): + if "Dragon" not in self.options.available_classes: continue if name == "Traitors": - if "Traitor" not in self.get_setting("available_classes"): + if "Traitor" not in self.options.available_classes: continue # Skills if name == "Health Up": - quantity = self.get_setting("health_pool") + quantity = self.options.health_pool.value elif name == "Mana Up": - quantity = self.get_setting("mana_pool") + quantity = self.options.mana_pool.value elif name == "Attack Up": - quantity = self.get_setting("attack_pool") + quantity = self.options.attack_pool.value elif name == "Magic Damage Up": - quantity = self.get_setting("magic_damage_pool") + quantity = self.options.magic_damage_pool.value elif name == "Armor Up": - quantity = self.get_setting("armor_pool") + quantity = self.options.armor_pool.value elif name == "Equip Up": - quantity = self.get_setting("equip_pool") + quantity = self.options.equip_pool.value elif name == "Crit Chance Up": - quantity = self.get_setting("crit_chance_pool") + quantity = self.options.crit_chance_pool.value elif name == "Crit Damage Up": - quantity = self.get_setting("crit_damage_pool") + quantity = self.options.crit_damage_pool.value # Ignore filler, it will be added in a later stage. if data.category == "Filler": @@ -215,7 +212,7 @@ class RLWorld(World): def get_filler_item_name(self) -> str: fillers = get_items_by_category("Filler") weights = [data.weight for data in fillers.values()] - return self.multiworld.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] + return self.random.choices([filler for filler in fillers.keys()], weights, k=1)[0] def create_item(self, name: str) -> RLItem: data = item_table[name] @@ -226,10 +223,10 @@ class RLWorld(World): return RLItem(name, data.classification, data.code, self.player) def set_rules(self): - set_rules(self.multiworld, self.player) + set_rules(self, self.player) def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self) self._place_events() def _place_events(self): @@ -238,7 +235,7 @@ class RLWorld(World): self.create_event("Defeat The Fountain")) # Khidr / Neo Khidr - if self.get_setting("khidr") == "vanilla": + if self.options.khidr == "vanilla": self.multiworld.get_location("Castle Hamson Boss Room", self.player).place_locked_item( self.create_event("Defeat Khidr")) else: @@ -246,7 +243,7 @@ class RLWorld(World): self.create_event("Defeat Neo Khidr")) # Alexander / Alexander IV - if self.get_setting("alexander") == "vanilla": + if self.options.alexander == "vanilla": self.multiworld.get_location("Forest Abkhazia Boss Room", self.player).place_locked_item( self.create_event("Defeat Alexander")) else: @@ -254,7 +251,7 @@ class RLWorld(World): self.create_event("Defeat Alexander IV")) # Ponce de Leon / Ponce de Freon - if self.get_setting("leon") == "vanilla": + if self.options.leon == "vanilla": self.multiworld.get_location("The Maya Boss Room", self.player).place_locked_item( self.create_event("Defeat Ponce de Leon")) else: @@ -262,7 +259,7 @@ class RLWorld(World): self.create_event("Defeat Ponce de Freon")) # Herodotus / Astrodotus - if self.get_setting("herodotus") == "vanilla": + if self.options.herodotus == "vanilla": self.multiworld.get_location("Land of Darkness Boss Room", self.player).place_locked_item( self.create_event("Defeat Herodotus")) else: diff --git a/worlds/rogue_legacy/test/__init__.py b/worlds/rogue_legacy/test/__init__.py index 2639e618c6..3346476ba6 100644 --- a/worlds/rogue_legacy/test/__init__.py +++ b/worlds/rogue_legacy/test/__init__.py @@ -1,4 +1,4 @@ -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase class RLTestBase(WorldTestBase): From cf375cbcc4c399290b7ebc893e10992029761230 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 8 Sep 2024 12:54:27 -0500 Subject: [PATCH 213/222] Core: Fix Generate's slot parsing to default unknown slot names to file name (#3795) * make Generate handle slots without names defined better * set name dict before loop so we don't have to check for its existence later * move setter so it's more obvious why --- Generate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index 6220c0eb81..4eba05cc52 100644 --- a/Generate.py +++ b/Generate.py @@ -155,6 +155,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.outputpath = args.outputpath erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_output = args.skip_output + erargs.name = {} settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) @@ -202,7 +203,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: if path == args.weights_file_path: # if name came from the weights file, just use base player name erargs.name[player] = f"Player{player}" - elif not erargs.name[player]: # if name was not specified, generate it from filename + elif player not in erargs.name: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) From 5a5162c9d3a93295eccaad74fe28226f5cc0342f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 8 Sep 2024 12:55:17 -0500 Subject: [PATCH 214/222] The Messenger: improve automated installation (#3083) * add deck support to the messenger mod setup * Add tkinter cleanup because it's janky * prompt about launching the game instead of just doing it * add "better" file validation to courier checking * make it a bit more palatable * make it a bit more palatable * add the executable's md5 to ensure the correct file is selected * handle a bad md5 and show a message * make the utils wrapper snake_case and add a docstring * use stored archive instead of head * don't give other people the convenience method ig --- worlds/messenger/__init__.py | 1 + worlds/messenger/client_setup.py | 106 +++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 1bca3a37ad..9a38953ffb 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -27,6 +27,7 @@ class MessengerSettings(Group): class GamePath(FilePath): description = "The Messenger game executable" is_exe = True + md5s = ["1b53534569060bc06179356cd968ed1d"] game_path: GamePath = GamePath("TheMessenger.exe") diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 6bff78df36..77a0f63432 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -5,7 +5,6 @@ import os.path import subprocess import urllib.request from shutil import which -from tkinter.messagebox import askyesnocancel from typing import Any, Optional from zipfile import ZipFile from Utils import open_file @@ -18,11 +17,33 @@ from Utils import is_windows, messagebox, tuplize_version MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" +def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: + """ + Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. + + :param title: Title to be displayed at the top of the message box. + :param text: Text to be displayed inside the message box. + :return: Returns True if yes, False if no, None if cancel. + """ + from tkinter import Tk, messagebox + root = Tk() + root.withdraw() + ret = messagebox.askyesnocancel(title, text) + root.update() + return ret + + + def launch_game(*args) -> None: """Check the game installation, then launch it""" def courier_installed() -> bool: """Check if Courier is installed""" - return os.path.exists(os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.Courier.mm.dll")) + assembly_path = os.path.join(game_folder, "TheMessenger_Data", "Managed", "Assembly-CSharp.dll") + with open(assembly_path, "rb") as assembly: + for line in assembly: + if b"Courier" in line: + return True + return False def mod_installed() -> bool: """Check if the mod is installed""" @@ -57,27 +78,34 @@ def launch_game(*args) -> None: if not is_windows: mono_exe = which("mono") if not mono_exe: - # steam deck support but doesn't currently work - messagebox("Failure", "Failed to install Courier", True) - raise RuntimeError("Failed to install Courier") - # # download and use mono kickstart - # # this allows steam deck support - # mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/refs/heads/master.zip" - # target = os.path.join(folder, "monoKickstart") - # os.makedirs(target, exist_ok=True) - # with urllib.request.urlopen(mono_kick_url) as download: - # with ZipFile(io.BytesIO(download.read()), "r") as zf: - # for member in zf.infolist(): - # zf.extract(member, path=target) - # installer = subprocess.Popen([os.path.join(target, "precompiled"), - # os.path.join(folder, "MiniInstaller.exe")], shell=False) - # os.remove(target) + # download and use mono kickstart + # this allows steam deck support + mono_kick_url = "https://github.com/flibitijibibo/MonoKickstart/archive/716f0a2bd5d75138969090494a76328f39a6dd78.zip" + files = [] + with urllib.request.urlopen(mono_kick_url) as download: + with ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + if "precompiled/" not in member.filename or member.filename.endswith("/"): + continue + member.filename = member.filename.split("/")[-1] + if member.filename.endswith("bin.x86_64"): + member.filename = "MiniInstaller.bin.x86_64" + zf.extract(member, path=game_folder) + files.append(member.filename) + mono_installer = os.path.join(game_folder, "MiniInstaller.bin.x86_64") + os.chmod(mono_installer, 0o755) + installer = subprocess.Popen(mono_installer, shell=False) + failure = installer.wait() + for file in files: + os.remove(file) else: - installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=False) + installer = subprocess.Popen([mono_exe, os.path.join(game_folder, "MiniInstaller.exe")], shell=True) + failure = installer.wait() else: - installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=False) + installer = subprocess.Popen(os.path.join(game_folder, "MiniInstaller.exe"), shell=True) + failure = installer.wait() - failure = installer.wait() + print(failure) if failure: messagebox("Failure", "Failed to install Courier", True) os.chdir(working_directory) @@ -125,18 +153,35 @@ def launch_game(*args) -> None: return "alpha" in latest_version or tuplize_version(latest_version) > tuplize_version(installed_version) from . import MessengerWorld - game_folder = os.path.dirname(MessengerWorld.settings.game_path) + try: + game_folder = os.path.dirname(MessengerWorld.settings.game_path) + except ValueError as e: + logging.error(e) + messagebox("Invalid File", "Selected file did not match expected hash. " + "Please try again and ensure you select The Messenger.exe.") + return working_directory = os.getcwd() + # setup ssl context + try: + import certifi + import ssl + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + context.set_alpn_protocols(["http/1.1"]) + https_handler = urllib.request.HTTPSHandler(context=context) + opener = urllib.request.build_opener(https_handler) + urllib.request.install_opener(opener) + except ImportError: + pass if not courier_installed(): - should_install = askyesnocancel("Install Courier", - "No Courier installation detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Courier", + "No Courier installation detected. Would you like to install now?") if not should_install: return logging.info("Installing Courier") install_courier() if not mod_installed(): - should_install = askyesnocancel("Install Mod", - "No randomizer mod detected. Would you like to install now?") + should_install = ask_yes_no_cancel("Install Mod", + "No randomizer mod detected. Would you like to install now?") if not should_install: return logging.info("Installing Mod") @@ -144,17 +189,24 @@ def launch_game(*args) -> None: else: latest = request_data(MOD_URL)["tag_name"] if available_mod_update(latest): - should_update = askyesnocancel("Update Mod", - f"New mod version detected. Would you like to update to {latest} now?") + should_update = ask_yes_no_cancel("Update Mod", + f"New mod version detected. Would you like to update to {latest} now?") if should_update: logging.info("Updating mod") install_mod() elif should_update is None: return + if not args: + should_launch = ask_yes_no_cancel("Launch Game", + "Mod installed and up to date. Would you like to launch the game now?") + if not should_launch: + return + parser = argparse.ArgumentParser(description="Messenger Client Launcher") parser.add_argument("url", type=str, nargs="?", help="Archipelago Webhost uri to auto connect to.") args = parser.parse_args(args) + if not is_windows: if args.url: open_file(f"steam://rungameid/764790//{args.url}/") From e52ce0149a0470b594ce6f675dbd5b0bb7c994c0 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:57:09 -0400 Subject: [PATCH 215/222] Rogue Legacy: Split Additional Names into two option classes #3908 --- worlds/rogue_legacy/Options.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/worlds/rogue_legacy/Options.py b/worlds/rogue_legacy/Options.py index 9210082f73..139ff60944 100644 --- a/worlds/rogue_legacy/Options.py +++ b/worlds/rogue_legacy/Options.py @@ -175,13 +175,21 @@ class NumberOfChildren(Range): default = 3 -class AdditionalNames(OptionSet): +class AdditionalLadyNames(OptionSet): """ Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list of names your children can have. The first value will also be your initial character's name depending on Starting Gender. """ - display_name = "Additional Names" + display_name = "Additional Lady Names" + +class AdditionalSirNames(OptionSet): + """ + Set of additional names your potential offspring can have. If Allow Default Names is disabled, this is the only list + of names your children can have. The first value will also be your initial character's name depending on Starting + Gender. + """ + display_name = "Additional Sir Names" class AllowDefaultNames(DefaultOnToggle): @@ -374,6 +382,6 @@ class RLOptions(PerGameCommonOptions): crit_chance_pool: CritChanceUpPool crit_damage_pool: CritDamageUpPool allow_default_names: AllowDefaultNames - additional_lady_names: AdditionalNames - additional_sir_names: AdditionalNames + additional_lady_names: AdditionalLadyNames + additional_sir_names: AdditionalSirNames death_link: DeathLink From 4aab317665d5d2d9a72d2bc5ea9446639eaf887e Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:56:15 -0500 Subject: [PATCH 216/222] ALTTP: Plando (#2904) fixes (#3834) --- Options.py | 14 ++++++++++++++ worlds/alttp/Options.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Options.py b/Options.py index ecde6275f1..b79714635d 100644 --- a/Options.py +++ b/Options.py @@ -973,7 +973,19 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + if isinstance(at, dict): + if at: + at = random.choices(list(at.keys()), + weights=list(at.values()), k=1)[0] + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") given_text = text.get("text", []) + if isinstance(given_text, dict): + if not given_text: + given_text = [] + else: + given_text = random.choices(list(given_text.keys()), + weights=list(given_text.values()), k=1) if isinstance(given_text, str): given_text = [given_text] texts.append(PlandoText( @@ -981,6 +993,8 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): given_text, text.get("percentage", 100) )) + else: + raise OptionError("\"at\" must be a valid string or weighted list of strings!") elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 20dd18038a..bd87cbf2c3 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -728,7 +728,7 @@ class ALttPPlandoConnections(PlandoConnections): entrances = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - exits = set([connection[1] for connection in ( + exits = set([connection[0] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) From 09c7f5f909e6ca23de5bc58f8174a1794b07f817 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:27 +0200 Subject: [PATCH 217/222] The Witness: Bump Required Client Version (#3891) The newest release of the Witness client connects with 0.5.1 https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/tag/7.0.0p10 --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index cdb17a483b..b4b38c883e 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -61,7 +61,7 @@ class WitnessWorld(World): item_name_groups = static_witness_items.ITEM_GROUPS location_name_groups = static_witness_locations.AREA_LOCATION_GROUPS - required_client_version = (0, 4, 5) + required_client_version = (0, 5, 1) player_logic: WitnessPlayerLogic player_locations: WitnessPlayerLocations From 170aedba8fbe35765289a8628b89acf9fd1515ec Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:36:47 +0200 Subject: [PATCH 218/222] The Witness: Fix hints always displaying the Witness player (#3861) * The Witness: Fix hints always displaying the Witness player Got a bit too trigger happy with changing instances of `world.multiworld.player_name` to `world.player_name` - Some of these were actually *supposed* to be other players. Alternate title: The Witness doesn't have a Silph Scope * that one i guess --- worlds/witness/hints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 2c5f816b2b..99e8eea2eb 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint: location_name = hint.location.name if hint.location.player != world.player: - location_name += " (" + world.player_name + ")" + location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")" item = hint.location.item @@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes item_name = item.name if item.player != world.player: - item_name += " (" + world.player_name + ")" + item_name += " (" + world.multiworld.get_player_name(item.player) + ")" hint_text = "" area: Optional[str] = None From 7ff201e32c859eeb1b3e07ee087f11da3249f833 Mon Sep 17 00:00:00 2001 From: Spineraks Date: Tue, 10 Sep 2024 17:01:36 +0200 Subject: [PATCH 219/222] Yacht Dice: add get_filler_item_name (#3916) * Add the yacht dice (from other git) world to the yacht dice fork * Update .gitignore * Removed zillion because it doesn't work * Update .gitignore * added zillion again... * Now you can have 0 extra fragments * Added alt categories, also options * Added item categories * Extra categories are now working! :dog: * changed options and added exceptions * Testing if I change the generate.py * Revert "Testing if I change the generate.py" This reverts commit 7c2b3df6170dcf8d8f36a1de9fcbc9dccdec81f8. * ignore gitignore * Delete .gitignore * Update .gitignore * Update .gitignore * Update logic, added multiplicative categories * Changed difficulties * Update offline mode so that it works again * Adjusted difficulty * New version of the apworld, with 1000 as final score, always Will still need to check difficulty and weights of adding items. Website is not ready yet, so this version is not usable yet :) * Changed yaml and small bug fixes Fix when goal and max are same Options: changed chance to weight * no changes, just whitespaces * changed how logic works Now you put an array of mults and the cpu gets a couple of tries * Changed logic, tweaked a bit too * Preparation for 2.0 * logic tweak * Logic for alt categories properly now * Update setup_en.md * Update en_YachtDice.md * Improve performance of add_distributions * Formatting style * restore gitignore to APMW * Tweaked generation parameters and methods * Version 2.0.3 manual input option max score in logic always 2.0.3 faster gen * Comments and editing * Renamed setup guide * Improved create_items code * init of locations: remove self.event line * Moved setting early items to generate_early * Add my name to CODEOWNERS * Added Yacht Dice to the readme in list of games * Improve performance of Yacht Dice * newline * Improve typing * This is actually just slower lol * Update worlds/yachtdice/Items.py Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> * Update Options.py * Styling * finished text whichstory option * removed roll and rollfragments; not used * import; worlds not world :) * Option groups! * ruff styling, fix * ruff format styling! * styling and capitalization of options * small comment * Cleaned up the "state_is_a_list" a little bit * RUFF :dog: * Changed filling the itempool for efficiency Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?). And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points. * :dog: * Removed plando "fix" * Changed indent of score multiplier * faster location function * Comments to docstrings * fixed making location closest to goal_score be goal_score * options format * iterate keys and values of a dict together * small optimization ListState * faster collection of categories * return arguments instead of making a list (will :dog: later) * Instead of turning it into a tuple, you can just make a tuple literal * remove .keys() * change .random and used enumerate * some readability improvements * Remove location "0", we don't use that one * Remove lookup_id_to_name entirely I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id. * .append instead of += for single items, percentile function changed Also an extra comment for location ids. * remove ) too many * Removed sorted from category list * Hash categories (which makes it slower :( ) Maybe I messed up or misunderstood... I'll revert this right away since it is 2x slower, probably because of sorted instead of sort? * Revert "Hash categories (which makes it slower :( )" This reverts commit 34f2c1aed8c8813b2d9c58896650b82a810d3578. * temporary push: 40% faster generation test Small changes in logic make the generation 40% faster. I'll have to think about how big the changes are. I suspect they are rather limited. If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here. * Add Points item category * Reverse changes of bad idea :) * ruff :dog: * Use numpy and pmf function to speed up gen Numpy has a built-in way to sum probability mass functions (pmf). This shaves of 60% of the generation time :D * Revert "Use numpy and pmf function to speed up gen" This reverts commit 9290191cb323ae92321d6c2cfcfe8c27370f439b. * Step inbetween to change the weights * Changed the weights to make it faster 135 -> 81 seconds on 100 random yamls * Adjusted max_dist, split dice_simulation function * Removed nonlocal and pass arguments instead * Change "weight-lists" to Dict[str, float] * Removed the return from ini_locations. Also added explanations to cat_weights * Choice options; dont'use .value (will ruff later) * Only put important options in slotdata * :dog: * Add Dict import * Split the cache per player, limit size to 400. * :dog: * added , because of style * Update apworld version to 2.0.6 2.0.5 is the apworld I released on github to be tested I never separately released 2.0.4. * Multiple smaller code improvements - changed names in YachtWeights so we don't need to translate them in Rules anymore - we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore - * :dog: ruff * Mostly minimize_extra_items improvements - Change logic, generation is now even faster (0.6s per default yaml). - Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now: - you start with 2 dice and 2 rolls - there will be less locations/items at the start of you game * ruff :dog: * Removed printing options * Reworded some option descriptions * Yacht Dice: setup: change release-link to latest On the installation page, link to the latest release, instead of the page with all releases * Several fixes and changes -change apworld version -Removed the extra roll (this was not intended) -change extra_points_added to a mutable list to that it actually does something -removed variables multipliers_added and items_added -Rules, don't order by quantity, just by mean_score -Changed the weights in general to make it faster * :dog: * Revert setup to what it was (latest, without S) * remove temp weights file, shouldn't be here * Made sure that there is not too many step score multipliers. Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game. * add filler item name --------- Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/yachtdice/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/worlds/yachtdice/__init__.py b/worlds/yachtdice/__init__.py index 3a79eff040..d86ee3382d 100644 --- a/worlds/yachtdice/__init__.py +++ b/worlds/yachtdice/__init__.py @@ -466,6 +466,9 @@ class YachtDiceWorld(World): menu.exits.append(connection) connection.connect(board) self.multiworld.regions += [menu, board] + + def get_filler_item_name(self) -> str: + return "Good RNG" def set_rules(self): """ From 874392756b706bc07f4c1ff9429ed0b16e52abd3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:20:07 -0700 Subject: [PATCH 220/222] Pokemon Emerald: Add normalize encounter rate option to slot data (#3917) --- worlds/pokemon_emerald/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index abdee26f57..d281dde23c 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -711,6 +711,7 @@ class PokemonEmeraldWorld(World): "trainersanity", "modify_118", "death_link", + "normalize_encounter_rates", ) slot_data["free_fly_location_id"] = self.free_fly_location_id slot_data["hm_requirements"] = self.hm_requirements From c9f1a21bd2b8888e5b4dc75123c19b0a016ee261 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 11 Sep 2024 04:22:04 -0700 Subject: [PATCH 221/222] BizHawkClient: Remove `run_gui` in favor of `make_gui` (#3910) --- CommonClient.py | 2 +- worlds/_bizhawk/context.py | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 911de4226d..6bdd8fc819 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -662,7 +662,7 @@ class CommonContext: logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) - def make_gui(self) -> type: + def make_gui(self) -> typing.Type["kvui.GameManager"]: """To return the Kivy App class needed for run_gui so it can be overridden before being built""" from kvui import GameManager diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 234faf3b65..896c8fb7b5 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -59,14 +59,10 @@ class BizHawkClientContext(CommonContext): self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 - def run_gui(self): - from kvui import GameManager - - class BizHawkManager(GameManager): - base_title = "Archipelago BizHawk Client" - - self.ui = BizHawkManager(self) - self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + def make_gui(self): + ui = super().make_gui() + ui.base_title = "Archipelago BizHawk Client" + return ui def on_package(self, cmd, args): if cmd == "Connected": From 7621889b8b626e89947d6258ddd5ade65d434ddb Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 11 Sep 2024 04:22:53 -0700 Subject: [PATCH 222/222] DS3: Add nex3 as a world maintainer (#3882) I've already discussed this with @Marechal-L and gotten his approval. --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 28dcc67362..ee7fd7ed86 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -46,7 +46,7 @@ /worlds/clique/ @ThePhar # Dark Souls III -/worlds/dark_souls_3/ @Marechal-L +/worlds/dark_souls_3/ @Marechal-L @nex3 # Donkey Kong Country 3 /worlds/dkc3/ @PoryGone