diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 79c057fc..5d32f775 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,15 +1,16 @@ from typing import Dict, Any, Iterable, Optional, Union -from BaseClasses import Region, Entrance, Location, Item, Tutorial +from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState from worlds.AutoWorld import World, WebWorld from . import rules, logic, options from .bundles import get_all_bundles, Bundle -from .items import item_table, create_items, ItemData, Group +from .items import item_table, create_items, ItemData, Group, items_by_group from .locations import location_table, create_locations, LocationData -from .logic import StardewLogic, StardewRule, _True, _And +from .logic import StardewLogic, StardewRule, True_ from .options import stardew_valley_options, StardewOptions, fetch_options from .regions import create_regions from .rules import set_rules +from ..generic.Rules import set_rule client_version = 0 @@ -52,8 +53,8 @@ class StardewValleyWorld(World): 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()} - data_version = 1 - required_client_version = (0, 3, 9) + data_version = 2 + required_client_version = (0, 4, 0) options: StardewOptions logic: StardewLogic @@ -88,38 +89,66 @@ class StardewValleyWorld(World): create_locations(add_location, self.options, self.multiworld.random) def create_items(self): - locations_count = len([location - for location in self.multiworld.get_locations(self.player) - if not location.event]) + self.precollect_starting_season() 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, Group.FRIENDSHIP_PACK)] - created_items = create_items(self.create_item, locations_count + len(items_to_exclude), self.options, + + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + items_to_exclude = [item for item in items_to_exclude + if item_table[item.name] not in items_by_group[Group.SEASON]] + + locations_count = len([location + for location in self.multiworld.get_locations(self.player) + if not location.event]) + + created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options, self.multiworld.random) + self.multiworld.itempool += created_items - for item in items_to_exclude: - self.multiworld.itempool.remove(item) - - self.setup_season_events() + self.setup_early_items() + self.setup_month_events() self.setup_victory() - def set_rules(self): - set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + def precollect_starting_season(self) -> Optional[StardewItem]: + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return - def create_item(self, item: Union[str, ItemData]) -> StardewItem: - if isinstance(item, str): - item = item_table[item] + season_pool = items_by_group[Group.SEASON] - return StardewItem(item.name, item.classification, item.code, self.player) + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + for season in season_pool: + self.multiworld.push_precollected(self.create_item(season)) + return - def setup_season_events(self): - self.multiworld.push_precollected(self.create_item("Spring")) - self.create_event_location(location_table["Summer"], self.logic.received("Spring"), "Summer") - self.create_event_location(location_table["Fall"], self.logic.received("Summer"), "Fall") - self.create_event_location(location_table["Winter"], self.logic.received("Fall"), "Winter") - self.create_event_location(location_table["Year Two"], self.logic.received("Winter"), "Year Two") + if [item for item in self.multiworld.precollected_items[self.player] + if item.name in {season.name for season in items_by_group[Group.SEASON]}]: + return + + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_randomized_not_winter: + season_pool = [season for season in season_pool if season.name != "Winter"] + + starting_season = self.create_item(self.multiworld.random.choice(season_pool)) + self.multiworld.push_precollected(starting_season) + + def setup_early_items(self): + if (self.options[options.BuildingProgression] == + options.BuildingProgression.option_progressive_early_shipping_bin): + self.multiworld.early_items[self.player]["Shipping Bin"] = 1 + + if self.options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: + self.multiworld.early_items[self.player]["Progressive Backpack"] = 1 + + def setup_month_events(self): + for i in range(0, 8): + month_end = LocationData(None, "Stardew Valley", f"Month End {i + 1}") + if i == 0: + self.create_event_location(month_end, True_(), "Month End") + continue + + self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End") def setup_victory(self): if self.options[options.Goal] == options.Goal.option_community_center: @@ -142,16 +171,70 @@ class StardewValleyWorld(World): self.create_event_location(location_table["Catch Every Fish"], self.logic.can_catch_every_fish().simplify(), "Victory") + elif self.options[options.Goal] == options.Goal.option_complete_collection: + self.create_event_location(location_table["Complete the Museum Collection"], + self.logic.can_complete_museum().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_full_house: + self.create_event_location(location_table["Full House"], + self.logic.can_have_two_children().simplify(), + "Victory") self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str): + def create_item(self, item: Union[str, ItemData]) -> StardewItem: + if isinstance(item, str): + item = item_table[item] + + return StardewItem(item.name, item.classification, item.code, self.player) + + def create_event_location(self, location_data: LocationData, rule: StardewRule, item: Optional[str] = None): + if item is None: + item = location_data.name + region = self.multiworld.get_region(location_data.region, self.player) 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)) + def set_rules(self): + set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + self.force_first_month_once_all_early_items_are_found() + + def force_first_month_once_all_early_items_are_found(self): + """ + The Fill algorithm sweeps all event when calculating the early location. This causes an issue where + location only locked behind event are considered early, which they are not really... + + This patches the issue, by adding a dependency to the first month end on all early items, so all the locations + that depends on it will not be considered early. This requires at least one early item to be progression, or + it just won't work... + """ + + early_items = [] + for player, item_count in self.multiworld.early_items.items(): + for item, count in item_count.items(): + if self.multiworld.worlds[player].create_item(item).advancement: + early_items.append((player, item, count)) + + for item, count in self.multiworld.local_early_items[self.player].items(): + if self.create_item(item).advancement: + early_items.append((self.player, item, count)) + + def first_month_require_all_early_items(state: CollectionState) -> bool: + for player, item, count in early_items: + if not state.has(item, player, count): + return False + + return True + + first_month_end = self.multiworld.get_location("Month End 1", self.player) + set_rule(first_month_end, first_month_require_all_early_items) + + def generate_basic(self): + pass + def get_filler_item_name(self) -> str: return "Joja Cola" @@ -162,28 +245,16 @@ class StardewValleyWorld(World): key, value = self.modified_bundles[bundle_key].to_pair() modified_bundles[key] = value - return { - "starting_money": self.options[options.StartingMoney], - "entrance_randomization": self.options[options.EntranceRandomization], - "backpack_progression": self.options[options.BackpackProgression], - "tool_progression": self.options[options.ToolProgression], - "elevator_progression": self.options[options.TheMinesElevatorsProgression], - "skill_progression": self.options[options.SkillProgression], - "building_progression": self.options[options.BuildingProgression], - "arcade_machine_progression": self.options[options.ArcadeMachineLocations], - "help_wanted_locations": self.options[options.HelpWantedLocations], - "fishsanity": self.options[options.Fishsanity], - "death_link": self.options["death_link"], - "goal": self.options[options.Goal], + excluded_options = [options.ResourcePackMultiplier, options.BundleRandomization, options.BundlePrice, + options.NumberOfPlayerBuffs] + slot_data = dict(self.options.options) + for option in excluded_options: + slot_data.pop(option.internal_name) + slot_data.update({ "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits - "multiple_day_sleep_enabled": self.options[options.MultipleDaySleepEnabled], - "multiple_day_sleep_cost": self.options[options.MultipleDaySleepCost], - "experience_multiplier": self.options[options.ExperienceMultiplier], - "debris_multiplier": self.options[options.DebrisMultiplier], - "quick_start": self.options[options.QuickStart], - "gifting": self.options[options.Gifting], - "gift_tax": self.options[options.GiftTax], - "modified_bundles": modified_bundles, "randomized_entrances": self.randomized_entrances, - "client_version": "2.2.2", - } + "modified_bundles": modified_bundles, + "client_version": "3.0.0", + }) + + return slot_data diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py index f87e3d67..7cbb1392 100644 --- a/worlds/stardew_valley/bundles.py +++ b/worlds/stardew_valley/bundles.py @@ -1,7 +1,7 @@ from random import Random from typing import List, Dict, Union -from .bundle_data import * +from .data.bundle_data import * from .logic import StardewLogic from .options import BundleRandomization, BundlePrice diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py index e69de29b..d14d9cfb 100644 --- a/worlds/stardew_valley/data/__init__.py +++ b/worlds/stardew_valley/data/__init__.py @@ -0,0 +1,2 @@ +from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds +from .fish_data import FishItem, all_fish diff --git a/worlds/stardew_valley/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py similarity index 92% rename from worlds/stardew_valley/bundle_data.py rename to worlds/stardew_valley/data/bundle_data.py index cfc5d482..d82a632a 100644 --- a/worlds/stardew_valley/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -1,14 +1,9 @@ from dataclasses import dataclass from . import fish_data +from .common_data import quality_dict from .game_item import GameItem - -quality_dict = { - 0: "", - 1: "Silver", - 2: "Gold", - 3: "Iridium" -} +from .museum_data import Mineral @dataclass(frozen=True) @@ -218,16 +213,16 @@ iridium_bar = BundleItem.item_bundle("Iridium Bar", 337, 1, 0) refined_quartz = BundleItem.item_bundle("Refined Quartz", 338, 2, 0) coal = BundleItem.item_bundle("Coal", 382, 5, 0) -quartz = BundleItem.item_bundle("Quartz", 80, 1, 0) -fire_quartz = BundleItem.item_bundle("Fire Quartz", 82, 1, 0) -frozen_tear = BundleItem.item_bundle("Frozen Tear", 84, 1, 0) -earth_crystal = BundleItem.item_bundle("Earth Crystal", 86, 1, 0) -emerald = BundleItem.item_bundle("Emerald", 60, 1, 0) -aquamarine = BundleItem.item_bundle("Aquamarine", 62, 1, 0) -ruby = BundleItem.item_bundle("Ruby", 64, 1, 0) -amethyst = BundleItem.item_bundle("Amethyst", 66, 1, 0) -topaz = BundleItem.item_bundle("Topaz", 68, 1, 0) -jade = BundleItem.item_bundle("Jade", 70, 1, 0) +quartz = BundleItem(Mineral.quartz, 1, 0) +fire_quartz = BundleItem(Mineral.fire_quartz, 1, 0) +frozen_tear = BundleItem(Mineral.frozen_tear, 1, 0) +earth_crystal = BundleItem(Mineral.earth_crystal, 1, 0) +emerald = BundleItem(Mineral.emerald, 1, 0) +aquamarine = BundleItem(Mineral.aquamarine, 1, 0) +ruby = BundleItem(Mineral.ruby, 1, 0) +amethyst = BundleItem(Mineral.amethyst, 1, 0) +topaz = BundleItem(Mineral.topaz, 1, 0) +jade = BundleItem(Mineral.jade, 1, 0) slime = BundleItem.item_bundle("Slime", 766, 99, 0) bug_meat = BundleItem.item_bundle("Bug Meat", 684, 10, 0) @@ -325,13 +320,12 @@ elvish_jewelry = BundleItem.item_bundle("Elvish Jewelry", 104, 1, 0) ancient_drum = BundleItem.item_bundle("Ancient Drum", 123, 1, 0) dried_starfish = BundleItem.item_bundle("Dried Starfish", 116, 1, 0) -# TODO Dye Bundle -dye_red_items = [cranberries, dwarf_scroll_1, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] -dye_yellow_items = [dried_starfish, dwarf_scroll_4, elvish_jewelry, corn, parsnip, summer_spangle, sunflower] -dye_green_items = [dwarf_scroll_2, fiddlehead_fern, kale, artichoke, bok_choy, green_bean] -dye_blue_items = [blueberry, dwarf_scroll_3, blue_jazz, blackberry, crystal_fruit] -dye_purple_items = [ancient_drum, beet, crocus, eggplant, red_cabbage, sweet_pea] +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_items = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] field_research_items = [purple_mushroom, nautilus_shell, chub, geode, frozen_geode, magma_geode, omni_geode, rainbow_shell, amethyst, bream, carp] diff --git a/worlds/stardew_valley/data/common_data.py b/worlds/stardew_valley/data/common_data.py new file mode 100644 index 00000000..8a2d0f5e --- /dev/null +++ b/worlds/stardew_valley/data/common_data.py @@ -0,0 +1,9 @@ +fishing_chest = "Fishing Chest" +secret_note = "Secret Note" + +quality_dict = { + 0: "", + 1: "Silver", + 2: "Gold", + 3: "Iridium" +} diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv new file mode 100644 index 00000000..749a9c74 --- /dev/null +++ b/worlds/stardew_valley/data/crops.csv @@ -0,0 +1,37 @@ +crop,farm_growth_seasons,seed,seed_seasons,seed_regions +Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store,JojaMart" +Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store,JojaMart" +Beet,Fall,Beet Seeds,Fall,The Desert +Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store,JojaMart" +Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store,JojaMart" +Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store,JojaMart" +Cactus Fruit,,Cactus Seeds,,The Desert +Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store,JojaMart" +Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store,JojaMart" +Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store,JojaMart" +Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store,JojaMart" +Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store,JojaMart" +Grape,Fall,Grape Starter,Fall,"Pierre's General Store,JojaMart" +Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store,JojaMart" +Hops,Summer,Hops Starter,Summer,"Pierre's General Store,JojaMart" +Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store,JojaMart" +Kale,Spring,Kale Seeds,Spring,"Pierre's General Store,JojaMart" +Melon,Summer,Melon Seeds,Summer,"Pierre's General Store,JojaMart" +Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store,JojaMart" +Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store,JojaMart" +Potato,Spring,Potato Seeds,Spring,"Pierre's General Store,JojaMart" +Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store,JojaMart" +Radish,Summer,Radish Seeds,Summer,"Pierre's General Store,JojaMart" +Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store,JojaMart" +Rhubarb,Spring,Rhubarb Seeds,Spring,The Desert +Starfruit,Summer,Starfruit Seeds,Summer,The Desert +Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store,JojaMart" +Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store,JojaMart" +Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart +Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store,JojaMart" +Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store,JojaMart" +Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store,JojaMart" +Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Yam,Fall,Yam Seeds,Fall,"Pierre's General Store,JojaMart" diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py new file mode 100644 index 00000000..f5e652b8 --- /dev/null +++ b/worlds/stardew_valley/data/crops_data.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass(frozen=True) +class SeedItem: + name: str + seasons: List[str] + regions: List[str] + + +@dataclass(frozen=True) +class CropItem: + name: str + farm_growth_seasons: List[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(__package__).joinpath("crops.csv").open() as file: + reader = csv.DictReader(file) + crops = [] + seeds = [] + + for item in reader: + seeds.append(SeedItem(item["seed"], + [season for season in item["seed_seasons"].split(",")] + if item["seed_seasons"] else [], + [region for region in item["seed_regions"].split(",")] + if item["seed_regions"] else [])) + crops.append(CropItem(item["crop"], + [season for season in item["farm_growth_seasons"].split(",")] + if item["farm_growth_seasons"] else [], + 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() diff --git a/worlds/stardew_valley/data/entrance_data.py b/worlds/stardew_valley/data/entrance_data.py new file mode 100644 index 00000000..585668cd --- /dev/null +++ b/worlds/stardew_valley/data/entrance_data.py @@ -0,0 +1,2 @@ +class Entrance: + to_stardew_valley = "To Stardew Valley" diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py new file mode 100644 index 00000000..0ab967b1 --- /dev/null +++ b/worlds/stardew_valley/data/fish_data.py @@ -0,0 +1,124 @@ +from dataclasses import dataclass +from typing import List, Tuple, Union + +from . import season_data as season +from .game_item import GameItem +from .region_data import SVRegion + + +@dataclass(frozen=True) +class FishItem(GameItem): + locations: Tuple[str] + seasons: Tuple[str] + difficulty: int + + def __repr__(self): + return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ + f" Seasons: {self.seasons} |" \ + f" Difficulty: {self.difficulty}) " + + +fresh_water = (SVRegion.farm, SVRegion.forest, SVRegion.town, SVRegion.mountain) +ocean = (SVRegion.beach,) +town_river = (SVRegion.town,) +mountain_lake = (SVRegion.mountain,) +forest_pond = (SVRegion.forest,) +forest_river = (SVRegion.forest,) +secret_woods = (SVRegion.secret_woods,) +mines_floor_20 = (SVRegion.mines_floor_20,) +mines_floor_60 = (SVRegion.mines_floor_60,) +mines_floor_100 = (SVRegion.mines_floor_100,) +sewers = (SVRegion.sewers,) +desert = (SVRegion.desert,) +mutant_bug_lair = (SVRegion.mutant_bug_lair,) +witch_swamp = (SVRegion.witch_swamp,) +night_market = (SVRegion.beach,) +ginger_island_ocean = (SVRegion.ginger_island,) +ginger_island_river = (SVRegion.ginger_island,) +pirate_cove = (SVRegion.pirate_cove,) + +all_fish: List[FishItem] = [] + + +def create_fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]], + difficulty: int) -> FishItem: + if isinstance(seasons, str): + seasons = (seasons,) + + fish_item = FishItem(name, item_id, locations, seasons, difficulty) + all_fish.append(fish_item) + return fish_item + + +albacore = create_fish("Albacore", 705, ocean, (season.fall, season.winter), 60) +anchovy = create_fish("Anchovy", 129, ocean, (season.spring, season.fall), 30) +blue_discus = create_fish("Blue Discus", 838, ginger_island_river, season.all_seasons, 60) +bream = create_fish("Bream", 132, town_river + forest_river, season.all_seasons, 35) +bullhead = create_fish("Bullhead", 700, mountain_lake, season.all_seasons, 46) +carp = create_fish("Carp", 142, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15) +catfish = create_fish("Catfish", 143, town_river + forest_river + secret_woods, (season.spring, season.fall), 75) +chub = create_fish("Chub", 702, forest_river + mountain_lake, season.all_seasons, 35) +dorado = create_fish("Dorado", 704, forest_river, season.summer, 78) +eel = create_fish("Eel", 148, ocean, (season.spring, season.fall), 70) +flounder = create_fish("Flounder", 267, ocean, (season.spring, season.summer), 50) +ghostfish = create_fish("Ghostfish", 156, mines_floor_20 + mines_floor_60, season.all_seasons, 50) +halibut = create_fish("Halibut", 708, ocean, season.not_fall, 50) +herring = create_fish("Herring", 147, ocean, (season.spring, season.winter), 25) +ice_pip = create_fish("Ice Pip", 161, mines_floor_60, season.all_seasons, 85) +largemouth_bass = create_fish("Largemouth Bass", 136, mountain_lake, season.all_seasons, 50) +lava_eel = create_fish("Lava Eel", 162, mines_floor_100, season.all_seasons, 90) +lingcod = create_fish("Lingcod", 707, town_river + forest_river + mountain_lake, season.winter, 85) +lionfish = create_fish("Lionfish", 837, ginger_island_ocean, season.all_seasons, 50) +midnight_carp = create_fish("Midnight Carp", 269, mountain_lake + forest_pond + ginger_island_river, + (season.fall, season.winter), 55) +octopus = create_fish("Octopus", 149, ocean, season.summer, 95) +perch = create_fish("Perch", 141, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) +pike = create_fish("Pike", 144, town_river + forest_river + forest_pond, (season.summer, season.winter), 60) +pufferfish = create_fish("Pufferfish", 128, ocean + ginger_island_ocean, season.summer, 80) +rainbow_trout = create_fish("Rainbow Trout", 138, town_river + forest_river + mountain_lake, season.summer, 45) +red_mullet = create_fish("Red Mullet", 146, ocean, (season.summer, season.winter), 55) +red_snapper = create_fish("Red Snapper", 150, ocean, (season.summer, season.fall), 40) +salmon = create_fish("Salmon", 139, town_river + forest_river, season.fall, 50) +sandfish = create_fish("Sandfish", 164, desert, season.all_seasons, 65) +sardine = create_fish("Sardine", 131, ocean, (season.spring, season.fall, season.winter), 30) +scorpion_carp = create_fish("Scorpion Carp", 165, desert, season.all_seasons, 90) +sea_cucumber = create_fish("Sea Cucumber", 154, ocean, (season.fall, season.winter), 40) +shad = create_fish("Shad", 706, town_river + forest_river, season.not_winter, 45) +slimejack = create_fish("Slimejack", 796, mutant_bug_lair, season.all_seasons, 55) +smallmouth_bass = create_fish("Smallmouth Bass", 137, town_river + forest_river, (season.spring, season.fall), 28) +squid = create_fish("Squid", 151, ocean, season.winter, 75) +stingray = create_fish("Stingray", 836, pirate_cove, season.all_seasons, 80) +stonefish = create_fish("Stonefish", 158, mines_floor_20, season.all_seasons, 65) +sturgeon = create_fish("Sturgeon", 698, mountain_lake, (season.summer, season.winter), 78) +sunfish = create_fish("Sunfish", 145, town_river + forest_river, (season.spring, season.summer), 30) +super_cucumber = create_fish("Super Cucumber", 155, ocean + ginger_island_ocean, (season.summer, season.fall), 80) +tiger_trout = create_fish("Tiger Trout", 699, town_river + forest_river, (season.fall, season.winter), 60) +tilapia = create_fish("Tilapia", 701, ocean + ginger_island_ocean, (season.summer, season.fall), 50) +tuna = create_fish("Tuna", 130, ocean + ginger_island_ocean, (season.summer, season.winter), 70) +void_salmon = create_fish("Void Salmon", 795, witch_swamp, season.all_seasons, 80) +walleye = create_fish("Walleye", 140, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) +woodskip = create_fish("Woodskip", 734, secret_woods, season.all_seasons, 50) + +blob_fish = create_fish("Blobfish", 800, night_market, season.winter, 75) +midnight_squid = create_fish("Midnight Squid", 798, night_market, season.winter, 55) +spook_fish = create_fish("Spook Fish", 799, night_market, season.winter, 60) + +angler = create_fish("Angler", 160, town_river, season.fall, 85) +crimsonfish = create_fish("Crimsonfish", 159, ocean, season.summer, 95) +glacierfish = create_fish("Glacierfish", 775, forest_river, season.winter, 100) +legend = create_fish("Legend", 163, mountain_lake, season.spring, 110) +mutant_carp = create_fish("Mutant Carp", 682, sewers, season.all_seasons, 80) + +clam = create_fish("Clam", 372, ocean, season.all_seasons, -1) +cockle = create_fish("Cockle", 718, ocean, season.all_seasons, -1) +crab = create_fish("Crab", 717, ocean, season.all_seasons, -1) +crayfish = create_fish("Crayfish", 716, fresh_water, season.all_seasons, -1) +lobster = create_fish("Lobster", 715, ocean, season.all_seasons, -1) +mussel = create_fish("Mussel", 719, ocean, season.all_seasons, -1) +oyster = create_fish("Oyster", 723, ocean, season.all_seasons, -1) +periwinkle = create_fish("Periwinkle", 722, fresh_water, season.all_seasons, -1) +shrimp = create_fish("Shrimp", 720, ocean, season.all_seasons, -1) +snail = create_fish("Snail", 721, fresh_water, season.all_seasons, -1) + +legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp] +special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py new file mode 100644 index 00000000..cac86d52 --- /dev/null +++ b/worlds/stardew_valley/data/game_item.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class GameItem: + name: str + item_id: int + + def __repr__(self): + return f"{self.name} [{self.item_id}]" + + def __lt__(self, other): + return self.name < other.name diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 425186ed..f2a45034 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -1,7 +1,7 @@ id,name,classification,groups 0,Joja Cola,filler,TRASH -15,Rusty Key,progression, -16,Dwarvish Translation Guide,progression, +15,Rusty Key,progression,MUSEUM +16,Dwarvish Translation Guide,progression,MUSEUM 17,Bridge Repair,progression,COMMUNITY_REWARD 18,Greenhouse,progression,COMMUNITY_REWARD 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD @@ -98,20 +98,99 @@ id,name,classification,groups 110,Luck Bonus,useful, 111,Lava Katana,progression,"MINES_FLOOR_110,WEAPON" 112,Progressive House,progression, -113,Traveling Merchant: Sunday,progression, -114,Traveling Merchant: Monday,progression, -115,Traveling Merchant: Tuesday,progression, -116,Traveling Merchant: Wednesday,progression, -117,Traveling Merchant: Thursday,progression, -118,Traveling Merchant: Friday,progression, -119,Traveling Merchant: Saturday,progression, +113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY +114,Traveling Merchant: Monday,progression,TRAVELING_MERCHANT_DAY +115,Traveling Merchant: Tuesday,progression,TRAVELING_MERCHANT_DAY +116,Traveling Merchant: Wednesday,progression,TRAVELING_MERCHANT_DAY +117,Traveling Merchant: Thursday,progression,TRAVELING_MERCHANT_DAY +118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY +119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY 120,Traveling Merchant Stock Size,progression, 121,Traveling Merchant Discount,progression, 122,Return Scepter,useful, -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" -5003,Resource Pack: 2000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +123,Progressive Season,progression, +124,Spring,progression,SEASON +125,Summer,progression,SEASON +126,Fall,progression,SEASON +127,Winter,progression,SEASON +128,Amaranth Seeds,progression,SEED_SHUFFLE +129,Artichoke Seeds,progression,SEED_SHUFFLE +130,Beet Seeds,progression,SEED_SHUFFLE +131,Jazz Seeds,progression,SEED_SHUFFLE +132,Blueberry Seeds,progression,SEED_SHUFFLE +133,Bok Choy Seeds,progression,SEED_SHUFFLE +134,Cauliflower Seeds,progression,SEED_SHUFFLE +135,Corn Seeds,progression,SEED_SHUFFLE +136,Cranberry Seeds,progression,SEED_SHUFFLE +137,Eggplant Seeds,progression,SEED_SHUFFLE +138,Fairy Seeds,progression,SEED_SHUFFLE +139,Garlic Seeds,progression,SEED_SHUFFLE +140,Grape Starter,progression,SEED_SHUFFLE +141,Bean Starter,progression,SEED_SHUFFLE +142,Hops Starter,progression,SEED_SHUFFLE +143,Pepper Seeds,progression,SEED_SHUFFLE +144,Kale Seeds,progression,SEED_SHUFFLE +145,Melon Seeds,progression,SEED_SHUFFLE +146,Parsnip Seeds,progression,SEED_SHUFFLE +147,Poppy Seeds,progression,SEED_SHUFFLE +148,Potato Seeds,progression,SEED_SHUFFLE +149,Pumpkin Seeds,progression,SEED_SHUFFLE +150,Radish Seeds,progression,SEED_SHUFFLE +151,Red Cabbage Seeds,progression,SEED_SHUFFLE +152,Rhubarb Seeds,progression,SEED_SHUFFLE +153,Starfruit Seeds,progression,SEED_SHUFFLE +154,Strawberry Seeds,progression,SEED_SHUFFLE +155,Spangle Seeds,progression,SEED_SHUFFLE +156,Sunflower Seeds,progression,SEED_SHUFFLE +157,Tomato Seeds,progression,SEED_SHUFFLE +158,Tulip Bulb,progression,SEED_SHUFFLE +159,Rice Shoot,progression,SEED_SHUFFLE +160,Wheat Seeds,progression,SEED_SHUFFLE +161,Yam Seeds,progression,SEED_SHUFFLE +162,Cactus Seeds,progression,SEED_SHUFFLE +163,Magic Rock Candy,useful,MUSEUM +164,Ancient Seeds Recipe,progression,MUSEUM +165,Ancient Seeds,useful,MUSEUM +166,Traveling Merchant Metal Detector,progression,MUSEUM +167,Alex: 1 <3,progression,FRIENDSANITY +168,Elliott: 1 <3,progression,FRIENDSANITY +169,Harvey: 1 <3,progression,FRIENDSANITY +170,Sam: 1 <3,progression,FRIENDSANITY +171,Sebastian: 1 <3,progression,FRIENDSANITY +172,Shane: 1 <3,progression,FRIENDSANITY +173,Abigail: 1 <3,progression,FRIENDSANITY +174,Emily: 1 <3,progression,FRIENDSANITY +175,Haley: 1 <3,progression,FRIENDSANITY +176,Leah: 1 <3,progression,FRIENDSANITY +177,Maru: 1 <3,progression,FRIENDSANITY +178,Penny: 1 <3,progression,FRIENDSANITY +179,Caroline: 1 <3,progression,FRIENDSANITY +180,Clint: 1 <3,progression,FRIENDSANITY +181,Demetrius: 1 <3,progression,FRIENDSANITY +182,Dwarf: 1 <3,progression,FRIENDSANITY +183,Evelyn: 1 <3,progression,FRIENDSANITY +184,George: 1 <3,progression,FRIENDSANITY +185,Gus: 1 <3,progression,FRIENDSANITY +186,Jas: 1 <3,progression,FRIENDSANITY +187,Jodi: 1 <3,progression,FRIENDSANITY +188,Kent: 1 <3,progression,FRIENDSANITY +189,Krobus: 1 <3,progression,FRIENDSANITY +190,Leo: 1 <3,progression,FRIENDSANITY +191,Lewis: 1 <3,progression,FRIENDSANITY +192,Linus: 1 <3,progression,FRIENDSANITY +193,Marnie: 1 <3,progression,FRIENDSANITY +194,Pam: 1 <3,progression,FRIENDSANITY +195,Pierre: 1 <3,progression,FRIENDSANITY +196,Robin: 1 <3,progression,FRIENDSANITY +197,Sandy: 1 <3,progression,FRIENDSANITY +198,Vincent: 1 <3,progression,FRIENDSANITY +199,Willy: 1 <3,progression,FRIENDSANITY +200,Wizard: 1 <3,progression,FRIENDSANITY +201,Pet: 1 <3,progression,FRIENDSANITY +5000,Resource Pack: 500 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5001,Resource Pack: 1000 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5002,Resource Pack: 1500 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5003,Resource Pack: 2000 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" 5004,Resource Pack: 25 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" 5005,Resource Pack: 50 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" 5006,Resource Pack: 75 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" @@ -120,10 +199,10 @@ id,name,classification,groups 5009,Resource Pack: 50 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5010,Resource Pack: 75 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5011,Resource Pack: 100 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" -5012,Resource Pack: 5 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5013,Resource Pack: 10 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5014,Resource Pack: 15 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5015,Resource Pack: 20 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5012,Resource Pack: 5 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5013,Resource Pack: 10 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5014,Resource Pack: 15 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5015,Resource Pack: 20 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5016,Resource Pack: 15 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" 5017,Resource Pack: 30 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" 5018,Resource Pack: 45 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" @@ -178,10 +257,10 @@ id,name,classification,groups 5067,Resource Pack: 6 Magma Geode,filler,"GEODE,RESOURCE_PACK" 5068,Resource Pack: 9 Magma Geode,filler,"GEODE,RESOURCE_PACK" 5069,Resource Pack: 12 Magma Geode,filler,"GEODE,RESOURCE_PACK" -5070,Resource Pack: 2 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5071,Resource Pack: 4 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5072,Resource Pack: 6 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5073,Resource Pack: 8 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5070,Resource Pack: 2 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5071,Resource Pack: 4 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5072,Resource Pack: 6 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5073,Resource Pack: 8 Omni Geode,filler,"GEODE,RESOURCE_PACK" 5074,Resource Pack: 25 Copper Ore,filler,"ORE,RESOURCE_PACK" 5075,Resource Pack: 50 Copper Ore,filler,"ORE,RESOURCE_PACK" 5076,Resource Pack: 75 Copper Ore,filler,"ORE,RESOURCE_PACK" @@ -192,14 +271,14 @@ id,name,classification,groups 5081,Resource Pack: 50 Iron Ore,filler,"ORE,RESOURCE_PACK" 5082,Resource Pack: 75 Iron Ore,filler,"ORE,RESOURCE_PACK" 5083,Resource Pack: 100 Iron Ore,filler,"ORE,RESOURCE_PACK" -5084,Resource Pack: 12 Gold Ore,useful,"ORE,RESOURCE_PACK" -5085,Resource Pack: 25 Gold Ore,useful,"ORE,RESOURCE_PACK" -5086,Resource Pack: 38 Gold Ore,useful,"ORE,RESOURCE_PACK" -5087,Resource Pack: 50 Gold Ore,useful,"ORE,RESOURCE_PACK" -5088,Resource Pack: 5 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5089,Resource Pack: 10 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5090,Resource Pack: 15 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5091,Resource Pack: 20 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5084,Resource Pack: 12 Gold Ore,filler,"ORE,RESOURCE_PACK" +5085,Resource Pack: 25 Gold Ore,filler,"ORE,RESOURCE_PACK" +5086,Resource Pack: 38 Gold Ore,filler,"ORE,RESOURCE_PACK" +5087,Resource Pack: 50 Gold Ore,filler,"ORE,RESOURCE_PACK" +5088,Resource Pack: 5 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5089,Resource Pack: 10 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5090,Resource Pack: 15 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5091,Resource Pack: 20 Iridium Ore,filler,"ORE,RESOURCE_PACK" 5092,Resource Pack: 5 Quartz,filler,"ORE,RESOURCE_PACK" 5093,Resource Pack: 10 Quartz,filler,"ORE,RESOURCE_PACK" 5094,Resource Pack: 15 Quartz,filler,"ORE,RESOURCE_PACK" @@ -240,24 +319,24 @@ id,name,classification,groups 5129,Resource Pack: 28 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5130,Resource Pack: 36 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5131,Resource Pack: 40 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" -5132,Resource Pack: 2 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5133,Resource Pack: 6 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5134,Resource Pack: 10 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5135,Resource Pack: 14 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5136,Resource Pack: 18 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5137,Resource Pack: 20 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5138,Resource Pack: 2 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5139,Resource Pack: 6 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5140,Resource Pack: 10 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5141,Resource Pack: 14 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5142,Resource Pack: 18 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5143,Resource Pack: 20 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5144,Resource Pack: 2 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5145,Resource Pack: 6 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5146,Resource Pack: 10 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5147,Resource Pack: 14 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5148,Resource Pack: 18 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5149,Resource Pack: 20 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5132,Resource Pack: 2 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5133,Resource Pack: 6 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5134,Resource Pack: 10 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5135,Resource Pack: 14 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5136,Resource Pack: 18 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5137,Resource Pack: 20 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5138,Resource Pack: 2 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5139,Resource Pack: 6 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5140,Resource Pack: 10 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5141,Resource Pack: 14 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5142,Resource Pack: 18 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5143,Resource Pack: 20 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5144,Resource Pack: 2 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5145,Resource Pack: 6 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5146,Resource Pack: 10 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5147,Resource Pack: 14 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5148,Resource Pack: 18 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5149,Resource Pack: 20 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5150,Resource Pack: 2 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" 5151,Resource Pack: 6 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" 5152,Resource Pack: 10 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" @@ -306,7 +385,7 @@ id,name,classification,groups 5195,Resource Pack: 4 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" 5196,Resource Pack: 5 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" 5197,Resource Pack: 6 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" -5198,Friendship Bonus (1 <3),useful,FRIENDSHIP_PACK -5199,Friendship Bonus (2 <3),useful,FRIENDSHIP_PACK -5200,Friendship Bonus (3 <3),useful,FRIENDSHIP_PACK -5201,Friendship Bonus (4 <3),useful,FRIENDSHIP_PACK +5198,Friendship Bonus (1 <3),filler,FRIENDSHIP_PACK +5199,Friendship Bonus (2 <3),filler,FRIENDSHIP_PACK +5200,Friendship Bonus (3 <3),filler,FRIENDSHIP_PACK +5201,Friendship Bonus (4 <3),filler,FRIENDSHIP_PACK diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index abad3c04..b36096e6 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -377,3 +377,534 @@ id,region,name,tags 1063,Beach,Fishsanity: Mussel,FISHSANITY 1064,Beach,Fishsanity: Shrimp,FISHSANITY 1065,Beach,Fishsanity: Oyster,FISHSANITY +1100,Stardew Valley,Museumsanity: 5 Donations,MUSEUM_MILESTONES +1101,Stardew Valley,Museumsanity: 10 Donations,MUSEUM_MILESTONES +1102,Stardew Valley,Museumsanity: 15 Donations,MUSEUM_MILESTONES +1103,Stardew Valley,Museumsanity: 20 Donations,MUSEUM_MILESTONES +1104,Stardew Valley,Museumsanity: 25 Donations,MUSEUM_MILESTONES +1105,Stardew Valley,Museumsanity: 30 Donations,MUSEUM_MILESTONES +1106,Stardew Valley,Museumsanity: 35 Donations,MUSEUM_MILESTONES +1107,Stardew Valley,Museumsanity: 40 Donations,MUSEUM_MILESTONES +1108,Stardew Valley,Museumsanity: 50 Donations,MUSEUM_MILESTONES +1109,Stardew Valley,Museumsanity: 60 Donations,MUSEUM_MILESTONES +1110,Stardew Valley,Museumsanity: 70 Donations,MUSEUM_MILESTONES +1111,Stardew Valley,Museumsanity: 80 Donations,MUSEUM_MILESTONES +1112,Stardew Valley,Museumsanity: 90 Donations,MUSEUM_MILESTONES +1113,Stardew Valley,Museumsanity: 95 Donations,MUSEUM_MILESTONES +1114,Stardew Valley,Museumsanity: 11 Minerals,MUSEUM_MILESTONES +1115,Stardew Valley,Museumsanity: 21 Minerals,MUSEUM_MILESTONES +1116,Stardew Valley,Museumsanity: 31 Minerals,MUSEUM_MILESTONES +1117,Stardew Valley,Museumsanity: 41 Minerals,MUSEUM_MILESTONES +1118,Stardew Valley,Museumsanity: 50 Minerals,MUSEUM_MILESTONES +1119,Stardew Valley,Museumsanity: 3 Artifacts,MUSEUM_MILESTONES +1120,Stardew Valley,Museumsanity: 6 Artifacts,MUSEUM_MILESTONES +1121,Stardew Valley,Museumsanity: 9 Artifacts,MUSEUM_MILESTONES +1122,Stardew Valley,Museumsanity: 11 Artifacts,MUSEUM_MILESTONES +1123,Stardew Valley,Museumsanity: 15 Artifacts,MUSEUM_MILESTONES +1124,Stardew Valley,Museumsanity: 20 Artifacts,MUSEUM_MILESTONES +1125,Stardew Valley,Museumsanity: Dwarf Scrolls,MUSEUM_MILESTONES +1126,Stardew Valley,Museumsanity: Skeleton Front,MUSEUM_MILESTONES +1127,Stardew Valley,Museumsanity: Skeleton Middle,MUSEUM_MILESTONES +1128,Stardew Valley,Museumsanity: Skeleton Back,MUSEUM_MILESTONES +1201,The Mines - Floor 20,Museumsanity: Dwarf Scroll I,MUSEUM_DONATIONS +1202,The Mines - Floor 20,Museumsanity: Dwarf Scroll II,MUSEUM_DONATIONS +1203,The Mines - Floor 60,Museumsanity: Dwarf Scroll III,MUSEUM_DONATIONS +1204,The Mines - Floor 100,Museumsanity: Dwarf Scroll IV,MUSEUM_DONATIONS +1205,Town,Museumsanity: Chipped Amphora,MUSEUM_DONATIONS +1206,Forest,Museumsanity: Arrowhead,MUSEUM_DONATIONS +1207,Forest,Museumsanity: Ancient Doll,MUSEUM_DONATIONS +1208,Forest,Museumsanity: Elvish Jewelry,MUSEUM_DONATIONS +1209,Forest,Museumsanity: Chewing Stick,MUSEUM_DONATIONS +1210,Forest,Museumsanity: Ornamental Fan,MUSEUM_DONATIONS +1211,Mountain,Museumsanity: Dinosaur Egg,MUSEUM_DONATIONS +1212,Stardew Valley,Museumsanity: Rare Disc,MUSEUM_DONATIONS +1213,Forest,Museumsanity: Ancient Sword,MUSEUM_DONATIONS +1214,Town,Museumsanity: Rusty Spoon,MUSEUM_DONATIONS +1215,Farm,Museumsanity: Rusty Spur,MUSEUM_DONATIONS +1216,Mountain,Museumsanity: Rusty Cog,MUSEUM_DONATIONS +1217,Farm,Museumsanity: Chicken Statue,MUSEUM_DONATIONS +1218,Forest,Museumsanity: Ancient Seed,"MUSEUM_DONATIONS,MUSEUM_MILESTONES" +1219,Forest,Museumsanity: Prehistoric Tool,MUSEUM_DONATIONS +1220,Beach,Museumsanity: Dried Starfish,MUSEUM_DONATIONS +1221,Beach,Museumsanity: Anchor,MUSEUM_DONATIONS +1222,Beach,Museumsanity: Glass Shards,MUSEUM_DONATIONS +1223,Forest,Museumsanity: Bone Flute,MUSEUM_DONATIONS +1224,Forest,Museumsanity: Prehistoric Handaxe,MUSEUM_DONATIONS +1225,The Mines - Floor 20,Museumsanity: Dwarvish Helm,MUSEUM_DONATIONS +1226,The Mines - Floor 60,Museumsanity: Dwarf Gadget,MUSEUM_DONATIONS +1227,Forest,Museumsanity: Ancient Drum,MUSEUM_DONATIONS +1228,The Desert,Museumsanity: Golden Mask,MUSEUM_DONATIONS +1229,The Desert,Museumsanity: Golden Relic,MUSEUM_DONATIONS +1230,Town,Museumsanity: Strange Doll (Green),MUSEUM_DONATIONS +1231,The Desert,Museumsanity: Strange Doll,MUSEUM_DONATIONS +1232,Forest,Museumsanity: Prehistoric Scapula,MUSEUM_DONATIONS +1233,Forest,Museumsanity: Prehistoric Tibia,MUSEUM_DONATIONS +1234,Ginger Island,Museumsanity: Prehistoric Skull,MUSEUM_DONATIONS +1235,Ginger Island,Museumsanity: Skeletal Hand,MUSEUM_DONATIONS +1236,Ginger Island,Museumsanity: Prehistoric Rib,MUSEUM_DONATIONS +1237,Ginger Island,Museumsanity: Prehistoric Vertebra,MUSEUM_DONATIONS +1238,Ginger Island,Museumsanity: Skeletal Tail,MUSEUM_DONATIONS +1239,Ginger Island,Museumsanity: Nautilus Fossil,MUSEUM_DONATIONS +1240,Forest,Museumsanity: Amphibian Fossil,MUSEUM_DONATIONS +1241,Forest,Museumsanity: Palm Fossil,MUSEUM_DONATIONS +1242,Forest,Museumsanity: Trilobite,MUSEUM_DONATIONS +1243,The Mines - Floor 20,Museumsanity: Quartz,MUSEUM_DONATIONS +1244,The Mines - Floor 100,Museumsanity: Fire Quartz,MUSEUM_DONATIONS +1245,The Mines - Floor 60,Museumsanity: Frozen Tear,MUSEUM_DONATIONS +1246,The Mines - Floor 20,Museumsanity: Earth Crystal,MUSEUM_DONATIONS +1247,The Mines - Floor 100,Museumsanity: Emerald,MUSEUM_DONATIONS +1248,The Mines - Floor 60,Museumsanity: Aquamarine,MUSEUM_DONATIONS +1249,The Mines - Floor 100,Museumsanity: Ruby,MUSEUM_DONATIONS +1250,The Mines - Floor 20,Museumsanity: Amethyst,MUSEUM_DONATIONS +1251,The Mines - Floor 20,Museumsanity: Topaz,MUSEUM_DONATIONS +1252,The Mines - Floor 60,Museumsanity: Jade,MUSEUM_DONATIONS +1253,The Mines - Floor 60,Museumsanity: Diamond,MUSEUM_DONATIONS +1254,Skull Cavern Floor 100,Museumsanity: Prismatic Shard,MUSEUM_DONATIONS +1255,Town,Museumsanity: Alamite,MUSEUM_DONATIONS +1256,Town,Museumsanity: Bixite,MUSEUM_DONATIONS +1257,Town,Museumsanity: Baryte,MUSEUM_DONATIONS +1258,Town,Museumsanity: Aerinite,MUSEUM_DONATIONS +1259,Town,Museumsanity: Calcite,MUSEUM_DONATIONS +1260,Town,Museumsanity: Dolomite,MUSEUM_DONATIONS +1261,Town,Museumsanity: Esperite,MUSEUM_DONATIONS +1262,Town,Museumsanity: Fluorapatite,MUSEUM_DONATIONS +1263,Town,Museumsanity: Geminite,MUSEUM_DONATIONS +1264,Town,Museumsanity: Helvite,MUSEUM_DONATIONS +1265,Town,Museumsanity: Jamborite,MUSEUM_DONATIONS +1266,Town,Museumsanity: Jagoite,MUSEUM_DONATIONS +1267,Town,Museumsanity: Kyanite,MUSEUM_DONATIONS +1268,Town,Museumsanity: Lunarite,MUSEUM_DONATIONS +1269,Town,Museumsanity: Malachite,MUSEUM_DONATIONS +1270,Town,Museumsanity: Neptunite,MUSEUM_DONATIONS +1271,Town,Museumsanity: Lemon Stone,MUSEUM_DONATIONS +1272,Town,Museumsanity: Nekoite,MUSEUM_DONATIONS +1273,Town,Museumsanity: Orpiment,MUSEUM_DONATIONS +1274,Town,Museumsanity: Petrified Slime,MUSEUM_DONATIONS +1275,Town,Museumsanity: Thunder Egg,MUSEUM_DONATIONS +1276,Town,Museumsanity: Pyrite,MUSEUM_DONATIONS +1277,Town,Museumsanity: Ocean Stone,MUSEUM_DONATIONS +1278,Town,Museumsanity: Ghost Crystal,MUSEUM_DONATIONS +1279,Town,Museumsanity: Tigerseye,MUSEUM_DONATIONS +1280,Town,Museumsanity: Jasper,MUSEUM_DONATIONS +1281,Town,Museumsanity: Opal,MUSEUM_DONATIONS +1282,Town,Museumsanity: Fire Opal,MUSEUM_DONATIONS +1283,Town,Museumsanity: Celestine,MUSEUM_DONATIONS +1284,Town,Museumsanity: Marble,MUSEUM_DONATIONS +1285,Town,Museumsanity: Sandstone,MUSEUM_DONATIONS +1286,Town,Museumsanity: Granite,MUSEUM_DONATIONS +1287,Town,Museumsanity: Basalt,MUSEUM_DONATIONS +1288,Town,Museumsanity: Limestone,MUSEUM_DONATIONS +1289,Town,Museumsanity: Soapstone,MUSEUM_DONATIONS +1290,Town,Museumsanity: Hematite,MUSEUM_DONATIONS +1291,Town,Museumsanity: Mudstone,MUSEUM_DONATIONS +1292,Town,Museumsanity: Obsidian,MUSEUM_DONATIONS +1293,Town,Museumsanity: Slate,MUSEUM_DONATIONS +1294,Town,Museumsanity: Fairy Stone,MUSEUM_DONATIONS +1295,Town,Museumsanity: Star Shards,MUSEUM_DONATIONS +1301,Town,Friendsanity: Alex 1 <3,FRIENDSANITY +1302,Town,Friendsanity: Alex 2 <3,FRIENDSANITY +1303,Town,Friendsanity: Alex 3 <3,FRIENDSANITY +1304,Town,Friendsanity: Alex 4 <3,FRIENDSANITY +1305,Town,Friendsanity: Alex 5 <3,FRIENDSANITY +1306,Town,Friendsanity: Alex 6 <3,FRIENDSANITY +1307,Town,Friendsanity: Alex 7 <3,FRIENDSANITY +1308,Town,Friendsanity: Alex 8 <3,FRIENDSANITY +1309,Town,Friendsanity: Alex 9 <3,FRIENDSANITY +1310,Town,Friendsanity: Alex 10 <3,FRIENDSANITY +1311,Town,Friendsanity: Alex 11 <3,FRIENDSANITY +1312,Town,Friendsanity: Alex 12 <3,FRIENDSANITY +1313,Town,Friendsanity: Alex 13 <3,FRIENDSANITY +1314,Town,Friendsanity: Alex 14 <3,FRIENDSANITY +1315,Beach,Friendsanity: Elliott 1 <3,FRIENDSANITY +1316,Beach,Friendsanity: Elliott 2 <3,FRIENDSANITY +1317,Beach,Friendsanity: Elliott 3 <3,FRIENDSANITY +1318,Beach,Friendsanity: Elliott 4 <3,FRIENDSANITY +1319,Beach,Friendsanity: Elliott 5 <3,FRIENDSANITY +1320,Beach,Friendsanity: Elliott 6 <3,FRIENDSANITY +1321,Beach,Friendsanity: Elliott 7 <3,FRIENDSANITY +1322,Beach,Friendsanity: Elliott 8 <3,FRIENDSANITY +1323,Beach,Friendsanity: Elliott 9 <3,FRIENDSANITY +1324,Beach,Friendsanity: Elliott 10 <3,FRIENDSANITY +1325,Beach,Friendsanity: Elliott 11 <3,FRIENDSANITY +1326,Beach,Friendsanity: Elliott 12 <3,FRIENDSANITY +1327,Beach,Friendsanity: Elliott 13 <3,FRIENDSANITY +1328,Beach,Friendsanity: Elliott 14 <3,FRIENDSANITY +1329,Town,Friendsanity: Harvey 1 <3,FRIENDSANITY +1330,Town,Friendsanity: Harvey 2 <3,FRIENDSANITY +1331,Town,Friendsanity: Harvey 3 <3,FRIENDSANITY +1332,Town,Friendsanity: Harvey 4 <3,FRIENDSANITY +1333,Town,Friendsanity: Harvey 5 <3,FRIENDSANITY +1334,Town,Friendsanity: Harvey 6 <3,FRIENDSANITY +1335,Town,Friendsanity: Harvey 7 <3,FRIENDSANITY +1336,Town,Friendsanity: Harvey 8 <3,FRIENDSANITY +1337,Town,Friendsanity: Harvey 9 <3,FRIENDSANITY +1338,Town,Friendsanity: Harvey 10 <3,FRIENDSANITY +1339,Town,Friendsanity: Harvey 11 <3,FRIENDSANITY +1340,Town,Friendsanity: Harvey 12 <3,FRIENDSANITY +1341,Town,Friendsanity: Harvey 13 <3,FRIENDSANITY +1342,Town,Friendsanity: Harvey 14 <3,FRIENDSANITY +1343,Town,Friendsanity: Sam 1 <3,FRIENDSANITY +1344,Town,Friendsanity: Sam 2 <3,FRIENDSANITY +1345,Town,Friendsanity: Sam 3 <3,FRIENDSANITY +1346,Town,Friendsanity: Sam 4 <3,FRIENDSANITY +1347,Town,Friendsanity: Sam 5 <3,FRIENDSANITY +1348,Town,Friendsanity: Sam 6 <3,FRIENDSANITY +1349,Town,Friendsanity: Sam 7 <3,FRIENDSANITY +1350,Town,Friendsanity: Sam 8 <3,FRIENDSANITY +1351,Town,Friendsanity: Sam 9 <3,FRIENDSANITY +1352,Town,Friendsanity: Sam 10 <3,FRIENDSANITY +1353,Town,Friendsanity: Sam 11 <3,FRIENDSANITY +1354,Town,Friendsanity: Sam 12 <3,FRIENDSANITY +1355,Town,Friendsanity: Sam 13 <3,FRIENDSANITY +1356,Town,Friendsanity: Sam 14 <3,FRIENDSANITY +1357,Carpenter Shop,Friendsanity: Sebastian 1 <3,FRIENDSANITY +1358,Carpenter Shop,Friendsanity: Sebastian 2 <3,FRIENDSANITY +1359,Carpenter Shop,Friendsanity: Sebastian 3 <3,FRIENDSANITY +1360,Carpenter Shop,Friendsanity: Sebastian 4 <3,FRIENDSANITY +1361,Carpenter Shop,Friendsanity: Sebastian 5 <3,FRIENDSANITY +1362,Carpenter Shop,Friendsanity: Sebastian 6 <3,FRIENDSANITY +1363,Carpenter Shop,Friendsanity: Sebastian 7 <3,FRIENDSANITY +1364,Carpenter Shop,Friendsanity: Sebastian 8 <3,FRIENDSANITY +1365,Carpenter Shop,Friendsanity: Sebastian 9 <3,FRIENDSANITY +1366,Carpenter Shop,Friendsanity: Sebastian 10 <3,FRIENDSANITY +1367,Carpenter Shop,Friendsanity: Sebastian 11 <3,FRIENDSANITY +1368,Carpenter Shop,Friendsanity: Sebastian 12 <3,FRIENDSANITY +1369,Carpenter Shop,Friendsanity: Sebastian 13 <3,FRIENDSANITY +1370,Carpenter Shop,Friendsanity: Sebastian 14 <3,FRIENDSANITY +1371,Marnie's Ranch,Friendsanity: Shane 1 <3,FRIENDSANITY +1372,Marnie's Ranch,Friendsanity: Shane 2 <3,FRIENDSANITY +1373,Marnie's Ranch,Friendsanity: Shane 3 <3,FRIENDSANITY +1374,Marnie's Ranch,Friendsanity: Shane 4 <3,FRIENDSANITY +1375,Marnie's Ranch,Friendsanity: Shane 5 <3,FRIENDSANITY +1376,Marnie's Ranch,Friendsanity: Shane 6 <3,FRIENDSANITY +1377,Marnie's Ranch,Friendsanity: Shane 7 <3,FRIENDSANITY +1378,Marnie's Ranch,Friendsanity: Shane 8 <3,FRIENDSANITY +1379,Marnie's Ranch,Friendsanity: Shane 9 <3,FRIENDSANITY +1380,Marnie's Ranch,Friendsanity: Shane 10 <3,FRIENDSANITY +1381,Marnie's Ranch,Friendsanity: Shane 11 <3,FRIENDSANITY +1382,Marnie's Ranch,Friendsanity: Shane 12 <3,FRIENDSANITY +1383,Marnie's Ranch,Friendsanity: Shane 13 <3,FRIENDSANITY +1384,Marnie's Ranch,Friendsanity: Shane 14 <3,FRIENDSANITY +1385,Town,Friendsanity: Abigail 1 <3,FRIENDSANITY +1386,Town,Friendsanity: Abigail 2 <3,FRIENDSANITY +1387,Town,Friendsanity: Abigail 3 <3,FRIENDSANITY +1388,Town,Friendsanity: Abigail 4 <3,FRIENDSANITY +1389,Town,Friendsanity: Abigail 5 <3,FRIENDSANITY +1390,Town,Friendsanity: Abigail 6 <3,FRIENDSANITY +1391,Town,Friendsanity: Abigail 7 <3,FRIENDSANITY +1392,Town,Friendsanity: Abigail 8 <3,FRIENDSANITY +1393,Town,Friendsanity: Abigail 9 <3,FRIENDSANITY +1394,Town,Friendsanity: Abigail 10 <3,FRIENDSANITY +1395,Town,Friendsanity: Abigail 11 <3,FRIENDSANITY +1396,Town,Friendsanity: Abigail 12 <3,FRIENDSANITY +1397,Town,Friendsanity: Abigail 13 <3,FRIENDSANITY +1398,Town,Friendsanity: Abigail 14 <3,FRIENDSANITY +1399,Town,Friendsanity: Emily 1 <3,FRIENDSANITY +1400,Town,Friendsanity: Emily 2 <3,FRIENDSANITY +1401,Town,Friendsanity: Emily 3 <3,FRIENDSANITY +1402,Town,Friendsanity: Emily 4 <3,FRIENDSANITY +1403,Town,Friendsanity: Emily 5 <3,FRIENDSANITY +1404,Town,Friendsanity: Emily 6 <3,FRIENDSANITY +1405,Town,Friendsanity: Emily 7 <3,FRIENDSANITY +1406,Town,Friendsanity: Emily 8 <3,FRIENDSANITY +1407,Town,Friendsanity: Emily 9 <3,FRIENDSANITY +1408,Town,Friendsanity: Emily 10 <3,FRIENDSANITY +1409,Town,Friendsanity: Emily 11 <3,FRIENDSANITY +1410,Town,Friendsanity: Emily 12 <3,FRIENDSANITY +1411,Town,Friendsanity: Emily 13 <3,FRIENDSANITY +1412,Town,Friendsanity: Emily 14 <3,FRIENDSANITY +1413,Town,Friendsanity: Haley 1 <3,FRIENDSANITY +1414,Town,Friendsanity: Haley 2 <3,FRIENDSANITY +1415,Town,Friendsanity: Haley 3 <3,FRIENDSANITY +1416,Town,Friendsanity: Haley 4 <3,FRIENDSANITY +1417,Town,Friendsanity: Haley 5 <3,FRIENDSANITY +1418,Town,Friendsanity: Haley 6 <3,FRIENDSANITY +1419,Town,Friendsanity: Haley 7 <3,FRIENDSANITY +1420,Town,Friendsanity: Haley 8 <3,FRIENDSANITY +1421,Town,Friendsanity: Haley 9 <3,FRIENDSANITY +1422,Town,Friendsanity: Haley 10 <3,FRIENDSANITY +1423,Town,Friendsanity: Haley 11 <3,FRIENDSANITY +1424,Town,Friendsanity: Haley 12 <3,FRIENDSANITY +1425,Town,Friendsanity: Haley 13 <3,FRIENDSANITY +1426,Town,Friendsanity: Haley 14 <3,FRIENDSANITY +1427,Forest,Friendsanity: Leah 1 <3,FRIENDSANITY +1428,Forest,Friendsanity: Leah 2 <3,FRIENDSANITY +1429,Forest,Friendsanity: Leah 3 <3,FRIENDSANITY +1430,Forest,Friendsanity: Leah 4 <3,FRIENDSANITY +1431,Forest,Friendsanity: Leah 5 <3,FRIENDSANITY +1432,Forest,Friendsanity: Leah 6 <3,FRIENDSANITY +1433,Forest,Friendsanity: Leah 7 <3,FRIENDSANITY +1434,Forest,Friendsanity: Leah 8 <3,FRIENDSANITY +1435,Forest,Friendsanity: Leah 9 <3,FRIENDSANITY +1436,Forest,Friendsanity: Leah 10 <3,FRIENDSANITY +1437,Forest,Friendsanity: Leah 11 <3,FRIENDSANITY +1438,Forest,Friendsanity: Leah 12 <3,FRIENDSANITY +1439,Forest,Friendsanity: Leah 13 <3,FRIENDSANITY +1440,Forest,Friendsanity: Leah 14 <3,FRIENDSANITY +1441,Carpenter Shop,Friendsanity: Maru 1 <3,FRIENDSANITY +1442,Carpenter Shop,Friendsanity: Maru 2 <3,FRIENDSANITY +1443,Carpenter Shop,Friendsanity: Maru 3 <3,FRIENDSANITY +1444,Carpenter Shop,Friendsanity: Maru 4 <3,FRIENDSANITY +1445,Carpenter Shop,Friendsanity: Maru 5 <3,FRIENDSANITY +1446,Carpenter Shop,Friendsanity: Maru 6 <3,FRIENDSANITY +1447,Carpenter Shop,Friendsanity: Maru 7 <3,FRIENDSANITY +1448,Carpenter Shop,Friendsanity: Maru 8 <3,FRIENDSANITY +1449,Carpenter Shop,Friendsanity: Maru 9 <3,FRIENDSANITY +1450,Carpenter Shop,Friendsanity: Maru 10 <3,FRIENDSANITY +1451,Carpenter Shop,Friendsanity: Maru 11 <3,FRIENDSANITY +1452,Carpenter Shop,Friendsanity: Maru 12 <3,FRIENDSANITY +1453,Carpenter Shop,Friendsanity: Maru 13 <3,FRIENDSANITY +1454,Carpenter Shop,Friendsanity: Maru 14 <3,FRIENDSANITY +1455,Town,Friendsanity: Penny 1 <3,FRIENDSANITY +1456,Town,Friendsanity: Penny 2 <3,FRIENDSANITY +1457,Town,Friendsanity: Penny 3 <3,FRIENDSANITY +1458,Town,Friendsanity: Penny 4 <3,FRIENDSANITY +1459,Town,Friendsanity: Penny 5 <3,FRIENDSANITY +1460,Town,Friendsanity: Penny 6 <3,FRIENDSANITY +1461,Town,Friendsanity: Penny 7 <3,FRIENDSANITY +1462,Town,Friendsanity: Penny 8 <3,FRIENDSANITY +1463,Town,Friendsanity: Penny 9 <3,FRIENDSANITY +1464,Town,Friendsanity: Penny 10 <3,FRIENDSANITY +1465,Town,Friendsanity: Penny 11 <3,FRIENDSANITY +1466,Town,Friendsanity: Penny 12 <3,FRIENDSANITY +1467,Town,Friendsanity: Penny 13 <3,FRIENDSANITY +1468,Town,Friendsanity: Penny 14 <3,FRIENDSANITY +1469,Town,Friendsanity: Caroline 1 <3,FRIENDSANITY +1470,Town,Friendsanity: Caroline 2 <3,FRIENDSANITY +1471,Town,Friendsanity: Caroline 3 <3,FRIENDSANITY +1472,Town,Friendsanity: Caroline 4 <3,FRIENDSANITY +1473,Town,Friendsanity: Caroline 5 <3,FRIENDSANITY +1474,Town,Friendsanity: Caroline 6 <3,FRIENDSANITY +1475,Town,Friendsanity: Caroline 7 <3,FRIENDSANITY +1476,Town,Friendsanity: Caroline 8 <3,FRIENDSANITY +1477,Town,Friendsanity: Caroline 9 <3,FRIENDSANITY +1478,Town,Friendsanity: Caroline 10 <3,FRIENDSANITY +1480,Town,Friendsanity: Clint 1 <3,FRIENDSANITY +1481,Town,Friendsanity: Clint 2 <3,FRIENDSANITY +1482,Town,Friendsanity: Clint 3 <3,FRIENDSANITY +1483,Town,Friendsanity: Clint 4 <3,FRIENDSANITY +1484,Town,Friendsanity: Clint 5 <3,FRIENDSANITY +1485,Town,Friendsanity: Clint 6 <3,FRIENDSANITY +1486,Town,Friendsanity: Clint 7 <3,FRIENDSANITY +1487,Town,Friendsanity: Clint 8 <3,FRIENDSANITY +1488,Town,Friendsanity: Clint 9 <3,FRIENDSANITY +1489,Town,Friendsanity: Clint 10 <3,FRIENDSANITY +1491,Carpenter Shop,Friendsanity: Demetrius 1 <3,FRIENDSANITY +1492,Carpenter Shop,Friendsanity: Demetrius 2 <3,FRIENDSANITY +1493,Carpenter Shop,Friendsanity: Demetrius 3 <3,FRIENDSANITY +1494,Carpenter Shop,Friendsanity: Demetrius 4 <3,FRIENDSANITY +1495,Carpenter Shop,Friendsanity: Demetrius 5 <3,FRIENDSANITY +1496,Carpenter Shop,Friendsanity: Demetrius 6 <3,FRIENDSANITY +1497,Carpenter Shop,Friendsanity: Demetrius 7 <3,FRIENDSANITY +1498,Carpenter Shop,Friendsanity: Demetrius 8 <3,FRIENDSANITY +1499,Carpenter Shop,Friendsanity: Demetrius 9 <3,FRIENDSANITY +1500,Carpenter Shop,Friendsanity: Demetrius 10 <3,FRIENDSANITY +1502,The Mines,Friendsanity: Dwarf 1 <3,FRIENDSANITY +1503,The Mines,Friendsanity: Dwarf 2 <3,FRIENDSANITY +1504,The Mines,Friendsanity: Dwarf 3 <3,FRIENDSANITY +1505,The Mines,Friendsanity: Dwarf 4 <3,FRIENDSANITY +1506,The Mines,Friendsanity: Dwarf 5 <3,FRIENDSANITY +1507,The Mines,Friendsanity: Dwarf 6 <3,FRIENDSANITY +1508,The Mines,Friendsanity: Dwarf 7 <3,FRIENDSANITY +1509,The Mines,Friendsanity: Dwarf 8 <3,FRIENDSANITY +1510,The Mines,Friendsanity: Dwarf 9 <3,FRIENDSANITY +1511,The Mines,Friendsanity: Dwarf 10 <3,FRIENDSANITY +1513,Town,Friendsanity: Evelyn 1 <3,FRIENDSANITY +1514,Town,Friendsanity: Evelyn 2 <3,FRIENDSANITY +1515,Town,Friendsanity: Evelyn 3 <3,FRIENDSANITY +1516,Town,Friendsanity: Evelyn 4 <3,FRIENDSANITY +1517,Town,Friendsanity: Evelyn 5 <3,FRIENDSANITY +1518,Town,Friendsanity: Evelyn 6 <3,FRIENDSANITY +1519,Town,Friendsanity: Evelyn 7 <3,FRIENDSANITY +1520,Town,Friendsanity: Evelyn 8 <3,FRIENDSANITY +1521,Town,Friendsanity: Evelyn 9 <3,FRIENDSANITY +1522,Town,Friendsanity: Evelyn 10 <3,FRIENDSANITY +1524,Town,Friendsanity: George 1 <3,FRIENDSANITY +1525,Town,Friendsanity: George 2 <3,FRIENDSANITY +1526,Town,Friendsanity: George 3 <3,FRIENDSANITY +1527,Town,Friendsanity: George 4 <3,FRIENDSANITY +1528,Town,Friendsanity: George 5 <3,FRIENDSANITY +1529,Town,Friendsanity: George 6 <3,FRIENDSANITY +1530,Town,Friendsanity: George 7 <3,FRIENDSANITY +1531,Town,Friendsanity: George 8 <3,FRIENDSANITY +1532,Town,Friendsanity: George 9 <3,FRIENDSANITY +1533,Town,Friendsanity: George 10 <3,FRIENDSANITY +1535,Town,Friendsanity: Gus 1 <3,FRIENDSANITY +1536,Town,Friendsanity: Gus 2 <3,FRIENDSANITY +1537,Town,Friendsanity: Gus 3 <3,FRIENDSANITY +1538,Town,Friendsanity: Gus 4 <3,FRIENDSANITY +1539,Town,Friendsanity: Gus 5 <3,FRIENDSANITY +1540,Town,Friendsanity: Gus 6 <3,FRIENDSANITY +1541,Town,Friendsanity: Gus 7 <3,FRIENDSANITY +1542,Town,Friendsanity: Gus 8 <3,FRIENDSANITY +1543,Town,Friendsanity: Gus 9 <3,FRIENDSANITY +1544,Town,Friendsanity: Gus 10 <3,FRIENDSANITY +1546,Marnie's Ranch,Friendsanity: Jas 1 <3,FRIENDSANITY +1547,Marnie's Ranch,Friendsanity: Jas 2 <3,FRIENDSANITY +1548,Marnie's Ranch,Friendsanity: Jas 3 <3,FRIENDSANITY +1549,Marnie's Ranch,Friendsanity: Jas 4 <3,FRIENDSANITY +1550,Marnie's Ranch,Friendsanity: Jas 5 <3,FRIENDSANITY +1551,Marnie's Ranch,Friendsanity: Jas 6 <3,FRIENDSANITY +1552,Marnie's Ranch,Friendsanity: Jas 7 <3,FRIENDSANITY +1553,Marnie's Ranch,Friendsanity: Jas 8 <3,FRIENDSANITY +1554,Marnie's Ranch,Friendsanity: Jas 9 <3,FRIENDSANITY +1555,Marnie's Ranch,Friendsanity: Jas 10 <3,FRIENDSANITY +1557,Town,Friendsanity: Jodi 1 <3,FRIENDSANITY +1558,Town,Friendsanity: Jodi 2 <3,FRIENDSANITY +1559,Town,Friendsanity: Jodi 3 <3,FRIENDSANITY +1560,Town,Friendsanity: Jodi 4 <3,FRIENDSANITY +1561,Town,Friendsanity: Jodi 5 <3,FRIENDSANITY +1562,Town,Friendsanity: Jodi 6 <3,FRIENDSANITY +1563,Town,Friendsanity: Jodi 7 <3,FRIENDSANITY +1564,Town,Friendsanity: Jodi 8 <3,FRIENDSANITY +1565,Town,Friendsanity: Jodi 9 <3,FRIENDSANITY +1566,Town,Friendsanity: Jodi 10 <3,FRIENDSANITY +1568,Town,Friendsanity: Kent 1 <3,FRIENDSANITY +1569,Town,Friendsanity: Kent 2 <3,FRIENDSANITY +1570,Town,Friendsanity: Kent 3 <3,FRIENDSANITY +1571,Town,Friendsanity: Kent 4 <3,FRIENDSANITY +1572,Town,Friendsanity: Kent 5 <3,FRIENDSANITY +1573,Town,Friendsanity: Kent 6 <3,FRIENDSANITY +1574,Town,Friendsanity: Kent 7 <3,FRIENDSANITY +1575,Town,Friendsanity: Kent 8 <3,FRIENDSANITY +1576,Town,Friendsanity: Kent 9 <3,FRIENDSANITY +1577,Town,Friendsanity: Kent 10 <3,FRIENDSANITY +1579,Sewers,Friendsanity: Krobus 1 <3,FRIENDSANITY +1580,Sewers,Friendsanity: Krobus 2 <3,FRIENDSANITY +1581,Sewers,Friendsanity: Krobus 3 <3,FRIENDSANITY +1582,Sewers,Friendsanity: Krobus 4 <3,FRIENDSANITY +1583,Sewers,Friendsanity: Krobus 5 <3,FRIENDSANITY +1584,Sewers,Friendsanity: Krobus 6 <3,FRIENDSANITY +1585,Sewers,Friendsanity: Krobus 7 <3,FRIENDSANITY +1586,Sewers,Friendsanity: Krobus 8 <3,FRIENDSANITY +1587,Sewers,Friendsanity: Krobus 9 <3,FRIENDSANITY +1588,Sewers,Friendsanity: Krobus 10 <3,FRIENDSANITY +1590,Ginger Island,Friendsanity: Leo 1 <3,FRIENDSANITY +1591,Ginger Island,Friendsanity: Leo 2 <3,FRIENDSANITY +1592,Ginger Island,Friendsanity: Leo 3 <3,FRIENDSANITY +1593,Ginger Island,Friendsanity: Leo 4 <3,FRIENDSANITY +1594,Ginger Island,Friendsanity: Leo 5 <3,FRIENDSANITY +1595,Ginger Island,Friendsanity: Leo 6 <3,FRIENDSANITY +1596,Ginger Island,Friendsanity: Leo 7 <3,FRIENDSANITY +1597,Ginger Island,Friendsanity: Leo 8 <3,FRIENDSANITY +1598,Ginger Island,Friendsanity: Leo 9 <3,FRIENDSANITY +1599,Ginger Island,Friendsanity: Leo 10 <3,FRIENDSANITY +1601,Town,Friendsanity: Lewis 1 <3,FRIENDSANITY +1602,Town,Friendsanity: Lewis 2 <3,FRIENDSANITY +1603,Town,Friendsanity: Lewis 3 <3,FRIENDSANITY +1604,Town,Friendsanity: Lewis 4 <3,FRIENDSANITY +1605,Town,Friendsanity: Lewis 5 <3,FRIENDSANITY +1606,Town,Friendsanity: Lewis 6 <3,FRIENDSANITY +1607,Town,Friendsanity: Lewis 7 <3,FRIENDSANITY +1608,Town,Friendsanity: Lewis 8 <3,FRIENDSANITY +1609,Town,Friendsanity: Lewis 9 <3,FRIENDSANITY +1610,Town,Friendsanity: Lewis 10 <3,FRIENDSANITY +1612,Mountain,Friendsanity: Linus 1 <3,FRIENDSANITY +1613,Mountain,Friendsanity: Linus 2 <3,FRIENDSANITY +1614,Mountain,Friendsanity: Linus 3 <3,FRIENDSANITY +1615,Mountain,Friendsanity: Linus 4 <3,FRIENDSANITY +1616,Mountain,Friendsanity: Linus 5 <3,FRIENDSANITY +1617,Mountain,Friendsanity: Linus 6 <3,FRIENDSANITY +1618,Mountain,Friendsanity: Linus 7 <3,FRIENDSANITY +1619,Mountain,Friendsanity: Linus 8 <3,FRIENDSANITY +1620,Mountain,Friendsanity: Linus 9 <3,FRIENDSANITY +1621,Mountain,Friendsanity: Linus 10 <3,FRIENDSANITY +1623,Marnie's Ranch,Friendsanity: Marnie 1 <3,FRIENDSANITY +1624,Marnie's Ranch,Friendsanity: Marnie 2 <3,FRIENDSANITY +1625,Marnie's Ranch,Friendsanity: Marnie 3 <3,FRIENDSANITY +1626,Marnie's Ranch,Friendsanity: Marnie 4 <3,FRIENDSANITY +1627,Marnie's Ranch,Friendsanity: Marnie 5 <3,FRIENDSANITY +1628,Marnie's Ranch,Friendsanity: Marnie 6 <3,FRIENDSANITY +1629,Marnie's Ranch,Friendsanity: Marnie 7 <3,FRIENDSANITY +1630,Marnie's Ranch,Friendsanity: Marnie 8 <3,FRIENDSANITY +1631,Marnie's Ranch,Friendsanity: Marnie 9 <3,FRIENDSANITY +1632,Marnie's Ranch,Friendsanity: Marnie 10 <3,FRIENDSANITY +1634,Town,Friendsanity: Pam 1 <3,FRIENDSANITY +1635,Town,Friendsanity: Pam 2 <3,FRIENDSANITY +1636,Town,Friendsanity: Pam 3 <3,FRIENDSANITY +1637,Town,Friendsanity: Pam 4 <3,FRIENDSANITY +1638,Town,Friendsanity: Pam 5 <3,FRIENDSANITY +1639,Town,Friendsanity: Pam 6 <3,FRIENDSANITY +1640,Town,Friendsanity: Pam 7 <3,FRIENDSANITY +1641,Town,Friendsanity: Pam 8 <3,FRIENDSANITY +1642,Town,Friendsanity: Pam 9 <3,FRIENDSANITY +1643,Town,Friendsanity: Pam 10 <3,FRIENDSANITY +1645,Town,Friendsanity: Pierre 1 <3,FRIENDSANITY +1646,Town,Friendsanity: Pierre 2 <3,FRIENDSANITY +1647,Town,Friendsanity: Pierre 3 <3,FRIENDSANITY +1648,Town,Friendsanity: Pierre 4 <3,FRIENDSANITY +1649,Town,Friendsanity: Pierre 5 <3,FRIENDSANITY +1650,Town,Friendsanity: Pierre 6 <3,FRIENDSANITY +1651,Town,Friendsanity: Pierre 7 <3,FRIENDSANITY +1652,Town,Friendsanity: Pierre 8 <3,FRIENDSANITY +1653,Town,Friendsanity: Pierre 9 <3,FRIENDSANITY +1654,Town,Friendsanity: Pierre 10 <3,FRIENDSANITY +1656,Carpenter Shop,Friendsanity: Robin 1 <3,FRIENDSANITY +1657,Carpenter Shop,Friendsanity: Robin 2 <3,FRIENDSANITY +1658,Carpenter Shop,Friendsanity: Robin 3 <3,FRIENDSANITY +1659,Carpenter Shop,Friendsanity: Robin 4 <3,FRIENDSANITY +1660,Carpenter Shop,Friendsanity: Robin 5 <3,FRIENDSANITY +1661,Carpenter Shop,Friendsanity: Robin 6 <3,FRIENDSANITY +1662,Carpenter Shop,Friendsanity: Robin 7 <3,FRIENDSANITY +1663,Carpenter Shop,Friendsanity: Robin 8 <3,FRIENDSANITY +1664,Carpenter Shop,Friendsanity: Robin 9 <3,FRIENDSANITY +1665,Carpenter Shop,Friendsanity: Robin 10 <3,FRIENDSANITY +1667,The Desert,Friendsanity: Sandy 1 <3,FRIENDSANITY +1668,The Desert,Friendsanity: Sandy 2 <3,FRIENDSANITY +1669,The Desert,Friendsanity: Sandy 3 <3,FRIENDSANITY +1670,The Desert,Friendsanity: Sandy 4 <3,FRIENDSANITY +1671,The Desert,Friendsanity: Sandy 5 <3,FRIENDSANITY +1672,The Desert,Friendsanity: Sandy 6 <3,FRIENDSANITY +1673,The Desert,Friendsanity: Sandy 7 <3,FRIENDSANITY +1674,The Desert,Friendsanity: Sandy 8 <3,FRIENDSANITY +1675,The Desert,Friendsanity: Sandy 9 <3,FRIENDSANITY +1676,The Desert,Friendsanity: Sandy 10 <3,FRIENDSANITY +1678,Town,Friendsanity: Vincent 1 <3,FRIENDSANITY +1679,Town,Friendsanity: Vincent 2 <3,FRIENDSANITY +1680,Town,Friendsanity: Vincent 3 <3,FRIENDSANITY +1681,Town,Friendsanity: Vincent 4 <3,FRIENDSANITY +1682,Town,Friendsanity: Vincent 5 <3,FRIENDSANITY +1683,Town,Friendsanity: Vincent 6 <3,FRIENDSANITY +1684,Town,Friendsanity: Vincent 7 <3,FRIENDSANITY +1685,Town,Friendsanity: Vincent 8 <3,FRIENDSANITY +1686,Town,Friendsanity: Vincent 9 <3,FRIENDSANITY +1687,Town,Friendsanity: Vincent 10 <3,FRIENDSANITY +1689,Beach,Friendsanity: Willy 1 <3,FRIENDSANITY +1690,Beach,Friendsanity: Willy 2 <3,FRIENDSANITY +1691,Beach,Friendsanity: Willy 3 <3,FRIENDSANITY +1692,Beach,Friendsanity: Willy 4 <3,FRIENDSANITY +1693,Beach,Friendsanity: Willy 5 <3,FRIENDSANITY +1694,Beach,Friendsanity: Willy 6 <3,FRIENDSANITY +1695,Beach,Friendsanity: Willy 7 <3,FRIENDSANITY +1696,Beach,Friendsanity: Willy 8 <3,FRIENDSANITY +1697,Beach,Friendsanity: Willy 9 <3,FRIENDSANITY +1698,Beach,Friendsanity: Willy 10 <3,FRIENDSANITY +1700,Forest,Friendsanity: Wizard 1 <3,FRIENDSANITY +1701,Forest,Friendsanity: Wizard 2 <3,FRIENDSANITY +1702,Forest,Friendsanity: Wizard 3 <3,FRIENDSANITY +1703,Forest,Friendsanity: Wizard 4 <3,FRIENDSANITY +1704,Forest,Friendsanity: Wizard 5 <3,FRIENDSANITY +1705,Forest,Friendsanity: Wizard 6 <3,FRIENDSANITY +1706,Forest,Friendsanity: Wizard 7 <3,FRIENDSANITY +1707,Forest,Friendsanity: Wizard 8 <3,FRIENDSANITY +1708,Forest,Friendsanity: Wizard 9 <3,FRIENDSANITY +1709,Forest,Friendsanity: Wizard 10 <3,FRIENDSANITY +1710,Farm,Friendsanity: Pet 1 <3,FRIENDSANITY +1711,Farm,Friendsanity: Pet 2 <3,FRIENDSANITY +1712,Farm,Friendsanity: Pet 3 <3,FRIENDSANITY +1713,Farm,Friendsanity: Pet 4 <3,FRIENDSANITY +1714,Farm,Friendsanity: Pet 5 <3,FRIENDSANITY +1715,Farm,Friendsanity: Friend 1 <3,FRIENDSANITY +1716,Farm,Friendsanity: Friend 2 <3,FRIENDSANITY +1717,Farm,Friendsanity: Friend 3 <3,FRIENDSANITY +1718,Farm,Friendsanity: Friend 4 <3,FRIENDSANITY +1719,Farm,Friendsanity: Friend 5 <3,FRIENDSANITY +1720,Farm,Friendsanity: Friend 6 <3,FRIENDSANITY +1721,Farm,Friendsanity: Friend 7 <3,FRIENDSANITY +1722,Farm,Friendsanity: Friend 8 <3,FRIENDSANITY +1723,Farm,Friendsanity: Suitor 9 <3,FRIENDSANITY +1724,Farm,Friendsanity: Suitor 10 <3,FRIENDSANITY +1725,Farm,Friendsanity: Spouse 11 <3,FRIENDSANITY +1726,Farm,Friendsanity: Spouse 12 <3,FRIENDSANITY +1727,Farm,Friendsanity: Spouse 13 <3,FRIENDSANITY +1728,Farm,Friendsanity: Spouse 14 <3,FRIENDSANITY diff --git a/worlds/stardew_valley/data/monster_data.py b/worlds/stardew_valley/data/monster_data.py new file mode 100644 index 00000000..6030571f --- /dev/null +++ b/worlds/stardew_valley/data/monster_data.py @@ -0,0 +1,8 @@ +class Monster: + duggy = "Duggy" + blue_slime = "Blue Slime" + pepper_rex = "Pepper Rex" + stone_golem = "Stone Golem" + + +frozen_monsters = (Monster.blue_slime,) diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py new file mode 100644 index 00000000..bc3197d1 --- /dev/null +++ b/worlds/stardew_valley/data/museum_data.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Tuple, Union, Optional + +from . import common_data as common +from .game_item import GameItem +from .monster_data import Monster +from .region_data import SVRegion + + +@dataclass(frozen=True) +class MuseumItem(GameItem): + locations: Tuple[str, ...] + geodes: Tuple[str, ...] + monsters: Tuple[str, ...] + difficulty: float + + @staticmethod + def of(name: str, + item_id: int, + difficulty: float, + locations: Union[str, Tuple[str, ...]], + geodes: Union[str, Tuple[str, ...]], + monsters: Union[str, Tuple[str, ...]]) -> MuseumItem: + if isinstance(locations, str): + locations = (locations,) + + if isinstance(geodes, str): + geodes = (geodes,) + + if isinstance(monsters, str): + monsters = (monsters,) + + return MuseumItem(name, item_id, locations, geodes, monsters, difficulty) + + def __repr__(self): + return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ + f" Geodes: {self.geodes} |" \ + f" Monsters: {self.monsters}) " + + +class Geode: + geode = "Geode" + frozen_geode = "Frozen Geode" + magma_geode = "Magma Geode" + omni_geode = "Omni Geode" + artifact_trove = "Artifact Trove" + + +unlikely = () + +all_artifact_items: List[MuseumItem] = [] +all_mineral_items: List[MuseumItem] = [] + +all_museum_items: List[MuseumItem] = [] + + +def create_artifact(name: str, + item_id: int, + difficulty: float, + locations: Union[str, Tuple[str, ...]] = (), + geodes: Union[str, Tuple[str, ...]] = (), + monsters: Union[str, Tuple[str, ...]] = ()) -> MuseumItem: + artifact_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) + all_artifact_items.append(artifact_item) + all_museum_items.append(artifact_item) + return artifact_item + + +def create_mineral(name: str, + item_id: int, + locations: Union[str, Tuple[str, ...]], + geodes: Union[str, Tuple[str, ...]] = (), + monsters: Union[str, Tuple[str, ...]] = (), + difficulty: Optional[float] = None) -> MuseumItem: + if difficulty is None: + difficulty = 0 + if "Geode" in geodes: + difficulty += 1.0 / 32.0 * 100 + if "Frozen Geode" in geodes: + difficulty += 1.0 / 30.0 * 100 + if "Magma Geode" in geodes: + difficulty += 1.0 / 26.0 * 100 + if "Omni Geode" in geodes: + difficulty += 31.0 / 2750.0 * 100 + + mineral_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) + all_mineral_items.append(mineral_item) + all_museum_items.append(mineral_item) + return mineral_item + + +class Artifact: + dwarf_scroll_i = create_artifact("Dwarf Scroll I", 96, 5.6, SVRegion.mines_floor_20, + monsters=unlikely) + dwarf_scroll_ii = create_artifact("Dwarf Scroll II", 97, 3, SVRegion.mines_floor_20, + monsters=unlikely) + dwarf_scroll_iii = create_artifact("Dwarf Scroll III", 98, 7.5, SVRegion.mines_floor_60, + monsters=Monster.blue_slime) + dwarf_scroll_iv = create_artifact("Dwarf Scroll IV", 99, 4, SVRegion.mines_floor_100) + chipped_amphora = create_artifact("Chipped Amphora", 100, 6.7, SVRegion.town, + geodes=Geode.artifact_trove) + arrowhead = create_artifact("Arrowhead", 101, 8.5, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=Geode.artifact_trove) + ancient_doll = create_artifact("Ancient Doll", 103, 13.1, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=(Geode.artifact_trove, common.fishing_chest)) + elvish_jewelry = create_artifact("Elvish Jewelry", 104, 5.3, SVRegion.forest, + geodes=(Geode.artifact_trove, common.fishing_chest)) + chewing_stick = create_artifact("Chewing Stick", 105, 10.3, (SVRegion.mountain, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + ornamental_fan = create_artifact("Ornamental Fan", 106, 7.4, (SVRegion.beach, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + dinosaur_egg = create_artifact("Dinosaur Egg", 107, 11.4, (SVRegion.mountain, SVRegion.skull_cavern), + geodes=common.fishing_chest, + monsters=Monster.pepper_rex) + rare_disc = create_artifact("Rare Disc", 108, 5.6, SVRegion.stardew_valley, + geodes=(Geode.artifact_trove, common.fishing_chest), + monsters=unlikely) + ancient_sword = create_artifact("Ancient Sword", 109, 5.8, (SVRegion.forest, SVRegion.mountain), + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_spoon = create_artifact("Rusty Spoon", 110, 9.6, SVRegion.town, + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_spur = create_artifact("Rusty Spur", 111, 15.6, SVRegion.farm, + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_cog = create_artifact("Rusty Cog", 112, 9.6, SVRegion.mountain, + geodes=(Geode.artifact_trove, common.fishing_chest)) + chicken_statue = create_artifact("Chicken Statue", 113, 13.5, SVRegion.farm, + geodes=(Geode.artifact_trove, common.fishing_chest)) + ancient_seed = create_artifact("Ancient Seed", 114, 8.4, (SVRegion.forest, SVRegion.mountain), + geodes=(Geode.artifact_trove, common.fishing_chest), + monsters=unlikely) + prehistoric_tool = create_artifact("Prehistoric Tool", 115, 11.1, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=(Geode.artifact_trove, common.fishing_chest)) + dried_starfish = create_artifact("Dried Starfish", 116, 12.5, SVRegion.beach, + geodes=(Geode.artifact_trove, common.fishing_chest)) + anchor = create_artifact("Anchor", 117, 8.5, SVRegion.beach, geodes=(Geode.artifact_trove, common.fishing_chest)) + glass_shards = create_artifact("Glass Shards", 118, 11.5, SVRegion.beach, + geodes=(Geode.artifact_trove, common.fishing_chest)) + bone_flute = create_artifact("Bone Flute", 119, 6.3, (SVRegion.mountain, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + prehistoric_handaxe = create_artifact("Prehistoric Handaxe", 120, 13.7, + (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=Geode.artifact_trove) + dwarvish_helm = create_artifact("Dwarvish Helm", 121, 8.7, SVRegion.mines_floor_20, + geodes=(Geode.geode, Geode.omni_geode, Geode.artifact_trove)) + dwarf_gadget = create_artifact("Dwarf Gadget", 122, 9.7, SVRegion.mines_floor_60, + geodes=(Geode.magma_geode, Geode.omni_geode, Geode.artifact_trove)) + ancient_drum = create_artifact("Ancient Drum", 123, 9.5, (SVRegion.bus_stop, SVRegion.forest, SVRegion.town), + geodes=(Geode.frozen_geode, Geode.omni_geode, Geode.artifact_trove)) + golden_mask = create_artifact("Golden Mask", 124, 6.7, SVRegion.desert, + geodes=Geode.artifact_trove) + golden_relic = create_artifact("Golden Relic", 125, 9.7, SVRegion.desert, + geodes=Geode.artifact_trove) + strange_doll_green = create_artifact("Strange Doll (Green)", 126, 10, SVRegion.town, + geodes=common.secret_note) + strange_doll = create_artifact("Strange Doll", 127, 10, SVRegion.desert, + geodes=common.secret_note) + prehistoric_scapula = create_artifact("Prehistoric Scapula", 579, 6.2, + (SVRegion.dig_site, SVRegion.forest, SVRegion.town)) + prehistoric_tibia = create_artifact("Prehistoric Tibia", 580, 16.6, + (SVRegion.dig_site, SVRegion.forest, SVRegion.railroad)) + prehistoric_skull = create_artifact("Prehistoric Skull", 581, 3.9, (SVRegion.dig_site, SVRegion.mountain)) + skeletal_hand = create_artifact("Skeletal Hand", 582, 7.9, (SVRegion.dig_site, SVRegion.backwoods, SVRegion.beach)) + prehistoric_rib = create_artifact("Prehistoric Rib", 583, 15, (SVRegion.dig_site, SVRegion.farm, SVRegion.town), + monsters=Monster.pepper_rex) + prehistoric_vertebra = create_artifact("Prehistoric Vertebra", 584, 12.7, (SVRegion.dig_site, SVRegion.bus_stop), + monsters=Monster.pepper_rex) + skeletal_tail = create_artifact("Skeletal Tail", 585, 5.1, (SVRegion.dig_site, SVRegion.mines_floor_20), + geodes=common.fishing_chest) + nautilus_fossil = create_artifact("Nautilus Fossil", 586, 6.9, (SVRegion.dig_site, SVRegion.beach), + geodes=common.fishing_chest) + amphibian_fossil = create_artifact("Amphibian Fossil", 587, 6.3, (SVRegion.dig_site, SVRegion.forest, SVRegion.mountain), + geodes=common.fishing_chest) + palm_fossil = create_artifact("Palm Fossil", 588, 10.2, + (SVRegion.dig_site, SVRegion.desert, SVRegion.forest, SVRegion.beach)) + trilobite = create_artifact("Trilobite", 589, 7.4, (SVRegion.dig_site, SVRegion.desert, SVRegion.forest, SVRegion.beach)) + + +class Mineral: + quartz = create_mineral("Quartz", 80, SVRegion.mines_floor_20, + monsters=Monster.stone_golem) + fire_quartz = create_mineral("Fire Quartz", 82, SVRegion.mines_floor_100, + geodes=(Geode.magma_geode, Geode.omni_geode, common.fishing_chest), + difficulty=1.0 / 12.0) + frozen_tear = create_mineral("Frozen Tear", 84, SVRegion.mines_floor_60, + geodes=(Geode.frozen_geode, Geode.omni_geode, common.fishing_chest), + monsters=unlikely, + difficulty=1.0 / 12.0) + earth_crystal = create_mineral("Earth Crystal", 86, SVRegion.mines_floor_20, + geodes=(Geode.geode, Geode.omni_geode, common.fishing_chest), + monsters=Monster.duggy, + difficulty=1.0 / 12.0) + emerald = create_mineral("Emerald", 60, SVRegion.mines_floor_100, + geodes=common.fishing_chest) + aquamarine = create_mineral("Aquamarine", 62, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + ruby = create_mineral("Ruby", 64, SVRegion.mines_floor_100, + geodes=common.fishing_chest) + amethyst = create_mineral("Amethyst", 66, SVRegion.mines_floor_20, + geodes=common.fishing_chest) + topaz = create_mineral("Topaz", 68, SVRegion.mines_floor_20, + geodes=common.fishing_chest) + jade = create_mineral("Jade", 70, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + diamond = create_mineral("Diamond", 72, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + prismatic_shard = create_mineral("Prismatic Shard", 74, SVRegion.perfect_skull_cavern, + geodes=unlikely, + monsters=unlikely) + alamite = create_mineral("Alamite", 538, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + bixite = create_mineral("Bixite", 539, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode), + monsters=unlikely) + baryte = create_mineral("Baryte", 540, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + aerinite = create_mineral("Aerinite", 541, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + calcite = create_mineral("Calcite", 542, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + dolomite = create_mineral("Dolomite", 543, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + esperite = create_mineral("Esperite", 544, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + fluorapatite = create_mineral("Fluorapatite", 545, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + geminite = create_mineral("Geminite", 546, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + helvite = create_mineral("Helvite", 547, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + jamborite = create_mineral("Jamborite", 548, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + jagoite = create_mineral("Jagoite", 549, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + kyanite = create_mineral("Kyanite", 550, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + lunarite = create_mineral("Lunarite", 551, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + malachite = create_mineral("Malachite", 552, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + neptunite = create_mineral("Neptunite", 553, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + lemon_stone = create_mineral("Lemon Stone", 554, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + nekoite = create_mineral("Nekoite", 555, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + orpiment = create_mineral("Orpiment", 556, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + petrified_slime = create_mineral("Petrified Slime", 557, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + thunder_egg = create_mineral("Thunder Egg", 558, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + pyrite = create_mineral("Pyrite", 559, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + ocean_stone = create_mineral("Ocean Stone", 560, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + ghost_crystal = create_mineral("Ghost Crystal", 561, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + tigerseye = create_mineral("Tigerseye", 562, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + jasper = create_mineral("Jasper", 563, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + opal = create_mineral("Opal", 564, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + fire_opal = create_mineral("Fire Opal", 565, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + celestine = create_mineral("Celestine", 566, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + marble = create_mineral("Marble", 567, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + sandstone = create_mineral("Sandstone", 568, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + granite = create_mineral("Granite", 569, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + basalt = create_mineral("Basalt", 570, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + limestone = create_mineral("Limestone", 571, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + soapstone = create_mineral("Soapstone", 572, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + hematite = create_mineral("Hematite", 573, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + mudstone = create_mineral("Mudstone", 574, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + obsidian = create_mineral("Obsidian", 575, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + slate = create_mineral("Slate", 576, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + fairy_stone = create_mineral("Fairy Stone", 577, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + star_shards = create_mineral("Star Shards", 578, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + + +dwarf_scrolls = (Artifact.dwarf_scroll_i, Artifact.dwarf_scroll_ii, Artifact.dwarf_scroll_iii, Artifact.dwarf_scroll_iv) +skeleton_front = (Artifact.prehistoric_skull, Artifact.skeletal_hand, Artifact.prehistoric_scapula) +skeleton_middle = (Artifact.prehistoric_rib, Artifact.prehistoric_vertebra) +skeleton_back = (Artifact.prehistoric_tibia, Artifact.skeletal_tail) + +all_museum_items_by_name = {item.name: item for item in all_museum_items} diff --git a/worlds/stardew_valley/data/region_data.py b/worlds/stardew_valley/data/region_data.py new file mode 100644 index 00000000..8f1eb1ab --- /dev/null +++ b/worlds/stardew_valley/data/region_data.py @@ -0,0 +1,99 @@ +class SVRegion: + menu = "Menu" + stardew_valley = "Stardew Valley" + farm_house = "Farmhouse" + cellar = "Cellar" + farm = "Farm" + town = "Town" + beach = "Beach" + mountain = "Mountain" + forest = "Forest" + bus_stop = "Bus Stop" + backwoods = "Backwoods" + railroad = "Railroad" + secret_woods = "Secret Woods" + community_center = "Community Center" + pantry = "Pantry" + crafts_room = "Crafts Room" + fish_tank = "Fish Tank" + boiler_room = "Boiler Room" + vault = "Vault" + bulletin_board = "Bulletin Board" + desert = "The Desert" + mines = "The Mines" + skull_cavern_entrance = "Skull Cavern Entrance" + skull_cavern = "Skull Cavern" + sewers = "Sewers" + mutant_bug_lair = "Mutant Bug Lair" + witch_swamp = "Witch's Swamp" + ginger_island = "Ginger Island" + pirate_cove = ginger_island + dig_site = ginger_island + perfect_skull_cavern = "Skull Cavern Floor 100" + hospital = "Hospital" + carpenter = "Carpenter Shop" + alex_house = "Alex's House" + elliott_house = "Elliott's House" + ranch = "Marnie's Ranch" + traveling_cart = "Traveling Cart" + farm_cave = "Farmcave" + greenhouse = "Greenhouse" + tunnel = "Tunnel" + tunnel_entrance = "Tunnel Entrance" + leah_house = "Leah's Cottage" + wizard_tower = "Wizard Tower" + wizard_basement = "Wizard Basement" + tent = "Tent" + sebastian_room = "Sebastian's Room" + adventurer_guild = "Adventurer's Guild" + quarry = "Quarry" + quarry_mine_entrance = "Quarry Mine Entrance" + quarry_mine = "Quarry Mine" + witch_warp_cave = "Witch Warp Cave" + harvey_room = "Harvey's Room" + pierre_store = "Pierre's General Store" + sunroom = "Sunroom" + saloon = "Saloon" + blacksmith = "Clint's Blacksmith" + trailer = "Trailer" + museum = "Museum" + mayor_house = "Mayor's Manor" + haley_house = "Haley's House" + sam_house = "Sam's House" + jojamart = "JojaMart" + fish_shop = "Willy's Fish Shop" + tide_pools = "Tide Pools" + bathhouse_entrance = "Bathhouse Entrance" + locker_room = "Locker Room" + public_bath = "Public Bath" + jotpk_world_1 = "JotPK World 1" + jotpk_world_2 = "JotPK World 2" + jotpk_world_3 = "JotPK World 3" + junimo_kart_1 = "Junimo Kart 1" + junimo_kart_2 = "Junimo Kart 2" + junimo_kart_3 = "Junimo Kart 3" + mines_floor_5 = "The Mines - Floor 5" + mines_floor_10 = "The Mines - Floor 10" + mines_floor_15 = "The Mines - Floor 15" + mines_floor_20 = "The Mines - Floor 20" + mines_floor_25 = "The Mines - Floor 25" + mines_floor_30 = "The Mines - Floor 30" + mines_floor_35 = "The Mines - Floor 35" + mines_floor_40 = "The Mines - Floor 40" + mines_floor_45 = "The Mines - Floor 45" + mines_floor_50 = "The Mines - Floor 50" + mines_floor_55 = "The Mines - Floor 55" + mines_floor_60 = "The Mines - Floor 60" + mines_floor_65 = "The Mines - Floor 65" + mines_floor_70 = "The Mines - Floor 70" + mines_floor_75 = "The Mines - Floor 75" + mines_floor_80 = "The Mines - Floor 80" + mines_floor_85 = "The Mines - Floor 85" + mines_floor_90 = "The Mines - Floor 90" + mines_floor_95 = "The Mines - Floor 95" + mines_floor_100 = "The Mines - Floor 100" + mines_floor_105 = "The Mines - Floor 105" + mines_floor_110 = "The Mines - Floor 110" + mines_floor_115 = "The Mines - Floor 115" + mines_floor_120 = "The Mines - Floor 120" + diff --git a/worlds/stardew_valley/data/season_data.py b/worlds/stardew_valley/data/season_data.py new file mode 100644 index 00000000..1ebef049 --- /dev/null +++ b/worlds/stardew_valley/data/season_data.py @@ -0,0 +1,10 @@ +spring = "Spring" +summer = "Summer" +fall = "Fall" +winter = "Winter" + +not_spring = (summer, fall, winter) +not_summer = (spring, fall, winter) +not_fall = (spring, summer, winter) +not_winter = (spring, summer, fall) +all_seasons = (spring, summer, fall, winter) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py new file mode 100644 index 00000000..14582d25 --- /dev/null +++ b/worlds/stardew_valley/data/villagers_data.py @@ -0,0 +1,250 @@ +from dataclasses import dataclass +from typing import Set, List, FrozenSet, Tuple +from .region_data import SVRegion + + +@dataclass(frozen=True) +class Villager: + name: str + bachelor: bool + locations: Tuple[str] + birthday: str + gifts: Tuple[str] + available: bool + + def __repr__(self): + return f"{self.name} [Bachelor: {self.bachelor}] [Available from start: {self.available}]" \ + f"(Locations: {self.locations} |" \ + f" Birthday: {self.birthday} |" \ + f" Gifts: {self.gifts}) " + + +town = (SVRegion.town,) +beach = (SVRegion.beach,) +forest = (SVRegion.forest,) +mountain = (SVRegion.mountain,) +hospital = (SVRegion.hospital,) +carpenter = (SVRegion.carpenter,) +alex_house = (SVRegion.alex_house,) +elliott_house = (SVRegion.elliott_house,) +ranch = (SVRegion.ranch,) +mines = (SVRegion.mines,) +desert = (SVRegion.desert,) +oasis = (SVRegion.desert,) +sewers = (SVRegion.sewers,) +island = (SVRegion.ginger_island,) + +golden_pumpkin = ("Golden Pumpkin",) +# magic_rock_candy = ("Magic Rock Candy",) +pearl = ("Pearl",) +prismatic_shard = ("Prismatic Shard",) +rabbit_foot = ("Rabbit's Foot",) +universal_loves = golden_pumpkin + pearl + prismatic_shard + rabbit_foot # , *magic_rock_candy} +universal_loves_no_prismatic_shard = golden_pumpkin + pearl + rabbit_foot # , *magic_rock_candy} +universal_loves_no_rabbit_foot = golden_pumpkin + pearl + prismatic_shard # , *magic_rock_candy} +complete_breakfast = ("Complete Breakfast",) +salmon_dinner = ("Salmon Dinner",) +crab_cakes = ("Crab Cakes",) +duck_feather = ("Duck Feather",) +lobster = ("Lobster",) +pomegranate = ("Pomegranate",) +squid_ink = ("Squid Ink",) +# tom_kha_soup = ("Tom Kha Soup",) +elliott_loves = duck_feather + lobster + pomegranate + squid_ink + crab_cakes # | tom_kha_soup +coffee = ("Coffee",) +pickles = ("Pickles",) +# super_meal = ("Super Meal",) +truffle_oil = ("Truffle Oil",) +wine = ("Wine",) +harvey_loves = coffee + pickles + truffle_oil + wine # | super_meal +cactus_fruit = ("Cactus Fruit",) +maple_bar = ("Maple Bar",) +pizza = ("Pizza",) +tigerseye = ("Tigerseye",) +sam_loves = cactus_fruit + maple_bar + pizza + tigerseye +frozen_tear = ("Frozen Tear",) +obsidian = ("Obsidian",) +# pumpkin_soup = ("Pumpkin Soup",) +# sashimi = ("Sashimi",) +void_egg = ("Void Egg",) +sebastian_loves = frozen_tear + obsidian + void_egg # | pumpkin_soup + sashimi +beer = ("Beer",) +hot_pepper = ("Hot Pepper",) +# pepper_poppers = ("Pepper Poppers",) +shane_loves = beer + hot_pepper + pizza # | pepper_poppers +amethyst = ("Amethyst",) +# banana_pudding = ("Banana Pudding",) +blackberry_cobbler = ("Blackberry Cobbler",) +chocolate_cake = ("Chocolate Cake",) +pufferfish = ("Pufferfish",) +pumpkin = ("Pumpkin",) +# spicy_eel = ("Spicy Eel",) +abigail_loves = amethyst + blackberry_cobbler + chocolate_cake + pufferfish + pumpkin # | banana_pudding + spicy_eel +aquamarine = ("Aquamarine",) +cloth = ("Cloth",) +emerald = ("Emerald",) +jade = ("Jade",) +ruby = ("Ruby",) +survival_burger = ("Survival Burger",) +topaz = ("Topaz",) +wool = ("Wool",) +emily_loves = amethyst + aquamarine + cloth + emerald + jade + ruby + survival_burger + topaz + wool +coconut = ("Coconut",) +fruit_salad = ("Fruit Salad",) +pink_cake = ("Pink Cake",) +sunflower = ("Sunflower",) +haley_loves = coconut + fruit_salad + pink_cake + sunflower +goat_cheese = ("Goat Cheese",) +poppyseed_muffin = ("Poppyseed Muffin",) +salad = ("Salad",) +stir_fry = ("Stir Fry",) +truffle = ("Truffle",) +# vegetable_medley = ("Vegetable Medley",) +leah_loves = goat_cheese + poppyseed_muffin + salad + stir_fry + truffle + wine # | vegetable_medley +battery_pack = ("Battery Pack",) +cauliflower = ("Cauliflower",) +cheese_cauliflower = ("Cheese Cauliflower",) +diamond = ("Diamond",) +gold_bar = ("Gold Bar",) +iridium_bar = ("Iridium Bar",) +miners_treat = ("Miner's Treat",) +pepper_poppers = ("Pepper Poppers",) +radioactive_bar = ("Radioactive Bar",) +rhubarb_pie = ("Rhubarb Pie",) +strawberry = ("Strawberry",) +maru_loves = battery_pack + cauliflower + diamond + gold_bar + iridium_bar + miners_treat + radioactive_bar + strawberry # | cheese_cauliflower + pepper_poppers + rhubarb_pie +melon = ("Melon",) +poppy = ("Poppy",) +# red_plate = ("Red Plate",) +roots_platter = ("Roots Platter",) +sandfish = ("Sandfish",) +penny_loves = diamond + emerald + melon + poppy + poppyseed_muffin + roots_platter + sandfish # | tom_kha_soup + red_plate +# fish_taco = ("Fish Taco",) +green_tea = ("Green Tea",) +summer_spangle = ("Summer Spangle",) +tropical_curry = ("Tropical Curry",) +caroline_loves = summer_spangle + tropical_curry # | fish_taco + green_tea +artichoke_dip = ("Artichoke Dip",) +fiddlehead_risotto = ("Fiddlehead Risotto",) +omni_geode = ("Omni Geode",) +clint_loves = amethyst + aquamarine + artichoke_dip + emerald + fiddlehead_risotto + gold_bar + iridium_bar + jade + \ + omni_geode + ruby + topaz +# bean_hotpot = ("Bean Hotpot",) +ice_cream = ("Ice Cream",) +# rice_pudding = ("Rice Pudding",) +demetrius_loves = ice_cream + strawberry # | bean_hotpot + rice_pudding +lemon_stone = ("Lemon Stone",) +dwarf_loves = amethyst + aquamarine + emerald + jade + lemon_stone + omni_geode + ruby + topaz +beet = ("Beet",) +fairy_rose = ("Fairy Rose",) +# stuffing = ("Stuffing",) +tulip = ("Tulip",) +evelyn_loves = beet + chocolate_cake + diamond + fairy_rose + tulip # | stuffing +# fried_mushroom = ("Fried Mushroom",) +leek = ("Leek",) +george_loves = leek # | fried_mushroom +# escargot = ("Escargot",) +orange = ("Orange",) +gus_loves = diamond + orange + tropical_curry # | escargot + fish_taco +plum_pudding = ("Plum Pudding",) +jas_loves = fairy_rose + pink_cake + plum_pudding +# crispy_bass = ("Crispy Bass",) +# eggplant_parmesan = ("Eggplant Parmesan",) +# fried_eel = ("Fried Eel",) +pancakes = ("Pancakes",) +jodi_loves = chocolate_cake + diamond + pancakes + rhubarb_pie # | vegetable_medley + crispy_bass + eggplant_parmesan + fried_eel +roasted_hazelnuts = ("Roasted Hazelnuts",) +kent_loves = fiddlehead_risotto + roasted_hazelnuts +void_mayonnaise = ("Void Mayonnaise",) +wild_horseradish = ("Wild Horseradish",) +krobus_loves = diamond + iridium_bar + pumpkin + void_egg + void_mayonnaise + wild_horseradish +mango = ("Mango",) +ostrich_egg = ("Ostrich Egg",) +# poi = ("Poi",) +leo_loves = duck_feather + mango + ostrich_egg # | poi +# autumns_bounty = ("Autumn's Bounty",) +glazed_yams = ("Glazed Yams",) +lewis_loves = glazed_yams + green_tea + hot_pepper # | autumns_bounty + vegetable_medley +# blueberry_tart = ("Blueberry Tart",) +dish_o_the_sea = ("Dish O' The Sea",) +yam = ("Yam",) +linus_loves = cactus_fruit + coconut + dish_o_the_sea + yam # | blueberry_tart +farmers_lunch = ("Farmer's Lunch",) +pumpkin_pie = ("Pumpkin Pie",) +marnie_loves = diamond + farmers_lunch + pink_cake + pumpkin_pie +mead = ("Mead",) +pale_ale = ("Pale Ale",) +parsnip = ("Parsnip",) +# parsnip_soup = ("Parsnip Soup",) +pina_colada = ("Piña Colada",) +pam_loves = beer + cactus_fruit + glazed_yams + mead + pale_ale + parsnip + pina_colada # | parsnip_soup +# fried_calamari = ("Fried Calamari",) +pierre_loves = () # fried_calamari +peach = ("Peach",) +spaghetti = ("Spaghetti",) +robin_loves = goat_cheese + peach + spaghetti +crocus = ("Crocus",) +daffodil = ("Daffodil",) +# mango_stocky_rice = ("Mango Sticky Rice",) +sweet_pea = ("Sweet Pea",) +sandy_loves = crocus + daffodil + sweet_pea # | mango_stocky_rice +cranberry_candy = ("Cranberry Candy",) +ginger_ale = ("Ginger Ale",) +grape = ("Grape",) +snail = ("Snail",) +vincent_loves = cranberry_candy + ginger_ale + grape + pink_cake + snail +catfish = ("Catfish",) +octopus = ("Octopus",) +willy_loves = catfish + diamond + iridium_bar + mead + octopus + pumpkin +purple_mushroom = ("Purple Mushroom",) +solar_essence = ("Solar Essence",) +super_cucumber = ("Super Cucumber",) +void_essence = ("Void Essence",) +wizard_loves = purple_mushroom + solar_essence + super_cucumber + void_essence + +all_villagers: List[Villager] = [] + + +def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...], + available: bool) -> Villager: + npc = Villager(name, bachelor, locations, birthday, gifts, available) + all_villagers.append(npc) + return npc + + +josh = villager("Alex", True, town + alex_house, "Summer", universal_loves + complete_breakfast + salmon_dinner, True) +elliott = villager("Elliott", True, town + beach + elliott_house, "Fall", universal_loves + elliott_loves, True) +harvey = villager("Harvey", True, town + hospital, "Winter", universal_loves + harvey_loves, True) +sam = villager("Sam", True, town, "Summer", universal_loves + sam_loves, True) +sebastian = villager("Sebastian", True, carpenter, "Winter", universal_loves + sebastian_loves, True) +shane = villager("Shane", True, ranch, "Spring", universal_loves + shane_loves, True) +best_girl = villager("Abigail", True, town, "Fall", universal_loves + abigail_loves, True) +emily = villager("Emily", True, town, "Spring", universal_loves + emily_loves, True) +hoe = villager("Haley", True, town, "Spring", universal_loves_no_prismatic_shard + haley_loves, True) +leah = villager("Leah", True, forest, "Winter", universal_loves + leah_loves, True) +nerd = villager("Maru", True, carpenter, "Summer", universal_loves + maru_loves, True) +penny = villager("Penny", True, town, "Fall", universal_loves_no_rabbit_foot + penny_loves, True) +caroline = villager("Caroline", False, town, "Winter", universal_loves + caroline_loves, True) +clint = villager("Clint", False, town, "Winter", universal_loves + clint_loves, True) +demetrius = villager("Demetrius", False, carpenter, "Summer", universal_loves + demetrius_loves, True) +dwarf = villager("Dwarf", False, mines, "Summer", universal_loves + dwarf_loves, False) +gilf = villager("Evelyn", False, town, "Winter", universal_loves + evelyn_loves, True) +boomer = villager("George", False, town, "Fall", universal_loves + george_loves, True) +gus = villager("Gus", False, town, "Summer", universal_loves + gus_loves, True) +jas = villager("Jas", False, ranch, "Summer", universal_loves + jas_loves, True) +jodi = villager("Jodi", False, town, "Fall", universal_loves + jodi_loves, True) +kent = villager("Kent", False, town, "Spring", universal_loves + kent_loves, False) +krobus = villager("Krobus", False, sewers, "Winter", universal_loves + krobus_loves, False) +leo = villager("Leo", False, island, "Summer", universal_loves + leo_loves, False) +lewis = villager("Lewis", False, town, "Spring", universal_loves + lewis_loves, True) +linus = villager("Linus", False, mountain, "Winter", universal_loves + linus_loves, True) +marnie = villager("Marnie", False, ranch, "Fall", universal_loves + marnie_loves, True) +pam = villager("Pam", False, town, "Spring", universal_loves + pam_loves, True) +pierre = villager("Pierre", False, town, "Spring", universal_loves + pierre_loves, True) +milf = villager("Robin", False, carpenter, "Fall", universal_loves + robin_loves, True) +sandy = villager("Sandy", False, oasis, "Fall", universal_loves + sandy_loves, False) +vincent = villager("Vincent", False, town, "Spring", universal_loves + vincent_loves, True) +willy = villager("Willy", False, beach, "Summer", universal_loves + willy_loves, True) +wizard = villager("Wizard", False, forest, "Winter", universal_loves + wizard_loves, True) + +all_villagers_by_name = {item.name: item for item in all_villagers} diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index aef28086..a8aa098b 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -19,6 +19,8 @@ The player can choose from a number of goals, using their YAML settings. - Reach the bottom of the Pelican Town Mineshaft - Complete the "Cryptic Note" quest, by meeting Mr Qi on floor 100 of the Skull Cavern - Get the achievement "Master Angler", which requires catching every fish in the game +- Get the achievement "A Complete Collection", which requires donating all the artifacts and minerals to the museum +- Get the achievement "Full House", which requires getting married and having two kids. ## What are location check in Stardew Valley? @@ -38,12 +40,26 @@ There also are a number of location checks that are optional, and individual pla - Arcade Machines - Help Wanted quests - Fishsanity: Catching individual fish +- Museumsanity: Donating individual items to the museum, or reaching the museum milestones for donations +- Friendsanity: Reaching specific friendship levels with NPCs ## Which items can be in another player's world? 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 are instead added to the pool. These can contain ores, seeds, fertilizers, warp totems, etc. -There are a few extra items, which are added to the pool but do not have a matching location. These include + +A player can enable some settings 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. +- Seed Shuffle: + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. + - 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. + +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 - Return Scepter diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 1b2ed156..37290d69 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -5,7 +5,7 @@ - Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) - [StardewArchipelago Mod Release 2.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 2.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. + - It is important to use a mod release of version 3.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 - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) @@ -56,7 +56,14 @@ The password is optional. Your game will connect automatically to Archipelago, and reconnect automatically when loading the save, later. -You will never need to enter this information again for this character. +You will never need to enter this information again for this character, unless your room changes its ip or port. +If the room's ip or port **does** change, you can follow these instructions to modify the connection information of your save file +- Launch modded Stardew Valley +- While **on the main menu** of the game, enter the follow command **in the SMAPI console**: +- `connect_override ip:port slot password` +- Example: `connect_override archipelago.gg:38281 StardewPlayer` +- Load your save game. The new connection information will be used, instead of the saved one +- Play a full day, sleep, and save the game. This connection information will overwrite the previous one and become permanent. ### Interacting with the MultiWorld from in-game diff --git a/worlds/stardew_valley/fish_data.py b/worlds/stardew_valley/fish_data.py deleted file mode 100644 index 270accb4..00000000 --- a/worlds/stardew_valley/fish_data.py +++ /dev/null @@ -1,127 +0,0 @@ -from typing import List, Tuple - -from .game_item import FishItem - -spring = ("Spring",) -summer = ("Summer",) -fall = ("Fall",) -winter = ("Winter",) -spring_summer = (*spring, *summer) -spring_fall = (*spring, *fall) -spring_winter = (*spring, *winter) -summer_fall = (*summer, *fall) -summer_winter = (*summer, *winter) -fall_winter = (*fall, *winter) -spring_summer_fall = (*spring, *summer, *fall) -spring_summer_winter = (*spring, *summer, *winter) -spring_fall_winter = (*spring, *fall, *winter) -all_seasons = (*spring, *summer, *fall, *winter) - -town = ("Town",) -beach = ("Beach",) -mountain = ("Mountain",) -forest = ("Forest",) -secret_woods = ("Secret Woods",) -desert = ("The Desert",) -mines_20 = ("The Mines - Floor 20",) -mines_60 = ("The Mines - Floor 60",) -mines_100 = ("The Mines - Floor 100",) -sewers = ("Sewers",) -mutant_bug_lair = ("Mutant Bug Lair",) -witch_swamp = ("Witch's Swamp",) -ginger_island = ("Ginger Island",) -ginger_island_ocean = ginger_island -ginger_island_river = ginger_island -pirate_cove = ginger_island -night_market = beach -lakes = (*mountain, *secret_woods, *sewers) -ocean = beach -rivers = (*town, *forest) -rivers_secret_woods = (*rivers, *secret_woods) -forest_mountain = (*forest, *mountain) -rivers_mountain_lake = (*town, *forest, *mountain) -mines_20_60 = (*mines_20, *mines_60) - -all_fish_items: List[FishItem] = [] - - -def fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Tuple[str, ...], difficulty: int) -> FishItem: - fish_item = FishItem(name, item_id, locations, seasons, difficulty) - all_fish_items.append(fish_item) - return fish_item - - -carp = fish("Carp", 142, lakes, all_seasons, 15) -herring = fish("Herring", 147, ocean, spring_winter, 25) -smallmouth_bass = fish("Smallmouth Bass", 137, rivers, spring_fall, 28) -anchovy = fish("Anchovy", 129, ocean, spring_fall, 30) -sardine = fish("Sardine", 131, ocean, spring_fall_winter, 30) -sunfish = fish("Sunfish", 145, rivers, spring_summer, 30) -perch = fish("Perch", 141, rivers_mountain_lake, winter, 35) -chub = fish("Chub", 702, forest_mountain, all_seasons, 35) -bream = fish("Bream", 132, rivers, all_seasons, 35) -red_snapper = fish("Red Snapper", 150, ocean, summer_fall, 40) -sea_cucumber = fish("Sea Cucumber", 154, ocean, fall_winter, 40) -rainbow_trout = fish("Rainbow Trout", 138, rivers_mountain_lake, summer, 45) -walleye = fish("Walleye", 140, rivers_mountain_lake, fall, 45) -shad = fish("Shad", 706, rivers, spring_summer_fall, 45) -bullhead = fish("Bullhead", 700, mountain, all_seasons, 46) -largemouth_bass = fish("Largemouth Bass", 136, mountain, all_seasons, 50) -salmon = fish("Salmon", 139, rivers, fall, 50) -ghostfish = fish("Ghostfish", 156, mines_20_60, all_seasons, 50) -tilapia = fish("Tilapia", 701, ocean, summer_fall, 50) -woodskip = fish("Woodskip", 734, secret_woods, all_seasons, 50) -flounder = fish("Flounder", 267, ocean, spring_summer, 50) -halibut = fish("Halibut", 708, ocean, spring_summer_winter, 50) -lionfish = fish("Lionfish", 837, ginger_island_ocean, all_seasons, 50) -slimejack = fish("Slimejack", 796, mutant_bug_lair, all_seasons, 55) -midnight_carp = fish("Midnight Carp", 269, forest_mountain, fall_winter, 55) -red_mullet = fish("Red Mullet", 146, ocean, summer_winter, 55) -pike = fish("Pike", 144, rivers, summer_winter, 60) -tiger_trout = fish("Tiger Trout", 699, rivers, fall_winter, 60) -blue_discus = fish("Blue Discus", 838, ginger_island_river, all_seasons, 60) -albacore = fish("Albacore", 705, ocean, fall_winter, 60) -sandfish = fish("Sandfish", 164, desert, all_seasons, 65) -stonefish = fish("Stonefish", 158, mines_20, all_seasons, 65) -tuna = fish("Tuna", 130, ocean, summer_winter, 70) -eel = fish("Eel", 148, ocean, spring_fall, 70) -catfish = fish("Catfish", 143, rivers_secret_woods, spring_fall, 75) -squid = fish("Squid", 151, ocean, winter, 75) -sturgeon = fish("Sturgeon", 698, mountain, summer_winter, 78) -dorado = fish("Dorado", 704, forest, summer, 78) -pufferfish = fish("Pufferfish", 128, ocean, summer, 80) -void_salmon = fish("Void Salmon", 795, witch_swamp, all_seasons, 80) -super_cucumber = fish("Super Cucumber", 155, ocean, summer_fall, 80) -stingray = fish("Stingray", 836, pirate_cove, all_seasons, 80) -ice_pip = fish("Ice Pip", 161, mines_60, all_seasons, 85) -lingcod = fish("Lingcod", 707, rivers_mountain_lake, winter, 85) -scorpion_carp = fish("Scorpion Carp", 165, desert, all_seasons, 90) -lava_eel = fish("Lava Eel", 162, mines_100, all_seasons, 90) -octopus = fish("Octopus", 149, ocean, summer, 95) - -midnight_squid = fish("Midnight Squid", 798, night_market, winter, 55) -spook_fish = fish("Spook Fish", 799, night_market, winter, 60) -blob_fish = fish("Blobfish", 800, night_market, winter, 75) - -crimsonfish = fish("Crimsonfish", 159, ocean, summer, 95) -angler = fish("Angler", 160, town, fall, 85) -legend = fish("Legend", 163, mountain, spring, 110) -glacierfish = fish("Glacierfish", 775, forest, winter, 100) -mutant_carp = fish("Mutant Carp", 682, sewers, all_seasons, 80) - -crayfish = fish("Crayfish", 716, rivers, all_seasons, -1) -snail = fish("Snail", 721, rivers, all_seasons, -1) -periwinkle = fish("Periwinkle", 722, rivers, all_seasons, -1) -lobster = fish("Lobster", 715, ocean, all_seasons, -1) -clam = fish("Clam", 372, ocean, all_seasons, -1) -crab = fish("Crab", 717, ocean, all_seasons, -1) -cockle = fish("Cockle", 718, ocean, all_seasons, -1) -mussel = fish("Mussel", 719, ocean, all_seasons, -1) -shrimp = fish("Shrimp", 720, ocean, all_seasons, -1) -oyster = fish("Oyster", 723, ocean, all_seasons, -1) - -legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp] -special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] - -all_fish_items_by_name = {fish.name: fish for fish in all_fish_items} -all_fish_items_by_id = {fish.item_id: fish for fish in all_fish_items} diff --git a/worlds/stardew_valley/game_item.py b/worlds/stardew_valley/game_item.py deleted file mode 100644 index 6b8eb6c6..00000000 --- a/worlds/stardew_valley/game_item.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - - -@dataclass(frozen=True) -class GameItem: - name: str - item_id: int - - def __repr__(self): - return f"{self.name} [{self.item_id}]" - - def __lt__(self, other): - return self.name < other.name - - -@dataclass(frozen=True) -class FishItem(GameItem): - locations: Tuple[str] - seasons: Tuple[str] - difficulty: int - - def __repr__(self): - return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ - f" Seasons: {self.seasons} |" \ - f" Difficulty: {self.difficulty}) " diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 03419a16..3f7c53b5 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -14,6 +14,8 @@ from typing import Dict, List, Protocol, Union, Set, Optional, FrozenSet from BaseClasses import Item, ItemClassification from . import options, data +from .data.villagers_data import all_villagers +from .options import StardewOptions ITEM_CODE_OFFSET = 717000 @@ -47,7 +49,12 @@ class Group(enum.Enum): ORE = enum.auto() FERTILIZER = enum.auto() SEED = enum.auto() + SEED_SHUFFLE = enum.auto() FISHING_RESOURCE = enum.auto() + SEASON = enum.auto() + TRAVELING_MERCHANT_DAY = enum.auto() + MUSEUM = enum.auto() + FRIENDSANITY = enum.auto() @dataclass(frozen=True) @@ -132,7 +139,7 @@ def load_item_csv(): try: from importlib.resources import files except ImportError: - from importlib_resources import files + from importlib_resources import files # noqa items = [] with files(data).joinpath("items.csv").open() as file: @@ -149,7 +156,7 @@ def load_resource_pack_csv() -> List[ResourcePackData]: try: from importlib.resources import files except ImportError: - from importlib_resources import files + from importlib_resources import files # noqa resource_packs = [] with files(data).joinpath("resource_packs.csv").open() as file: @@ -166,11 +173,7 @@ def load_resource_pack_csv() -> List[ResourcePackData]: events = [ ItemData(None, "Victory", ItemClassification.progression), - ItemData(None, "Spring", ItemClassification.progression), - ItemData(None, "Summer", ItemClassification.progression), - ItemData(None, "Fall", ItemClassification.progression), - ItemData(None, "Winter", ItemClassification.progression), - ItemData(None, "Year Two", ItemClassification.progression), + ItemData(None, "Month End", ItemClassification.progression), ] all_items: List[ItemData] = load_item_csv() + events @@ -197,10 +200,14 @@ initialize_item_table() initialize_groups() -def create_items(item_factory: StardewItemFactory, locations_count: int, world_options: options.StardewOptions, - random: Random) \ - -> List[Item]: +def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], world_options: StardewOptions, + random: Random) -> List[Item]: items = create_unique_items(item_factory, world_options, random) + + for item in items_to_exclude: + if item in items: + items.remove(item) + assert len(items) <= locations_count, \ "There should be at least as many locations as there are mandatory items" logger.debug(f"Created {len(items)} unique items") @@ -212,7 +219,37 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, world_o return items -def create_backpack_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_unique_items(item_factory: StardewItemFactory, world_options: StardewOptions, random: Random) -> List[Item]: + items = [] + + items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) + + create_backpack_items(item_factory, world_options, items) + create_mine_rewards(item_factory, items, random) + create_mine_elevators(item_factory, world_options, items) + create_tools(item_factory, world_options, items) + create_skills(item_factory, world_options, items) + create_wizard_buildings(item_factory, items) + create_carpenter_buildings(item_factory, world_options, items) + items.append(item_factory("Beach Bridge")) + create_special_quest_rewards(item_factory, items) + create_stardrops(item_factory, items) + create_museum_items(item_factory, world_options, items) + create_arcade_machine_items(item_factory, world_options, items) + items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) + items.append( + item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier]))) + create_player_buffs(item_factory, world_options, items) + items.extend(create_traveling_merchant_items(item_factory)) + items.append(item_factory("Return Scepter")) + items.extend(create_seasons(item_factory, world_options)) + items.extend(create_seeds(item_factory, world_options)) + create_friendsanity_items(item_factory, world_options, items) + + return items + + +def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive): items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2) @@ -232,7 +269,7 @@ def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], ran items.append(item_factory("Skull Key")) -def create_mine_elevators(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_mine_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if (world_options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_progressive or world_options[options.TheMinesElevatorsProgression] == @@ -240,13 +277,13 @@ def create_mine_elevators(item_factory: StardewItemFactory, world_options: optio items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24]) -def create_tools(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_tools(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.ToolProgression] == options.ToolProgression.option_progressive: items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4) items.append(item_factory("Golden Scythe")) -def create_skills(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.SkillProgression] == options.SkillProgression.option_progressive: items.extend([item_factory(item) for item in items_by_group[Group.SKILL_LEVEL_UP] * 10]) @@ -260,7 +297,7 @@ def create_wizard_buildings(item_factory: StardewItemFactory, items: List[Item]) items.append(item_factory("Gold Clock")) -def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: options.StardewOptions, +def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, options.BuildingProgression.option_progressive_early_shipping_bin}: @@ -297,7 +334,41 @@ def create_stardrops(item_factory: StardewItemFactory, items: List[Item]): items.append(item_factory("Stardrop")) # Old Master Cannoli -def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, +def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): + if world_options[options.Museumsanity] == options.Museumsanity.option_none: + return + items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) + items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) + items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4) + items.append(item_factory("Ancient Seeds Recipe")) + items.append(item_factory("Stardrop")) + items.append(item_factory("Rusty Key")) + items.append(item_factory("Dwarvish Translation Guide")) + + +def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): + if world_options[options.Friendsanity] == options.Friendsanity.option_none: + return + exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors + exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ + world_options[options.Friendsanity] == options.Friendsanity.option_bachelors + exclude_post_marriage_hearts = world_options[options.Friendsanity] != options.Friendsanity.option_all_with_marriage + for villager in all_villagers: + if not villager.available and exclude_locked_villagers: + continue + if not villager.bachelor and exclude_non_bachelors: + continue + for heart in range(1, 15): + if villager.bachelor and exclude_post_marriage_hearts and heart > 8: + continue + if villager.bachelor or heart < 11: + items.append(item_factory(f"{villager.name}: 1 <3")) + if not exclude_non_bachelors: + for heart in range(1, 6): + items.append(item_factory(f"Pet: 1 <3")) + + +def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: items.append(item_factory("JotPK: Progressive Boots")) @@ -321,43 +392,29 @@ def create_player_buffs(item_factory: StardewItemFactory, world_options: options items.extend(item_factory(item) for item in ["Luck Bonus"] * number_of_buffs) -def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): - items.append(item_factory("Traveling Merchant: Sunday")) - items.append(item_factory("Traveling Merchant: Monday")) - items.append(item_factory("Traveling Merchant: Tuesday")) - items.append(item_factory("Traveling Merchant: Wednesday")) - items.append(item_factory("Traveling Merchant: Thursday")) - items.append(item_factory("Traveling Merchant: Friday")) - items.append(item_factory("Traveling Merchant: Saturday")) - items.extend(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6) - items.extend(item_factory(item) for item in ["Traveling Merchant Discount"] * 8) +def create_traveling_merchant_items(item_factory: StardewItemFactory) -> List[Item]: + return [ + *(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), + ] -def create_unique_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random) -> \ - List[Item]: - items = [] +def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]: + if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + return [] - items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) + if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return [item_factory(item) for item in ["Progressive Season"] * 3] - create_backpack_items(item_factory, world_options, items) - create_mine_rewards(item_factory, items, random) - create_mine_elevators(item_factory, world_options, items) - create_tools(item_factory, world_options, items) - create_skills(item_factory, world_options, items) - create_wizard_buildings(item_factory, items) - create_carpenter_buildings(item_factory, world_options, items) - items.append(item_factory("Beach Bridge")) - create_special_quest_rewards(item_factory, items) - create_stardrops(item_factory, items) - create_arcade_machine_items(item_factory, world_options, items) - items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) - items.append( - item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier]))) - create_player_buffs(item_factory, world_options, items) - create_traveling_merchant_items(item_factory, items) - items.append(item_factory("Return Scepter")) + return [item_factory(item) for item in items_by_group[Group.SEASON]] - return items + +def create_seeds(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]: + if world_options[options.SeedShuffle] == options.SeedShuffle.option_disabled: + return [] + + return [item_factory(item) for item in items_by_group[Group.SEED_SHUFFLE]] def fill_with_resource_packs(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index a7cb70c5..13af203b 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -5,7 +5,9 @@ from random import Random from typing import Optional, Dict, Protocol, List, FrozenSet from . import options, data -from .fish_data import legendary_fish, special_fish, all_fish_items +from .data.fish_data import legendary_fish, special_fish, all_fish +from .data.museum_data import all_museum_items +from .data.villagers_data import all_villagers LOCATION_CODE_OFFSET = 717000 @@ -46,6 +48,9 @@ class LocationTags(enum.Enum): HELP_WANTED = enum.auto() TRAVELING_MERCHANT = enum.auto() FISHSANITY = enum.auto() + MUSEUM_MILESTONES = enum.auto() + MUSEUM_DONATIONS = enum.auto() + FRIENDSANITY = enum.auto() @dataclass(frozen=True) @@ -88,10 +93,8 @@ events_locations = [ LocationData(None, "The Mines - Floor 120", "Reach the Bottom of The Mines"), LocationData(None, "Skull Cavern", "Complete Quest Cryptic Note"), LocationData(None, "Stardew Valley", "Catch Every Fish"), - LocationData(None, "Stardew Valley", "Summer"), - LocationData(None, "Stardew Valley", "Fall"), - LocationData(None, "Stardew Valley", "Winter"), - LocationData(None, "Stardew Valley", "Year Two"), + LocationData(None, "Stardew Valley", "Complete the Museum Collection"), + LocationData(None, "Stardew Valley", "Full House"), ] all_locations = load_location_csv() + events_locations @@ -133,11 +136,46 @@ def extend_fishsanity_locations(randomized_locations: List[LocationData], fishsa randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish) elif fishsanity == options.Fishsanity.option_special: randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif fishsanity == options.Fishsanity.option_random_selection: + elif fishsanity == options.Fishsanity.option_randomized: randomized_locations.extend(location_table[f"{prefix}{fish.name}"] - for fish in all_fish_items if random.random() < 0.4) + for fish in all_fish if random.random() < 0.4) elif fishsanity == options.Fishsanity.option_all: - randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish_items) + randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish) + + +def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random): + prefix = "Museumsanity: " + if museumsanity == options.Museumsanity.option_none: + return + elif museumsanity == options.Museumsanity.option_milestones: + randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES]) + elif museumsanity == options.Museumsanity.option_randomized: + randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] + for museum_item in all_museum_items if random.random() < 0.4) + elif museumsanity == options.Museumsanity.option_all: + randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items) + + +def extend_friendsanity_locations(randomized_locations: List[LocationData], friendsanity: int): + if friendsanity == options.Friendsanity.option_none: + return + exclude_non_bachelors = friendsanity == options.Friendsanity.option_bachelors + exclude_locked_villagers = friendsanity == options.Friendsanity.option_starting_npcs or \ + friendsanity == options.Friendsanity.option_bachelors + exclude_post_marriage_hearts = friendsanity != options.Friendsanity.option_all_with_marriage + for villager in all_villagers: + if not villager.available and exclude_locked_villagers: + continue + if not villager.bachelor and exclude_non_bachelors: + continue + for heart in range(1, 15): + if villager.bachelor and exclude_post_marriage_hearts and heart > 8: + continue + if villager.bachelor or heart < 11: + randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) + if not exclude_non_bachelors: + for heart in range(1, 6): + randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) def create_locations(location_collector: StardewLocationCollector, @@ -170,6 +208,8 @@ def create_locations(location_collector: StardewLocationCollector, extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) extend_fishsanity_locations(randomized_locations, world_options[options.Fishsanity], random) + extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random) + extend_friendsanity_locations(randomized_locations, world_options[options.Friendsanity]) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 85a5bb08..c4d157b8 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -1,16 +1,19 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List, FrozenSet +from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List -from BaseClasses import CollectionState, ItemClassification from . import options -from .bundle_data import BundleItem -from .fish_data import all_fish_items -from .game_item import FishItem -from .items import all_items, Group, item_table +from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem +from .data.bundle_data import BundleItem +from .data.museum_data import all_museum_items, MuseumItem +from .data.region_data import SVRegion +from .data.villagers_data import all_villagers_by_name +from .items import all_items, Group from .options import StardewOptions +from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule +MONEY_PER_MONTH = 15000 MISSING_ITEM = "THIS ITEM IS MISSING" tool_materials = { @@ -27,36 +30,36 @@ tool_prices = { "Iridium": 25000 } -skill_level_per_season = { - "Spring": { +skill_level_per_month_end = { + 0: { "Farming": 2, "Fishing": 2, "Foraging": 2, "Mining": 2, "Combat": 2, }, - "Summer": { + 1: { "Farming": 4, "Fishing": 4, "Foraging": 4, "Mining": 4, "Combat": 3, }, - "Fall": { + 2: { "Farming": 7, "Fishing": 5, "Foraging": 5, "Mining": 5, "Combat": 4, }, - "Winter": { + 3: { "Farming": 7, "Fishing": 7, "Foraging": 6, "Mining": 7, "Combat": 5, }, - "Year Two": { + 4: { "Farming": 10, "Fishing": 10, "Foraging": 10, @@ -64,8 +67,8 @@ skill_level_per_season = { "Combat": 10, }, } -season_per_skill_level: Dict[Tuple[str, int], str] = {} -season_per_total_level: Dict[int, str] = {} +month_end_per_skill_level: Dict[Tuple[str, int], int] = {} +month_end_per_total_level: Dict[int, int] = {} def initialize_season_per_skill_level(): @@ -76,509 +79,188 @@ def initialize_season_per_skill_level(): "Mining": 0, "Combat": 0, } - for season, skills in skill_level_per_season.items(): + for month_end, skills in skill_level_per_month_end.items(): for skill, expected_level in skills.items(): for level_up in range(current_level[skill] + 1, expected_level + 1): skill_level = (skill, level_up) - if skill_level not in season_per_skill_level: - season_per_skill_level[skill_level] = season + if skill_level not in month_end_per_skill_level: + month_end_per_skill_level[skill_level] = month_end level_up = 0 for level_up in range(level_up + 1, sum(skills.values()) + 1): - if level_up not in season_per_total_level: - season_per_total_level[level_up] = season + if level_up not in month_end_per_total_level: + month_end_per_total_level[level_up] = month_end initialize_season_per_skill_level() week_days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] -class StardewRule: - def __call__(self, state: CollectionState) -> bool: - raise NotImplementedError - - def __or__(self, other) -> StardewRule: - if isinstance(other, _Or): - return _Or(self, *other.rules) - - return _Or(self, other) - - def __and__(self, other) -> StardewRule: - if isinstance(other, _And): - return _And(other.rules.union({self})) - - return _And(self, other) - - def get_difficulty(self): - raise NotImplementedError - - def simplify(self) -> StardewRule: - return self - - -class _True(StardewRule): - - def __new__(cls, _cache=[]): # noqa - if not _cache: - _cache.append(super(_True, cls).__new__(cls)) - return _cache[0] - - def __call__(self, state: CollectionState) -> bool: - return True - - def __or__(self, other) -> StardewRule: - return self - - def __and__(self, other) -> StardewRule: - return other - - def __repr__(self): - return "True" - - def get_difficulty(self): - return 0 - - -class _False(StardewRule): - - def __new__(cls, _cache=[]): # noqa - if not _cache: - _cache.append(super(_False, cls).__new__(cls)) - return _cache[0] - - def __call__(self, state: CollectionState) -> bool: - return False - - def __or__(self, other) -> StardewRule: - return other - - def __and__(self, other) -> StardewRule: - return self - - def __repr__(self): - return "False" - - def get_difficulty(self): - return 999999999 - - -class _Or(StardewRule): - rules: FrozenSet[StardewRule] - - def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() - if isinstance(rule, Iterable): - rules_list.update(rule) - else: - rules_list.add(rule) - - if rules is not None: - rules_list.update(rules) - - assert rules_list, "Can't create a Or conditions without rules" - - new_rules = set() - for rule in rules_list: - if isinstance(rule, _Or): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules - - self.rules = frozenset(rules_list) - - def __call__(self, state: CollectionState) -> bool: - return any(rule(state) for rule in self.rules) - - def __repr__(self): - return f"({' | '.join(repr(rule) for rule in self.rules)})" - - def __or__(self, other): - if isinstance(other, _True): - return other - if isinstance(other, _False): - return self - if isinstance(other, _Or): - return _Or(self.rules.union(other.rules)) - - return _Or(self.rules.union({other})) - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.rules == self.rules - - def __hash__(self): - return hash(self.rules) - - def get_difficulty(self): - return min(rule.get_difficulty() for rule in self.rules) - - def simplify(self) -> StardewRule: - if any(isinstance(rule, _True) for rule in self.rules): - return _True() - - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not _False()} - - if not simplified_rules: - return _False() - - if len(simplified_rules) == 1: - return next(iter(simplified_rules)) - - return _Or(simplified_rules) - - -class _And(StardewRule): - rules: frozenset[StardewRule] - - def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() - if isinstance(rule, Iterable): - rules_list.update(rule) - else: - rules_list.add(rule) - - if rules is not None: - rules_list.update(rules) - - assert rules_list, "Can't create a And conditions without rules" - - new_rules = set() - for rule in rules_list: - if isinstance(rule, _And): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules - - self.rules = frozenset(rules_list) - - def __call__(self, state: CollectionState) -> bool: - return all(rule(state) for rule in self.rules) - - def __repr__(self): - return f"({' & '.join(repr(rule) for rule in self.rules)})" - - def __and__(self, other): - if isinstance(other, _True): - return self - if isinstance(other, _False): - return other - if isinstance(other, _And): - return _And(self.rules.union(other.rules)) - - return _And(self.rules.union({other})) - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.rules == self.rules - - def __hash__(self): - return hash(self.rules) - - def get_difficulty(self): - return max(rule.get_difficulty() for rule in self.rules) - - def simplify(self) -> StardewRule: - if any(isinstance(rule, _False) for rule in self.rules): - return _False() - - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not _True()} - - if not simplified_rules: - return _True() - - if len(simplified_rules) == 1: - return next(iter(simplified_rules)) - - return _And(simplified_rules) - - -class _Count(StardewRule): - count: int - rules: List[StardewRule] - - def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = [] - if isinstance(rule, Iterable): - rules_list.extend(rule) - else: - rules_list.append(rule) - - if rules is not None: - rules_list.extend(rules) - - assert rules_list, "Can't create a Count conditions without rules" - assert len(rules_list) >= count, "Count need at least as many rules at the count" - - self.rules = rules_list - self.count = count - - def __call__(self, state: CollectionState) -> bool: - c = 0 - for r in self.rules: - if r(state): - c += 1 - if c >= self.count: - return True - return False - - def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" - - def get_difficulty(self): - rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty()) - easiest_n_rules = rules_sorted_by_difficulty[0:self.count] - return max(rule.get_difficulty() for rule in easiest_n_rules) - - def simplify(self): - return _Count(self.count, [rule.simplify() for rule in self.rules]) - - -class _TotalReceived(StardewRule): - count: int - items: Iterable[str] - player: int - - def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): - items_list = [] - if isinstance(items, Iterable): - items_list.extend(items) - else: - items_list.append(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, \ - "Item has to be progression to be used in logic" - - self.player = player - self.items = items_list - self.count = count - - def __call__(self, state: CollectionState) -> bool: - c = 0 - for item in self.items: - c += state.count(item, self.player) - if c >= self.count: - return True - return False - - def __repr__(self): - return f"Received {self.count} {self.items}" - - def get_difficulty(self): - return self.count - - -@dataclass(frozen=True) -class _Received(StardewRule): - item: str - player: int - count: int - - def __post_init__(self): - assert item_table[self.item].classification & ItemClassification.progression, \ - "Item has to be progression to be used in logic" - - def __call__(self, state: CollectionState) -> bool: - return state.has(self.item, self.player, self.count) - - def __repr__(self): - if self.count == 1: - return f"Received {self.item}" - return f"Received {self.count} {self.item}" - - def get_difficulty(self): - if self.item == "Spring": - return 0 - if self.item == "Summer": - return 1 - if self.item == "Fall": - return 2 - if self.item == "Winter": - return 3 - if self.item == "Year Two": - return 4 - return self.count - - -@dataclass(frozen=True) -class _Reach(StardewRule): - spot: str - resolution_hint: str - player: int - - def __call__(self, state: CollectionState) -> bool: - return state.can_reach(self.spot, self.resolution_hint, self.player) - - def __repr__(self): - return f"Reach {self.resolution_hint} {self.spot}" - - def get_difficulty(self): - return 1 - - -@dataclass(frozen=True) -class _Has(StardewRule): - item: str - # For sure there is a better way than just passing all the rules everytime - other_rules: Dict[str, StardewRule] - - def __call__(self, state: CollectionState) -> bool: - if isinstance(self.item, str): - return self.other_rules[self.item](state) - - def __repr__(self): - if not self.item in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item} -> {repr(self.other_rules[self.item])}" - - def get_difficulty(self): - return self.other_rules[self.item].get_difficulty() + 1 - - def __hash__(self): - return hash(self.item) - - def simplify(self) -> StardewRule: - return self.other_rules[self.item].simplify() - - -@dataclass(frozen=True) +@dataclass(frozen=True, repr=False) class StardewLogic: player: int options: StardewOptions item_rules: Dict[str, StardewRule] = field(default_factory=dict) + tree_fruit_rules: Dict[str, StardewRule] = field(default_factory=dict) + seed_rules: Dict[str, StardewRule] = field(default_factory=dict) + crops_rules: Dict[str, StardewRule] = field(default_factory=dict) fish_rules: Dict[str, StardewRule] = field(default_factory=dict) + museum_rules: Dict[str, StardewRule] = field(default_factory=dict) building_rules: Dict[str, StardewRule] = field(default_factory=dict) quest_rules: Dict[str, StardewRule] = field(default_factory=dict) def __post_init__(self): - self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish_items}) + self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish}) + self.museum_rules.update({donation.name: self.can_find_museum_item(donation) for donation in all_museum_items}) + + self.tree_fruit_rules.update({ + "Apple": self.has_lived_months(1) & (self.has_season("Fall") | self.can_reach_region(SVRegion.greenhouse)), + "Apricot": self.has_lived_months(1) & (self.has_season("Spring") | self.can_reach_region(SVRegion.greenhouse)), + "Cherry": self.has_lived_months(1) & (self.has_season("Spring") | self.can_reach_region(SVRegion.greenhouse)), + "Orange": self.has_lived_months(1) & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Peach": self.has_lived_months(1) & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Pomegranate": self.has_lived_months(1) & (self.has_season("Fall") | self.can_reach_region(SVRegion.greenhouse)), + "Banana Sapling": self.can_reach_region(SVRegion.ginger_island), + "Mango Sapling": self.can_reach_region(SVRegion.ginger_island), + "Banana": self.has("Banana Sapling") & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Mango": self.has("Mango Sapling") & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + }) + + self.seed_rules.update({seed.name: self.can_buy_seed(seed) for seed in all_purchasable_seeds}) + self.crops_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops}) + self.crops_rules.update({ + "Coffee Bean": (self.has_season("Spring") | self.has_season("Summer")) & self.has_traveling_merchant(), + }) self.item_rules.update({ "Aged Roe": self.has("Preserves Jar") & self.has("Roe"), - "Algae Soup": self.can_cook() & self.has("Green Algae") & self.can_have_relationship("Clint", 3), - "Amaranth": self.received("Fall"), - "Amethyst": self.can_mine_in_the_mines_floor_1_40(), - "Ancient Drum": self.has("Frozen Geode"), + "Algae Soup": self.can_cook() & self.has("Green Algae") & self.has_relationship("Clint", 3), "Any Egg": self.has("Chicken Egg") | self.has("Duck Egg"), - "Apple": self.received("Fall"), - "Apricot": self.received("Year Two"), - "Aquamarine": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), - "Artichoke": self.received("Year Two") & self.received("Fall"), + "Artichoke Dip": self.can_cook() & self.has_season("Fall") & self.has("Artichoke") & self.has("Cow Milk"), + "Artifact Trove": self.has("Omni Geode") & self.can_reach_region(SVRegion.desert), "Bait": self.has_skill_level("Fishing", 2), "Bat Wing": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), "Battery Pack": self.has("Lightning Rod"), + "Bean Hotpot": self.can_cook() & self.has_relationship("Clint", 7) & self.has("Green Bean"), "Bee House": self.has_skill_level("Farming", 3) & self.has("Iron Bar") & self.has("Maple Syrup"), "Beer": (self.has("Keg") & self.has("Wheat")) | self.can_spend_money(400), - "Beet": self.received("Fall") & self.can_reach_region("The Desert"), - "Blackberry": self.received("Fall"), - "Blue Jazz": self.received("Spring"), - "Blueberry": self.received("Summer"), - "Blueberry Tart": self.has("Blueberry") & self.has("Any Egg") & self.can_have_relationship("Pierre", 3), - "Bok Choy": self.received("Fall"), - "Bouquet": self.can_have_relationship("Any", 8), + "Blackberry": self.has_season("Fall"), + "Blackberry Cobbler": self.can_cook() & self.has_season("Fall") & self.has_year_two() & + self.has("Blackberry") & self.has("Sugar") & self.has("Wheat Flour"), + "Blueberry Tart": self.has("Blueberry") & self.has("Any Egg") & self.has_relationship("Pierre", 3), + "Bouquet": self.has_relationship("Bachelor", 8), "Bread": self.can_spend_money(120) | (self.can_spend_money(100) & self.can_cook()), "Broken CD": self.can_crab_pot(), "Broken Glasses": self.can_crab_pot(), "Bug Meat": self.can_mine_in_the_mines_floor_1_40(), - "Cactus Fruit": self.can_reach_region("The Desert"), - "Cauliflower": self.received("Spring"), - "Cave Carrot": self.has_mine_elevator_to_floor(10), + "Cactus Fruit": self.can_reach_region(SVRegion.desert), + "Cave Carrot": self.can_mine_to_floor(10), "Caviar": self.has("Preserves Jar") & self.has("Sturgeon Roe"), - "Chanterelle": self.received("Fall") & self.can_reach_region("Secret Woods"), + "Chanterelle": self.has_season("Fall") & self.can_reach_region(SVRegion.secret_woods), "Cheese Press": self.has_skill_level("Farming", 6) & self.has("Hardwood") & self.has("Copper Bar"), "Cheese": (self.has("Cow Milk") & self.has("Cheese Press")) | - (self.can_reach_region("The Desert") & self.has("Emerald")), - "Cheese Cauliflower": self.has(["Cheese", "Cauliflower"]) & self.can_have_relationship("Pam", 3) & + (self.can_reach_region(SVRegion.desert) & self.has("Emerald")), + "Cheese Cauliflower": self.has(["Cheese", "Cauliflower"]) & self.has_relationship("Pam", 3) & self.can_cook(), - "Cherry": self.received("Year Two"), "Chicken": self.has_building("Coop"), "Chicken Egg": self.has(["Egg", "Egg (Brown)", "Large Egg", "Large Egg (Brown)"], 1), - "Chowder": self.can_cook() & self.can_have_relationship("Willy", 3) & self.has(["Clam", "Cow Milk"]), - "Clam": _True(), - "Clay": _True(), + "Chocolate Cake": self.can_cook() & self.has_season("Winter") & self.has("Wheat Flour") & self.has( + "Sugar") & self.has("Any Egg"), + "Chowder": self.can_cook() & self.has_relationship("Willy", 3) & self.has(["Clam", "Cow Milk"]), + "Clam": True_(), + "Clay": True_(), + "Glazed Yams": self.can_cook() & self.has_season("Fall") & self.has("Yam") & self.has("Sugar"), "Cloth": (self.has("Wool") & self.has("Loom")) | - (self.can_reach_region("The Desert") & self.has("Aquamarine")), - "Coal": _True(), - "Cockle": _True(), - "Coconut": self.can_reach_region("The Desert"), + (self.can_reach_region(SVRegion.desert) & self.has("Aquamarine")), + "Coal": True_(), + "Cockle": True_(), + "Coconut": self.can_reach_region(SVRegion.desert), "Coffee": (self.has("Keg") & self.has("Coffee Bean")) | self.has("Coffee Maker") | self.can_spend_money(300) | self.has("Hot Java Ring"), - "Coffee Bean": (self.received("Spring") | self.received("Summer")) & - (self.can_mine_in_the_mines_floor_41_80() | _True()), # Travelling merchant - "Coffee Maker": _False(), - "Common Mushroom": self.received("Fall") | - (self.received("Spring") & self.can_reach_region("Secret Woods")), + "Coffee Maker": False_(), + "Common Mushroom": self.has_season("Fall") | + (self.has_season("Spring") & self.can_reach_region(SVRegion.secret_woods)), + "Complete Breakfast": self.can_cook() & self.has_season("Spring") & self.has_lived_months(4) & + self.has("Fried Egg") & self.has("Cow Milk") & self.has("Hashbrowns") | self.has( + "Pancakes"), "Copper Bar": self.can_smelt("Copper Ore"), "Copper Ore": self.can_mine_in_the_mines_floor_1_40() | self.can_mine_in_the_skull_cavern(), - "Coral": self.can_reach_region("Tide Pools") | self.received("Summer"), - "Corn": self.received("Summer") | self.received("Fall"), + "Coral": self.can_reach_region(SVRegion.tide_pools) | self.has_season("Summer"), "Cow": self.has_building("Barn"), "Cow Milk": self.has("Milk") | self.has("Large Milk"), "Crab": self.can_crab_pot(), + "Crab Cakes": self.can_mine_in_the_skull_cavern() | + (self.can_cook() & self.has_season("Fall") & self.has_year_two() & self.has("Crab") & + self.has("Wheat Flour") & self.has("Chicken Egg") & self.has("Oil")), "Crab Pot": self.has_skill_level("Fishing", 3), - "Cranberries": self.received("Fall"), + "Cranberry Candy": self.can_cook() & self.has_season("Winter") & self.has("Cranberries") & + self.has("Apple") & self.has("Sugar"), "Crayfish": self.can_crab_pot(), - "Crocus": self.received("Winter"), - "Crystal Fruit": self.received("Winter"), - "Daffodil": self.received("Spring"), - "Dandelion": self.received("Spring"), + "Crispy Bass": self.can_cook() & self.has_relationship("Kent", 3) & self.has("Largemouth Bass") & + self.has("Wheat Flour") & self.has("Oil"), + "Crocus": self.has_season("Winter"), + "Crystal Fruit": self.has_season("Winter"), + "Daffodil": self.has_season("Spring"), + "Dandelion": self.has_season("Spring"), "Dish O' The Sea": self.can_cook() & self.has_skill_level("Fishing", 3) & self.has(["Sardine", "Hashbrowns"]), - "Dorado": self.can_fish(78) & self.received("Summer"), - "Dried Starfish": self.can_fish() & self.can_reach_region("Beach"), + "Dorado": self.can_fish(78) & self.has_season("Summer"), + "Dried Starfish": self.can_fish() & self.can_reach_region(SVRegion.beach), "Driftwood": self.can_crab_pot(), "Duck Egg": self.has("Duck"), "Duck Feather": self.has("Duck"), "Duck": self.has_building("Big Coop"), - "Dwarf Scroll I": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll II": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll III": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll IV": self.can_mine_in_the_mines_floor_81_120(), - "Earth Crystal": self.can_mine_in_the_mines_floor_1_40(), "Egg": self.has("Chicken"), "Egg (Brown)": self.has("Chicken"), - "Eggplant": self.received("Fall"), - "Elvish Jewelry": self.can_fish() & self.can_reach_region("Forest"), - "Emerald": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Fairy Rose": self.received("Fall"), + "Eggplant Parmesan": self.can_cook() & self.has_relationship("Lewis", 7) & self.has("Eggplant") & self.has( + "Tomato"), + "Escargot": self.can_cook() & self.has_relationship("Willy", 5) & self.has("Snail") & self.has("Garlic"), "Farmer's Lunch": self.can_cook() & self.has_skill_level("Farming", 3) & self.has("Omelet") & self.has( "Parsnip"), - "Fiber": _True(), - "Fiddlehead Fern": self.can_reach_region("Secret Woods") & self.received("Summer"), - "Fire Quartz": self.can_mine_in_the_mines_floor_81_120(), + "Fiber": True_(), + "Fiddlehead Fern": self.can_reach_region(SVRegion.secret_woods) & self.has_season("Summer"), + "Fiddlehead Risotto": self.can_cook() & self.has_season("Fall") & self.has("Oil") & + self.has("Fiddlehead Fern") & self.has("Garlic"), + "Fishing Chest": self.can_fish_chests(), + "Fish Taco": self.can_cook() & self.has_relationship("Linus", 7) & self.has("Tuna") & self.has("Tortilla") & + self.has("Red Cabbage") & self.has("Mayonnaise"), + "Fried Calamari": self.can_cook() & self.has_relationship("Jodi", 3) & self.has("Squid") & + self.has("Wheat Flour") & self.has("Oil"), + "Fried Eel": self.can_cook() & self.has_relationship("George", 3) & self.has("Eel") & self.has("Oil"), "Fried Egg": self.can_cook() & self.has("Any Egg"), - "Fried Mushroom": self.can_cook() & self.can_have_relationship("Demetrius", 3) & self.has( + "Fried Mushroom": self.can_cook() & self.has_relationship("Demetrius", 3) & self.has( "Morel") & self.has("Common Mushroom"), "Frozen Geode": self.can_mine_in_the_mines_floor_41_80(), - "Frozen Tear": self.can_mine_in_the_mines_floor_41_80(), + "Fruit Salad": self.can_cook() & self.has_season("Fall") & self.has_year_two() & self.has("Blueberry") & + self.has("Melon") & self.has("Apricot"), "Furnace": self.has("Stone") & self.has("Copper Ore"), "Geode": self.can_mine_in_the_mines_floor_1_40(), + "Ginger": self.can_reach_region(SVRegion.ginger_island), + "Ginger Ale": self.can_cook() & self.can_reach_region(SVRegion.ginger_island) & self.has("Ginger") & self.has( + "Sugar"), "Goat Cheese": self.has("Goat Milk") & self.has("Cheese Press"), "Goat Milk": self.has("Goat"), "Goat": self.has_building("Big Barn"), "Gold Bar": self.can_smelt("Gold Ore"), "Gold Ore": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Grape": self.received("Summer"), + "Golden Pumpkin": self.has_season("Fall") | self.has("Artifact Trove"), "Green Algae": self.can_fish(), - "Green Bean": self.received("Spring"), + "Green Tea": self.has("Keg") & self.has("Tea Leaves"), "Hardwood": self.has_tool("Axe", "Copper"), "Hashbrowns": self.can_cook() & self.can_spend_money(50) & self.has("Potato"), - "Hazelnut": self.received("Fall"), - "Holly": self.received("Winter"), - "Honey": self.can_reach_region("The Desert") | + "Hazelnut": self.has_season("Fall"), + "Holly": self.has_season("Winter"), + "Honey": self.can_reach_region(SVRegion.desert) | (self.has("Bee House") & - (self.received("Spring") | self.received("Summer") | self.received("Fall"))), - "Hops": self.received("Summer"), - "Hot Java Ring": self.can_reach_region("Ginger Island"), - "Hot Pepper": self.received("Summer"), + (self.has_season("Spring") | self.has_season("Summer") | self.has_season("Fall"))), + "Hot Java Ring": self.can_reach_region(SVRegion.ginger_island), + "Ice Cream": (self.has_season("Summer") & self.can_reach_region(SVRegion.town)) | self.can_reach_region( + "The Desert"), + # | (self.can_cook() & self.has_relationship("Jodi", 7) & self.has("Cow Milk") & self.has("Sugar")), "Iridium Bar": self.can_smelt("Iridium Ore"), "Iridium Ore": self.can_mine_in_the_skull_cavern(), "Iron Bar": self.can_smelt("Iron Ore"), "Iron Ore": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), - "Jade": self.can_mine_in_the_mines_floor_41_80(), "Jelly": self.has("Preserves Jar"), "JotPK Small Buff": self.has_jotpk_power_level(2), "JotPK Medium Buff": self.has_jotpk_power_level(4), @@ -589,134 +271,158 @@ class StardewLogic: "Junimo Kart Medium Buff": self.has_junimo_kart_power_level(4), "Junimo Kart Big Buff": self.has_junimo_kart_power_level(6), "Junimo Kart Max Buff": self.has_junimo_kart_power_level(8), - "Kale": self.received("Spring"), "Keg": self.has_skill_level("Farming", 8) & self.has("Iron Bar") & self.has("Copper Bar") & self.has( "Oak Resin"), "Large Egg": self.has("Chicken"), "Large Egg (Brown)": self.has("Chicken"), "Large Goat Milk": self.has("Goat"), "Large Milk": self.has("Cow"), - "Leek": self.received("Spring"), + "Leek": self.has_season("Spring"), "Lightning Rod": self.has_skill_level("Foraging", 6), "Lobster": self.can_crab_pot(), "Loom": self.has_skill_level("Farming", 7) & self.has("Pine Tar"), + "Magic Rock Candy": self.can_reach_region(SVRegion.desert) & self.has("Prismatic Shard"), "Magma Geode": self.can_mine_in_the_mines_floor_81_120() | (self.has("Lava Eel") & self.has_building("Fish Pond")), "Maki Roll": self.can_cook() & self.can_fish(), + "Maple Bar": self.can_cook() & self.has_season("Summer") & self.has_year_two() & self.has("Maple Syrup") & + self.has("Sugar") & self.has("Wheat Flour"), "Maple Syrup": self.has("Tapper"), + "Mayonnaise": self.has("Mayonnaise Machine") & self.has("Chicken Egg"), + "Mayonnaise Machine": self.has_skill_level("Farming", 2) & self.has("Wood") & self.has("Stone") & + self.has("Earth Crystal") & self.has("Copper Bar"), "Mead": self.has("Keg") & self.has("Honey"), - "Melon": self.received("Summer"), "Milk": self.has("Cow"), "Miner's Treat": self.can_cook() & self.has_skill_level("Mining", 3) & self.has("Cow Milk") & self.has( "Cave Carrot"), - "Morel": self.can_reach_region("Secret Woods") & self.received("Year Two"), - "Mussel": _True(), - "Nautilus Shell": self.received("Winter"), + "Morel": self.can_reach_region(SVRegion.secret_woods), + "Mussel": True_(), + "Nautilus Shell": self.has_season("Winter"), "Oak Resin": self.has("Tapper"), + "Oil": True_(), "Oil Maker": self.has_skill_level("Farming", 8) & self.has("Hardwood") & self.has("Gold Bar"), "Omelet": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg") & self.has("Cow Milk"), "Omni Geode": self.can_mine_in_the_mines_floor_41_80() | - self.can_reach_region("The Desert") | + self.can_reach_region(SVRegion.desert) | self.can_do_panning() | self.received("Rusty Key") | (self.has("Octopus") & self.has_building("Fish Pond")) | - self.can_reach_region("Ginger Island"), - "Orange": self.received("Summer"), - "Ostrich": self.has_building("Barn"), - "Oyster": _True(), + self.can_reach_region(SVRegion.ginger_island), + "Ostrich": self.has_building("Barn") & self.has("Ostrich Egg"), + "Ostrich Egg": self.can_reach_region(SVRegion.ginger_island), + "Oyster": True_(), "Pale Ale": self.has("Keg") & self.has("Hops"), - "Pale Broth": self.can_cook() & self.can_have_relationship("Marnie", 3) & self.has("White Algae"), + "Pale Broth": self.can_cook() & self.has_relationship("Marnie", 3) & self.has("White Algae"), "Pancakes": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg"), - "Parsnip": self.received("Spring"), - "Parsnip Soup": self.can_cook() & self.can_have_relationship("Caroline", 3) & self.has( + "Parsnip Soup": self.can_cook() & self.has_relationship("Caroline", 3) & self.has( "Parsnip") & self.has("Cow Milk"), - "Peach": self.received("Summer"), + "Pearl": (self.has("Blobfish") & self.has_building("Fish Pond")) | + (self.has_lived_months(4) & self.has("Artifact Trove")), "Pepper Poppers": self.can_cook() & self.has("Cheese") & self.has( - "Hot Pepper") & self.can_have_relationship("Shane", 3), + "Hot Pepper") & self.has_relationship("Shane", 3), "Periwinkle": self.can_crab_pot(), "Pickles": self.has("Preserves Jar"), "Pig": self.has_building("Deluxe Barn"), + "Piña Colada": False_(), # self.can_reach_region(SVRegion.ginger_island), "Pine Tar": self.has("Tapper"), + "Pink Cake": self.can_cook() & self.has_season("Summer") & self.has("Melon") & self.has( + "Wheat Flour") & self.has("Sugar") & self.has("Any Egg"), "Pizza": self.can_spend_money(600), - "Pomegranate": self.received("Fall"), - "Poppy": self.received("Summer"), - "Potato": self.received("Spring"), + "Plum Pudding": self.can_cook() & self.has_season("Winter") & self.has("Wild Plum") & + self.has("Wheat Flour") & self.has("Sugar"), + "Poppyseed Muffin": self.can_cook() & self.has_season("Winter") & self.has_year_two() & + self.has("Poppy") & self.has("Wheat Flour") & self.has("Sugar"), "Preserves Jar": self.has_skill_level("Farming", 4), - "Prismatic Shard": self.received("Year Two"), - "Pumpkin": self.received("Fall"), + "Pumpkin Pie": self.can_cook() & self.has_season("Winter") & self.has("Wheat Flour") & + self.has("Cow Milk") & self.has("Sugar"), "Purple Mushroom": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Quartz": self.can_mine_in_the_mines_floor_1_40(), "Rabbit": self.has_building("Deluxe Coop"), "Rabbit's Foot": self.has("Rabbit"), - "Radish": self.received("Summer"), - "Rainbow Shell": self.received("Summer"), + "Radioactive Bar": self.can_smelt("Radioactive Ore"), + "Radioactive Ore": self.can_mine_perfectly_in_the_skull_cavern() & self.can_reach_region(SVRegion.ginger_island), + "Rainbow Shell": self.has_season("Summer"), "Rain Totem": self.has_skill_level("Foraging", 9), "Recycling Machine": self.has_skill_level("Fishing", 4) & self.has("Wood") & self.has("Stone") & self.has("Iron Bar"), - "Red Cabbage": self.received("Year Two"), - "Red Mushroom": self.can_reach_region("Secret Woods") & (self.received("Summer") | self.received("Fall")), - "Refined Quartz": self.has("Quartz") | self.has("Fire Quartz") | + "Red Mushroom": self.can_reach_region(SVRegion.secret_woods) & ( + self.has_season("Summer") | self.has_season("Fall")), + "Refined Quartz": self.can_smelt("Quartz") | self.can_smelt("Fire Quartz") | (self.has("Recycling Machine") & (self.has("Broken CD") | self.has("Broken Glasses"))), - "Rhubarb": self.received("Spring") & self.can_reach_region("The Desert"), + "Rhubarb Pie": self.can_cook() & self.has_relationship("Marnie", 7) & self.has("Rhubarb") & + self.has("Wheat Flour") & self.has("Sugar"), + "Rice": True_(), + "Rice Pudding": self.can_cook() & self.has_relationship("Evelyn", 7) & self.has("Cow Milk") & + self.has("Sugar") & self.has("Rice"), "Roe": self.can_fish() & self.has_building("Fish Pond"), "Roots Platter": self.can_cook() & self.has_skill_level("Combat", 3) & self.has("Cave Carrot") & self.has("Winter Root"), - "Ruby": self.can_mine_in_the_mines_floor_81_120() | self.can_do_panning(), - "Salad": self.can_spend_money(220) | ( - self.can_cook() & self.can_have_relationship("Emily", 3) & self.has("Leek") & self.has( - "Dandelion")), - "Salmonberry": self.received("Spring"), - "Salmon Dinner": self.can_cook() & self.can_have_relationship("Gus", 3) & self.has("Salmon") & self.has( + "Roasted Hazelnuts": self.can_cook() & self.has_season("Summer") & self.has("Hazelnut"), + "Salad": self.can_spend_money(220), + # | (self.can_cook() & self.has_relationship("Emily", 3) & self.has("Leek") & self.has("Dandelion") & self.has("Vinegar")), + "Salmonberry": self.has_season("Spring"), + "Salmon Dinner": self.can_cook() & self.has_relationship("Gus", 3) & self.has("Salmon") & self.has( "Amaranth") & self.has("Kale"), - "Sashimi": self.can_fish() & self.can_cook() & self.can_have_relationship("Linus", 3), - "Sea Urchin": self.can_reach_region("Tide Pools") | self.received("Summer"), - "Seaweed": self.can_fish() | self.can_reach_region("Tide Pools"), + "Sashimi": self.can_fish() & self.can_cook() & self.has_relationship("Linus", 3), + "Sea Urchin": self.can_reach_region(SVRegion.tide_pools) | self.has_season("Summer"), + "Seaweed": self.can_fish() | self.can_reach_region(SVRegion.tide_pools), + "Secret Note": self.received("Magnifying Glass"), "Sheep": self.has_building("Deluxe Barn"), "Shrimp": self.can_crab_pot(), "Slime": self.can_mine_in_the_mines_floor_1_40(), "Snail": self.can_crab_pot(), - "Snow Yam": self.received("Winter"), + "Snow Yam": self.has_season("Winter"), "Soggy Newspaper": self.can_crab_pot(), "Solar Essence": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), "Spaghetti": self.can_spend_money(240), - "Spice Berry": self.received("Summer"), - "Spring Onion": self.received("Spring"), + "Spice Berry": self.has_season("Summer"), + "Spring Onion": self.has_season("Spring"), + "Squid Ink": self.can_mine_in_the_mines_floor_81_120() | ( + self.has_building("Fish Pond") & self.has("Squid")), "Staircase": self.has_skill_level("Mining", 2), - "Starfruit": (self.received("Summer") | self.received("Greenhouse")) & self.can_reach_region("The Desert"), + "Stir Fry": self.can_cook() & self.has_season("Spring") & self.has("Cave Carrot") & + self.has("Common Mushroom") & self.has("Kale") & self.has("Oil"), "Stone": self.has_tool("Pickaxe"), - "Strawberry": self.received("Spring"), + "Stuffing": self.has_season("Winter") | + (self.can_cook() & self.has_relationship("Pam", 7) & self.has("Bread") & + self.has("Cranberries") & self.has("Hazelnut")), "Sturgeon Roe": self.has("Sturgeon") & self.has_building("Fish Pond"), - "Summer Spangle": self.received("Summer"), - "Sunflower": self.received("Summer") | self.received("Fall"), + "Sugar": True_(), "Survival Burger": self.can_cook() & self.has_skill_level("Foraging", 2) & self.has(["Bread", "Cave Carrot", "Eggplant"]), - "Sweet Gem Berry": (self.received("Fall") | self.received("Greenhouse")) & self.has_traveling_merchant(), - "Sweet Pea": self.received("Summer"), + "Sweet Pea": self.has_season("Summer"), "Tapper": self.has_skill_level("Foraging", 3), - "Tomato": self.received("Summer"), - "Topaz": self.can_mine_in_the_mines_floor_1_40(), + "Tea Bush": self.has_relationship("Caroline", 2), + "Tea Leaves": self.has_lived_months(1) & self.has("Tea Bush"), "Tortilla": self.can_cook() & self.can_spend_money(100) & self.has("Corn"), "Trash": self.can_crab_pot(), "Triple Shot Espresso": (self.has("Hot Java Ring") | (self.can_cook() & self.can_spend_money(5000) & self.has("Coffee"))), + "Tropical Curry": False_(), + # self.can_cook() & self.can_reach_region(SVRegion.ginger_island) & self.has("Coconut") & self.has("Pineapple") & self.has("Hot Pepper"), "Truffle Oil": self.has("Truffle") & self.has("Oil Maker"), - "Truffle": self.has("Pig") & self.received("Year Two"), - "Tulip": self.received("Spring"), - "Unmilled Rice": self.received("Year Two"), + "Truffle": self.has("Pig") & self.has_spring_summer_or_fall(), + "Vegetable Medley": self.can_cook() & self.has_relationship("Caroline", 7) & self.has("Tomato") & self.has( + "Beet"), + "Vinegar": True_(), + "Void Egg": self.can_meet("Krobus") | (self.has_building("Fish Pond") & self.has("Void Salmon")), "Void Essence": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Wheat": self.received("Summer") | self.received("Fall"), + "Void Mayonnaise": self.has("Mayonnaise Machine") & self.has("Void Egg"), + "Wheat Flour": True_(), "White Algae": self.can_fish() & self.can_mine_in_the_mines_floor_1_40(), - "Wild Horseradish": self.received("Spring"), - "Wild Plum": self.received("Fall"), + "Wild Horseradish": self.has_season("Spring"), + "Wild Plum": self.has_season("Fall"), "Wilted Bouquet": self.has("Furnace") & self.has("Bouquet") & self.has("Coal"), "Wine": self.has("Keg"), - "Winter Root": self.received("Winter"), + "Winter Root": self.has_season("Winter"), "Wood": self.has_tool("Axe"), "Wool": self.has("Rabbit") | self.has("Sheep"), - "Yam": self.received("Fall"), "Hay": self.has_building("Silo"), }) self.item_rules.update(self.fish_rules) + self.item_rules.update(self.museum_rules) + self.item_rules.update(self.tree_fruit_rules) + self.item_rules.update(self.seed_rules) + self.item_rules.update(self.crops_rules) self.building_rules.update({ "Barn": self.can_spend_money(6000) & self.has(["Wood", "Stone"]), @@ -740,117 +446,109 @@ class StardewLogic: }) self.quest_rules.update({ - "Introductions": _True(), + "Introductions": True_(), "How To Win Friends": self.can_complete_quest("Introductions"), - "Getting Started": self.received("Spring") & self.has_tool("Hoe") & self.has_tool("Watering Can"), - "To The Beach": self.received("Spring"), + "Getting Started": self.has("Parsnip") & self.has_tool("Hoe") & self.has_tool("Watering Can"), + "To The Beach": True_(), "Raising Animals": self.can_complete_quest("Getting Started") & self.has_building("Coop"), "Advancement": self.can_complete_quest("Getting Started") & self.has_skill_level("Farming", 1), "Archaeology": self.has_tool("Hoe") | self.can_mine_in_the_mines_floor_1_40() | self.can_fish(), - "Meet The Wizard": self.received("Spring") & self.can_reach_region("Community Center"), + "Meet The Wizard": True_() & self.can_reach_region(SVRegion.community_center), "Forging Ahead": self.has("Copper Ore") & self.has("Furnace"), "Smelting": self.has("Copper Bar"), "Initiation": self.can_mine_in_the_mines_floor_1_40(), - "Robin's Lost Axe": self.received("Spring"), - "Jodi's Request": self.received("Spring") & self.has("Cauliflower"), - "Mayor's \"Shorts\"": self.received("Summer") & self.can_have_relationship("Marnie", 4), - "Blackberry Basket": self.received("Fall"), - "Marnie's Request": self.can_have_relationship("Marnie", 3) & self.has("Cave Carrot"), - "Pam Is Thirsty": self.received("Summer") & self.has("Pale Ale"), - "A Dark Reagent": self.received("Winter") & self.has("Void Essence"), - "Cow's Delight": self.received("Fall") & self.has("Amaranth"), - "The Skull Key": self.received("Skull Key") & self.can_reach_region("The Desert"), - "Crop Research": self.received("Summer") & self.has("Melon"), - "Knee Therapy": self.received("Summer") & self.has("Hot Pepper"), - "Robin's Request": self.received("Winter") & self.has("Hardwood"), + "Robin's Lost Axe": self.has_season("Spring"), + "Jodi's Request": self.has_season("Spring") & self.has("Cauliflower"), + "Mayor's \"Shorts\"": self.has_season("Summer") & self.has_relationship("Marnie", 4), + "Blackberry Basket": self.has_season("Fall"), + "Marnie's Request": self.has_relationship("Marnie", 3) & self.has("Cave Carrot"), + "Pam Is Thirsty": self.has_season("Summer") & self.has("Pale Ale"), + "A Dark Reagent": self.has_season("Winter") & self.has("Void Essence"), + "Cow's Delight": self.has_season("Fall") & self.has("Amaranth"), + "The Skull Key": self.received("Skull Key") & self.can_reach_region(SVRegion.desert), + "Crop Research": self.has_season("Summer") & self.has("Melon"), + "Knee Therapy": self.has_season("Summer") & self.has("Hot Pepper"), + "Robin's Request": self.has_season("Winter") & self.has("Hardwood"), "Qi's Challenge": self.can_mine_in_the_skull_cavern(), - "The Mysterious Qi": self.has("Battery Pack") & self.can_reach_region("The Desert") & self.has( + "The Mysterious Qi": self.has("Battery Pack") & self.can_reach_region(SVRegion.desert) & self.has( "Rainbow Shell") & self.has("Beet") & self.has("Solar Essence"), - "Carving Pumpkins": self.received("Fall") & self.has("Pumpkin"), - "A Winter Mystery": self.received("Winter"), - "Strange Note": self.received("Magnifying Glass") & self.can_reach_region("Secret Woods") & self.has( + "Carving Pumpkins": self.has_season("Fall") & self.has("Pumpkin"), + "A Winter Mystery": self.has_season("Winter"), + "Strange Note": self.received("Magnifying Glass") & self.can_reach_region(SVRegion.secret_woods) & self.has( "Maple Syrup"), - "Cryptic Note": self.received("Magnifying Glass") & self.can_mine_perfectly_in_the_skull_cavern(), - "Fresh Fruit": self.received("Year Two") & self.has("Apricot"), - "Aquatic Research": self.received("Year Two") & self.has("Pufferfish"), - "A Soldier's Star": self.received("Year Two") & self.has("Starfruit"), - "Mayor's Need": self.received("Year Two") & self.has("Truffle Oil"), - "Wanted: Lobster": self.received("Year Two") & self.has("Lobster"), - "Pam Needs Juice": self.received("Year Two") & self.has("Battery Pack"), - "Fish Casserole": self.received("Year Two") & self.can_have_relationship("Jodi", 4) & self.has( - "Largemouth Bass"), - "Catch A Squid": self.received("Year Two") & self.has("Squid"), - "Fish Stew": self.received("Year Two") & self.has("Albacore"), - "Pierre's Notice": self.received("Year Two") & self.has("Sashimi"), - "Clint's Attempt": self.received("Year Two") & self.has("Amethyst"), - "A Favor For Clint": self.received("Year Two") & self.has("Iron Bar"), - "Staff Of Power": self.received("Year Two") & self.has("Iridium Bar"), - "Granny's Gift": self.received("Year Two") & self.has("Leek"), - "Exotic Spirits": self.received("Year Two") & self.has("Coconut"), - "Catch a Lingcod": self.received("Year Two") & self.has("Lingcod"), + "Cryptic Note": self.received("Magnifying Glass") & self.can_reach_region(SVRegion.perfect_skull_cavern), + "Fresh Fruit": self.has("Apricot"), + "Aquatic Research": self.has("Pufferfish"), + "A Soldier's Star": self.has_relationship("Kent") & self.has("Starfruit"), + "Mayor's Need": self.has("Truffle Oil"), + "Wanted: Lobster": self.has("Lobster"), + "Pam Needs Juice": self.has("Battery Pack"), + "Fish Casserole": self.has_relationship("Jodi", 4) & self.has("Largemouth Bass"), + "Catch A Squid": self.has("Squid"), + "Fish Stew": self.has("Albacore"), + "Pierre's Notice": self.has("Sashimi"), + "Clint's Attempt": self.has("Amethyst"), + "A Favor For Clint": self.has("Iron Bar"), + "Staff Of Power": self.has("Iridium Bar"), + "Granny's Gift": self.has("Leek"), + "Exotic Spirits": self.has("Coconut"), + "Catch a Lingcod": self.has("Lingcod"), }) def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule: if isinstance(items, str): - return _Has(items, self.item_rules) + return Has(items, self.item_rules) + + if len(items) == 0: + return True_() if count is None or count == len(items): - return _And(self.has(item) for item in items) + return And(self.has(item) for item in items) if count == 1: - return _Or(self.has(item) for item in items) + return Or(self.has(item) for item in items) - return _Count(count, (self.has(item) for item in items)) + return Count(count, (self.has(item) for item in items)) def received(self, items: Union[str, Iterable[str]], count: Optional[int] = 1) -> StardewRule: + if count <= 0 or not items: + return True_() + if isinstance(items, str): - return _Received(items, self.player, count) + return Received(items, self.player, count) if count is None: - return _And(self.received(item) for item in items) + return And(self.received(item) for item in items) if count == 1: - return _Or(self.received(item) for item in items) + return Or(self.received(item) for item in items) - return _TotalReceived(count, items, self.player) + return TotalReceived(count, items, self.player) def can_reach_region(self, spot: str) -> StardewRule: - return _Reach(spot, "Region", self.player) + return Reach(spot, "Region", self.player) def can_reach_any_region(self, spots: Iterable[str]) -> StardewRule: - return _Or(self.can_reach_region(spot) for spot in spots) + return Or(self.can_reach_region(spot) for spot in spots) + + def can_reach_all_regions(self, spots: Iterable[str]) -> StardewRule: + return And(self.can_reach_region(spot) for spot in spots) def can_reach_location(self, spot: str) -> StardewRule: - return _Reach(spot, "Location", self.player) + return Reach(spot, "Location", self.player) def can_reach_entrance(self, spot: str) -> StardewRule: - return _Reach(spot, "Entrance", self.player) + return Reach(spot, "Entrance", self.player) def can_have_earned_total_money(self, amount: int) -> StardewRule: - if amount <= 10000: - return self.received("Spring") - elif amount <= 30000: - return self.received("Summer") - elif amount <= 60000: - return self.received("Fall") - elif amount <= 70000: - return self.received("Winter") - return self.received("Year Two") + return self.has_lived_months(min(8, amount // MONEY_PER_MONTH)) def can_spend_money(self, amount: int) -> StardewRule: - if amount <= 2000: - return self.received("Spring") - elif amount <= 8000: - return self.received("Summer") - elif amount <= 15000: - return self.received("Fall") - elif amount <= 18000: - return self.received("Winter") - return self.received("Year Two") + return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5))) def has_tool(self, tool: str, material: str = "Basic") -> StardewRule: if material == "Basic": - return _True() + return True_() if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: return self.received(f"Progressive {tool}", count=tool_materials[material]) @@ -859,7 +557,7 @@ class StardewLogic: def has_skill_level(self, skill: str, level: int) -> StardewRule: if level == 0: - return _True() + return True_() if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: return self.received(f"{skill} Level", count=level) @@ -867,11 +565,11 @@ class StardewLogic: if skill == "Fishing" and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: return self.can_get_fishing_xp() - return self.received(season_per_skill_level[(skill, level)]) + return self.has_lived_months(month_end_per_skill_level[(skill, level)]) def has_total_skill_level(self, level: int) -> StardewRule: if level == 0: - return _True() + return True_() if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: skills_items = ["Farming Level", "Mining Level", "Foraging Level", @@ -879,9 +577,9 @@ class StardewLogic: return self.received(skills_items, count=level) if level > 40 and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: - return self.received(season_per_total_level[level]) & self.can_get_fishing_xp() + return self.has_lived_months(month_end_per_total_level[level]) & self.can_get_fishing_xp() - return self.received(season_per_total_level[level]) + return self.has_lived_months(month_end_per_total_level[level]) def has_building(self, building: str) -> StardewRule: if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: @@ -896,29 +594,29 @@ class StardewLogic: building = " ".join(["Progressive", *building.split(" ")[1:]]) return self.received(f"{building}", count) - return _Has(building, self.building_rules) + return Has(building, self.building_rules) def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level < 1: - return _True() + return True_() if upgrade_level > 3: - return _False() + return False_() if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: return self.received(f"Progressive House", upgrade_level) if upgrade_level == 1: - return _Has("Kitchen", self.building_rules) + return Has("Kitchen", self.building_rules) if upgrade_level == 2: - return _Has("Kids Room", self.building_rules) + return Has("Kids Room", self.building_rules) # if upgrade_level == 3: - return _Has("Cellar", self.building_rules) + return Has("Cellar", self.building_rules) def can_complete_quest(self, quest: str) -> StardewRule: - return _Has(quest, self.quest_rules) + return Has(quest, self.quest_rules) def can_get_fishing_xp(self) -> StardewRule: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: @@ -926,6 +624,11 @@ class StardewLogic: return self.can_fish() + def has_max_fishing_rod(self) -> StardewRule: + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", 4) + return self.can_get_fishing_xp() + def can_fish(self, difficulty: int = 0) -> StardewRule: skill_required = max(0, int((difficulty / 10) - 1)) if difficulty <= 40: @@ -937,9 +640,33 @@ class StardewLogic: return skill_rule + def has_max_fishing(self) -> StardewRule: + skill_rule = self.has_skill_level("Fishing", 10) + return self.has_max_fishing_rod() & skill_rule + + def can_fish_chests(self) -> StardewRule: + skill_rule = self.has_skill_level("Fishing", 4) + return self.has_max_fishing_rod() & skill_rule + + def can_buy_seed(self, seed: SeedItem): + if self.options[options.SeedShuffle] == options.SeedShuffle.option_disabled or seed.name == "Rare Seed": + item_rule = True_() + else: + item_rule = self.received(seed.name) + season_rule = self.has_any_season(seed.seasons) + region_rule = self.can_reach_any_region(seed.regions) + return season_rule & region_rule & item_rule + + def can_grow_crop(self, crop: CropItem): + season_rule = self.has_any_season(crop.farm_growth_seasons) + seed_rule = self.has(crop.seed.name) + farm_rule = self.can_reach_region(SVRegion.farm) & season_rule + region_rule = farm_rule | self.can_reach_region(SVRegion.greenhouse) + return seed_rule & region_rule + def can_catch_fish(self, fish: FishItem) -> StardewRule: region_rule = self.can_reach_any_region(fish.locations) - season_rule = self.received(fish.seasons) + season_rule = self.has_any_season(fish.seasons) difficulty_rule = self.can_fish(fish.difficulty) if fish.difficulty == -1: difficulty_rule = self.can_crab_pot() @@ -947,9 +674,9 @@ class StardewLogic: def can_catch_every_fish(self) -> StardewRule: rules = [self.has_skill_level("Fishing", 10), self.has_max_fishing_rod()] - for fish in all_fish_items: + for fish in all_fish: rules.append(self.can_catch_fish(fish)) - return _And(rules) + return And(rules) def has_max_fishing_rod(self) -> StardewRule: if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: @@ -966,28 +693,28 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: return self.has("Crab Pot") - return _True() + return True_() def can_do_panning(self) -> StardewRule: return self.received("Glittering Boulder Removed") # Regions def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 5") + return self.can_reach_region(SVRegion.mines_floor_5) def can_mine_in_the_mines_floor_41_80(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 45") + return self.can_reach_region(SVRegion.mines_floor_45) def can_mine_in_the_mines_floor_81_120(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 85") + return self.can_reach_region(SVRegion.mines_floor_85) def can_mine_in_the_skull_cavern(self) -> StardewRule: return (self.can_progress_in_the_mines_from_floor(120) & - self.can_reach_region("Skull Cavern")) + self.can_reach_region(SVRegion.skull_cavern)) def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule: return (self.can_progress_in_the_mines_from_floor(160) & - self.can_reach_region("Skull Cavern")) + self.can_reach_region(SVRegion.skull_cavern)) def get_weapon_rule_for_floor_tier(self, tier: int): if tier >= 4: @@ -1010,7 +737,7 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level("Combat", combat_tier)) - return _And(rules) + return And(rules) def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule: tier = int(floor / 40) + 1 @@ -1022,7 +749,7 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level("Combat", combat_tier)) - return _And(rules) + return And(rules) def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: if (self.options[options.TheMinesElevatorsProgression] == @@ -1030,7 +757,7 @@ class StardewLogic: self.options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): return self.received("Progressive Mine Elevator", count=int(floor / 5)) - return _True() + return True_() def can_mine_to_floor(self, floor: int) -> StardewRule: previous_elevator = max(floor - 5, 0) @@ -1042,14 +769,14 @@ class StardewLogic: def has_jotpk_power_level(self, power_level: int) -> StardewRule: if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: - return _True() + return True_() jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] return self.received(jotpk_buffs, power_level) def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: - return _True() + return True_() return self.received("Junimo Kart: Extra Life", power_level) def has_traveling_merchant(self, tier: int = 1): @@ -1057,25 +784,77 @@ class StardewLogic: return self.received(traveling_merchant_days, tier) def can_get_married(self) -> StardewRule: - return self.can_reach_region("Tide Pools") & self.can_have_relationship("Bachelor", 10) & self.has_house(1) + return self.can_reach_region(SVRegion.tide_pools) & self.has_relationship("Bachelor", 10) & self.has_house(1) - def can_have_relationship(self, npc: str, hearts: int) -> StardewRule: - if npc == "Leo": - return self.can_reach_region("Ginger Island") + def can_have_two_children(self) -> StardewRule: + return self.can_get_married() & self.has_house(2) & self.has_relationship("Bachelor", 12) - if npc == "Sandy": - return self.can_reach_region("The Desert") + def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: + if hearts <= 0: + return True_() + if self.options[options.Friendsanity] == options.Friendsanity.option_none: + return self.can_earn_relationship(npc, hearts) + if npc not in all_villagers_by_name: + if npc == "Any" or npc == "Bachelor": + possible_friends = [] + for name in all_villagers_by_name: + if npc == "Any" or all_villagers_by_name[name].bachelor: + possible_friends.append(self.has_relationship(name, hearts)) + return Or(possible_friends) + if npc == "All": + mandatory_friends = [] + for name in all_villagers_by_name: + mandatory_friends.append(self.has_relationship(name, hearts)) + return And(mandatory_friends) + return self.can_earn_relationship(npc, hearts) + villager = all_villagers_by_name[npc] + if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors and not villager.bachelor: + return self.can_earn_relationship(npc, hearts) + if self.options[options.Friendsanity] == options.Friendsanity.option_starting_npcs and not villager.available: + return self.can_earn_relationship(npc, hearts) + if self.options[ + options.Friendsanity] != options.Friendsanity.option_all_with_marriage and villager.bachelor and hearts > 8: + return self.received(f"{villager.name}: 1 <3", 8) & self.can_earn_relationship(npc, hearts) + return self.received(f"{villager.name}: 1 <3", hearts) + + def can_meet(self, npc: str) -> StardewRule: + if npc not in all_villagers_by_name: + return True_() + villager = all_villagers_by_name[npc] + rules = [self.can_reach_any_region(villager.locations)] if npc == "Kent": - return self.received("Year Two") + rules.append(self.has_lived_months(4)) + if npc == "Dwarf": + rules.append(self.received("Dwarvish Translation Guide")) + rules.append(self.has_tool("Pickaxe", "Iron")) - if hearts <= 3: - return self.received("Spring") - if hearts <= 6: - return self.received("Summer") - if hearts <= 9: - return self.received("Fall") - return self.received("Winter") + return And(rules) + + def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: + if npc == "Pet": + return self.can_befriend_pet(hearts) + if npc in all_villagers_by_name: + villager = all_villagers_by_name[npc] + option1 = self.has_season(villager.birthday) & self.has(villager.gifts) & self.has_lived_months(1) + option2 = self.has_season(villager.birthday) & self.has(villager.gifts, 1) & self.has_lived_months( + hearts // 3) + option3 = (self.has_season(villager.birthday) | self.has(villager.gifts, 1)) & self.has_lived_months( + hearts // 2) + option4 = self.has_lived_months(hearts) + return self.can_meet(npc) & (option1 | option2 | option3 | option4) + else: + return self.has_lived_months(min(hearts // 2, 8)) + + def can_befriend_pet(self, hearts: int): + if hearts == 0: + return True_() + points = hearts * 200 + points_per_month = 12 * 14 + points_per_water_month = 18 * 14 + return self.can_reach_region(SVRegion.farm) & \ + ((self.has_tool("Watering Can") & self.has_lived_months(points // points_per_water_month)) | + self.has_lived_months(points // points_per_month)) def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule: item_rules = [] @@ -1109,16 +888,16 @@ class StardewLogic: # Catching every fish not expected # Shipping every item not expected self.can_get_married() & self.has_house(2), - self.received("Fall"), # 5 Friends (TODO) - self.received("Winter"), # 10 friends (TODO) - self.received("Fall"), # Max Pet takes 56 days min + self.has_lived_months(3), # 5 Friends (TODO) + self.has_lived_months(4), # 10 friends (TODO) + self.can_befriend_pet(5), # Max Pet self.can_complete_community_center(), # Community Center Completion self.can_complete_community_center(), # CC Ceremony first point self.can_complete_community_center(), # CC Ceremony second point self.received("Skull Key"), # Skull Key obtained - # Rusty key not expected + self.has_rusty_key(), # Rusty key not expected ] - return _Count(12, rules_worth_a_point) + return Count(12, rules_worth_a_point) def has_any_weapon(self) -> StardewRule: return self.has_decent_weapon() | self.received(item.name for item in all_items if Group.WEAPON in item.groups) @@ -1146,3 +925,51 @@ class StardewLogic: return (self.received(item.name for item in all_items if Group.WEAPON in item.groups and Group.GALAXY_WEAPONS in item.groups) & self.received("Adventurer's Guild")) + + def has_year_two(self) -> StardewRule: + return self.has_lived_months(4) + + def has_spring_summer_or_fall(self) -> StardewRule: + return self.has_season("Spring") | self.has_season("Summer") | self.has_season("Fall") + + def can_find_museum_item(self, item: MuseumItem) -> StardewRule: + region_rule = self.can_reach_all_regions(item.locations) + geodes_rule = self.has(item.geodes) + # monster_rule = self.can_farm_monster(item.monsters) + # extra_rule = True_() + return region_rule & geodes_rule # & monster_rule & extra_rule + + def can_complete_museum(self) -> StardewRule: + rules = [self.can_mine_perfectly_in_the_skull_cavern(), self.received("Traveling Merchant Metal Detector", 4)] + for donation in all_museum_items: + rules.append(self.can_find_museum_item(donation)) + return And(rules) + + def has_season(self, season: str) -> StardewRule: + seasons_order = ["Spring", "Summer", "Fall", "Winter"] + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return self.received("Progressive Season", seasons_order.index(season)) + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + if season == "Spring": + return True_() + return self.has_lived_months(1) + return self.received(season) + + def has_any_season(self, seasons: Iterable[str]): + if not seasons: + return True_() + return Or([self.has_season(season) for season in seasons]) + + def has_all_seasons(self, seasons: Iterable[str]): + if not seasons: + return True_() + return And([self.has_season(season) for season in seasons]) + + def has_lived_months(self, number: int) -> StardewRule: + number = max(0, min(number, 8)) + return self.received("Month End", number) + + def has_rusty_key(self) -> StardewRule: + if self.options[options.Museumsanity] == options.Museumsanity.option_none: + return True_() + return self.received("Rusty Key") diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index e7478c7d..d8daed8f 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from typing import Dict, Union, Protocol, runtime_checkable +from typing import Dict, Union, Protocol, runtime_checkable, ClassVar from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice @runtime_checkable class StardewOption(Protocol): - internal_name: str + internal_name: ClassVar[str] @dataclass @@ -22,18 +22,27 @@ class StardewOptions: class Goal(Choice): """What's your goal with this play-through? - With Community Center, the world will be completed once you complete the Community Center. - With Grandpa's Evaluation, the world will be completed once 4 candles are lit around Grandpa's Shrine. - With Bottom of the Mines, the world will be completed once you reach level 120 in the local mineshaft. - With Cryptic Note, the world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern - With Master Angler, the world will be completed once you have caught every fish in the game. Pairs well with Fishsanity""" + Community Center: The world will be completed once you complete the Community Center. + Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine. + Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft. + Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to + reach floor 100 in the Skull Cavern. + Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with + Fishsanity. + Complete Collection: The world will be completed once you have completed the museum by donating every possible + item. Pairs well with Museumsanity. + Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity. + """ internal_name = "goal" display_name = "Goal" + default = 0 option_community_center = 0 option_grandpa_evaluation = 1 option_bottom_of_the_mines = 2 option_cryptic_note = 3 option_master_angler = 4 + option_complete_collection = 5 + option_full_house = 6 @classmethod def get_option_name(cls, value) -> str: @@ -63,9 +72,8 @@ class StartingMoney(SpecialRange): class ResourcePackMultiplier(SpecialRange): - """How many items will be in the resource pack. A lower setting mean fewer resources in each pack. - A higher setting means more resources in each pack. Easy (200) doubles the default quantity. - This also include Friendship bonuses that replace the one from the Bulletin Board.""" + """How many items will be in the resource packs. A lower setting mean fewer resources in each pack. + A higher setting means more resources in each pack. Easy (200) doubles the default quantity""" internal_name = "resource_pack_multiplier" default = 100 range_start = 0 @@ -83,9 +91,9 @@ class ResourcePackMultiplier(SpecialRange): class BundleRandomization(Choice): """What items are needed for the community center bundles? - With Vanilla, you get the standard bundles from the game - With Thematic, every bundle will require random items within their original category - With Shuffled, every bundle will require random items without logic""" + Vanilla: Standard bundles from the vanilla game + Thematic: Every bundle will require random items compatible with their original theme + Shuffled: Every bundle will require random items and follow no particular structure""" internal_name = "bundle_randomization" display_name = "Bundle Randomization" default = 1 @@ -96,10 +104,10 @@ class BundleRandomization(Choice): class BundlePrice(Choice): """How many items are needed for the community center bundles? - With Very Cheap, every bundle will require two items fewer than usual - With Cheap, every bundle will require 1 item fewer than usual - With Normal, every bundle will require the vanilla number of items - With Expensive, every bundle will require 1 extra item""" + Very Cheap: Every bundle will require 2 items fewer than usual + Cheap: Every bundle will require 1 item fewer than usual + Normal: Every bundle will require the vanilla number of items + Expensive: Every bundle will require 1 extra item when applicable""" internal_name = "bundle_price" display_name = "Bundle Price" default = 2 @@ -111,13 +119,16 @@ class BundlePrice(Choice): class EntranceRandomization(Choice): """Should area entrances be randomized? - With Disabled, no entrance randomization is done - With Pelican Town, only buildings in the main town area are randomized with each other - With Non Progression, only buildings that are always available are randomized with each other + Disabled: No entrance randomization is done + Pelican Town: Only buildings in the main town area are randomized among each other + Non Progression: Only buildings that are always available are randomized with each other """ - # With Buildings, All buildings in the world are randomized with each other - # With Everything, All buildings and areas are randomized with each other - # With Chaos, same as everything, but the buildings are shuffled again every in-game day. You can't learn it! + # Buildings: All buildings in the world are randomized with each other + # Everything: All buildings and areas are randomized with each other + # Chaos, same as everything: but the buildings are shuffled again every in-game day. You can't learn it! + # Buildings One-way: Entrance pairs are disconnected, they aren't two-way! + # Everything One-way: Entrance pairs are disconnected, and every entrance is in the shuffle + # Chaos One-way: Entrance pairs are disconnected, and they change every day! internal_name = "entrance_randomization" display_name = "Entrance Randomization" @@ -128,14 +139,47 @@ class EntranceRandomization(Choice): # option_buildings = 3 # option_everything = 4 # option_chaos = 4 + # option_buildings_one_way = 5 + # option_everything_one_way = 6 + # option_chaos_one_way = 7 + + +class SeasonRandomization(Choice): + """Should seasons be randomized? + All settings allow you to choose which season you want to play next (from those unlocked) at the end of a season. + Disabled: You will start in Spring with all seasons unlocked. + Randomized: The seasons will be unlocked randomly as Archipelago items. + Randomized Not Winter: The seasons are randomized, but you're guaranteed not to start with winter. + Progressive: You will start in Spring and unlock the seasons in their original order. + """ + internal_name = "season_randomization" + display_name = "Season Randomization" + default = 1 + option_disabled = 0 + option_randomized = 1 + option_randomized_not_winter = 2 + option_progressive = 3 + + +class SeedShuffle(Choice): + """Should seeds be randomized? + Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in + huge packs. + Disabled: All the seeds will be unlocked from the start. + Randomized: The seeds will be unlocked as Archipelago items + """ + internal_name = "seed_shuffle" + display_name = "Seed Shuffle" + default = 1 + option_disabled = 0 + option_shuffled = 1 class BackpackProgression(Choice): """How is the backpack progression handled? - With Vanilla, you can buy them at Pierre's. - With Progressive, you will randomly find Progressive Backpack to upgrade. - With Early Progressive, you can expect you first Backpack before the second season, and the third before the forth - season. + Vanilla: You can buy them at Pierre's General Store. + Progressive: You will randomly find Progressive Backpack upgrades. + Early Progressive: You can expect your first Backpack in sphere 1. """ internal_name = "backpack_progression" display_name = "Backpack Progression" @@ -147,9 +191,8 @@ class BackpackProgression(Choice): class ToolProgression(Choice): """How is the tool progression handled? - With Vanilla, Clint will upgrade your tools with ore. - With Progressive, you will randomly find Progressive Tool to upgrade. - With World Checks, the tools of different quality will be found in the world.""" + Vanilla: Clint will upgrade your tools with ore. + Progressive: You will randomly find Progressive Tool upgrades.""" internal_name = "tool_progression" display_name = "Tool Progression" default = 1 @@ -159,11 +202,11 @@ class ToolProgression(Choice): class TheMinesElevatorsProgression(Choice): """How is The Mines' Elevator progression handled? - With Vanilla, you will unlock a new elevator floor every 5 floor in the mine. - With Progressive, you will randomly find Progressive Mine Elevator to go deeper. Location are sent for reaching + Vanilla: You will unlock a new elevator floor every 5 floor in the mine. + Progressive: You will randomly find Progressive Mine Elevator to go deeper. Location are sent for reaching every level multiple of 5. - With Progressive from previous floor, you will randomly find Progressive Mine Elevator to go deeper. Location are - sent for taking the ladder or stair to every level multiple of 5, taking the elevator does not count.""" + Progressive from previous floor: Locations are sent for taking the ladder or stairs to every 5 + levels, taking the elevator does not count.""" internal_name = "elevator_progression" display_name = "Elevator Progression" default = 2 @@ -174,9 +217,9 @@ class TheMinesElevatorsProgression(Choice): class SkillProgression(Choice): """How is the skill progression handled? - With Vanilla, you will level up and get the normal reward at each level. - With Progressive, the xp will be counted internally, locations will be sent when you gain a virtual level. Your real - levels will be scattered around the world.""" + Vanilla: You will level up and get the normal reward at each level. + Progressive: The xp will be earned internally, locations will be sent when you earn a level. Your real + levels will be scattered around the multiworld.""" internal_name = "skill_progression" display_name = "Skill Progression" default = 1 @@ -186,11 +229,10 @@ class SkillProgression(Choice): class BuildingProgression(Choice): """How is the building progression handled? - With Vanilla, you will buy each building and upgrade one at the time. - With Progressive, you will receive the buildings and will be able to build the first one of each building for free, + Vanilla: You will buy each building normally. + Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - This option INCLUDES the shipping bin as a building you need to receive. - With Progressive early shipping bin, you can expect to receive the shipping bin before the end of the first season. + Progressive early shipping bin: You can expect your shipping bin in sphere 1. """ internal_name = "building_progression" display_name = "Building Progression" @@ -202,11 +244,11 @@ class BuildingProgression(Choice): class ArcadeMachineLocations(Choice): """How are the Arcade Machines handled? - With Vanilla, the arcade machines are not included in the Archipelago shuffling. - With Victories, each Arcade Machine will contain one check on victory - With Victories Easy, the arcade machines are both made considerably easier to be more accessible for the average + Vanilla: The arcade machines are not included in the Archipelago shuffling. + Victories: Each Arcade Machine will contain one check on victory + Victories Easy: The arcade machines are both made considerably easier to be more accessible for the average player. - With Full Shuffling, the arcade machines will contain multiple checks each, and different buffs that make the game + Full Shuffling: The arcade machines will contain multiple checks each, and different buffs that make the game easier are in the item pool. Junimo Kart has one check at the end of each level. Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment. """ @@ -220,7 +262,7 @@ class ArcadeMachineLocations(Choice): class HelpWantedLocations(SpecialRange): - """How many "Help Wanted" quests need to be completed as ArchipelagoLocations + """How many "Help Wanted" quests need to be completed as Archipelago Locations Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" internal_name = "help_wanted_locations" @@ -241,11 +283,11 @@ class HelpWantedLocations(SpecialRange): class Fishsanity(Choice): """Locations for catching fish? - With None, there are no locations for catching fish - With Legendaries, each of the 5 legendary fish are locations that contain items - With Special, a curated selection of strong fish are locations that contain items - With Random Selection, a random selection of fish are locations that contain items - With All, every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal + None: There are no locations for catching fish + Legendaries: Each of the 5 legendary fish are checks + Special: A curated selection of strong fish are checks + Randomized: A random selection of fish are checks + All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal """ internal_name = "fishsanity" display_name = "Fishsanity" @@ -253,10 +295,45 @@ class Fishsanity(Choice): option_none = 0 option_legendaries = 1 option_special = 2 - option_random_selection = 3 + option_randomized = 3 option_all = 4 +class Museumsanity(Choice): + """Locations for museum donations? + None: There are no locations for donating artifacts and minerals to the museum + Milestones: The donation milestones from the vanilla game are checks + Randomized: A random selection of minerals and artifacts are checks + All: Every single donation will be a check + """ + internal_name = "museumsanity" + display_name = "Museumsanity" + default = 1 + option_none = 0 + option_milestones = 1 + option_randomized = 2 + option_all = 3 + + +class Friendsanity(Choice): + """Locations for friendships? + None: There are no checks for befriending villagers + Bachelors: Each heart of a bachelor is a check + Starting NPCs: Each heart for npcs that are immediately available is a check + All: Every heart with every NPC is a check, including Leo, Kent, Sandy, etc + All With Marriage: Marriage candidates must also be dated, married, and befriended up to 14 hearts. + """ + internal_name = "friendsanity" + display_name = "Friendsanity" + default = 0 + option_none = 0 + # option_marry_one_person = 1 + option_bachelors = 2 + option_starting_npcs = 3 + option_all = 4 + option_all_with_marriage = 5 + + class NumberOfPlayerBuffs(Range): """Number of buffs to the player of each type that exist as items in the pool. Buffs include movement speed (+25% multiplier, stacks additively) @@ -270,14 +347,14 @@ class NumberOfPlayerBuffs(Range): class MultipleDaySleepEnabled(Toggle): - """Should you be able to sleep automatically multiple day strait?""" + """Enable the ability to sleep automatically for multiple days straight?""" internal_name = "multiple_day_sleep_enabled" display_name = "Multiple Day Sleep Enabled" default = 1 class MultipleDaySleepCost(SpecialRange): - """How must gold it cost to sleep through multiple days? You will have to pay that amount for each day slept.""" + """How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped.""" internal_name = "multiple_day_sleep_cost" display_name = "Multiple Day Sleep Cost" range_start = 0 @@ -293,7 +370,7 @@ class MultipleDaySleepCost(SpecialRange): class ExperienceMultiplier(SpecialRange): - """How fast do you want to level up. A lower setting mean less experience. + """How fast you want to earn skill experience. A lower setting mean less experience. A higher setting means more experience.""" internal_name = "experience_multiplier" display_name = "Experience Multiplier" @@ -311,13 +388,33 @@ class ExperienceMultiplier(SpecialRange): } +class FriendshipMultiplier(SpecialRange): + """How fast you want to earn friendship points with villagers. + A lower setting mean less friendship per action. + A higher setting means more friendship per action.""" + internal_name = "friendship_multiplier" + display_name = "Friendship Multiplier" + range_start = 25 + range_end = 400 + # step = 25 + default = 200 + + special_range_names = { + "half": 50, + "vanilla": 100, + "double": 200, + "triple": 300, + "quadruple": 400, + } + + class DebrisMultiplier(Choice): - """How much debris spawn on the player's farm? - With Vanilla, debris spawns normally - With Half, debris will spawn at half the normal rate - With Quarter, debris will spawn at one quarter of the normal rate - With None, No debris will spawn on the farm, ever - With Start Clear, debris will spawn at the normal rate, but the farm will be completely clear when starting the game + """How much debris will spawn on the player's farm? + Vanilla: debris spawns normally + Half: debris will spawn at half the normal rate + Quarter: debris will spawn at one quarter of the normal rate + None: No debris will spawn on the farm, ever + Start Clear: debris will spawn at the normal rate, but the farm will be completely clear when starting the game """ internal_name = "debris_multiplier" display_name = "Debris Multiplier" @@ -364,33 +461,37 @@ class GiftTax(SpecialRange): } -stardew_valley_options: Dict[str, type(Option)] = { - option.internal_name: option - for option in [ - StartingMoney, - ResourcePackMultiplier, - BundleRandomization, - BundlePrice, - EntranceRandomization, - BackpackProgression, - ToolProgression, - SkillProgression, - BuildingProgression, - TheMinesElevatorsProgression, - ArcadeMachineLocations, - HelpWantedLocations, - Fishsanity, - NumberOfPlayerBuffs, - Goal, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - ExperienceMultiplier, - DebrisMultiplier, - QuickStart, - Gifting, - GiftTax, - ] -} +stardew_valley_option_classes = [ + StartingMoney, + ResourcePackMultiplier, + BundleRandomization, + BundlePrice, + EntranceRandomization, + SeasonRandomization, + SeedShuffle, + BackpackProgression, + ToolProgression, + SkillProgression, + BuildingProgression, + TheMinesElevatorsProgression, + ArcadeMachineLocations, + HelpWantedLocations, + Fishsanity, + Museumsanity, + Friendsanity, + NumberOfPlayerBuffs, + Goal, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + QuickStart, + Gifting, + GiftTax, +] +stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in + stardew_valley_option_classes} default_options = {option.internal_name: option.default for option in stardew_valley_options.values()} stardew_valley_options["death_link"] = DeathLink diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 0979d7f8..5c6dcfd9 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -5,6 +5,7 @@ from typing import Iterable, Dict, Protocol, Optional, List, Tuple from BaseClasses import Region, Entrance from . import options +from .data.region_data import SVRegion from .options import StardewOptions @@ -42,219 +43,237 @@ class ConnectionData: stardew_valley_regions = [ - RegionData("Menu", ["To Stardew Valley"]), - RegionData("Stardew Valley", ["To Farmhouse"]), - RegionData("Farmhouse", ["Outside to Farm", "Downstairs to Cellar"]), - RegionData("Cellar"), - RegionData("Farm", ["Farm to Backwoods", "Farm to Bus Stop", "Farm to Forest", "Farm to Farmcave", "Enter Greenhouse", - "Use Desert Obelisk", "Use Island Obelisk"]), - RegionData("Backwoods", ["Backwoods to Mountain"]), - RegionData("Bus Stop", ["Bus Stop to Town", "Take Bus to Desert", "Bus Stop to Tunnel Entrance"]), - RegionData("Forest", ["Forest to Town", "Enter Secret Woods", "Forest to Wizard Tower", "Forest to Marnie's Ranch", - "Forest to Leah's Cottage", "Forest to Sewers"]), - RegionData("Farmcave"), - RegionData("Greenhouse"), - RegionData("Mountain", ["Mountain to Railroad", "Mountain to Tent", "Mountain to Carpenter Shop", "Mountain to The Mines", - "Enter Quarry", "Mountain to Adventurer's Guild", "Mountain to Town"]), - RegionData("Tunnel Entrance", ["Enter Tunnel"]), - RegionData("Tunnel"), - RegionData("Town", ["Town to Community Center", "Town to Beach", "Town to Hospital", - "Town to Pierre's General Store", "Town to Saloon", "Town to Alex's House", "Town to Trailer", "Town to Mayor's Manor", - "Town to Sam's House", "Town to Haley's House", "Town to Sewers", "Town to Clint's Blacksmith", "Town to Museum", + RegionData(SVRegion.menu, ["To Stardew Valley"]), + RegionData(SVRegion.stardew_valley, ["To Farmhouse"]), + RegionData(SVRegion.farm_house, ["Outside to Farm", "Downstairs to Cellar"]), + RegionData(SVRegion.cellar), + RegionData(SVRegion.farm, + ["Farm to Backwoods", "Farm to Bus Stop", "Farm to Forest", "Farm to Farmcave", "Enter Greenhouse", + "Use Desert Obelisk", "Use Island Obelisk"]), + RegionData(SVRegion.backwoods, ["Backwoods to Mountain"]), + RegionData(SVRegion.bus_stop, ["Bus Stop to Town", "Take Bus to Desert", "Bus Stop to Tunnel Entrance"]), + RegionData(SVRegion.forest, ["Forest to Town", "Enter Secret Woods", "Forest to Wizard Tower", "Forest to Marnie's Ranch", + "Forest to Leah's Cottage", "Forest to Sewers", "Talk to Traveling Merchant"]), + RegionData(SVRegion.traveling_cart), + RegionData(SVRegion.farm_cave), + RegionData(SVRegion.greenhouse), + RegionData(SVRegion.mountain, + ["Mountain to Railroad", "Mountain to Tent", "Mountain to Carpenter Shop", "Mountain to The Mines", + "Enter Quarry", "Mountain to Adventurer's Guild", "Mountain to Town"]), + RegionData(SVRegion.tunnel_entrance, ["Enter Tunnel"]), + RegionData(SVRegion.tunnel), + RegionData(SVRegion.town, ["Town to Community Center", "Town to Beach", "Town to Hospital", + "Town to Pierre's General Store", "Town to Saloon", "Town to Alex's House", "Town to Trailer", + "Town to Mayor's Manor", + "Town to Sam's House", "Town to Haley's House", "Town to Sewers", "Town to Clint's Blacksmith", + "Town to Museum", "Town to JojaMart"]), - RegionData("Beach", ["Beach to Willy's Fish Shop", "Enter Elliott's House", "Enter Tide Pools"]), - RegionData("Railroad", ["Enter Bathhouse Entrance", "Enter Witch Warp Cave"]), # "Enter Perfection Cutscene Area" - RegionData("Marnie's Ranch"), - RegionData("Leah's Cottage"), - RegionData("Sewers", ["Enter Mutant Bug Lair"]), - RegionData("Mutant Bug Lair"), - RegionData("Wizard Tower", ["Enter Wizard Basement"]), - RegionData("Wizard Basement"), - RegionData("Tent"), - RegionData("Carpenter Shop", ["Enter Sebastian's Room"]), - RegionData("Sebastian's Room"), - RegionData("Adventurer's Guild"), - RegionData("Community Center", - ["Access Crafts Room", "Access Pantry", "Access Fish Tank", "Access Boiler Room", "Access Bulletin Board", + RegionData(SVRegion.beach, ["Beach to Willy's Fish Shop", "Enter Elliott's House", "Enter Tide Pools"]), + RegionData(SVRegion.railroad, ["Enter Bathhouse Entrance", "Enter Witch Warp Cave"]), # "Enter Perfection Cutscene Area" + RegionData(SVRegion.ranch), + RegionData(SVRegion.leah_house), + RegionData(SVRegion.sewers, ["Enter Mutant Bug Lair"]), + RegionData(SVRegion.mutant_bug_lair), + RegionData(SVRegion.wizard_tower, ["Enter Wizard Basement"]), + RegionData(SVRegion.wizard_basement), + RegionData(SVRegion.tent), + RegionData(SVRegion.carpenter, ["Enter Sebastian's Room"]), + RegionData(SVRegion.sebastian_room), + RegionData(SVRegion.adventurer_guild), + RegionData(SVRegion.community_center, + ["Access Crafts Room", "Access Pantry", "Access Fish Tank", "Access Boiler Room", + "Access Bulletin Board", "Access Vault"]), - RegionData("Crafts Room"), - RegionData("Pantry"), - RegionData("Fish Tank"), - RegionData("Boiler Room"), - RegionData("Bulletin Board"), - RegionData("Vault"), - RegionData("Hospital", ["Enter Harvey's Room"]), - RegionData("Harvey's Room"), - RegionData("Pierre's General Store", ["Enter Sunroom"]), - RegionData("Sunroom"), - RegionData("Saloon", ["Play Journey of the Prairie King", "Play Junimo Kart"]), - RegionData("Alex's House"), - RegionData("Trailer"), - RegionData("Mayor's Manor"), - RegionData("Sam's House"), - RegionData("Haley's House"), - RegionData("Clint's Blacksmith"), - RegionData("Museum"), - RegionData("JojaMart"), - RegionData("Willy's Fish Shop"), - RegionData("Elliott's House"), - RegionData("Tide Pools"), - RegionData("Bathhouse Entrance", ["Enter Locker Room"]), - RegionData("Locker Room", ["Enter Public Bath"]), - RegionData("Public Bath"), - RegionData("Witch Warp Cave", ["Enter Witch's Swamp"]), - RegionData("Witch's Swamp"), - RegionData("Quarry", ["Enter Quarry Mine Entrance"]), - RegionData("Quarry Mine Entrance", ["Enter Quarry Mine"]), - RegionData("Quarry Mine"), - RegionData("Secret Woods"), - RegionData("The Desert", ["Enter Skull Cavern Entrance"]), - RegionData("Skull Cavern Entrance", ["Enter Skull Cavern"]), - RegionData("Skull Cavern"), - RegionData("Ginger Island"), - RegionData("JotPK World 1", ["Reach JotPK World 2"]), - RegionData("JotPK World 2", ["Reach JotPK World 3"]), - RegionData("JotPK World 3"), - RegionData("Junimo Kart 1", ["Reach Junimo Kart 2"]), - RegionData("Junimo Kart 2", ["Reach Junimo Kart 3"]), - RegionData("Junimo Kart 3"), - RegionData("The Mines", ["Dig to The Mines - Floor 5", "Dig to The Mines - Floor 10", "Dig to The Mines - Floor 15", - "Dig to The Mines - Floor 20", "Dig to The Mines - Floor 25", "Dig to The Mines - Floor 30", - "Dig to The Mines - Floor 35", "Dig to The Mines - Floor 40", "Dig to The Mines - Floor 45", - "Dig to The Mines - Floor 50", "Dig to The Mines - Floor 55", "Dig to The Mines - Floor 60", - "Dig to The Mines - Floor 65", "Dig to The Mines - Floor 70", "Dig to The Mines - Floor 75", - "Dig to The Mines - Floor 80", "Dig to The Mines - Floor 85", "Dig to The Mines - Floor 90", - "Dig to The Mines - Floor 95", "Dig to The Mines - Floor 100", "Dig to The Mines - Floor 105", - "Dig to The Mines - Floor 110", "Dig to The Mines - Floor 115", "Dig to The Mines - Floor 120"]), - RegionData("The Mines - Floor 5"), - RegionData("The Mines - Floor 10"), - RegionData("The Mines - Floor 15"), - RegionData("The Mines - Floor 20"), - RegionData("The Mines - Floor 25"), - RegionData("The Mines - Floor 30"), - RegionData("The Mines - Floor 35"), - RegionData("The Mines - Floor 40"), - RegionData("The Mines - Floor 45"), - RegionData("The Mines - Floor 50"), - RegionData("The Mines - Floor 55"), - RegionData("The Mines - Floor 60"), - RegionData("The Mines - Floor 65"), - RegionData("The Mines - Floor 70"), - RegionData("The Mines - Floor 75"), - RegionData("The Mines - Floor 80"), - RegionData("The Mines - Floor 85"), - RegionData("The Mines - Floor 90"), - RegionData("The Mines - Floor 95"), - RegionData("The Mines - Floor 100"), - RegionData("The Mines - Floor 105"), - RegionData("The Mines - Floor 110"), - RegionData("The Mines - Floor 115"), - RegionData("The Mines - Floor 120"), + RegionData(SVRegion.crafts_room), + RegionData(SVRegion.pantry), + RegionData(SVRegion.fish_tank), + RegionData(SVRegion.boiler_room), + RegionData(SVRegion.bulletin_board), + RegionData(SVRegion.vault), + RegionData(SVRegion.hospital, ["Enter Harvey's Room"]), + RegionData(SVRegion.harvey_room), + RegionData(SVRegion.pierre_store, ["Enter Sunroom"]), + RegionData(SVRegion.sunroom), + RegionData(SVRegion.saloon, ["Play Journey of the Prairie King", "Play Junimo Kart"]), + RegionData(SVRegion.alex_house), + RegionData(SVRegion.trailer), + RegionData(SVRegion.mayor_house), + RegionData(SVRegion.sam_house), + RegionData(SVRegion.haley_house), + RegionData(SVRegion.blacksmith), + RegionData(SVRegion.museum), + RegionData(SVRegion.jojamart), + RegionData(SVRegion.fish_shop), + RegionData(SVRegion.elliott_house), + RegionData(SVRegion.tide_pools), + RegionData(SVRegion.bathhouse_entrance, ["Enter Locker Room"]), + RegionData(SVRegion.locker_room, ["Enter Public Bath"]), + RegionData(SVRegion.public_bath), + RegionData(SVRegion.witch_warp_cave, ["Enter Witch's Swamp"]), + RegionData(SVRegion.witch_swamp), + RegionData(SVRegion.quarry, ["Enter Quarry Mine Entrance"]), + RegionData(SVRegion.quarry_mine_entrance, ["Enter Quarry Mine"]), + RegionData(SVRegion.quarry_mine), + RegionData(SVRegion.secret_woods), + RegionData(SVRegion.desert, ["Enter Skull Cavern Entrance"]), + RegionData(SVRegion.skull_cavern_entrance, ["Enter Skull Cavern"]), + RegionData(SVRegion.skull_cavern, ["Mine to Skull Cavern Floor 100"]), + RegionData(SVRegion.perfect_skull_cavern), + RegionData(SVRegion.ginger_island), + RegionData(SVRegion.jotpk_world_1, ["Reach JotPK World 2"]), + RegionData(SVRegion.jotpk_world_2, ["Reach JotPK World 3"]), + RegionData(SVRegion.jotpk_world_3), + RegionData(SVRegion.junimo_kart_1, ["Reach Junimo Kart 2"]), + RegionData(SVRegion.junimo_kart_2, ["Reach Junimo Kart 3"]), + RegionData(SVRegion.junimo_kart_3), + RegionData(SVRegion.mines, ["Dig to The Mines - Floor 5", "Dig to The Mines - Floor 10", "Dig to The Mines - Floor 15", + "Dig to The Mines - Floor 20", "Dig to The Mines - Floor 25", + "Dig to The Mines - Floor 30", + "Dig to The Mines - Floor 35", "Dig to The Mines - Floor 40", + "Dig to The Mines - Floor 45", + "Dig to The Mines - Floor 50", "Dig to The Mines - Floor 55", + "Dig to The Mines - Floor 60", + "Dig to The Mines - Floor 65", "Dig to The Mines - Floor 70", + "Dig to The Mines - Floor 75", + "Dig to The Mines - Floor 80", "Dig to The Mines - Floor 85", + "Dig to The Mines - Floor 90", + "Dig to The Mines - Floor 95", "Dig to The Mines - Floor 100", + "Dig to The Mines - Floor 105", + "Dig to The Mines - Floor 110", "Dig to The Mines - Floor 115", + "Dig to The Mines - Floor 120"]), + RegionData(SVRegion.mines_floor_5), + RegionData(SVRegion.mines_floor_10), + RegionData(SVRegion.mines_floor_15), + RegionData(SVRegion.mines_floor_20), + RegionData(SVRegion.mines_floor_25), + RegionData(SVRegion.mines_floor_30), + RegionData(SVRegion.mines_floor_35), + RegionData(SVRegion.mines_floor_40), + RegionData(SVRegion.mines_floor_45), + RegionData(SVRegion.mines_floor_50), + RegionData(SVRegion.mines_floor_55), + RegionData(SVRegion.mines_floor_60), + RegionData(SVRegion.mines_floor_65), + RegionData(SVRegion.mines_floor_70), + RegionData(SVRegion.mines_floor_75), + RegionData(SVRegion.mines_floor_80), + RegionData(SVRegion.mines_floor_85), + RegionData(SVRegion.mines_floor_90), + RegionData(SVRegion.mines_floor_95), + RegionData(SVRegion.mines_floor_100), + RegionData(SVRegion.mines_floor_105), + RegionData(SVRegion.mines_floor_110), + RegionData(SVRegion.mines_floor_115), + RegionData(SVRegion.mines_floor_120), ] # Exists and where they lead mandatory_connections = [ - ConnectionData("To Stardew Valley", "Stardew Valley"), - ConnectionData("To Farmhouse", "Farmhouse"), - ConnectionData("Outside to Farm", "Farm"), - ConnectionData("Downstairs to Cellar", "Cellar"), - ConnectionData("Farm to Backwoods", "Backwoods"), - ConnectionData("Farm to Bus Stop", "Bus Stop"), - ConnectionData("Farm to Forest", "Forest"), - ConnectionData("Farm to Farmcave", "Farmcave", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Greenhouse", "Greenhouse"), - ConnectionData("Use Desert Obelisk", "The Desert"), - ConnectionData("Use Island Obelisk", "Ginger Island"), - ConnectionData("Backwoods to Mountain", "Mountain"), - ConnectionData("Bus Stop to Town", "Town"), - ConnectionData("Bus Stop to Tunnel Entrance", "Tunnel Entrance"), - ConnectionData("Take Bus to Desert", "The Desert"), - ConnectionData("Enter Tunnel", "Tunnel"), - ConnectionData("Forest to Town", "Town"), - ConnectionData("Forest to Wizard Tower", "Wizard Tower", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Wizard Basement", "Wizard Basement"), - ConnectionData("Forest to Marnie's Ranch", "Marnie's Ranch", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Forest to Leah's Cottage", "Leah's Cottage"), - ConnectionData("Enter Secret Woods", "Secret Woods"), - ConnectionData("Forest to Sewers", "Sewers"), - ConnectionData("Town to Sewers", "Sewers"), - ConnectionData("Enter Mutant Bug Lair", "Mutant Bug Lair"), - ConnectionData("Mountain to Railroad", "Railroad"), - ConnectionData("Mountain to Tent", "Tent", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Mountain to Carpenter Shop", "Carpenter Shop", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Sebastian's Room", "Sebastian's Room"), - ConnectionData("Mountain to Adventurer's Guild", "Adventurer's Guild"), - ConnectionData("Enter Quarry", "Quarry"), - ConnectionData("Enter Quarry Mine Entrance", "Quarry Mine Entrance"), - ConnectionData("Enter Quarry Mine", "Quarry Mine"), - ConnectionData("Mountain to Town", "Town"), - ConnectionData("Town to Community Center", "Community Center", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Access Crafts Room", "Crafts Room"), - ConnectionData("Access Pantry", "Pantry"), - ConnectionData("Access Fish Tank", "Fish Tank"), - ConnectionData("Access Boiler Room", "Boiler Room"), - ConnectionData("Access Bulletin Board", "Bulletin Board"), - ConnectionData("Access Vault", "Vault"), - ConnectionData("Town to Hospital", "Hospital", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Enter Harvey's Room", "Harvey's Room"), - ConnectionData("Town to Pierre's General Store", "Pierre's General Store", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Enter Sunroom", "Sunroom"), - ConnectionData("Town to Clint's Blacksmith", "Clint's Blacksmith", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Saloon", "Saloon", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Play Journey of the Prairie King", "JotPK World 1"), - ConnectionData("Reach JotPK World 2", "JotPK World 2"), - ConnectionData("Reach JotPK World 3", "JotPK World 3"), - ConnectionData("Play Junimo Kart", "Junimo Kart 1"), - ConnectionData("Reach Junimo Kart 2", "Junimo Kart 2"), - ConnectionData("Reach Junimo Kart 3", "Junimo Kart 3"), - ConnectionData("Town to Sam's House", "Sam's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Haley's House", "Haley's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Mayor's Manor", "Mayor's Manor", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Alex's House", "Alex's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Trailer", "Trailer", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Museum", "Museum", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to JojaMart", "JojaMart", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Beach", "Beach"), - ConnectionData("Enter Elliott's House", "Elliott's House"), - ConnectionData("Beach to Willy's Fish Shop", "Willy's Fish Shop", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Tide Pools", "Tide Pools"), - ConnectionData("Mountain to The Mines", "The Mines", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Dig to The Mines - Floor 5", "The Mines - Floor 5"), - ConnectionData("Dig to The Mines - Floor 10", "The Mines - Floor 10"), - ConnectionData("Dig to The Mines - Floor 15", "The Mines - Floor 15"), - ConnectionData("Dig to The Mines - Floor 20", "The Mines - Floor 20"), - ConnectionData("Dig to The Mines - Floor 25", "The Mines - Floor 25"), - ConnectionData("Dig to The Mines - Floor 30", "The Mines - Floor 30"), - ConnectionData("Dig to The Mines - Floor 35", "The Mines - Floor 35"), - ConnectionData("Dig to The Mines - Floor 40", "The Mines - Floor 40"), - ConnectionData("Dig to The Mines - Floor 45", "The Mines - Floor 45"), - ConnectionData("Dig to The Mines - Floor 50", "The Mines - Floor 50"), - ConnectionData("Dig to The Mines - Floor 55", "The Mines - Floor 55"), - ConnectionData("Dig to The Mines - Floor 60", "The Mines - Floor 60"), - ConnectionData("Dig to The Mines - Floor 65", "The Mines - Floor 65"), - ConnectionData("Dig to The Mines - Floor 70", "The Mines - Floor 70"), - ConnectionData("Dig to The Mines - Floor 75", "The Mines - Floor 75"), - ConnectionData("Dig to The Mines - Floor 80", "The Mines - Floor 80"), - ConnectionData("Dig to The Mines - Floor 85", "The Mines - Floor 85"), - ConnectionData("Dig to The Mines - Floor 90", "The Mines - Floor 90"), - ConnectionData("Dig to The Mines - Floor 95", "The Mines - Floor 95"), - ConnectionData("Dig to The Mines - Floor 100", "The Mines - Floor 100"), - ConnectionData("Dig to The Mines - Floor 105", "The Mines - Floor 105"), - ConnectionData("Dig to The Mines - Floor 110", "The Mines - Floor 110"), - ConnectionData("Dig to The Mines - Floor 115", "The Mines - Floor 115"), - ConnectionData("Dig to The Mines - Floor 120", "The Mines - Floor 120"), - ConnectionData("Enter Skull Cavern Entrance", "Skull Cavern Entrance"), - ConnectionData("Enter Skull Cavern", "Skull Cavern"), - ConnectionData("Enter Witch Warp Cave", "Witch Warp Cave"), - ConnectionData("Enter Witch's Swamp", "Witch's Swamp"), - ConnectionData("Enter Bathhouse Entrance", "Bathhouse Entrance"), - ConnectionData("Enter Locker Room", "Locker Room"), - ConnectionData("Enter Public Bath", "Public Bath"), + ConnectionData("To Stardew Valley", SVRegion.stardew_valley), + ConnectionData("To Farmhouse", SVRegion.farm_house), + ConnectionData("Outside to Farm", SVRegion.farm), + ConnectionData("Downstairs to Cellar", SVRegion.cellar), + ConnectionData("Farm to Backwoods", SVRegion.backwoods), + ConnectionData("Farm to Bus Stop", SVRegion.bus_stop), + ConnectionData("Farm to Forest", SVRegion.forest), + ConnectionData("Farm to Farmcave", SVRegion.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Greenhouse", SVRegion.greenhouse), + ConnectionData("Use Desert Obelisk", SVRegion.desert), + ConnectionData("Use Island Obelisk", SVRegion.ginger_island), + ConnectionData("Backwoods to Mountain", SVRegion.mountain), + ConnectionData("Bus Stop to Town", SVRegion.town), + ConnectionData("Bus Stop to Tunnel Entrance", SVRegion.tunnel_entrance), + ConnectionData("Take Bus to Desert", SVRegion.desert), + ConnectionData("Enter Tunnel", SVRegion.tunnel), + ConnectionData("Forest to Town", SVRegion.town), + ConnectionData("Forest to Wizard Tower", SVRegion.wizard_tower, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Wizard Basement", SVRegion.wizard_basement), + ConnectionData("Forest to Marnie's Ranch", SVRegion.ranch, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Forest to Leah's Cottage", SVRegion.leah_house), + ConnectionData("Enter Secret Woods", SVRegion.secret_woods), + ConnectionData("Forest to Sewers", SVRegion.sewers), + ConnectionData("Talk to Traveling Merchant", SVRegion.traveling_cart), + ConnectionData("Town to Sewers", SVRegion.sewers), + ConnectionData("Enter Mutant Bug Lair", SVRegion.mutant_bug_lair), + ConnectionData("Mountain to Railroad", SVRegion.railroad), + ConnectionData("Mountain to Tent", SVRegion.tent, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Mountain to Carpenter Shop", SVRegion.carpenter, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Sebastian's Room", SVRegion.sebastian_room), + ConnectionData("Mountain to Adventurer's Guild", SVRegion.adventurer_guild), + ConnectionData("Enter Quarry", SVRegion.quarry), + ConnectionData("Enter Quarry Mine Entrance", SVRegion.quarry_mine_entrance), + ConnectionData("Enter Quarry Mine", SVRegion.quarry_mine), + ConnectionData("Mountain to Town", SVRegion.town), + ConnectionData("Town to Community Center", SVRegion.community_center, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Access Crafts Room", SVRegion.crafts_room), + ConnectionData("Access Pantry", SVRegion.pantry), + ConnectionData("Access Fish Tank", SVRegion.fish_tank), + ConnectionData("Access Boiler Room", SVRegion.boiler_room), + ConnectionData("Access Bulletin Board", SVRegion.bulletin_board), + ConnectionData("Access Vault", SVRegion.vault), + ConnectionData("Town to Hospital", SVRegion.hospital, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Harvey's Room", SVRegion.harvey_room), + ConnectionData("Town to Pierre's General Store", SVRegion.pierre_store, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Sunroom", SVRegion.sunroom), + ConnectionData("Town to Clint's Blacksmith", SVRegion.blacksmith, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Saloon", SVRegion.saloon, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Play Journey of the Prairie King", SVRegion.jotpk_world_1), + ConnectionData("Reach JotPK World 2", SVRegion.jotpk_world_2), + ConnectionData("Reach JotPK World 3", SVRegion.jotpk_world_3), + ConnectionData("Play Junimo Kart", SVRegion.junimo_kart_1), + ConnectionData("Reach Junimo Kart 2", SVRegion.junimo_kart_2), + ConnectionData("Reach Junimo Kart 3", SVRegion.junimo_kart_3), + ConnectionData("Town to Sam's House", SVRegion.sam_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Haley's House", SVRegion.haley_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Mayor's Manor", SVRegion.mayor_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Alex's House", SVRegion.alex_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Trailer", SVRegion.trailer, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Museum", SVRegion.museum, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to JojaMart", SVRegion.jojamart, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Beach", SVRegion.beach), + ConnectionData("Enter Elliott's House", SVRegion.elliott_house), + ConnectionData("Beach to Willy's Fish Shop", SVRegion.fish_shop, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Tide Pools", SVRegion.tide_pools), + ConnectionData("Mountain to The Mines", SVRegion.mines, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Dig to The Mines - Floor 5", SVRegion.mines_floor_5), + ConnectionData("Dig to The Mines - Floor 10", SVRegion.mines_floor_10), + ConnectionData("Dig to The Mines - Floor 15", SVRegion.mines_floor_15), + ConnectionData("Dig to The Mines - Floor 20", SVRegion.mines_floor_20), + ConnectionData("Dig to The Mines - Floor 25", SVRegion.mines_floor_25), + ConnectionData("Dig to The Mines - Floor 30", SVRegion.mines_floor_30), + ConnectionData("Dig to The Mines - Floor 35", SVRegion.mines_floor_35), + ConnectionData("Dig to The Mines - Floor 40", SVRegion.mines_floor_40), + ConnectionData("Dig to The Mines - Floor 45", SVRegion.mines_floor_45), + ConnectionData("Dig to The Mines - Floor 50", SVRegion.mines_floor_50), + ConnectionData("Dig to The Mines - Floor 55", SVRegion.mines_floor_55), + ConnectionData("Dig to The Mines - Floor 60", SVRegion.mines_floor_60), + ConnectionData("Dig to The Mines - Floor 65", SVRegion.mines_floor_65), + ConnectionData("Dig to The Mines - Floor 70", SVRegion.mines_floor_70), + ConnectionData("Dig to The Mines - Floor 75", SVRegion.mines_floor_75), + ConnectionData("Dig to The Mines - Floor 80", SVRegion.mines_floor_80), + ConnectionData("Dig to The Mines - Floor 85", SVRegion.mines_floor_85), + ConnectionData("Dig to The Mines - Floor 90", SVRegion.mines_floor_90), + ConnectionData("Dig to The Mines - Floor 95", SVRegion.mines_floor_95), + ConnectionData("Dig to The Mines - Floor 100", SVRegion.mines_floor_100), + ConnectionData("Dig to The Mines - Floor 105", SVRegion.mines_floor_105), + ConnectionData("Dig to The Mines - Floor 110", SVRegion.mines_floor_110), + ConnectionData("Dig to The Mines - Floor 115", SVRegion.mines_floor_115), + ConnectionData("Dig to The Mines - Floor 120", SVRegion.mines_floor_120), + ConnectionData("Enter Skull Cavern Entrance", SVRegion.skull_cavern_entrance), + ConnectionData("Enter Skull Cavern", SVRegion.skull_cavern), + ConnectionData("Mine to Skull Cavern Floor 100", SVRegion.perfect_skull_cavern), + ConnectionData("Enter Witch Warp Cave", SVRegion.witch_warp_cave), + ConnectionData("Enter Witch's Swamp", SVRegion.witch_swamp), + ConnectionData("Enter Bathhouse Entrance", SVRegion.bathhouse_entrance), + ConnectionData("Enter Locker Room", SVRegion.locker_room), + ConnectionData("Enter Public Bath", SVRegion.public_bath), ] -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[Iterable[Region], Dict[str, str]]: - regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in stardew_valley_regions} +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[ + Iterable[Region], Dict[str, str]]: + regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in + stardew_valley_regions} entrances: Dict[str: Entrance] = {entrance.name: entrance for region in regions.values() for entrance in region.exits} @@ -272,9 +291,11 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: def randomize_connections(random: Random, world_options: StardewOptions) -> Tuple[List[ConnectionData], Dict[str, str]]: connections_to_randomize = [] if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town: - connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.PELICAN_TOWN in connection.flag] + connections_to_randomize = [connection for connection in mandatory_connections if + RandomizationFlag.PELICAN_TOWN in connection.flag] elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression: - connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.NON_PROGRESSION in connection.flag] + connections_to_randomize = [connection for connection in mandatory_connections if + RandomizationFlag.NON_PROGRESSION in connection.flag] random.shuffle(connections_to_randomize) destination_pool = list(connections_to_randomize) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index f9ba31cc..cbefe648 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,49 +1,51 @@ import itertools -from typing import Dict +from typing import Dict, List from BaseClasses import MultiWorld from worlds.generic import Rules as MultiWorldRules from . import options, locations from .bundles import Bundle +from .data.museum_data import all_museum_items, all_mineral_items, all_artifact_items, \ + dwarf_scrolls, skeleton_front, \ + skeleton_middle, skeleton_back, all_museum_items_by_name from .locations import LocationTags -from .logic import StardewLogic, _And, season_per_skill_level, tool_prices, week_days - -help_wanted_per_season = { - 1: "Spring", - 2: "Summer", - 3: "Fall", - 4: "Winter", - 5: "Year Two", - 6: "Year Two", - 7: "Year Two", - 8: "Year Two", - 9: "Year Two", - 10: "Year Two", -} +from .logic import StardewLogic, And, month_end_per_skill_level, tool_prices, week_days +from .options import StardewOptions -def set_rules(multi_world: MultiWorld, player: int, world_options: options.StardewOptions, logic: StardewLogic, +def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic, current_bundles: Dict[str, Bundle]): - summer = multi_world.get_location("Summer", player) all_location_names = list(location.name for location in multi_world.get_locations(player)) for floor in range(5, 120 + 5, 5): - MultiWorldRules.add_rule(multi_world.get_entrance(f"Dig to The Mines - Floor {floor}", player), + MultiWorldRules.set_rule(multi_world.get_entrance(f"Dig to The Mines - Floor {floor}", player), logic.can_mine_to_floor(floor).simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Quarry", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Tide Pools", player), + logic.received("Beach Bridge").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Quarry", player), logic.received("Bridge Repair").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Secret Woods", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Secret Woods", player), logic.has_tool("Axe", "Iron").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Take Bus to Desert", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Forest to Sewers", player), + logic.has_rusty_key().simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Town to Sewers", player), + logic.has_rusty_key().simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Take Bus to Desert", player), logic.received("Bus Repair").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Skull Cavern", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Skull Cavern", player), logic.received("Skull Key").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Mine to Skull Cavern Floor 100", player), + logic.can_mine_perfectly_in_the_skull_cavern().simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Use Desert Obelisk", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Use Desert Obelisk", player), logic.received("Desert Obelisk").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Use Island Obelisk", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Use Island Obelisk", player), logic.received("Island Obelisk").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Talk to Traveling Merchant", player), + logic.has_traveling_merchant()) + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Greenhouse", player), + logic.received("Greenhouse")) # Those checks do not exist if ToolProgression is vanilla if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: @@ -68,43 +70,46 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla: for i in range(1, 11): MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Farming", player), - (logic.received(season_per_skill_level["Farming", i])).simplify()) + (logic.received("Month End", month_end_per_skill_level["Farming", i])).simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Fishing", player), (logic.can_get_fishing_xp() & - logic.received(season_per_skill_level["Fishing", i])).simplify()) + logic.received("Month End", month_end_per_skill_level["Fishing", i])).simplify()) MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), - logic.received(season_per_skill_level["Foraging", i]).simplify()) + logic.received("Month End", month_end_per_skill_level["Foraging", i]).simplify()) if i >= 6: MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), logic.has_tool("Axe", "Iron").simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Mining", player), - logic.received(season_per_skill_level["Mining", i]).simplify()) + logic.received("Month End", month_end_per_skill_level["Mining", i]).simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Combat", player), - (logic.received(season_per_skill_level["Combat", i]) & + (logic.received("Month End", month_end_per_skill_level["Combat", i]) & logic.has_any_weapon()).simplify()) # Bundles for bundle in current_bundles.values(): - MultiWorldRules.set_rule(multi_world.get_location(bundle.get_name_with_bundle(), player), - logic.can_complete_bundle(bundle.requirements, bundle.number_required).simplify()) + location = multi_world.get_location(bundle.get_name_with_bundle(), player) + rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required) + simplified_rules = rules.simplify() + MultiWorldRules.set_rule(location, simplified_rules) MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle + in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) # Buildings if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: @@ -122,7 +127,7 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard for i in range(1, desired_number_help_wanted + 1): prefix = "Help Wanted:" delivery = "Item Delivery" - rule = logic.received(help_wanted_per_season[min(5, i)]) + rule = logic.received("Month End", i - 1) fishing_rule = rule & logic.can_fish() slay_rule = rule & logic.has_any_weapon() for j in range(i, i + 4): @@ -136,6 +141,21 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i}", player), slay_rule.simplify()) + set_fishsanity_rules(all_location_names, logic, multi_world, player) + set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options) + set_friendsanity_rules(all_location_names, logic, multi_world, player) + set_backpack_rules(logic, multi_world, player, world_options) + + MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), + logic.has("Sweet Gem Berry").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), + logic.has("Prismatic Shard").simplify()) + + set_traveling_merchant_rules(logic, multi_world, player) + set_arcade_machine_rules(logic, multi_world, player, world_options) + + +def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): fish_prefix = "Fishsanity: " for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]: if fish_location.name in all_location_names: @@ -143,27 +163,78 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player), logic.has(fish_name).simplify()) - if world_options[options.BuildingProgression] == options.BuildingProgression.option_progressive_early_shipping_bin: - summer.access_rule = summer.access_rule & logic.received("Shipping Bin") - # Backpacks +def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int, + world_options: StardewOptions): + museum_prefix = "Museumsanity: " + if world_options[options.Museumsanity] == options.Museumsanity.option_milestones: + for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player) + elif world_options[options.Museumsanity] != options.Museumsanity.option_none: + set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player) + + +def set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player): + all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS], + key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True) + counter = 0 + number_donations = len(all_donations) + for museum_location in all_donations: + if museum_location.name in all_location_names: + donation_name = museum_location.name[len(museum_prefix):] + required_detectors = counter * 5 // number_donations + rule = logic.has(donation_name) & logic.received("Traveling Merchant Metal Detector", required_detectors) + MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player), + rule.simplify()) + counter += 1 + + +def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, museum_milestone, museum_prefix: str, + player: int): + milestone_name = museum_milestone.name[len(museum_prefix):] + donations_suffix = " Donations" + minerals_suffix = " Minerals" + artifacts_suffix = " Artifacts" + metal_detector = "Traveling Merchant Metal Detector" + rule = None + if milestone_name.endswith(donations_suffix): + rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items) + elif milestone_name.endswith(minerals_suffix): + rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_mineral_items) + elif milestone_name.endswith(artifacts_suffix): + rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_artifact_items) + elif milestone_name == "Dwarf Scrolls": + rule = logic.has([item.name for item in dwarf_scrolls]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Front": + rule = logic.has([item.name for item in skeleton_front]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Middle": + rule = logic.has([item.name for item in skeleton_middle]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Back": + rule = logic.has([item.name for item in skeleton_back]) & logic.received(metal_detector, 4) + elif milestone_name == "Ancient Seed": + rule = logic.has("Ancient Seed") & logic.received(metal_detector, 4) + if rule is None: + return + MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify()) + + +def get_museum_item_count_rule(logic, suffix, milestone_name, accepted_items): + metal_detector = "Traveling Merchant Metal Detector" + num = int(milestone_name[:milestone_name.index(suffix)]) + required_detectors = (num - 1) * 5 // len(accepted_items) + rule = logic.has([item.name for item in accepted_items], num) & logic.received(metal_detector, required_detectors) + return rule + + +def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: - MultiWorldRules.add_rule(multi_world.get_location("Large Pack", player), + MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player), logic.can_spend_money(2000).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Deluxe Pack", player), - logic.can_spend_money(10000).simplify()) + MultiWorldRules.set_rule(multi_world.get_location("Deluxe Pack", player), + (logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify()) - if world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: - summer.access_rule = summer.access_rule & logic.received("Progressive Backpack") - MultiWorldRules.add_rule(multi_world.get_location("Winter", player), - logic.received("Progressive Backpack", 2).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), - logic.has("Sweet Gem Berry").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), - logic.has("Prismatic Shard").simplify()) - - # Traveling Merchant +def set_traveling_merchant_rules(logic: StardewLogic, multi_world: MultiWorld, player: int): for day in week_days: item_for_day = f"Traveling Merchant: {day}" for i in range(1, 4): @@ -171,6 +242,8 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(location_name, player), logic.received(item_for_day)) + +def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: MultiWorldRules.add_rule(multi_world.get_entrance("Play Junimo Kart", player), (logic.received("Skull Key") & logic.has("Junimo Kart Small Buff")).simplify()) @@ -188,3 +261,18 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard logic.has("JotPK Big Buff").simplify()) MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player), logic.has("JotPK Max Buff").simplify()) + + +def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): + 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)] + parts = friend_location_trimmed.split(" ") + friend_name = parts[0] + num_hearts = int(parts[1]) + MultiWorldRules.set_rule(multi_world.get_location(friend_location.name, player), + logic.can_earn_relationship(friend_name, num_hearts).simplify()) diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py new file mode 100644 index 00000000..7f568f9f --- /dev/null +++ b/worlds/stardew_valley/stardew_rule.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Dict, List, Union, FrozenSet + +from BaseClasses import CollectionState, ItemClassification +from .items import item_table + +MISSING_ITEM = "THIS ITEM IS MISSING" + + +class StardewRule: + def __call__(self, state: CollectionState) -> bool: + raise NotImplementedError + + def __or__(self, other) -> StardewRule: + if isinstance(other, Or): + return Or(self, *other.rules) + + return Or(self, other) + + def __and__(self, other) -> StardewRule: + if isinstance(other, And): + return And(other.rules.union({self})) + + return And(self, other) + + def get_difficulty(self): + raise NotImplementedError + + def simplify(self) -> StardewRule: + return self + + +class True_(StardewRule): # noqa + + def __new__(cls, _cache=[]): # noqa + # Only one single instance will be ever created. + if not _cache: + _cache.append(super(True_, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return True + + def __or__(self, other) -> StardewRule: + return self + + def __and__(self, other) -> StardewRule: + return other + + def __repr__(self): + return "True" + + def get_difficulty(self): + return 0 + + +class False_(StardewRule): # noqa + + def __new__(cls, _cache=[]): # noqa + # Only one single instance will be ever created. + if not _cache: + _cache.append(super(False_, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return False + + def __or__(self, other) -> StardewRule: + return other + + def __and__(self, other) -> StardewRule: + return self + + def __repr__(self): + return "False" + + def get_difficulty(self): + return 999999999 + + +class Or(StardewRule): + rules: FrozenSet[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a Or conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, Or): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return any(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' | '.join(repr(rule) for rule in self.rules)})" + + def __or__(self, other): + if isinstance(other, True_): + return other + if isinstance(other, False_): + return self + if isinstance(other, Or): + return Or(self.rules.union(other.rules)) + + return Or(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return min(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, True_) for rule in self.rules): + return True_() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not False_()} + + if not simplified_rules: + return False_() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return Or(simplified_rules) + + +class And(StardewRule): + rules: FrozenSet[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a And conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, And): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return all(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' & '.join(repr(rule) for rule in self.rules)})" + + def __and__(self, other): + if isinstance(other, True_): + return self + if isinstance(other, False_): + return other + if isinstance(other, And): + return And(self.rules.union(other.rules)) + + return And(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return max(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, False_) for rule in self.rules): + return False_() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not True_()} + + if not simplified_rules: + return True_() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return And(simplified_rules) + + +class Count(StardewRule): + count: int + rules: List[StardewRule] + + def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = [] + if isinstance(rule, Iterable): + rules_list.extend(rule) + else: + rules_list.append(rule) + + if rules is not None: + rules_list.extend(rules) + + assert rules_list, "Can't create a Count conditions without rules" + assert len(rules_list) >= count, "Count need at least as many rules at the count" + + self.rules = rules_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for r in self.rules: + if r(state): + c += 1 + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {repr(self.rules)}" + + def get_difficulty(self): + rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty()) + easiest_n_rules = rules_sorted_by_difficulty[0:self.count] + return max(rule.get_difficulty() for rule in easiest_n_rules) + + def simplify(self): + return Count(self.count, [rule.simplify() for rule in self.rules]) + + +class TotalReceived(StardewRule): + count: int + items: Iterable[str] + player: int + + def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): + items_list = [] + if isinstance(items, Iterable): + items_list.extend(items) + else: + items_list.append(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, \ + "Item has to be progression to be used in logic" + + self.player = player + self.items = items_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for item in self.items: + c += state.count(item, self.player) + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {self.items}" + + def get_difficulty(self): + return self.count + + +@dataclass(frozen=True) +class Received(StardewRule): + item: str + player: int + count: int + + def __post_init__(self): + assert item_table[self.item].classification & ItemClassification.progression, \ + "Item has to be progression to be used in logic" + + def __call__(self, state: CollectionState) -> bool: + return state.has(self.item, self.player, self.count) + + def __repr__(self): + if self.count == 1: + return f"Received {self.item}" + return f"Received {self.count} {self.item}" + + def get_difficulty(self): + if self.item == "Spring": + return 0 + if self.item == "Summer": + return 1 + if self.item == "Fall": + return 2 + if self.item == "Winter": + return 3 + return self.count + + +@dataclass(frozen=True) +class Reach(StardewRule): + spot: str + resolution_hint: str + player: int + + def __call__(self, state: CollectionState) -> bool: + return state.can_reach(self.spot, self.resolution_hint, self.player) + + def __repr__(self): + return f"Reach {self.resolution_hint} {self.spot}" + + def get_difficulty(self): + return 1 + + +@dataclass(frozen=True) +class Has(StardewRule): + item: str + # For sure there is a better way than just passing all the rules everytime + other_rules: Dict[str, StardewRule] + + def __call__(self, state: CollectionState) -> bool: + if isinstance(self.item, str): + return self.other_rules[self.item](state) + + def __repr__(self): + if not self.item in self.other_rules: + return f"Has {self.item} -> {MISSING_ITEM}" + return f"Has {self.item} -> {repr(self.other_rules[self.item])}" + + def get_difficulty(self): + return self.other_rules[self.item].get_difficulty() + 1 + + def __hash__(self): + return hash(self.item) + + def simplify(self) -> StardewRule: + return self.other_rules[self.item].simplify() diff --git a/worlds/stardew_valley/test/TestAllLogic.py b/worlds/stardew_valley/test/TestAllLogic.py deleted file mode 100644 index de1c0049..00000000 --- a/worlds/stardew_valley/test/TestAllLogic.py +++ /dev/null @@ -1,53 +0,0 @@ -import unittest - -from test.general import setup_solo_multiworld -from .. import StardewValleyWorld -from ..bundle_data import all_bundle_items_except_money -from ..logic import MISSING_ITEM, _False - - -class TestAllLogicalItem(unittest.TestCase): - multi_world = setup_solo_multiworld(StardewValleyWorld) - world = multi_world.worlds[1] - logic = world.logic - - def setUp(self) -> None: - for item in self.multi_world.get_items(): - self.multi_world.state.collect(item, event=True) - - def test_given_bundle_item_then_is_available_in_logic(self): - for bundle_item in all_bundle_items_except_money: - with self.subTest(bundle_item=bundle_item): - assert bundle_item.item.name in self.logic.item_rules - - def test_given_item_rule_then_can_be_resolved(self): - for item in self.logic.item_rules.keys(): - with self.subTest(item=item): - rule = self.logic.item_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_building_rule_then_can_be_resolved(self): - for item in self.logic.building_rules.keys(): - with self.subTest(item=item): - rule = self.logic.building_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_quest_rule_then_can_be_resolved(self): - for item in self.logic.quest_rules.keys(): - with self.subTest(item=item): - rule = self.logic.quest_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_location_rule_then_can_be_resolved(self): - for location in self.multi_world.get_locations(1): - with self.subTest(location=location): - rule = location.access_rule - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index 58017377..1e75bd9b 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,6 +1,6 @@ import unittest -from ..bundle_data import all_bundle_items +from ..data.bundle_data import all_bundle_items class TestBundles(unittest.TestCase): diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 840052d3..9bf4fbcb 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,6 +1,7 @@ from BaseClasses import ItemClassification from . import SVTestBase from .. import locations, items, location_table, options +from ..data.villagers_data import all_villagers_by_name from ..items import items_by_group, Group from ..locations import LocationTags @@ -113,15 +114,157 @@ class TestLocationGeneration(SVTestBase): class TestLocationAndItemCount(SVTestBase): options = { + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.SeedShuffle.internal_name: options.SeedShuffle.option_shuffled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_vanilla, options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_vanilla, options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.HelpWantedLocations.internal_name: 0, + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Friendsanity.internal_name: options.Museumsanity.option_none, options.NumberOfPlayerBuffs.internal_name: 12, } def test_minimal_location_maximal_items_still_valid(self): assert len(self.multiworld.get_locations()) >= len(self.multiworld.get_items()) + + +class TestFriendsanityNone(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_none, + } + + def test_no_friendsanity_items(self): + for item in self.multiworld.get_items(): + assert not item.name.endswith(": 1 <3") + + def test_no_friendsanity_locations(self): + for location in self.multiworld.get_locations(): + assert not location.name.startswith("Friendsanity") + + +class TestFriendsanityBachelors(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_bachelors, + } + bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", + "Maru"} + + def test_friendsanity_only_bachelor_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in self.bachelors + + def test_friendsanity_only_bachelor_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + 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] + assert name in self.bachelors + assert int(hearts) <= 8 + + +class TestFriendsanityStartingNpcs(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs, + } + excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"} + + def test_friendsanity_only_starting_npcs_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name not in self.excluded_npcs + + def test_friendsanity_only_starting_npcs_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + 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] + assert name not in self.excluded_npcs + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 8 + else: + assert int(hearts) <= 10 + + +class TestFriendsanityAllNpcs(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_all, + } + + def test_friendsanity_all_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in all_villagers_by_name or villager_name == "Pet" + + def test_friendsanity_all_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + 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] + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 8 + else: + assert int(hearts) <= 10 + + +class TestFriendsanityAllNpcsWithMarriage(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + } + + def test_friendsanity_all_with_marriage_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in all_villagers_by_name or villager_name == "Pet" + + def test_friendsanity_all_with_marriage_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + 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] + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 14 + else: + assert int(hearts) <= 10 diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 98d251eb..8c318184 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -1,8 +1,11 @@ +import itertools +import math import unittest -from BaseClasses import MultiWorld -from .. import StardewValleyWorld -from ..items import item_table + +from BaseClasses import ItemClassification, MultiWorld +from .. import ItemData, StardewValleyWorld +from ..items import Group, ResourcePackData, item_table class TestItems(unittest.TestCase): @@ -24,3 +27,70 @@ class TestItems(unittest.TestCase): assert item_with_lowest_id.code >= 717000 assert item_with_highest_id.code < 727000 + + +class TestResourcePacks: + def test_can_transform_resource_pack_data_into_idem_data(self): + resource_pack = ResourcePackData("item name", 1, 1, ItemClassification.filler, frozenset()) + + items = resource_pack.as_item_data(itertools.count()) + + assert ItemData(0, "Resource Pack: 1 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert ItemData(1, "Resource Pack: 2 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert len(items) == 2 + + def test_when_scale_quantity_then_generate_a_possible_quantity_from_minimal_scaling_to_double(self): + resource_pack = ResourcePackData("item name", default_amount=4, scaling_factor=2) + + quantities = resource_pack.scale_quantity.items() + + assert (50, 2) in quantities + assert (100, 4) in quantities + assert (150, 6) in quantities + assert (200, 8) in quantities + assert len(quantities) == (4 / 2) * 2 + + def test_given_scaling_not_multiple_of_default_amount_when_scale_quantity_then_double_is_added_at_200_scaling(self): + resource_pack = ResourcePackData("item name", default_amount=5, scaling_factor=3) + + quantities = resource_pack.scale_quantity.items() + + assert (40, 2) in quantities + assert (100, 5) in quantities + assert (160, 8) in quantities + assert (200, 10) in quantities + assert len(quantities) == math.ceil(5 / 3) * 2 + + def test_given_large_default_amount_multiple_of_scaling_factor_when_scale_quantity_then_scaled_amount_multiple(self): + resource_pack = ResourcePackData("item name", default_amount=500, scaling_factor=50) + + quantities = resource_pack.scale_quantity.items() + + assert (10, 50) in quantities + assert (20, 100) in quantities + assert (30, 150) in quantities + assert (40, 200) in quantities + assert (50, 250) in quantities + assert (60, 300) in quantities + assert (70, 350) in quantities + assert (80, 400) in quantities + assert (90, 450) in quantities + assert (100, 500) in quantities + assert (110, 550) in quantities + assert (120, 600) in quantities + assert (130, 650) in quantities + assert (140, 700) in quantities + assert (150, 750) in quantities + assert (160, 800) in quantities + assert (170, 850) in quantities + assert (180, 900) in quantities + assert (190, 950) in quantities + assert (200, 1000) in quantities + assert len(quantities) == math.ceil(500 / 50) * 2 + + def test_given_smallest_multiplier_possible_when_generate_resource_pack_name_then_quantity_is_not_0(self): + resource_pack = ResourcePackData("item name", default_amount=10, scaling_factor=5) + + name = resource_pack.create_name_from_multiplier(1) + + assert name == "Resource Pack: 5 item name" diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 83129a56..beb61023 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,293 +1,57 @@ -from . import SVTestBase -from .. import options +import pytest + +from test.general import setup_solo_multiworld +from .. import StardewValleyWorld, StardewLocation +from ..data.bundle_data import BundleItem, all_bundle_items_except_money +from ..stardew_rule import MISSING_ITEM, False_ + +multi_world = setup_solo_multiworld(StardewValleyWorld) +world = multi_world.worlds[1] +logic = world.logic -class TestProgressiveToolsLogic(SVTestBase): - options = { - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - } - - def test_sturgeon(self): - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - fishing_rod = self.get_item_by_name("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, event=True) - self.multiworld.state.collect(fishing_rod, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - fishing_level = self.get_item_by_name("Fishing Level") - self.multiworld.state.collect(fishing_level, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - assert self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.remove(summer) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - winter = self.get_item_by_name("Winter") - self.multiworld.state.collect(winter, event=True) - assert self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.remove(fishing_rod) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - def test_old_master_cannoli(self): - self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) - - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - fall = self.get_item_by_name("Fall") - self.multiworld.state.collect(fall, event=True) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - tuesday = self.get_item_by_name("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=True) - assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - self.remove(fall) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - self.remove(tuesday) - - green_house = self.get_item_by_name("Greenhouse") - self.multiworld.state.collect(green_house, event=True) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - friday = self.get_item_by_name("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=True) - assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - self.remove(green_house) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - self.remove(friday) +def collect_all(mw): + for item in mw.get_items(): + mw.state.collect(item, event=True) -class TestBundlesLogic(SVTestBase): - options = { - } - - def test_vault_2500g_bundle(self): - assert not self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) +collect_all(multi_world) -class TestBuildingLogic(SVTestBase): - options = { - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin - } - - def test_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) - - def test_big_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Fall"), event=True) - assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - def test_deluxe_big_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - def test_big_shed_blueprint(self): - assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) - assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Progressive Shed"), event=True) - assert self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" +@pytest.mark.parametrize("bundle_item", all_bundle_items_except_money, + ids=[i.item.name for i in all_bundle_items_except_money]) +def test_given_bundle_item_then_is_available_in_logic(bundle_item: BundleItem): + assert bundle_item.item.name in logic.item_rules -class TestArcadeMachinesLogic(SVTestBase): - options = { - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - } +@pytest.mark.parametrize("item", logic.item_rules.keys(), ids=logic.item_rules.keys()) +def test_given_item_rule_then_can_be_resolved(item: str): + rule = logic.item_rules[item] - def test_prairie_king(self): - assert not self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - - boots = self.get_item_by_name("JotPK: Progressive Boots") - gun = self.get_item_by_name("JotPK: Progressive Gun") - ammo = self.get_item_by_name("JotPK: Progressive Ammo") - life = self.get_item_by_name("JotPK: Extra Life") - drop = self.get_item_by_name("JotPK: Increased Drop Rate") - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.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) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.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) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.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) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.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) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert self.world.logic.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) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" -class TestWeaponsLogic(SVTestBase): - options = { - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - } +@pytest.mark.parametrize("item", logic.building_rules.keys(), ids=logic.building_rules.keys()) +def test_given_building_rule_then_can_be_resolved(item: str): + rule = logic.building_rules[item] - def test_mine(self): - self.collect(self.get_item_by_name("Adventurer's Guild")) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.collect([self.get_item_by_name("Combat Level")] * 10) - self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.get_item_by_name("Bus Repair"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Skull Key"), event=True) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" - self.GiveItemAndCheckReachableMine("Rusty Sword", 1) - self.GiveItemAndCheckReachableMine("Wooden Blade", 1) - self.GiveItemAndCheckReachableMine("Elf Blade", 1) - self.GiveItemAndCheckReachableMine("Silver Saber", 2) - self.GiveItemAndCheckReachableMine("Crystal Dagger", 2) +@pytest.mark.parametrize("item", logic.quest_rules.keys(), ids=logic.quest_rules.keys()) +def test_given_quest_rule_then_can_be_resolved(item: str): + rule = logic.quest_rules[item] - self.GiveItemAndCheckReachableMine("Claymore", 3) - self.GiveItemAndCheckReachableMine("Obsidian Edge", 3) - self.GiveItemAndCheckReachableMine("Bone Sword", 3) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" - self.GiveItemAndCheckReachableMine("The Slammer", 4) - self.GiveItemAndCheckReachableMine("Lava Katana", 4) - self.GiveItemAndCheckReachableMine("Galaxy Sword", 5) - self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5) - self.GiveItemAndCheckReachableMine("Galaxy Dagger", 5) +@pytest.mark.parametrize("location", multi_world.get_locations(1), + ids=[loc.name for loc in multi_world.get_locations(1)]) +def test_given_location_rule_then_can_be_resolved(location: StardewLocation): + rule = location.access_rule - 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) - if reachable_level > 0: - assert self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) - - if reachable_level > 1: - assert self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) - - if reachable_level > 2: - assert self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) - - if reachable_level > 3: - assert self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) - - if reachable_level > 4: - assert self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) - - self.remove(item) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 1a3d5a1d..c37f5782 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,52 +1,65 @@ -import unittest - -from .. import _True -from ..logic import _Received, _Has, _False, _And, _Or +from .. import True_ +from ..logic import Received, Has, False_, And, Or, StardewLogic +from ..options import default_options, StardewOptions -class TestLogicSimplification(unittest.TestCase): - def test_simplify_true_in_and(self): - rules = { - "Wood": _True(), - "Rock": _True(), - } - summer = _Received("Summer", 0, 1) - assert (_Has("Wood", rules) & summer & _Has("Rock", rules)).simplify() == summer +def test_simplify_true_in_and(): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer - def test_simplify_false_in_or(self): - rules = { - "Wood": _False(), - "Rock": _False(), - } - summer = _Received("Summer", 0, 1) - assert (_Has("Wood", rules) | summer | _Has("Rock", rules)).simplify() == summer - def test_simplify_and_in_and(self): - rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _And(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) - assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), - _Received("Spring", 0, 1)) +def test_simplify_false_in_or(): + rules = { + "Wood": False_(), + "Rock": False_(), + } + summer = Received("Summer", 0, 1) + assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer - def test_simplify_duplicated_and(self): - rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) - assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) - def test_simplify_or_in_or(self): - rule = _Or(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _Or(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) - assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), - _Received("Spring", 0, 1)) +def test_simplify_and_in_and(): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Winter', 0, 1), Received('Spring', 0, 1))) + assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1)) - def test_simplify_duplicated_or(self): - rule = _And(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) - assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) - def test_simplify_true_in_or(self): - rule = _Or(_True(), _Received("Summer", 0, 1)) - assert rule.simplify() == _True() +def test_simplify_duplicated_and(): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) + assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1)) - def test_simplify_false_in_and(self): - rule = _And(_False(), _Received("Summer", 0, 1)) - assert rule.simplify() == _False() + +def test_simplify_or_in_or(): + rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) + assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1)) + + +def test_simplify_duplicated_or(): + rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) + assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1)) + + +def test_simplify_true_in_or(): + rule = Or(True_(), Received('Summer', 0, 1)) + assert rule.simplify() == True_() + + +def test_simplify_false_in_and(): + rule = And(False_(), Received('Summer', 0, 1)) + assert rule.simplify() == False_() + + +def test_simplify_coffee(): + logic = StardewLogic(1, StardewOptions(default_options)) + + simplified_coffee = logic.has("Coffee").simplify() + + assert simplified_coffee == True_() diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 063d9c2b..9907b45d 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,8 +1,159 @@ -from worlds.stardew_valley.test import SVTestBase +import itertools + +import pytest + +from BaseClasses import ItemClassification, MultiWorld +from Options import SpecialRange +from . import setup_solo_multiworld +from .. import StardewItem, options +from ..options import StardewOption, stardew_valley_option_classes + +SEASONS = {"Spring", "Summer", "Fall", "Winter"} +TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} -class TestMasterAnglerVanillaTools(SVTestBase): - options = { - "goal": "master_angler", - "tool_progression": "vanilla", - } +def basic_checks(multi_world: MultiWorld): + assert StardewItem("Victory", ItemClassification.progression, None, 1) in multi_world.get_items() + assert_can_win(multi_world) + assert len(multi_world.itempool) == len( + [location for location in multi_world.get_locations() if not location.event]) + + +def assert_can_win(multi_world: MultiWorld): + for item in multi_world.get_items(): + multi_world.state.collect(item) + + assert multi_world.find_item("Victory", 1).can_reach(multi_world.state) + + +@pytest.mark.parametrize("option, value", [(option, value) + for option in stardew_valley_option_classes + if issubclass(option, SpecialRange) + for value in option.special_range_names]) +def test_given_special_range_when_generate_then_basic_checks(option: (SpecialRange, StardewOption), value): + multi_world = setup_solo_multiworld({option.internal_name: option.special_range_names[value]}) + + basic_checks(multi_world) + + +@pytest.mark.parametrize("option, value", [(option, value) + for option in stardew_valley_option_classes + if option.options + for value in option.options]) +def test_given_choice_when_generate_then_basic_checks(option, value): + multi_world = setup_solo_multiworld({option.internal_name: option.options[value]}) + + basic_checks(multi_world) + + +@pytest.mark.parametrize("option_combination", + [{options.Goal.internal_name: options.Goal.option_master_angler, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}], + ids=["Master Angler + Vanilla tools"]) +def test_given_option_combination_when_generate_then_basic_checks(option_combination): + multi_world = setup_solo_multiworld(option_combination) + + basic_checks(multi_world) + + +class TestGoal: + @pytest.mark.parametrize("goal,location", [("community_center", "Complete Community Center"), + ("grandpa_evaluation", "Succeed Grandpa's Evaluation"), + ("bottom_of_the_mines", "Reach the Bottom of The Mines"), + ("cryptic_note", "Complete Quest Cryptic Note"), + ("master_angler", "Catch Every Fish")]) + def test_given_goal_when_generate_then_victory_is_in_correct_location(self, goal, location): + multi_world = setup_solo_multiworld({options.Goal.internal_name: options.Goal.options[goal]}) + victory = multi_world.find_item("Victory", 1) + + assert victory.name == location + + +class TestSeasonRandomization: + def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.option_disabled}) + + precollected_items = {item.name for item in multi_world.precollected_items[1]} + assert all([season in precollected_items for season in SEASONS]) + + @pytest.mark.parametrize("value", [value for value in options.SeasonRandomization.options if "randomized" in value]) + def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self, value): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.options[value]}) + + precollected_items = {item.name for item in multi_world.precollected_items[1]} + items = {item.name for item in multi_world.get_items()} | precollected_items + assert all([season in items for season in SEASONS]) + assert len(SEASONS.intersection(precollected_items)) == 1 + + def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.option_progressive}) + + items = [item.name for item in multi_world.get_items()] + assert items.count("Progressive Season") == 3 + + +class TestBackpackProgression: + def test_given_vanilla_when_generate_then_no_backpack_in_pool(self): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.option_vanilla}) + + assert "Progressive Backpack" not in {item.name for item in multi_world.get_items()} + + @pytest.mark.parametrize("value", + [value for value in options.BackpackProgression.options if "progressive" in value]) + def test_given_progressive_when_generate_then_progressive_backpack_is_in_pool_two_times(self, value): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.options[value]}) + + items = [item.name for item in multi_world.get_items()] + assert items.count("Progressive Backpack") == 2 + + @pytest.mark.parametrize("value", + [value for value in options.BackpackProgression.options if "progressive" in value]) + def test_given_progressive_when_generate_then_backpack_upgrades_are_locations(self, value): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.options[value]}) + + locations = {locations.name for locations in multi_world.get_locations(1)} + assert "Large Pack" in locations + assert "Deluxe Pack" in locations + + def test_given_early_progressive_when_generate_then_progressive_backpack_is_in_early_pool(self): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.option_early_progressive}) + + assert "Progressive Backpack" in multi_world.early_items[1] + + +class TestToolProgression: + def test_given_vanilla_when_generate_then_no_tool_in_pool(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_vanilla}) + + items = {item.name for item in multi_world.get_items()} + for tool in TOOLS: + assert tool not in items + + def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_progressive}) + + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + assert items.count("Progressive " + tool) == 4 + + def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_progressive}) + + locations = {locations.name for locations in multi_world.get_locations(1)} + for material, tool in itertools.product(["Copper", "Iron", "Gold", "Iridium"], + ["Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can"]): + assert f"{material} {tool} Upgrade" in locations + assert "Purchase Training Rod" in locations + assert "Bamboo Pole Cutscene" in locations + assert "Purchase Fiberglass Rod" in locations + assert "Purchase Iridium Rod" in locations diff --git a/worlds/stardew_valley/test/TestResourcePack.py b/worlds/stardew_valley/test/TestResourcePack.py deleted file mode 100644 index d25505bb..00000000 --- a/worlds/stardew_valley/test/TestResourcePack.py +++ /dev/null @@ -1,76 +0,0 @@ -import itertools -import math -import unittest - -from BaseClasses import ItemClassification -from .. import ItemData -from ..items import Group, ResourcePackData - - -class TestResourcePack(unittest.TestCase): - - def test_can_transform_resource_pack_data_into_idem_data(self): - resource_pack = ResourcePackData("item name", 1, 1, ItemClassification.filler, frozenset()) - - items = resource_pack.as_item_data(itertools.count()) - - assert ItemData(0, "Resource Pack: 1 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items - assert ItemData(1, "Resource Pack: 2 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items - assert len(items) == 2 - - def test_when_scale_quantity_then_generate_a_possible_quantity_from_minimal_scaling_to_double(self): - resource_pack = ResourcePackData("item name", default_amount=4, scaling_factor=2) - - quantities = resource_pack.scale_quantity.items() - - assert (50, 2) in quantities - assert (100, 4) in quantities - assert (150, 6) in quantities - assert (200, 8) in quantities - assert len(quantities) == (4 / 2) * 2 - - def test_given_scaling_not_multiple_of_default_amount_when_scale_quantity_then_double_is_added_at_200_scaling(self): - resource_pack = ResourcePackData("item name", default_amount=5, scaling_factor=3) - - quantities = resource_pack.scale_quantity.items() - - assert (40, 2) in quantities - assert (100, 5) in quantities - assert (160, 8) in quantities - assert (200, 10) in quantities - assert len(quantities) == math.ceil(5 / 3) * 2 - - def test_given_large_default_amount_multiple_of_scaling_factor_when_scale_quantity_then_scaled_amount_multiple( - self): - resource_pack = ResourcePackData("item name", default_amount=500, scaling_factor=50) - - quantities = resource_pack.scale_quantity.items() - - assert (10, 50) in quantities - assert (20, 100) in quantities - assert (30, 150) in quantities - assert (40, 200) in quantities - assert (50, 250) in quantities - assert (60, 300) in quantities - assert (70, 350) in quantities - assert (80, 400) in quantities - assert (90, 450) in quantities - assert (100, 500) in quantities - assert (110, 550) in quantities - assert (120, 600) in quantities - assert (130, 650) in quantities - assert (140, 700) in quantities - assert (150, 750) in quantities - assert (160, 800) in quantities - assert (170, 850) in quantities - assert (180, 900) in quantities - assert (190, 950) in quantities - assert (200, 1000) in quantities - assert len(quantities) == math.ceil(500 / 50) * 2 - - def test_given_smallest_multiplier_possible_when_generate_resource_pack_name_then_quantity_is_not_0(self): - resource_pack = ResourcePackData("item name", default_amount=10, scaling_factor=5) - - name = resource_pack.create_name_from_multiplier(1) - - assert name == "Resource Pack: 5 item name" diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py new file mode 100644 index 00000000..19638012 --- /dev/null +++ b/worlds/stardew_valley/test/TestRules.py @@ -0,0 +1,309 @@ +from collections import Counter + +from . import SVTestBase +from .. import options + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + } + + def setUp(self): + super().setUp() + self.multiworld.state.prog_items = Counter() + + def test_sturgeon(self): + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + summer = self.world.create_item("Summer") + self.multiworld.state.collect(summer, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_rod = self.world.create_item("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod, event=True) + self.multiworld.state.collect(fishing_rod, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_level = self.world.create_item("Fishing Level") + self.multiworld.state.collect(fishing_level, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(summer) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + winter = self.world.create_item("Winter") + self.multiworld.state.collect(winter, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(fishing_rod) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True) + self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True) + self.multiworld.state.collect(self.world.create_item("Summer"), event=True) + + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + fall = self.world.create_item("Fall") + self.multiworld.state.collect(fall, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + tuesday = self.world.create_item("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday, event=True) + assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + self.remove(fall) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(tuesday) + + green_house = self.world.create_item("Greenhouse") + self.multiworld.state.collect(green_house, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + friday = self.world.create_item("Traveling Merchant: Friday") + self.multiworld.state.collect(friday, event=True) + assert self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state) + + self.remove(green_house) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(friday) + + +class TestBundlesLogic(SVTestBase): + options = { + } + + def test_vault_2500g_bundle(self): + assert self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) + + +class TestBuildingLogic(SVTestBase): + options = { + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin + } + + def test_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + def test_big_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(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("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(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=True) + assert self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + def test_deluxe_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) + assert self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + def test_big_shed_blueprint(self): + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(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("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(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) + assert self.world.logic.can_reach_location("Big Shed Blueprint")(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): + assert not self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.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) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.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) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.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) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.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) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.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) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert self.world.logic.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 = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.collect(self.world.create_item("Adventurer's Guild")) + 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("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("Rusty Sword", 1) + self.GiveItemAndCheckReachableMine("Wooden Blade", 1) + self.GiveItemAndCheckReachableMine("Elf Blade", 1) + + self.GiveItemAndCheckReachableMine("Silver Saber", 2) + self.GiveItemAndCheckReachableMine("Crystal Dagger", 2) + + self.GiveItemAndCheckReachableMine("Claymore", 3) + self.GiveItemAndCheckReachableMine("Obsidian Edge", 3) + self.GiveItemAndCheckReachableMine("Bone Sword", 3) + + self.GiveItemAndCheckReachableMine("The Slammer", 4) + self.GiveItemAndCheckReachableMine("Lava Katana", 4) + + self.GiveItemAndCheckReachableMine("Galaxy Sword", 5) + self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5) + self.GiveItemAndCheckReachableMine("Galaxy Dagger", 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) + if reachable_level > 0: + assert self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + + if reachable_level > 1: + assert self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + + if reachable_level > 2: + assert self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + + if reachable_level > 3: + assert self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + + if reachable_level > 4: + assert self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + + self.remove(item) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index c9a8c746..9d2fac02 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,7 +1,11 @@ -from typing import ClassVar +from argparse import Namespace +from typing import Dict, FrozenSet, Tuple, Any, ClassVar +from BaseClasses import MultiWorld from test.TestBase import WorldTestBase +from test.general import gen_steps from .. import StardewValleyWorld +from ...AutoWorld import call_all class SVTestBase(WorldTestBase): @@ -12,9 +16,41 @@ class SVTestBase(WorldTestBase): def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) if self.constructed: - self.world = self.multiworld.worlds[self.player] + self.world = self.multiworld.worlds[self.player] # noqa @property def run_default_tests(self) -> bool: # world_setup is overridden, so it'd always run default tests when importing SVTestBase return type(self) is not SVTestBase and super().run_default_tests + + +pre_generated_worlds = {} + + +# Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core. +def setup_solo_multiworld(test_options=None, + _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: # noqa + if test_options is None: + test_options = {} + + # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + frozen_options = frozenset(test_options.items()) + if frozen_options in _cache: + return _cache[frozen_options] + + multiworld = MultiWorld(1) + multiworld.game[1] = StardewValleyWorld.game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed() + args = Namespace() + for name, option in StardewValleyWorld.option_definitions.items(): + value = option(test_options[name]) if name in test_options else option.from_any(option.default) + setattr(args, name, {1: value}) + multiworld.set_options(args) + multiworld.set_default_common_options() + for step in gen_steps: + call_all(multiworld, step) + + _cache[frozen_options] = multiworld + + return multiworld