diff --git a/setup.py b/setup.py index 382d3dc5..509981da 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ apworlds: set = { "Rogue Legacy", "Donkey Kong Country 3", "Super Mario World", + "Stardew Valley", "Timespinner", } diff --git a/test/TestBase.py b/test/TestBase.py index 5ffcb5ce..eea8e81a 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -1,6 +1,6 @@ +import pathlib import typing import unittest -import pathlib from argparse import Namespace import Utils diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py new file mode 100644 index 00000000..306a3ec7 --- /dev/null +++ b/worlds/stardew_valley/__init__.py @@ -0,0 +1,188 @@ +from typing import Dict, Any, Iterable, Optional, Union + +from BaseClasses import Region, Entrance, Location, Item, Tutorial +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 .locations import location_table, create_locations, LocationData +from .logic import StardewLogic, StardewRule, _True, _And +from .options import stardew_valley_options, StardewOptions, fetch_options +from .regions import create_regions +from .rules import set_rules + +client_version = 0 + + +class StardewLocation(Location): + game: str = "Stardew Valley" + + def __init__(self, player: int, name: str, address: Optional[int], parent=None): + super().__init__(player, name, address, parent) + self.event = not address + + +class StardewItem(Item): + game: str = "Stardew Valley" + + +class StardewWebWorld(WebWorld): + theme = "dirt" + bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here" + + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to playing Stardew Valley with Archipelago.", + "English", + "setup_en.md", + "setup/en", + ["KaitoKid", "Jouramie"] + )] + + +class StardewValleyWorld(World): + """ + Stardew Valley farming simulator game where the objective is basically to spend the least possible time on your farm. + """ + game = "Stardew Valley" + option_definitions = stardew_valley_options + topology_present = False + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = {name: data.code for name, data in location_table.items()} + + data_version = 1 + required_client_version = (0, 3, 9) + + options: StardewOptions + logic: StardewLogic + + web = StardewWebWorld() + modified_bundles: Dict[str, Bundle] + randomized_entrances: Dict[str, str] + + def generate_early(self): + self.options = fetch_options(self.multiworld, self.player) + self.logic = StardewLogic(self.player, self.options) + self.modified_bundles = get_all_bundles(self.multiworld.random, + self.logic, + self.options[options.BundleRandomization], + self.options[options.BundlePrice]) + + def create_regions(self): + def create_region(name: str, exits: Iterable[str]) -> Region: + region = Region(name, self.player, self.multiworld) + region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits] + return region + + world_regions, self.randomized_entrances = create_regions(create_region, self.multiworld.random, self.options) + self.multiworld.regions.extend(world_regions) + + def add_location(name: str, code: Optional[int], region: str): + region = self.multiworld.get_region(region, self.player) + location = StardewLocation(self.player, name, code, region) + location.access_rule = lambda _: True + region.locations.append(location) + + 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]) + 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, + 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_victory() + + def set_rules(self): + set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + + 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 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") + + def setup_victory(self): + if self.options[options.Goal] == options.Goal.option_community_center: + self.create_event_location(location_table["Complete Community Center"], + self.logic.can_complete_community_center().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_grandpa_evaluation: + self.create_event_location(location_table["Succeed Grandpa's Evaluation"], + self.logic.can_finish_grandpa_evaluation().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_bottom_of_the_mines: + self.create_event_location(location_table["Reach the Bottom of The Mines"], + self.logic.can_mine_to_floor(120).simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_cryptic_note: + self.create_event_location(location_table["Complete Quest Cryptic Note"], + self.logic.can_complete_quest("Cryptic Note").simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_master_angler: + self.create_event_location(location_table["Catch Every Fish"], + self.logic.can_catch_every_fish().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): + 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 get_filler_item_name(self) -> str: + return "Joja Cola" + + def fill_slot_data(self) -> Dict[str, Any]: + + modified_bundles = {} + for bundle_key in self.modified_bundles: + 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], + "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", + } diff --git a/worlds/stardew_valley/bundle_data.py b/worlds/stardew_valley/bundle_data.py new file mode 100644 index 00000000..cfc5d482 --- /dev/null +++ b/worlds/stardew_valley/bundle_data.py @@ -0,0 +1,414 @@ +from dataclasses import dataclass + +from . import fish_data +from .game_item import GameItem + +quality_dict = { + 0: "", + 1: "Silver", + 2: "Gold", + 3: "Iridium" +} + + +@dataclass(frozen=True) +class BundleItem: + item: GameItem + amount: int + quality: int + + @staticmethod + def item_bundle(name: str, item_id: int, amount: int, quality: int): + return BundleItem(GameItem(name, item_id), amount, quality) + + @staticmethod + def money_bundle(amount: int): + return BundleItem.item_bundle("Money", -1, amount, amount) + + def as_amount(self, amount: int): + return BundleItem.item_bundle(self.item.name, self.item.item_id, amount, self.quality) + + def as_quality(self, quality: int): + return BundleItem.item_bundle(self.item.name, self.item.item_id, self.amount, quality) + + def __repr__(self): + return f"{self.amount} {quality_dict[self.quality]} {self.item.name}" + + def __lt__(self, other): + return self.item < other.item + + +wild_horseradish = BundleItem.item_bundle("Wild Horseradish", 16, 1, 0) +daffodil = BundleItem.item_bundle("Daffodil", 18, 1, 0) +leek = BundleItem.item_bundle("Leek", 20, 1, 0) +dandelion = BundleItem.item_bundle("Dandelion", 22, 1, 0) +morel = BundleItem.item_bundle("Morel", 257, 1, 0) +common_mushroom = BundleItem.item_bundle("Common Mushroom", 404, 1, 0) +salmonberry = BundleItem.item_bundle("Salmonberry", 296, 1, 0) +spring_onion = BundleItem.item_bundle("Spring Onion", 399, 1, 0) + +grape = BundleItem.item_bundle("Grape", 398, 1, 0) +spice_berry = BundleItem.item_bundle("Spice Berry", 396, 1, 0) +sweet_pea = BundleItem.item_bundle("Sweet Pea", 402, 1, 0) +red_mushroom = BundleItem.item_bundle("Red Mushroom", 420, 1, 0) +fiddlehead_fern = BundleItem.item_bundle("Fiddlehead Fern", 259, 1, 0) + +wild_plum = BundleItem.item_bundle("Wild Plum", 406, 1, 0) +hazelnut = BundleItem.item_bundle("Hazelnut", 408, 1, 0) +blackberry = BundleItem.item_bundle("Blackberry", 410, 1, 0) +chanterelle = BundleItem.item_bundle("Chanterelle", 281, 1, 0) + +winter_root = BundleItem.item_bundle("Winter Root", 412, 1, 0) +crystal_fruit = BundleItem.item_bundle("Crystal Fruit", 414, 1, 0) +snow_yam = BundleItem.item_bundle("Snow Yam", 416, 1, 0) +crocus = BundleItem.item_bundle("Crocus", 418, 1, 0) +holly = BundleItem.item_bundle("Holly", 283, 1, 0) + +coconut = BundleItem.item_bundle("Coconut", 88, 1, 0) +cactus_fruit = BundleItem.item_bundle("Cactus Fruit", 90, 1, 0) +cave_carrot = BundleItem.item_bundle("Cave Carrot", 78, 1, 0) +purple_mushroom = BundleItem.item_bundle("Purple Mushroom", 422, 1, 0) +maple_syrup = BundleItem.item_bundle("Maple Syrup", 724, 1, 0) +oak_resin = BundleItem.item_bundle("Oak Resin", 725, 1, 0) +pine_tar = BundleItem.item_bundle("Pine Tar", 726, 1, 0) +nautilus_shell = BundleItem.item_bundle("Nautilus Shell", 392, 1, 0) +coral = BundleItem.item_bundle("Coral", 393, 1, 0) +sea_urchin = BundleItem.item_bundle("Sea Urchin", 397, 1, 0) +rainbow_shell = BundleItem.item_bundle("Rainbow Shell", 394, 1, 0) +clam = BundleItem(fish_data.clam, 1, 0) +cockle = BundleItem(fish_data.cockle, 1, 0) +mussel = BundleItem(fish_data.mussel, 1, 0) +oyster = BundleItem(fish_data.oyster, 1, 0) +seaweed = BundleItem.item_bundle("Seaweed", 152, 1, 0) + +wood = BundleItem.item_bundle("Wood", 388, 99, 0) +stone = BundleItem.item_bundle("Stone", 390, 99, 0) +hardwood = BundleItem.item_bundle("Hardwood", 709, 10, 0) +clay = BundleItem.item_bundle("Clay", 330, 10, 0) +fiber = BundleItem.item_bundle("Fiber", 771, 99, 0) + +blue_jazz = BundleItem.item_bundle("Blue Jazz", 597, 1, 0) +cauliflower = BundleItem.item_bundle("Cauliflower", 190, 1, 0) +green_bean = BundleItem.item_bundle("Green Bean", 188, 1, 0) +kale = BundleItem.item_bundle("Kale", 250, 1, 0) +parsnip = BundleItem.item_bundle("Parsnip", 24, 1, 0) +potato = BundleItem.item_bundle("Potato", 192, 1, 0) +strawberry = BundleItem.item_bundle("Strawberry", 400, 1, 0) +tulip = BundleItem.item_bundle("Tulip", 591, 1, 0) +unmilled_rice = BundleItem.item_bundle("Unmilled Rice", 271, 1, 0) +blueberry = BundleItem.item_bundle("Blueberry", 258, 1, 0) +corn = BundleItem.item_bundle("Corn", 270, 1, 0) +hops = BundleItem.item_bundle("Hops", 304, 1, 0) +hot_pepper = BundleItem.item_bundle("Hot Pepper", 260, 1, 0) +melon = BundleItem.item_bundle("Melon", 254, 1, 0) +poppy = BundleItem.item_bundle("Poppy", 376, 1, 0) +radish = BundleItem.item_bundle("Radish", 264, 1, 0) +summer_spangle = BundleItem.item_bundle("Summer Spangle", 593, 1, 0) +sunflower = BundleItem.item_bundle("Sunflower", 421, 1, 0) +tomato = BundleItem.item_bundle("Tomato", 256, 1, 0) +wheat = BundleItem.item_bundle("Wheat", 262, 1, 0) +hay = BundleItem.item_bundle("Hay", 178, 1, 0) +amaranth = BundleItem.item_bundle("Amaranth", 300, 1, 0) +bok_choy = BundleItem.item_bundle("Bok Choy", 278, 1, 0) +cranberries = BundleItem.item_bundle("Cranberries", 282, 1, 0) +eggplant = BundleItem.item_bundle("Eggplant", 272, 1, 0) +fairy_rose = BundleItem.item_bundle("Fairy Rose", 595, 1, 0) +pumpkin = BundleItem.item_bundle("Pumpkin", 276, 1, 0) +yam = BundleItem.item_bundle("Yam", 280, 1, 0) +sweet_gem_berry = BundleItem.item_bundle("Sweet Gem Berry", 417, 1, 0) +rhubarb = BundleItem.item_bundle("Rhubarb", 252, 1, 0) +beet = BundleItem.item_bundle("Beet", 284, 1, 0) +red_cabbage = BundleItem.item_bundle("Red Cabbage", 266, 1, 0) +artichoke = BundleItem.item_bundle("Artichoke", 274, 1, 0) + +egg = BundleItem.item_bundle("Egg", 176, 1, 0) +large_egg = BundleItem.item_bundle("Large Egg", 174, 1, 0) +brown_egg = BundleItem.item_bundle("Egg (Brown)", 180, 1, 0) +large_brown_egg = BundleItem.item_bundle("Large Egg (Brown)", 182, 1, 0) +wool = BundleItem.item_bundle("Wool", 440, 1, 0) +milk = BundleItem.item_bundle("Milk", 184, 1, 0) +large_milk = BundleItem.item_bundle("Large Milk", 186, 1, 0) +goat_milk = BundleItem.item_bundle("Goat Milk", 436, 1, 0) +large_goat_milk = BundleItem.item_bundle("Large Goat Milk", 438, 1, 0) +truffle = BundleItem.item_bundle("Truffle", 430, 1, 0) +duck_feather = BundleItem.item_bundle("Duck Feather", 444, 1, 0) +duck_egg = BundleItem.item_bundle("Duck Egg", 442, 1, 0) +rabbit_foot = BundleItem.item_bundle("Rabbit's Foot", 446, 1, 0) + +truffle_oil = BundleItem.item_bundle("Truffle Oil", 432, 1, 0) +cloth = BundleItem.item_bundle("Cloth", 428, 1, 0) +goat_cheese = BundleItem.item_bundle("Goat Cheese", 426, 1, 0) +cheese = BundleItem.item_bundle("Cheese", 424, 1, 0) +honey = BundleItem.item_bundle("Honey", 340, 1, 0) +beer = BundleItem.item_bundle("Beer", 346, 1, 0) +juice = BundleItem.item_bundle("Juice", 350, 1, 0) +mead = BundleItem.item_bundle("Mead", 459, 1, 0) +pale_ale = BundleItem.item_bundle("Pale Ale", 303, 1, 0) +wine = BundleItem.item_bundle("Wine", 348, 1, 0) +jelly = BundleItem.item_bundle("Jelly", 344, 1, 0) +pickles = BundleItem.item_bundle("Pickles", 342, 1, 0) +caviar = BundleItem.item_bundle("Caviar", 445, 1, 0) +aged_roe = BundleItem.item_bundle("Aged Roe", 447, 1, 0) +apple = BundleItem.item_bundle("Apple", 613, 1, 0) +apricot = BundleItem.item_bundle("Apricot", 634, 1, 0) +orange = BundleItem.item_bundle("Orange", 635, 1, 0) +peach = BundleItem.item_bundle("Peach", 636, 1, 0) +pomegranate = BundleItem.item_bundle("Pomegranate", 637, 1, 0) +cherry = BundleItem.item_bundle("Cherry", 638, 1, 0) +lobster = BundleItem(fish_data.lobster, 1, 0) +crab = BundleItem(fish_data.crab, 1, 0) +shrimp = BundleItem(fish_data.shrimp, 1, 0) +crayfish = BundleItem(fish_data.crayfish, 1, 0) +snail = BundleItem(fish_data.snail, 1, 0) +periwinkle = BundleItem(fish_data.periwinkle, 1, 0) +trash = BundleItem.item_bundle("Trash", 168, 1, 0) +driftwood = BundleItem.item_bundle("Driftwood", 169, 1, 0) +soggy_newspaper = BundleItem.item_bundle("Soggy Newspaper", 172, 1, 0) +broken_cd = BundleItem.item_bundle("Broken CD", 171, 1, 0) +broken_glasses = BundleItem.item_bundle("Broken Glasses", 170, 1, 0) + +chub = BundleItem(fish_data.chub, 1, 0) +catfish = BundleItem(fish_data.catfish, 1, 0) +rainbow_trout = BundleItem(fish_data.rainbow_trout, 1, 0) +lingcod = BundleItem(fish_data.lingcod, 1, 0) +walleye = BundleItem(fish_data.walleye, 1, 0) +perch = BundleItem(fish_data.perch, 1, 0) +pike = BundleItem(fish_data.pike, 1, 0) +bream = BundleItem(fish_data.bream, 1, 0) +salmon = BundleItem(fish_data.salmon, 1, 0) +sunfish = BundleItem(fish_data.sunfish, 1, 0) +tiger_trout = BundleItem(fish_data.tiger_trout, 1, 0) +shad = BundleItem(fish_data.shad, 1, 0) +smallmouth_bass = BundleItem(fish_data.smallmouth_bass, 1, 0) +dorado = BundleItem(fish_data.dorado, 1, 0) +carp = BundleItem(fish_data.carp, 1, 0) +midnight_carp = BundleItem(fish_data.midnight_carp, 1, 0) +largemouth_bass = BundleItem(fish_data.largemouth_bass, 1, 0) +sturgeon = BundleItem(fish_data.sturgeon, 1, 0) +bullhead = BundleItem(fish_data.bullhead, 1, 0) +tilapia = BundleItem(fish_data.tilapia, 1, 0) +pufferfish = BundleItem(fish_data.pufferfish, 1, 0) +tuna = BundleItem(fish_data.tuna, 1, 0) +super_cucumber = BundleItem(fish_data.super_cucumber, 1, 0) +flounder = BundleItem(fish_data.flounder, 1, 0) +anchovy = BundleItem(fish_data.anchovy, 1, 0) +sardine = BundleItem(fish_data.sardine, 1, 0) +red_mullet = BundleItem(fish_data.red_mullet, 1, 0) +herring = BundleItem(fish_data.herring, 1, 0) +eel = BundleItem(fish_data.eel, 1, 0) +octopus = BundleItem(fish_data.octopus, 1, 0) +red_snapper = BundleItem(fish_data.red_snapper, 1, 0) +squid = BundleItem(fish_data.squid, 1, 0) +sea_cucumber = BundleItem(fish_data.sea_cucumber, 1, 0) +albacore = BundleItem(fish_data.albacore, 1, 0) +halibut = BundleItem(fish_data.halibut, 1, 0) +scorpion_carp = BundleItem(fish_data.scorpion_carp, 1, 0) +sandfish = BundleItem(fish_data.sandfish, 1, 0) +woodskip = BundleItem(fish_data.woodskip, 1, 0) +lava_eel = BundleItem(fish_data.lava_eel, 1, 0) +ice_pip = BundleItem(fish_data.ice_pip, 1, 0) +stonefish = BundleItem(fish_data.stonefish, 1, 0) +ghostfish = BundleItem(fish_data.ghostfish, 1, 0) + +wilted_bouquet = BundleItem.item_bundle("Wilted Bouquet", 277, 1, 0) +copper_bar = BundleItem.item_bundle("Copper Bar", 334, 2, 0) +iron_Bar = BundleItem.item_bundle("Iron Bar", 335, 2, 0) +gold_bar = BundleItem.item_bundle("Gold Bar", 336, 1, 0) +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) + +slime = BundleItem.item_bundle("Slime", 766, 99, 0) +bug_meat = BundleItem.item_bundle("Bug Meat", 684, 10, 0) +bat_wing = BundleItem.item_bundle("Bat Wing", 767, 10, 0) +solar_essence = BundleItem.item_bundle("Solar Essence", 768, 1, 0) +void_essence = BundleItem.item_bundle("Void Essence", 769, 1, 0) + +maki_roll = BundleItem.item_bundle("Maki Roll", 228, 1, 0) +fried_egg = BundleItem.item_bundle("Fried Egg", 194, 1, 0) +omelet = BundleItem.item_bundle("Omelet", 195, 1, 0) +pizza = BundleItem.item_bundle("Pizza", 206, 1, 0) +hashbrowns = BundleItem.item_bundle("Hashbrowns", 210, 1, 0) +pancakes = BundleItem.item_bundle("Pancakes", 211, 1, 0) +bread = BundleItem.item_bundle("Bread", 216, 1, 0) +tortilla = BundleItem.item_bundle("Tortilla", 229, 1, 0) +triple_shot_espresso = BundleItem.item_bundle("Triple Shot Espresso", 253, 1, 0) +farmer_s_lunch = BundleItem.item_bundle("Farmer's Lunch", 240, 1, 0) +survival_burger = BundleItem.item_bundle("Survival Burger", 241, 1, 0) +dish_o_the_sea = BundleItem.item_bundle("Dish O' The Sea", 242, 1, 0) +miner_s_treat = BundleItem.item_bundle("Miner's Treat", 243, 1, 0) +roots_platter = BundleItem.item_bundle("Roots Platter", 244, 1, 0) +salad = BundleItem.item_bundle("Salad", 196, 1, 0) +cheese_cauliflower = BundleItem.item_bundle("Cheese Cauliflower", 197, 1, 0) +parsnip_soup = BundleItem.item_bundle("Parsnip Soup", 199, 1, 0) +fried_mushroom = BundleItem.item_bundle("Fried Mushroom", 205, 1, 0) +salmon_dinner = BundleItem.item_bundle("Salmon Dinner", 212, 1, 0) +pepper_poppers = BundleItem.item_bundle("Pepper Poppers", 215, 1, 0) +spaghetti = BundleItem.item_bundle("Spaghetti", 224, 1, 0) +sashimi = BundleItem.item_bundle("Sashimi", 227, 1, 0) +blueberry_tart = BundleItem.item_bundle("Blueberry Tart", 234, 1, 0) +algae_soup = BundleItem.item_bundle("Algae Soup", 456, 1, 0) +pale_broth = BundleItem.item_bundle("Pale Broth", 457, 1, 0) +chowder = BundleItem.item_bundle("Chowder", 727, 1, 0) +green_algae = BundleItem.item_bundle("Green Algae", 153, 1, 0) +white_algae = BundleItem.item_bundle("White Algae", 157, 1, 0) +geode = BundleItem.item_bundle("Geode", 535, 1, 0) +frozen_geode = BundleItem.item_bundle("Frozen Geode", 536, 1, 0) +magma_geode = BundleItem.item_bundle("Magma Geode", 537, 1, 0) +omni_geode = BundleItem.item_bundle("Omni Geode", 749, 1, 0) + +spring_foraging_items = [wild_horseradish, daffodil, leek, dandelion, salmonberry, spring_onion] +summer_foraging_items = [grape, spice_berry, sweet_pea, fiddlehead_fern, rainbow_shell] +fall_foraging_items = [common_mushroom, wild_plum, hazelnut, blackberry] +winter_foraging_items = [winter_root, crystal_fruit, snow_yam, crocus, holly, nautilus_shell] +exotic_foraging_items = [coconut, cactus_fruit, cave_carrot, red_mushroom, purple_mushroom, + maple_syrup, oak_resin, pine_tar, morel, coral, + sea_urchin, clam, cockle, mussel, oyster, seaweed] +construction_items = [wood, stone, hardwood, clay, fiber] + +# TODO coffee_bean, garlic, rhubarb, tea_leaves +spring_crop_items = [blue_jazz, cauliflower, green_bean, kale, parsnip, potato, strawberry, tulip, unmilled_rice] +# TODO red_cabbage, starfruit, ancient_fruit, pineapple, taro_root +summer_crops_items = [blueberry, corn, hops, hot_pepper, melon, poppy, + radish, summer_spangle, sunflower, tomato, wheat] +# TODO artichoke, beet +fall_crops_items = [corn, sunflower, wheat, amaranth, bok_choy, cranberries, + eggplant, fairy_rose, grape, pumpkin, yam, sweet_gem_berry] +all_crops_items = sorted({*spring_crop_items, *summer_crops_items, *fall_crops_items}) +quality_crops_items = [item.as_quality(2).as_amount(5) for item in all_crops_items] +# TODO void_egg, dinosaur_egg, ostrich_egg, golden_egg +animal_product_items = [egg, large_egg, brown_egg, large_brown_egg, wool, milk, large_milk, + goat_milk, large_goat_milk, truffle, duck_feather, duck_egg, rabbit_foot] +# TODO coffee, green_tea +artisan_goods_items = [truffle_oil, cloth, goat_cheese, cheese, honey, beer, juice, mead, pale_ale, wine, jelly, + pickles, caviar, aged_roe, apple, apricot, orange, peach, pomegranate, cherry] + +river_fish_items = [chub, catfish, rainbow_trout, lingcod, walleye, perch, pike, bream, + salmon, sunfish, tiger_trout, shad, smallmouth_bass, dorado] +lake_fish_items = [chub, rainbow_trout, lingcod, walleye, perch, carp, midnight_carp, + largemouth_bass, sturgeon, bullhead, midnight_carp] +ocean_fish_items = [tilapia, pufferfish, tuna, super_cucumber, flounder, anchovy, sardine, red_mullet, + herring, eel, octopus, red_snapper, squid, sea_cucumber, albacore, halibut] +night_fish_items = [walleye, bream, super_cucumber, eel, squid, midnight_carp] +# TODO void_salmon +specialty_fish_items = [scorpion_carp, sandfish, woodskip, pufferfish, eel, octopus, + squid, lava_eel, ice_pip, stonefish, ghostfish, dorado] +crab_pot_items = [lobster, clam, crab, cockle, mussel, shrimp, oyster, crayfish, snail, + periwinkle, trash, driftwood, soggy_newspaper, broken_cd, broken_glasses] + +# TODO radioactive_bar +blacksmith_items = [wilted_bouquet, copper_bar, iron_Bar, gold_bar, iridium_bar, refined_quartz, coal] +geologist_items = [quartz, earth_crystal, frozen_tear, fire_quartz, emerald, aquamarine, ruby, amethyst, topaz, jade] +adventurer_items = [slime, bug_meat, bat_wing, solar_essence, void_essence, coal] + +chef_items = [maki_roll, fried_egg, omelet, pizza, hashbrowns, pancakes, bread, tortilla, triple_shot_espresso, + farmer_s_lunch, survival_burger, dish_o_the_sea, miner_s_treat, roots_platter, salad, + cheese_cauliflower, parsnip_soup, fried_mushroom, salmon_dinner, pepper_poppers, spaghetti, + sashimi, blueberry_tart, algae_soup, pale_broth, chowder] + +dwarf_scroll_1 = BundleItem.item_bundle("Dwarf Scroll I", 96, 1, 0) +dwarf_scroll_2 = BundleItem.item_bundle("Dwarf Scroll II", 97, 1, 0) +dwarf_scroll_3 = BundleItem.item_bundle("Dwarf Scroll III", 98, 1, 0) +dwarf_scroll_4 = BundleItem.item_bundle("Dwarf Scroll IV", 99, 1, 0) +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_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_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] +fodder_items = [wheat.as_amount(10), hay.as_amount(10), apple.as_amount(3), kale.as_amount(3), corn.as_amount(3), + green_bean.as_amount(3), potato.as_amount(3), green_algae.as_amount(5), white_algae.as_amount(3)] +enchanter_items = [oak_resin, wine, rabbit_foot, pomegranate, purple_mushroom, solar_essence, + super_cucumber, void_essence, fire_quartz, frozen_tear, jade] + +vault_2500_items = [BundleItem.money_bundle(2500)] +vault_5000_items = [BundleItem.money_bundle(5000)] +vault_10000_items = [BundleItem.money_bundle(10000)] +vault_25000_items = [BundleItem.money_bundle(25000)] + +crafts_room_bundle_items = [ + *spring_foraging_items, + *summer_foraging_items, + *fall_foraging_items, + *winter_foraging_items, + *exotic_foraging_items, + *construction_items, +] + +pantry_bundle_items = sorted({ + *spring_crop_items, + *summer_crops_items, + *fall_crops_items, + *quality_crops_items, + *animal_product_items, + *artisan_goods_items, +}) + +fish_tank_bundle_items = sorted({ + *river_fish_items, + *lake_fish_items, + *ocean_fish_items, + *night_fish_items, + *crab_pot_items, + *specialty_fish_items, +}) + +boiler_room_bundle_items = sorted({ + *blacksmith_items, + *geologist_items, + *adventurer_items, +}) + +bulletin_board_bundle_items = sorted({ + *chef_items, + *[item for dye_color_items in dye_items for item in dye_color_items], + *field_research_items, + *fodder_items, + *enchanter_items +}) + +vault_bundle_items = [ + *vault_2500_items, + *vault_5000_items, + *vault_10000_items, + *vault_25000_items, +] + +all_bundle_items_except_money = sorted({ + *crafts_room_bundle_items, + *pantry_bundle_items, + *fish_tank_bundle_items, + *boiler_room_bundle_items, + *bulletin_board_bundle_items, +}, key=lambda x: x.item.name) + +all_bundle_items = sorted({ + *crafts_room_bundle_items, + *pantry_bundle_items, + *fish_tank_bundle_items, + *boiler_room_bundle_items, + *bulletin_board_bundle_items, + *vault_bundle_items, +}, key=lambda x: x.item.name) + +all_bundle_items_by_name = {item.item.name: item for item in all_bundle_items} +all_bundle_items_by_id = {item.item.item_id: item for item in all_bundle_items} diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py new file mode 100644 index 00000000..f87e3d67 --- /dev/null +++ b/worlds/stardew_valley/bundles.py @@ -0,0 +1,254 @@ +from random import Random +from typing import List, Dict, Union + +from .bundle_data import * +from .logic import StardewLogic +from .options import BundleRandomization, BundlePrice + +vanilla_bundles = { + "Pantry/0": "Spring Crops/O 465 20/24 1 0 188 1 0 190 1 0 192 1 0/0", + "Pantry/1": "Summer Crops/O 621 1/256 1 0 260 1 0 258 1 0 254 1 0/3", + "Pantry/2": "Fall Crops/BO 10 1/270 1 0 272 1 0 276 1 0 280 1 0/2", + "Pantry/3": "Quality Crops/BO 15 1/24 5 2 254 5 2 276 5 2 270 5 2/6/3", + "Pantry/4": "Animal/BO 16 1/186 1 0 182 1 0 174 1 0 438 1 0 440 1 0 442 1 0/4/5", + # 639 1 0 640 1 0 641 1 0 642 1 0 643 1 0 + "Pantry/5": "Artisan/BO 12 1/432 1 0 428 1 0 426 1 0 424 1 0 340 1 0 344 1 0 613 1 0 634 1 0 635 1 0 636 1 0 637 1 0 638 1 0/1/6", + "Crafts Room/13": "Spring Foraging/O 495 30/16 1 0 18 1 0 20 1 0 22 1 0/0", + "Crafts Room/14": "Summer Foraging/O 496 30/396 1 0 398 1 0 402 1 0/3", + "Crafts Room/15": "Fall Foraging/O 497 30/404 1 0 406 1 0 408 1 0 410 1 0/2", + "Crafts Room/16": "Winter Foraging/O 498 30/412 1 0 414 1 0 416 1 0 418 1 0/6", + "Crafts Room/17": "Construction/BO 114 1/388 99 0 388 99 0 390 99 0 709 10 0/4", + "Crafts Room/19": "Exotic Foraging/O 235 5/88 1 0 90 1 0 78 1 0 420 1 0 422 1 0 724 1 0 725 1 0 726 1 0 257 1 0/1/5", + "Fish Tank/6": "River Fish/O 685 30/145 1 0 143 1 0 706 1 0 699 1 0/6", + "Fish Tank/7": "Lake Fish/O 687 1/136 1 0 142 1 0 700 1 0 698 1 0/0", + "Fish Tank/8": "Ocean Fish/O 690 5/131 1 0 130 1 0 150 1 0 701 1 0/5", + "Fish Tank/9": "Night Fishing/R 516 1/140 1 0 132 1 0 148 1 0/1", + "Fish Tank/10": "Specialty Fish/O 242 5/128 1 0 156 1 0 164 1 0 734 1 0/4", + "Fish Tank/11": "Crab Pot/O 710 3/715 1 0 716 1 0 717 1 0 718 1 0 719 1 0 720 1 0 721 1 0 722 1 0 723 1 0 372 1 0/1/5", + "Boiler Room/20": "Blacksmith's/BO 13 1/334 1 0 335 1 0 336 1 0/2", + "Boiler Room/21": "Geologist's/O 749 5/80 1 0 86 1 0 84 1 0 82 1 0/1", + "Boiler Room/22": "Adventurer's/R 518 1/766 99 0 767 10 0 768 1 0 769 1 0/1/2", + "Vault/23": "2,500g/O 220 3/-1 2500 2500/4", + "Vault/24": "5,000g/O 369 30/-1 5000 5000/2", + "Vault/25": "10,000g/BO 9 1/-1 10000 10000/3", + "Vault/26": "25,000g/BO 21 1/-1 25000 25000/1", + "Bulletin Board/31": "Chef's/O 221 3/724 1 0 259 1 0 430 1 0 376 1 0 228 1 0 194 1 0/4", + "Bulletin Board/32": "Field Research/BO 20 1/422 1 0 392 1 0 702 1 0 536 1 0/5", + "Bulletin Board/33": "Enchanter's/O 336 5/725 1 0 348 1 0 446 1 0 637 1 0/1", + "Bulletin Board/34": "Dye/BO 25 1/420 1 0 397 1 0 421 1 0 444 1 0 62 1 0 266 1 0/6", + "Bulletin Board/35": "Fodder/BO 104 1/262 10 0 178 10 0 613 3 0/3", + # "Abandoned Joja Mart/36": "The Missing//348 1 1 807 1 0 74 1 0 454 5 2 795 1 2 445 1 0/1/5" +} + + +class Bundle: + room: str + sprite: str + original_name: str + name: str + rewards: List[str] + requirements: List[BundleItem] + color: str + number_required: int + + def __init__(self, key: str, value: str): + key_parts = key.split("/") + self.room = key_parts[0] + self.sprite = key_parts[1] + + value_parts = value.split("/") + self.original_name = value_parts[0] + self.name = value_parts[0] + self.rewards = self.parse_stardew_objects(value_parts[1]) + self.requirements = self.parse_stardew_bundle_items(value_parts[2]) + self.color = value_parts[3] + if len(value_parts) > 4: + self.number_required = int(value_parts[4]) + else: + self.number_required = len(self.requirements) + + def __repr__(self): + return f"{self.original_name} -> {repr(self.requirements)}" + + def get_name_with_bundle(self) -> str: + return f"{self.original_name} Bundle" + + def to_pair(self) -> (str, str): + key = f"{self.room}/{self.sprite}" + str_rewards = "" + for reward in self.rewards: + str_rewards += f" {reward}" + str_rewards = str_rewards.strip() + str_requirements = "" + for requirement in self.requirements: + str_requirements += f" {requirement.item.item_id} {requirement.amount} {requirement.quality}" + str_requirements = str_requirements.strip() + value = f"{self.name}/{str_rewards}/{str_requirements}/{self.color}/{self.number_required}" + return key, value + + def remove_rewards(self): + self.rewards = [] + + def change_number_required(self, difference: int): + self.number_required = min(len(self.requirements), max(1, self.number_required + difference)) + if len(self.requirements) == 1 and self.requirements[0].item.item_id == -1: + one_fifth = self.requirements[0].amount / 5 + new_amount = int(self.requirements[0].amount + (difference * one_fifth)) + self.requirements[0] = BundleItem.money_bundle(new_amount) + thousand_amount = int(new_amount / 1000) + dollar_amount = str(new_amount % 1000) + while len(dollar_amount) < 3: + dollar_amount = f"0{dollar_amount}" + self.name = f"{thousand_amount},{dollar_amount}g" + + def randomize_requirements(self, random: Random, + potential_requirements: Union[List[BundleItem], List[List[BundleItem]]]): + if not potential_requirements: + return + + number_to_generate = len(self.requirements) + self.requirements.clear() + if number_to_generate > len(potential_requirements): + choices: Union[BundleItem, List[BundleItem]] = random.choices(potential_requirements, k=number_to_generate) + else: + choices: Union[BundleItem, List[BundleItem]] = random.sample(potential_requirements, number_to_generate) + for choice in choices: + if isinstance(choice, BundleItem): + self.requirements.append(choice) + else: + self.requirements.append(random.choice(choice)) + + def assign_requirements(self, new_requirements: List[BundleItem]) -> List[BundleItem]: + number_to_generate = len(self.requirements) + self.requirements.clear() + for requirement in new_requirements: + self.requirements.append(requirement) + if len(self.requirements) >= number_to_generate: + return new_requirements[number_to_generate:] + + @staticmethod + def parse_stardew_objects(string_objects: str) -> List[str]: + objects = [] + if len(string_objects) < 5: + return objects + rewards_parts = string_objects.split(" ") + for index in range(0, len(rewards_parts), 3): + objects.append(f"{rewards_parts[index]} {rewards_parts[index + 1]} {rewards_parts[index + 2]}") + return objects + + @staticmethod + def parse_stardew_bundle_items(string_objects: str) -> List[BundleItem]: + bundle_items = [] + parts = string_objects.split(" ") + for index in range(0, len(parts), 3): + item_id = int(parts[index]) + bundle_item = BundleItem(all_bundle_items_by_id[item_id].item, + int(parts[index + 1]), + int(parts[index + 2])) + bundle_items.append(bundle_item) + return bundle_items + + # Shuffling the Vault doesn't really work with the stardew system in place + # shuffle_vault_amongst_themselves(random, bundles) + + +def get_all_bundles(random: Random, logic: StardewLogic, randomization: int, price: int) -> Dict[str, Bundle]: + bundles = {} + for bundle_key in vanilla_bundles: + bundle_value = vanilla_bundles[bundle_key] + bundle = Bundle(bundle_key, bundle_value) + bundles[bundle.get_name_with_bundle()] = bundle + + if randomization == BundleRandomization.option_thematic: + shuffle_bundles_thematically(random, bundles) + elif randomization == BundleRandomization.option_shuffled: + shuffle_bundles_completely(random, logic, bundles) + + price_difference = 0 + if price == BundlePrice.option_very_cheap: + price_difference = -2 + elif price == BundlePrice.option_cheap: + price_difference = -1 + elif price == BundlePrice.option_expensive: + price_difference = 1 + + for bundle_key in bundles: + bundles[bundle_key].remove_rewards() + bundles[bundle_key].change_number_required(price_difference) + + return bundles + + +def shuffle_bundles_completely(random: Random, logic: StardewLogic, bundles: Dict[str, Bundle]): + total_required_item_number = sum(len(bundle.requirements) for bundle in bundles.values()) + quality_crops_items_set = set(quality_crops_items) + all_bundle_items_without_quality_and_money = [item + for item in all_bundle_items_except_money + if item not in quality_crops_items_set] + \ + random.sample(quality_crops_items, 10) + choices = random.sample(all_bundle_items_without_quality_and_money, total_required_item_number - 4) + + items_sorted = sorted(choices, key=lambda x: logic.item_rules[x.item.name].get_difficulty()) + + keys = sorted(bundles.keys()) + random.shuffle(keys) + + for key in keys: + if not bundles[key].original_name.endswith("00g"): + items_sorted = bundles[key].assign_requirements(items_sorted) + + +def shuffle_bundles_thematically(random: Random, bundles: Dict[str, Bundle]): + shuffle_crafts_room_bundle_thematically(random, bundles) + shuffle_pantry_bundle_thematically(random, bundles) + shuffle_fish_tank_thematically(random, bundles) + shuffle_boiler_room_thematically(random, bundles) + shuffle_bulletin_board_thematically(random, bundles) + + +def shuffle_crafts_room_bundle_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Spring Foraging Bundle"].randomize_requirements(random, spring_foraging_items) + bundles["Summer Foraging Bundle"].randomize_requirements(random, summer_foraging_items) + bundles["Fall Foraging Bundle"].randomize_requirements(random, fall_foraging_items) + bundles["Winter Foraging Bundle"].randomize_requirements(random, winter_foraging_items) + bundles["Exotic Foraging Bundle"].randomize_requirements(random, exotic_foraging_items) + bundles["Construction Bundle"].randomize_requirements(random, construction_items) + + +def shuffle_pantry_bundle_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Spring Crops Bundle"].randomize_requirements(random, spring_crop_items) + bundles["Summer Crops Bundle"].randomize_requirements(random, summer_crops_items) + bundles["Fall Crops Bundle"].randomize_requirements(random, fall_crops_items) + bundles["Quality Crops Bundle"].randomize_requirements(random, quality_crops_items) + bundles["Animal Bundle"].randomize_requirements(random, animal_product_items) + bundles["Artisan Bundle"].randomize_requirements(random, artisan_goods_items) + + +def shuffle_fish_tank_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["River Fish Bundle"].randomize_requirements(random, river_fish_items) + bundles["Lake Fish Bundle"].randomize_requirements(random, lake_fish_items) + bundles["Ocean Fish Bundle"].randomize_requirements(random, ocean_fish_items) + bundles["Night Fishing Bundle"].randomize_requirements(random, night_fish_items) + bundles["Crab Pot Bundle"].randomize_requirements(random, crab_pot_items) + bundles["Specialty Fish Bundle"].randomize_requirements(random, specialty_fish_items) + + +def shuffle_boiler_room_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Blacksmith's Bundle"].randomize_requirements(random, blacksmith_items) + bundles["Geologist's Bundle"].randomize_requirements(random, geologist_items) + bundles["Adventurer's Bundle"].randomize_requirements(random, adventurer_items) + + +def shuffle_bulletin_board_thematically(random: Random, bundles: Dict[str, Bundle]): + bundles["Chef's Bundle"].randomize_requirements(random, chef_items) + bundles["Dye Bundle"].randomize_requirements(random, dye_items) + bundles["Field Research Bundle"].randomize_requirements(random, field_research_items) + bundles["Fodder Bundle"].randomize_requirements(random, fodder_items) + bundles["Enchanter's Bundle"].randomize_requirements(random, enchanter_items) + + +def shuffle_vault_amongst_themselves(random: Random, bundles: Dict[str, Bundle]): + bundles["2,500g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["5,000g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["10,000g Bundle"].randomize_requirements(random, vault_bundle_items) + bundles["25,000g Bundle"].randomize_requirements(random, vault_bundle_items) diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv new file mode 100644 index 00000000..425186ed --- /dev/null +++ b/worlds/stardew_valley/data/items.csv @@ -0,0 +1,312 @@ +id,name,classification,groups +0,Joja Cola,filler,TRASH +15,Rusty Key,progression, +16,Dwarvish Translation Guide,progression, +17,Bridge Repair,progression,COMMUNITY_REWARD +18,Greenhouse,progression,COMMUNITY_REWARD +19,Glittering Boulder Removed,progression,COMMUNITY_REWARD +20,Minecarts Repair,useful,COMMUNITY_REWARD +21,Bus Repair,progression,COMMUNITY_REWARD +22,Movie Theater,useful, +23,Stardrop,useful, +24,Progressive Backpack,progression, +25,Rusty Sword,progression,WEAPON +26,Leather Boots,progression,"FOOTWEAR,MINES_FLOOR_10" +27,Work Boots,useful,"FOOTWEAR,MINES_FLOOR_10" +28,Wooden Blade,progression,"MINES_FLOOR_10,WEAPON" +29,Iron Dirk,progression,"MINES_FLOOR_10,WEAPON" +30,Wind Spire,progression,"MINES_FLOOR_10,WEAPON" +31,Femur,progression,"MINES_FLOOR_10,WEAPON" +32,Steel Smallsword,progression,"MINES_FLOOR_20,WEAPON" +33,Wood Club,progression,"MINES_FLOOR_20,WEAPON" +34,Elf Blade,progression,"MINES_FLOOR_20,WEAPON" +35,Glow Ring,useful,"MINES_FLOOR_20,RING" +36,Magnet Ring,useful,"MINES_FLOOR_20,RING" +37,Slingshot,progression,WEAPON +38,Tundra Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +39,Thermal Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +40,Combat Boots,useful,"FOOTWEAR,MINES_FLOOR_50" +41,Silver Saber,progression,"MINES_FLOOR_50,WEAPON" +42,Pirate's Sword,progression,"MINES_FLOOR_50,WEAPON" +43,Crystal Dagger,progression,"MINES_FLOOR_60,WEAPON" +44,Cutlass,progression,"MINES_FLOOR_60,WEAPON" +45,Iron Edge,progression,"MINES_FLOOR_60,WEAPON" +46,Burglar's Shank,progression,"MINES_FLOOR_60,WEAPON" +47,Wood Mallet,progression,"MINES_FLOOR_60,WEAPON" +48,Master Slingshot,progression,WEAPON +49,Firewalker Boots,useful,"FOOTWEAR,MINES_FLOOR_80" +50,Dark Boots,useful,"FOOTWEAR,MINES_FLOOR_80" +51,Claymore,progression,"MINES_FLOOR_80,WEAPON" +52,Templar's Blade,progression,"MINES_FLOOR_80,WEAPON" +53,Kudgel,progression,"MINES_FLOOR_80,WEAPON" +54,Shadow Dagger,progression,"MINES_FLOOR_80,WEAPON" +55,Obsidian Edge,progression,"MINES_FLOOR_90,WEAPON" +56,Tempered Broadsword,progression,"MINES_FLOOR_90,WEAPON" +57,Wicked Kris,progression,"MINES_FLOOR_90,WEAPON" +58,Bone Sword,progression,"MINES_FLOOR_90,WEAPON" +59,Ossified Blade,progression,"MINES_FLOOR_90,WEAPON" +60,Space Boots,useful,"FOOTWEAR,MINES_FLOOR_110" +61,Crystal Shoes,useful,"FOOTWEAR,MINES_FLOOR_110" +62,Steel Falchion,progression,"MINES_FLOOR_110,WEAPON" +63,The Slammer,progression,"MINES_FLOOR_110,WEAPON" +64,Skull Key,progression, +65,Progressive Hoe,progression,PROGRESSIVE_TOOLS +66,Progressive Pickaxe,progression,PROGRESSIVE_TOOLS +67,Progressive Axe,progression,PROGRESSIVE_TOOLS +68,Progressive Watering Can,progression,PROGRESSIVE_TOOLS +69,Progressive Trash Can,progression,PROGRESSIVE_TOOLS +70,Progressive Fishing Rod,progression,PROGRESSIVE_TOOLS +71,Golden Scythe,useful, +72,Progressive Mine Elevator,progression, +73,Farming Level,progression,SKILL_LEVEL_UP +74,Fishing Level,progression,SKILL_LEVEL_UP +75,Foraging Level,progression,SKILL_LEVEL_UP +76,Mining Level,progression,SKILL_LEVEL_UP +77,Combat Level,progression,SKILL_LEVEL_UP +78,Earth Obelisk,useful, +79,Water Obelisk,useful, +80,Desert Obelisk,progression, +81,Island Obelisk,progression, +82,Junimo Hut,useful, +83,Gold Clock,useful, +84,Progressive Coop,progression, +85,Progressive Barn,progression, +86,Well,useful, +87,Silo,progression, +88,Mill,progression, +89,Progressive Shed,progression, +90,Fish Pond,progression, +91,Stable,useful, +92,Slime Hutch,useful, +93,Shipping Bin,progression, +94,Beach Bridge,progression, +95,Adventurer's Guild,progression, +96,Club Card,progression, +97,Magnifying Glass,progression, +98,Bear's Knowledge,progression, +99,Iridium Snake Milk,progression, +100,JotPK: Progressive Boots,progression,ARCADE_MACHINE_BUFFS +101,JotPK: Progressive Gun,progression,ARCADE_MACHINE_BUFFS +102,JotPK: Progressive Ammo,progression,ARCADE_MACHINE_BUFFS +103,JotPK: Extra Life,progression,ARCADE_MACHINE_BUFFS +104,JotPK: Increased Drop Rate,progression,ARCADE_MACHINE_BUFFS +105,Junimo Kart: Extra Life,progression,ARCADE_MACHINE_BUFFS +106,Galaxy Sword,progression,"GALAXY_WEAPONS,WEAPON" +107,Galaxy Dagger,progression,"GALAXY_WEAPONS,WEAPON" +108,Galaxy Hammer,progression,"GALAXY_WEAPONS,WEAPON" +109,Movement Speed Bonus,useful, +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, +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" +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" +5007,Resource Pack: 100 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" +5008,Resource Pack: 25 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" +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" +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" +5019,Resource Pack: 60 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" +5020,Resource Pack: 5 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5021,Resource Pack: 10 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5022,Resource Pack: 15 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5023,Resource Pack: 20 Coal,filler,"BASE_RESOURCE,RESOURCE_PACK" +5024,Resource Pack: 5 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5025,Resource Pack: 10 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5026,Resource Pack: 15 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5027,Resource Pack: 20 Clay,filler,"BASE_RESOURCE,RESOURCE_PACK" +5028,Resource Pack: 1 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5029,Resource Pack: 3 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5030,Resource Pack: 5 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5031,Resource Pack: 7 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5032,Resource Pack: 9 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5033,Resource Pack: 10 Warp Totem: Beach,filler,"RESOURCE_PACK,WARP_TOTEM" +5034,Resource Pack: 1 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5035,Resource Pack: 3 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5036,Resource Pack: 5 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5037,Resource Pack: 7 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5038,Resource Pack: 9 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5039,Resource Pack: 10 Warp Totem: Desert,filler,"RESOURCE_PACK,WARP_TOTEM" +5040,Resource Pack: 1 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5041,Resource Pack: 3 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5042,Resource Pack: 5 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5043,Resource Pack: 7 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5044,Resource Pack: 9 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5045,Resource Pack: 10 Warp Totem: Farm,filler,"RESOURCE_PACK,WARP_TOTEM" +5046,Resource Pack: 1 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5047,Resource Pack: 3 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5048,Resource Pack: 5 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5049,Resource Pack: 7 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5050,Resource Pack: 9 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5051,Resource Pack: 10 Warp Totem: Island,filler,"RESOURCE_PACK,WARP_TOTEM" +5052,Resource Pack: 1 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5053,Resource Pack: 3 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5054,Resource Pack: 5 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5055,Resource Pack: 7 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5056,Resource Pack: 9 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5057,Resource Pack: 10 Warp Totem: Mountains,filler,"RESOURCE_PACK,WARP_TOTEM" +5058,Resource Pack: 6 Geode,filler,"GEODE,RESOURCE_PACK" +5059,Resource Pack: 12 Geode,filler,"GEODE,RESOURCE_PACK" +5060,Resource Pack: 18 Geode,filler,"GEODE,RESOURCE_PACK" +5061,Resource Pack: 24 Geode,filler,"GEODE,RESOURCE_PACK" +5062,Resource Pack: 4 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5063,Resource Pack: 8 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5064,Resource Pack: 12 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5065,Resource Pack: 16 Frozen Geode,filler,"GEODE,RESOURCE_PACK" +5066,Resource Pack: 3 Magma Geode,filler,"GEODE,RESOURCE_PACK" +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" +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" +5077,Resource Pack: 100 Copper Ore,filler,"ORE,RESOURCE_PACK" +5078,Resource Pack: 125 Copper Ore,filler,"ORE,RESOURCE_PACK" +5079,Resource Pack: 150 Copper Ore,filler,"ORE,RESOURCE_PACK" +5080,Resource Pack: 25 Iron Ore,filler,"ORE,RESOURCE_PACK" +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" +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" +5095,Resource Pack: 20 Quartz,filler,"ORE,RESOURCE_PACK" +5096,Resource Pack: 10 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5097,Resource Pack: 20 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5098,Resource Pack: 30 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5099,Resource Pack: 40 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5100,Resource Pack: 50 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5101,Resource Pack: 60 Basic Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5102,Resource Pack: 10 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5103,Resource Pack: 20 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5104,Resource Pack: 30 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5105,Resource Pack: 40 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5106,Resource Pack: 50 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5107,Resource Pack: 60 Basic Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5108,Resource Pack: 10 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5109,Resource Pack: 20 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5110,Resource Pack: 30 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5111,Resource Pack: 40 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5112,Resource Pack: 50 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5113,Resource Pack: 60 Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5114,Resource Pack: 4 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5115,Resource Pack: 12 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5116,Resource Pack: 20 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5117,Resource Pack: 28 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5118,Resource Pack: 36 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5119,Resource Pack: 40 Quality Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5120,Resource Pack: 4 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5121,Resource Pack: 12 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5122,Resource Pack: 20 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5123,Resource Pack: 28 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5124,Resource Pack: 36 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5125,Resource Pack: 40 Quality Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5126,Resource Pack: 4 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5127,Resource Pack: 12 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5128,Resource Pack: 20 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +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" +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" +5153,Resource Pack: 14 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5154,Resource Pack: 18 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5155,Resource Pack: 20 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5156,Resource Pack: 10 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5157,Resource Pack: 20 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5158,Resource Pack: 30 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5159,Resource Pack: 40 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5160,Resource Pack: 50 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5161,Resource Pack: 60 Spring Seeds,filler,"RESOURCE_PACK,SEED" +5162,Resource Pack: 10 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5163,Resource Pack: 20 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5164,Resource Pack: 30 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5165,Resource Pack: 40 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5166,Resource Pack: 50 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5167,Resource Pack: 60 Summer Seeds,filler,"RESOURCE_PACK,SEED" +5168,Resource Pack: 10 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5169,Resource Pack: 20 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5170,Resource Pack: 30 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5171,Resource Pack: 40 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5172,Resource Pack: 50 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5173,Resource Pack: 60 Fall Seeds,filler,"RESOURCE_PACK,SEED" +5174,Resource Pack: 10 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5175,Resource Pack: 20 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5176,Resource Pack: 30 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5177,Resource Pack: 40 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5178,Resource Pack: 50 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5179,Resource Pack: 60 Winter Seeds,filler,"RESOURCE_PACK,SEED" +5180,Resource Pack: 1 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5181,Resource Pack: 3 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5182,Resource Pack: 5 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5183,Resource Pack: 7 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5184,Resource Pack: 9 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5185,Resource Pack: 10 Mahogany Seed,filler,"RESOURCE_PACK,SEED" +5186,Resource Pack: 10 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5187,Resource Pack: 20 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5188,Resource Pack: 30 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5189,Resource Pack: 40 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5190,Resource Pack: 50 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5191,Resource Pack: 60 Bait,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5192,Resource Pack: 1 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5193,Resource Pack: 2 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +5194,Resource Pack: 3 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" +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 diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv new file mode 100644 index 00000000..abad3c04 --- /dev/null +++ b/worlds/stardew_valley/data/locations.csv @@ -0,0 +1,379 @@ +id,region,name,tags +1,Crafts Room,Spring Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +2,Crafts Room,Summer Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +3,Crafts Room,Fall Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +4,Crafts Room,Winter Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +5,Crafts Room,Construction Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +6,Crafts Room,Exotic Foraging Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,CRAFTS_ROOM_BUNDLE,MANDATORY" +7,Pantry,Spring Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +8,Pantry,Summer Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +9,Pantry,Fall Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +10,Pantry,Quality Crops Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +11,Pantry,Animal Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +12,Pantry,Artisan Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,PANTRY_BUNDLE" +13,Fish Tank,River Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +14,Fish Tank,Lake Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +15,Fish Tank,Ocean Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +16,Fish Tank,Night Fishing Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +17,Fish Tank,Crab Pot Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +18,Fish Tank,Specialty Fish Bundle,"BUNDLE,COMMUNITY_CENTER_BUNDLE,FISH_TANK_BUNDLE,MANDATORY" +19,Boiler Room,Blacksmith's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +20,Boiler Room,Geologist's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +21,Boiler Room,Adventurer's Bundle,"BOILER_ROOM_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +22,Bulletin Board,Chef's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +23,Bulletin Board,Dye Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +24,Bulletin Board,Field Research Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +25,Bulletin Board,Fodder Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +26,Bulletin Board,Enchanter's Bundle,"BULLETIN_BOARD_BUNDLE,BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY" +27,Vault,"2,500g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +28,Vault,"5,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +29,Vault,"10,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +30,Vault,"25,000g Bundle","BUNDLE,COMMUNITY_CENTER_BUNDLE,MANDATORY,VAULT_BUNDLE" +31,Abandoned JojaMart,The Missing Bundle,BUNDLE +32,Crafts Room,Complete Crafts Room,"COMMUNITY_CENTER_ROOM,MANDATORY" +33,Pantry,Complete Pantry,"COMMUNITY_CENTER_ROOM,MANDATORY" +34,Fish Tank,Complete Fish Tank,"COMMUNITY_CENTER_ROOM,MANDATORY" +35,Boiler Room,Complete Boiler Room,"COMMUNITY_CENTER_ROOM,MANDATORY" +36,Bulletin Board,Complete Bulletin Board,"COMMUNITY_CENTER_ROOM,MANDATORY" +37,Vault,Complete Vault,"COMMUNITY_CENTER_ROOM,MANDATORY" +101,Pierre's General Store,Large Pack,BACKPACK +102,Pierre's General Store,Deluxe Pack,BACKPACK +103,Clint's Blacksmith,Copper Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +104,Clint's Blacksmith,Iron Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +105,Clint's Blacksmith,Gold Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +106,Clint's Blacksmith,Iridium Hoe Upgrade,"HOE_UPGRADE,TOOL_UPGRADE" +107,Clint's Blacksmith,Copper Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +108,Clint's Blacksmith,Iron Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +109,Clint's Blacksmith,Gold Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +110,Clint's Blacksmith,Iridium Pickaxe Upgrade,"PICKAXE_UPGRADE,TOOL_UPGRADE" +111,Clint's Blacksmith,Copper Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +112,Clint's Blacksmith,Iron Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +113,Clint's Blacksmith,Gold Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +114,Clint's Blacksmith,Iridium Axe Upgrade,"AXE_UPGRADE,TOOL_UPGRADE" +115,Clint's Blacksmith,Copper Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +116,Clint's Blacksmith,Iron Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +117,Clint's Blacksmith,Gold Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +118,Clint's Blacksmith,Iridium Watering Can Upgrade,"TOOL_UPGRADE,WATERING_CAN_UPGRADE" +119,Clint's Blacksmith,Copper Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +120,Clint's Blacksmith,Iron Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +121,Clint's Blacksmith,Gold Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +122,Clint's Blacksmith,Iridium Trash Can Upgrade,"TOOL_UPGRADE,TRASH_CAN_UPGRADE" +123,Willy's Fish Shop,Purchase Training Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +124,Stardew Valley,Bamboo Pole Cutscene,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +125,Willy's Fish Shop,Purchase Fiberglass Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +126,Willy's Fish Shop,Purchase Iridium Rod,"FISHING_ROD_UPGRADE,TOOL_UPGRADE" +201,The Mines - Floor 10,The Mines Floor 10 Treasure,"MANDATORY,THE_MINES_TREASURE" +202,The Mines - Floor 20,The Mines Floor 20 Treasure,"MANDATORY,THE_MINES_TREASURE" +203,The Mines - Floor 40,The Mines Floor 40 Treasure,"MANDATORY,THE_MINES_TREASURE" +204,The Mines - Floor 50,The Mines Floor 50 Treasure,"MANDATORY,THE_MINES_TREASURE" +205,The Mines - Floor 60,The Mines Floor 60 Treasure,"MANDATORY,THE_MINES_TREASURE" +206,The Mines - Floor 70,The Mines Floor 70 Treasure,"MANDATORY,THE_MINES_TREASURE" +207,The Mines - Floor 80,The Mines Floor 80 Treasure,"MANDATORY,THE_MINES_TREASURE" +208,The Mines - Floor 90,The Mines Floor 90 Treasure,"MANDATORY,THE_MINES_TREASURE" +209,The Mines - Floor 100,The Mines Floor 100 Treasure,"MANDATORY,THE_MINES_TREASURE" +210,The Mines - Floor 110,The Mines Floor 110 Treasure,"MANDATORY,THE_MINES_TREASURE" +211,The Mines - Floor 120,The Mines Floor 120 Treasure,"MANDATORY,THE_MINES_TREASURE" +212,Quarry Mine,Grim Reaper statue,MANDATORY +213,The Mines,The Mines Entrance Cutscene,MANDATORY +214,The Mines - Floor 5,Floor 5 Elevator,THE_MINES_ELEVATOR +215,The Mines - Floor 10,Floor 10 Elevator,THE_MINES_ELEVATOR +216,The Mines - Floor 15,Floor 15 Elevator,THE_MINES_ELEVATOR +217,The Mines - Floor 20,Floor 20 Elevator,THE_MINES_ELEVATOR +218,The Mines - Floor 25,Floor 25 Elevator,THE_MINES_ELEVATOR +219,The Mines - Floor 30,Floor 30 Elevator,THE_MINES_ELEVATOR +220,The Mines - Floor 35,Floor 35 Elevator,THE_MINES_ELEVATOR +221,The Mines - Floor 40,Floor 40 Elevator,THE_MINES_ELEVATOR +222,The Mines - Floor 45,Floor 45 Elevator,THE_MINES_ELEVATOR +223,The Mines - Floor 50,Floor 50 Elevator,THE_MINES_ELEVATOR +224,The Mines - Floor 55,Floor 55 Elevator,THE_MINES_ELEVATOR +225,The Mines - Floor 60,Floor 60 Elevator,THE_MINES_ELEVATOR +226,The Mines - Floor 65,Floor 65 Elevator,THE_MINES_ELEVATOR +227,The Mines - Floor 70,Floor 70 Elevator,THE_MINES_ELEVATOR +228,The Mines - Floor 75,Floor 75 Elevator,THE_MINES_ELEVATOR +229,The Mines - Floor 80,Floor 80 Elevator,THE_MINES_ELEVATOR +230,The Mines - Floor 85,Floor 85 Elevator,THE_MINES_ELEVATOR +231,The Mines - Floor 90,Floor 90 Elevator,THE_MINES_ELEVATOR +232,The Mines - Floor 95,Floor 95 Elevator,THE_MINES_ELEVATOR +233,The Mines - Floor 100,Floor 100 Elevator,THE_MINES_ELEVATOR +234,The Mines - Floor 105,Floor 105 Elevator,THE_MINES_ELEVATOR +235,The Mines - Floor 110,Floor 110 Elevator,THE_MINES_ELEVATOR +236,The Mines - Floor 115,Floor 115 Elevator,THE_MINES_ELEVATOR +237,The Mines - Floor 120,Floor 120 Elevator,THE_MINES_ELEVATOR +301,Stardew Valley,Level 1 Farming,"FARMING_LEVEL,SKILL_LEVEL" +302,Stardew Valley,Level 2 Farming,"FARMING_LEVEL,SKILL_LEVEL" +303,Stardew Valley,Level 3 Farming,"FARMING_LEVEL,SKILL_LEVEL" +304,Stardew Valley,Level 4 Farming,"FARMING_LEVEL,SKILL_LEVEL" +305,Stardew Valley,Level 5 Farming,"FARMING_LEVEL,SKILL_LEVEL" +306,Stardew Valley,Level 6 Farming,"FARMING_LEVEL,SKILL_LEVEL" +307,Stardew Valley,Level 7 Farming,"FARMING_LEVEL,SKILL_LEVEL" +308,Stardew Valley,Level 8 Farming,"FARMING_LEVEL,SKILL_LEVEL" +309,Stardew Valley,Level 9 Farming,"FARMING_LEVEL,SKILL_LEVEL" +310,Stardew Valley,Level 10 Farming,"FARMING_LEVEL,SKILL_LEVEL" +311,Stardew Valley,Level 1 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +312,Stardew Valley,Level 2 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +313,Stardew Valley,Level 3 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +314,Stardew Valley,Level 4 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +315,Stardew Valley,Level 5 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +316,Stardew Valley,Level 6 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +317,Stardew Valley,Level 7 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +318,Stardew Valley,Level 8 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +319,Stardew Valley,Level 9 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +320,Stardew Valley,Level 10 Fishing,"FISHING_LEVEL,SKILL_LEVEL" +321,Stardew Valley,Level 1 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +322,Stardew Valley,Level 2 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +323,Stardew Valley,Level 3 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +324,Stardew Valley,Level 4 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +325,Stardew Valley,Level 5 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +326,Stardew Valley,Level 6 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +327,Stardew Valley,Level 7 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +328,Stardew Valley,Level 8 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +329,Stardew Valley,Level 9 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +330,Stardew Valley,Level 10 Foraging,"FORAGING_LEVEL,SKILL_LEVEL" +331,Stardew Valley,Level 1 Mining,"MINING_LEVEL,SKILL_LEVEL" +332,Stardew Valley,Level 2 Mining,"MINING_LEVEL,SKILL_LEVEL" +333,Stardew Valley,Level 3 Mining,"MINING_LEVEL,SKILL_LEVEL" +334,Stardew Valley,Level 4 Mining,"MINING_LEVEL,SKILL_LEVEL" +335,Stardew Valley,Level 5 Mining,"MINING_LEVEL,SKILL_LEVEL" +336,Stardew Valley,Level 6 Mining,"MINING_LEVEL,SKILL_LEVEL" +337,Stardew Valley,Level 7 Mining,"MINING_LEVEL,SKILL_LEVEL" +338,Stardew Valley,Level 8 Mining,"MINING_LEVEL,SKILL_LEVEL" +339,Stardew Valley,Level 9 Mining,"MINING_LEVEL,SKILL_LEVEL" +340,Stardew Valley,Level 10 Mining,"MINING_LEVEL,SKILL_LEVEL" +341,Stardew Valley,Level 1 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +342,Stardew Valley,Level 2 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +343,Stardew Valley,Level 3 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +344,Stardew Valley,Level 4 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +345,Stardew Valley,Level 5 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +346,Stardew Valley,Level 6 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +347,Stardew Valley,Level 7 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +348,Stardew Valley,Level 8 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +349,Stardew Valley,Level 9 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +350,Stardew Valley,Level 10 Combat,"COMBAT_LEVEL,SKILL_LEVEL" +401,Carpenter Shop,Coop Blueprint,BUILDING_BLUEPRINT +402,Carpenter Shop,Big Coop Blueprint,BUILDING_BLUEPRINT +403,Carpenter Shop,Deluxe Coop Blueprint,BUILDING_BLUEPRINT +404,Carpenter Shop,Barn Blueprint,BUILDING_BLUEPRINT +405,Carpenter Shop,Big Barn Blueprint,BUILDING_BLUEPRINT +406,Carpenter Shop,Deluxe Barn Blueprint,BUILDING_BLUEPRINT +407,Carpenter Shop,Well Blueprint,BUILDING_BLUEPRINT +408,Carpenter Shop,Silo Blueprint,BUILDING_BLUEPRINT +409,Carpenter Shop,Mill Blueprint,BUILDING_BLUEPRINT +410,Carpenter Shop,Shed Blueprint,BUILDING_BLUEPRINT +411,Carpenter Shop,Big Shed Blueprint,BUILDING_BLUEPRINT +412,Carpenter Shop,Fish Pond Blueprint,BUILDING_BLUEPRINT +413,Carpenter Shop,Stable Blueprint,BUILDING_BLUEPRINT +414,Carpenter Shop,Slime Hutch Blueprint,BUILDING_BLUEPRINT +415,Carpenter Shop,Shipping Bin Blueprint,BUILDING_BLUEPRINT +416,Carpenter Shop,Kitchen Blueprint,BUILDING_BLUEPRINT +417,Carpenter Shop,Kids Room Blueprint,BUILDING_BLUEPRINT +418,Carpenter Shop,Cellar Blueprint,BUILDING_BLUEPRINT +501,Town,Introductions,"MANDATORY,QUEST" +502,Town,How To Win Friends,"MANDATORY,QUEST" +503,Farm,Getting Started,"MANDATORY,QUEST" +504,Farm,Raising Animals,"MANDATORY,QUEST" +505,Farm,Advancement,"MANDATORY,QUEST" +506,Museum,Archaeology,"MANDATORY,QUEST" +507,Wizard Tower,Meet The Wizard,"MANDATORY,QUEST" +508,Farm,Forging Ahead,"MANDATORY,QUEST" +509,Farm,Smelting,"MANDATORY,QUEST" +510,The Mines - Floor 5,Initiation,"MANDATORY,QUEST" +511,Forest,Robin's Lost Axe,"MANDATORY,QUEST" +512,Sam's House,Jodi's Request,"MANDATORY,QUEST" +513,Marnie's Ranch,"Mayor's ""Shorts""","MANDATORY,QUEST" +514,Tunnel Entrance,Blackberry Basket,"MANDATORY,QUEST" +515,Marnie's Ranch,Marnie's Request,"MANDATORY,QUEST" +516,Town,Pam Is Thirsty,"MANDATORY,QUEST" +517,Wizard Tower,A Dark Reagent,"MANDATORY,QUEST" +518,Marnie's Ranch,Cow's Delight,"MANDATORY,QUEST" +519,Skull Cavern Entrance,The Skull Key,"MANDATORY,QUEST" +520,Town,Crop Research,"MANDATORY,QUEST" +521,Town,Knee Therapy,"MANDATORY,QUEST" +522,Town,Robin's Request,"MANDATORY,QUEST" +523,Skull Cavern,Qi's Challenge,"MANDATORY,QUEST" +524,The Desert,The Mysterious Qi,"MANDATORY,QUEST" +525,Town,Carving Pumpkins,"MANDATORY,QUEST" +526,Town,A Winter Mystery,"MANDATORY,QUEST" +527,Secret Woods,Strange Note,"MANDATORY,QUEST" +528,Skull Cavern,Cryptic Note,"MANDATORY,QUEST" +529,Town,Fresh Fruit,"MANDATORY,QUEST" +530,Town,Aquatic Research,"MANDATORY,QUEST" +531,Town,A Soldier's Star,"MANDATORY,QUEST" +532,Town,Mayor's Need,"MANDATORY,QUEST" +533,Saloon,Wanted: Lobster,"MANDATORY,QUEST" +534,Town,Pam Needs Juice,"MANDATORY,QUEST" +535,Sam's House,Fish Casserole,"MANDATORY,QUEST" +536,Beach,Catch A Squid,"MANDATORY,QUEST" +537,Saloon,Fish Stew,"MANDATORY,QUEST" +538,Town,Pierre's Notice,"MANDATORY,QUEST" +539,Town,Clint's Attempt,"MANDATORY,QUEST" +540,Town,A Favor For Clint,"MANDATORY,QUEST" +541,Wizard Tower,Staff Of Power,"MANDATORY,QUEST" +542,Town,Granny's Gift,"MANDATORY,QUEST" +543,Saloon,Exotic Spirits,"MANDATORY,QUEST" +544,Town,Catch a Lingcod,"MANDATORY,QUEST" +601,JotPK World 1,JotPK: Boots 1,"ARCADE_MACHINE,JOTPK" +602,JotPK World 1,JotPK: Boots 2,"ARCADE_MACHINE,JOTPK" +603,JotPK World 1,JotPK: Gun 1,"ARCADE_MACHINE,JOTPK" +604,JotPK World 2,JotPK: Gun 2,"ARCADE_MACHINE,JOTPK" +605,JotPK World 2,JotPK: Gun 3,"ARCADE_MACHINE,JOTPK" +606,JotPK World 3,JotPK: Super Gun,"ARCADE_MACHINE,JOTPK" +607,JotPK World 1,JotPK: Ammo 1,"ARCADE_MACHINE,JOTPK" +608,JotPK World 2,JotPK: Ammo 2,"ARCADE_MACHINE,JOTPK" +609,JotPK World 3,JotPK: Ammo 3,"ARCADE_MACHINE,JOTPK" +610,JotPK World 1,JotPK: Cowboy 1,"ARCADE_MACHINE,JOTPK" +611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK" +612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART" +613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART" +614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART" +615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART" +616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART" +617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART" +618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART" +619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART" +620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JUNIMO_KART" +621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART" +701,Secret Woods,Old Master Cannoli,MANDATORY +702,Beach,Beach Bridge Repair,MANDATORY +703,The Desert,Galaxy Sword Shrine,MANDATORY +801,Town,Help Wanted: Gathering 1,HELP_WANTED +802,Town,Help Wanted: Gathering 2,HELP_WANTED +803,Town,Help Wanted: Gathering 3,HELP_WANTED +804,Town,Help Wanted: Gathering 4,HELP_WANTED +805,Town,Help Wanted: Gathering 5,HELP_WANTED +806,Town,Help Wanted: Gathering 6,HELP_WANTED +807,Town,Help Wanted: Gathering 7,HELP_WANTED +808,Town,Help Wanted: Gathering 8,HELP_WANTED +811,Town,Help Wanted: Slay Monsters 1,HELP_WANTED +812,Town,Help Wanted: Slay Monsters 2,HELP_WANTED +813,Town,Help Wanted: Slay Monsters 3,HELP_WANTED +814,Town,Help Wanted: Slay Monsters 4,HELP_WANTED +815,Town,Help Wanted: Slay Monsters 5,HELP_WANTED +816,Town,Help Wanted: Slay Monsters 6,HELP_WANTED +817,Town,Help Wanted: Slay Monsters 7,HELP_WANTED +818,Town,Help Wanted: Slay Monsters 8,HELP_WANTED +821,Town,Help Wanted: Fishing 1,HELP_WANTED +822,Town,Help Wanted: Fishing 2,HELP_WANTED +823,Town,Help Wanted: Fishing 3,HELP_WANTED +824,Town,Help Wanted: Fishing 4,HELP_WANTED +825,Town,Help Wanted: Fishing 5,HELP_WANTED +826,Town,Help Wanted: Fishing 6,HELP_WANTED +827,Town,Help Wanted: Fishing 7,HELP_WANTED +828,Town,Help Wanted: Fishing 8,HELP_WANTED +841,Town,Help Wanted: Item Delivery 1,HELP_WANTED +842,Town,Help Wanted: Item Delivery 2,HELP_WANTED +843,Town,Help Wanted: Item Delivery 3,HELP_WANTED +844,Town,Help Wanted: Item Delivery 4,HELP_WANTED +845,Town,Help Wanted: Item Delivery 5,HELP_WANTED +846,Town,Help Wanted: Item Delivery 6,HELP_WANTED +847,Town,Help Wanted: Item Delivery 7,HELP_WANTED +848,Town,Help Wanted: Item Delivery 8,HELP_WANTED +849,Town,Help Wanted: Item Delivery 9,HELP_WANTED +850,Town,Help Wanted: Item Delivery 10,HELP_WANTED +851,Town,Help Wanted: Item Delivery 11,HELP_WANTED +852,Town,Help Wanted: Item Delivery 12,HELP_WANTED +853,Town,Help Wanted: Item Delivery 13,HELP_WANTED +854,Town,Help Wanted: Item Delivery 14,HELP_WANTED +855,Town,Help Wanted: Item Delivery 15,HELP_WANTED +856,Town,Help Wanted: Item Delivery 16,HELP_WANTED +857,Town,Help Wanted: Item Delivery 17,HELP_WANTED +858,Town,Help Wanted: Item Delivery 18,HELP_WANTED +859,Town,Help Wanted: Item Delivery 19,HELP_WANTED +860,Town,Help Wanted: Item Delivery 20,HELP_WANTED +861,Town,Help Wanted: Item Delivery 21,HELP_WANTED +862,Town,Help Wanted: Item Delivery 22,HELP_WANTED +863,Town,Help Wanted: Item Delivery 23,HELP_WANTED +864,Town,Help Wanted: Item Delivery 24,HELP_WANTED +865,Town,Help Wanted: Item Delivery 25,HELP_WANTED +866,Town,Help Wanted: Item Delivery 26,HELP_WANTED +867,Town,Help Wanted: Item Delivery 27,HELP_WANTED +868,Town,Help Wanted: Item Delivery 28,HELP_WANTED +869,Town,Help Wanted: Item Delivery 29,HELP_WANTED +870,Town,Help Wanted: Item Delivery 30,HELP_WANTED +871,Town,Help Wanted: Item Delivery 31,HELP_WANTED +872,Town,Help Wanted: Item Delivery 32,HELP_WANTED +901,Forest,Traveling Merchant Sunday Item 1,"MANDATORY,TRAVELING_MERCHANT" +902,Forest,Traveling Merchant Sunday Item 2,"MANDATORY,TRAVELING_MERCHANT" +903,Forest,Traveling Merchant Sunday Item 3,"MANDATORY,TRAVELING_MERCHANT" +911,Forest,Traveling Merchant Monday Item 1,"MANDATORY,TRAVELING_MERCHANT" +912,Forest,Traveling Merchant Monday Item 2,"MANDATORY,TRAVELING_MERCHANT" +913,Forest,Traveling Merchant Monday Item 3,"MANDATORY,TRAVELING_MERCHANT" +921,Forest,Traveling Merchant Tuesday Item 1,"MANDATORY,TRAVELING_MERCHANT" +922,Forest,Traveling Merchant Tuesday Item 2,"MANDATORY,TRAVELING_MERCHANT" +923,Forest,Traveling Merchant Tuesday Item 3,"MANDATORY,TRAVELING_MERCHANT" +931,Forest,Traveling Merchant Wednesday Item 1,"MANDATORY,TRAVELING_MERCHANT" +932,Forest,Traveling Merchant Wednesday Item 2,"MANDATORY,TRAVELING_MERCHANT" +933,Forest,Traveling Merchant Wednesday Item 3,"MANDATORY,TRAVELING_MERCHANT" +941,Forest,Traveling Merchant Thursday Item 1,"MANDATORY,TRAVELING_MERCHANT" +942,Forest,Traveling Merchant Thursday Item 2,"MANDATORY,TRAVELING_MERCHANT" +943,Forest,Traveling Merchant Thursday Item 3,"MANDATORY,TRAVELING_MERCHANT" +951,Forest,Traveling Merchant Friday Item 1,"MANDATORY,TRAVELING_MERCHANT" +952,Forest,Traveling Merchant Friday Item 2,"MANDATORY,TRAVELING_MERCHANT" +953,Forest,Traveling Merchant Friday Item 3,"MANDATORY,TRAVELING_MERCHANT" +961,Forest,Traveling Merchant Saturday Item 1,"MANDATORY,TRAVELING_MERCHANT" +962,Forest,Traveling Merchant Saturday Item 2,"MANDATORY,TRAVELING_MERCHANT" +963,Forest,Traveling Merchant Saturday Item 3,"MANDATORY,TRAVELING_MERCHANT" +1001,Mountain,Fishsanity: Carp,FISHSANITY +1002,Beach,Fishsanity: Herring,FISHSANITY +1003,Forest,Fishsanity: Smallmouth Bass,FISHSANITY +1004,Beach,Fishsanity: Anchovy,FISHSANITY +1005,Beach,Fishsanity: Sardine,FISHSANITY +1006,Forest,Fishsanity: Sunfish,FISHSANITY +1007,Forest,Fishsanity: Perch,FISHSANITY +1008,Forest,Fishsanity: Chub,FISHSANITY +1009,Forest,Fishsanity: Bream,FISHSANITY +1010,Beach,Fishsanity: Red Snapper,FISHSANITY +1011,Beach,Fishsanity: Sea Cucumber,FISHSANITY +1012,Forest,Fishsanity: Rainbow Trout,FISHSANITY +1013,Forest,Fishsanity: Walleye,FISHSANITY +1014,Forest,Fishsanity: Shad,FISHSANITY +1015,Mountain,Fishsanity: Bullhead,FISHSANITY +1016,Mountain,Fishsanity: Largemouth Bass,FISHSANITY +1017,Forest,Fishsanity: Salmon,FISHSANITY +1018,The Mines - Floor 20,Fishsanity: Ghostfish,FISHSANITY +1019,Beach,Fishsanity: Tilapia,FISHSANITY +1020,Secret Woods,Fishsanity: Woodskip,FISHSANITY +1021,Beach,Fishsanity: Flounder,FISHSANITY +1022,Beach,Fishsanity: Halibut,FISHSANITY +1023,Ginger Island,Fishsanity: Lionfish,FISHSANITY +1024,Mutant Bug Lair,Fishsanity: Slimejack,FISHSANITY +1025,Forest,Fishsanity: Midnight Carp,FISHSANITY +1026,Beach,Fishsanity: Red Mullet,FISHSANITY +1027,Forest,Fishsanity: Pike,FISHSANITY +1028,Forest,Fishsanity: Tiger Trout,FISHSANITY +1029,Ginger Island,Fishsanity: Blue Discus,FISHSANITY +1030,Beach,Fishsanity: Albacore,FISHSANITY +1031,The Desert,Fishsanity: Sandfish,FISHSANITY +1032,The Mines - Floor 20,Fishsanity: Stonefish,FISHSANITY +1033,Beach,Fishsanity: Tuna,FISHSANITY +1034,Beach,Fishsanity: Eel,FISHSANITY +1035,Forest,Fishsanity: Catfish,FISHSANITY +1036,Beach,Fishsanity: Squid,FISHSANITY +1037,Mountain,Fishsanity: Sturgeon,FISHSANITY +1038,Forest,Fishsanity: Dorado,FISHSANITY +1039,Beach,Fishsanity: Pufferfish,FISHSANITY +1040,Witch's Swamp,Fishsanity: Void Salmon,FISHSANITY +1041,Beach,Fishsanity: Super Cucumber,FISHSANITY +1042,Ginger Island,Fishsanity: Stingray,FISHSANITY +1043,The Mines - Floor 60,Fishsanity: Ice Pip,FISHSANITY +1044,Forest,Fishsanity: Lingcod,FISHSANITY +1045,The Desert,Fishsanity: Scorpion Carp,FISHSANITY +1046,The Mines - Floor 100,Fishsanity: Lava Eel,FISHSANITY +1047,Beach,Fishsanity: Octopus,FISHSANITY +1048,Beach,Fishsanity: Midnight Squid,FISHSANITY +1049,Beach,Fishsanity: Spook Fish,FISHSANITY +1050,Beach,Fishsanity: Blobfish,FISHSANITY +1051,Beach,Fishsanity: Crimsonfish,FISHSANITY +1052,Town,Fishsanity: Angler,FISHSANITY +1053,Mountain,Fishsanity: Legend,FISHSANITY +1054,Forest,Fishsanity: Glacierfish,FISHSANITY +1055,Sewers,Fishsanity: Mutant Carp,FISHSANITY +1056,Town,Fishsanity: Crayfish,FISHSANITY +1057,Town,Fishsanity: Snail,FISHSANITY +1058,Town,Fishsanity: Periwinkle,FISHSANITY +1059,Beach,Fishsanity: Lobster,FISHSANITY +1060,Beach,Fishsanity: Clam,FISHSANITY +1061,Beach,Fishsanity: Crab,FISHSANITY +1062,Beach,Fishsanity: Cockle,FISHSANITY +1063,Beach,Fishsanity: Mussel,FISHSANITY +1064,Beach,Fishsanity: Shrimp,FISHSANITY +1065,Beach,Fishsanity: Oyster,FISHSANITY diff --git a/worlds/stardew_valley/data/resource_packs.csv b/worlds/stardew_valley/data/resource_packs.csv new file mode 100644 index 00000000..0508ee35 --- /dev/null +++ b/worlds/stardew_valley/data/resource_packs.csv @@ -0,0 +1,39 @@ +name,default_amount,scaling_factor,classification,groups +Money,1000,500,useful,BASE_RESOURCE +Stone,50,25,filler,BASE_RESOURCE +Wood,50,25,filler,BASE_RESOURCE +Hardwood,10,5,useful,BASE_RESOURCE +Fiber,30,15,filler,BASE_RESOURCE +Coal,10,5,filler,BASE_RESOURCE +Clay,10,5,filler,BASE_RESOURCE +Warp Totem: Beach,5,2,filler,WARP_TOTEM +Warp Totem: Desert,5,2,filler,WARP_TOTEM +Warp Totem: Farm,5,2,filler,WARP_TOTEM +Warp Totem: Island,5,2,filler,WARP_TOTEM +Warp Totem: Mountains,5,2,filler,WARP_TOTEM +Geode,12,6,filler,GEODE +Frozen Geode,8,4,filler,GEODE +Magma Geode,6,3,filler,GEODE +Omni Geode,4,2,useful,GEODE +Copper Ore,75,25,filler,ORE +Iron Ore,50,25,filler,ORE +Gold Ore,25,13,useful,ORE +Iridium Ore,10,5,useful,ORE +Quartz,10,5,filler,ORE +Basic Fertilizer,30,10,filler,FERTILIZER +Basic Retaining Soil,30,10,filler,FERTILIZER +Speed-Gro,30,10,filler,FERTILIZER +Quality Fertilizer,20,8,filler,FERTILIZER +Quality Retaining Soil,20,8,filler,FERTILIZER +Deluxe Speed-Gro,20,8,filler,FERTILIZER +Deluxe Fertilizer,10,4,useful,FERTILIZER +Deluxe Retaining Soil,10,4,useful,FERTILIZER +Hyper Speed-Gro,10,4,useful,FERTILIZER +Tree Fertilizer,10,4,filler,FERTILIZER +Spring Seeds,30,10,filler,SEED +Summer Seeds,30,10,filler,SEED +Fall Seeds,30,10,filler,SEED +Winter Seeds,30,10,filler,SEED +Mahogany Seed,5,2,filler,SEED +Bait,30,10,filler,FISHING_RESOURCE +Crab Pot,3,1,filler,FISHING_RESOURCE \ No newline at end of file diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md new file mode 100644 index 00000000..aef28086 --- /dev/null +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -0,0 +1,71 @@ +# Stardew Valley + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +A vast number of optional objectives in stardew valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file. + +For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply a stack of an item that may be useful to the player. + +## What is the goal of Stardew Valley? + +The player can choose from a number of goals, using their YAML settings. +- Complete the Community Center +- Succeed Grandpa's Evaluation with 4 lit candles +- 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 + +## What are location check in Stardew Valley? + +Location checks in Stardew Valley always include: +- Community Center Bundles +- Mineshaft chest rewards +- Story Quests +- Traveling Merchant items +- Isolated objectives such as the beach bridge, Old Master Cannoli, Grim Reaper Statue, etc + +There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: +- Tools and Fishing Rod Upgrades +- Carpenter Buildings +- Backpack Upgrades +- Mine elevator levels +- Skill Levels +- Arcade Machines +- Help Wanted quests +- Fishsanity: Catching individual fish + +## 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 +- Wizard Buildings +- Return Scepter + +And lastly, some Archipelago-exclusive items exist in the pool, which are designed around game balance and QoL. These include: +- Arcade Machine buffs (Only if the arcade machines are randomized) + - Journey of the Prairie King has drop rate increases, extra lives, and equipment + - Junimo Kart has extra lives. +- Permanent Movement Speed Bonuses (customizable) +- Permanent Luck Bonuses (customizable) +- Traveling Merchant buffs + +## When the player receives an item, what happens? + +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. +If an item is received while offline, it will be in the mailbox as soon as the player logs in. + +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. + +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. + +## Multiplayer + +You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. + +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md new file mode 100644 index 00000000..7ac9c8a8 --- /dev/null +++ b/worlds/stardew_valley/docs/setup_en.md @@ -0,0 +1,74 @@ +# Stardew Valley Randomizer Setup Guide + +## Required Software + +- 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. + +## Optional Software +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) + - (Only for the TextClient) +- Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) + - It is **not** recommended to further mod Stardew Valley, altough it is possible to do so. Mod interactions can be unpredictable, and no support will be offered for related bugs. + - The more mods you have, and the bigger they are, the more likely things are to break. + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a YAML file? + +You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew Valley/player-settings) + +## Joining a MultiWorld Game + +### Installing the mod + +- Install [SMAPI](https://smapi.io/) by following the instructions on their website +- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder +- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: + - "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command% +- Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly +- Stardew Valley should launch itself alongside a console which allows you to read mod information and interact with some of them. + +### Connect to the MultiServer + +Launch Stardew Valley with SMAPI. Once you have reached the Stardew Valley title screen, create a new farm. + +On the new character creation page, you will see 3 new fields, used to link your new character to an archipelago multiworld + +![image](https://i.imgur.com/b8KZy2F.png) + +You can customize your farm and character as much as desired. + +The Server text box needs to have both the address and the port, and your slotname is the name specified in your yaml + +`archipelago.gg:38281` + +`StardewPlayer` + +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. + +### Interacting with the MultiWorld from in-game + +When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other Stardew-exclusive chat commands you can use. + +Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game that supports chatting. + +Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain items, or check missing locations. + +It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the full history as well and may be better suited to read older messages. +For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run Stardew-exclusive commands. + +### Multiplayer + +You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature. \ No newline at end of file diff --git a/worlds/stardew_valley/fish_data.py b/worlds/stardew_valley/fish_data.py new file mode 100644 index 00000000..270accb4 --- /dev/null +++ b/worlds/stardew_valley/fish_data.py @@ -0,0 +1,127 @@ +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 new file mode 100644 index 00000000..6b8eb6c6 --- /dev/null +++ b/worlds/stardew_valley/game_item.py @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..03419a16 --- /dev/null +++ b/worlds/stardew_valley/items.py @@ -0,0 +1,376 @@ +import bisect +import csv +import enum +import itertools +import logging +import math +import typing +from collections import OrderedDict +from dataclasses import dataclass, field +from functools import cached_property +from pathlib import Path +from random import Random +from typing import Dict, List, Protocol, Union, Set, Optional, FrozenSet + +from BaseClasses import Item, ItemClassification +from . import options, data + +ITEM_CODE_OFFSET = 717000 + +logger = logging.getLogger(__name__) +world_folder = Path(__file__).parent + + +class Group(enum.Enum): + RESOURCE_PACK = enum.auto() + FRIENDSHIP_PACK = enum.auto() + COMMUNITY_REWARD = enum.auto() + TRASH = enum.auto() + MINES_FLOOR_10 = enum.auto() + MINES_FLOOR_20 = enum.auto() + MINES_FLOOR_50 = enum.auto() + MINES_FLOOR_60 = enum.auto() + MINES_FLOOR_80 = enum.auto() + MINES_FLOOR_90 = enum.auto() + MINES_FLOOR_110 = enum.auto() + FOOTWEAR = enum.auto() + HATS = enum.auto() + RING = enum.auto() + WEAPON = enum.auto() + PROGRESSIVE_TOOLS = enum.auto() + SKILL_LEVEL_UP = enum.auto() + ARCADE_MACHINE_BUFFS = enum.auto() + GALAXY_WEAPONS = enum.auto() + BASE_RESOURCE = enum.auto() + WARP_TOTEM = enum.auto() + GEODE = enum.auto() + ORE = enum.auto() + FERTILIZER = enum.auto() + SEED = enum.auto() + FISHING_RESOURCE = enum.auto() + + +@dataclass(frozen=True) +class ItemData: + code_without_offset: Optional[int] + name: str + classification: ItemClassification + groups: Set[Group] = field(default_factory=frozenset) + + def __post_init__(self): + if not isinstance(self.groups, frozenset): + super().__setattr__("groups", frozenset(self.groups)) + + @property + def code(self): + return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None + + def has_any_group(self, *group: Group) -> bool: + groups = set(group) + return bool(groups.intersection(self.groups)) + + +@dataclass(frozen=True) +class ResourcePackData: + name: str + default_amount: int = 1 + scaling_factor: int = 1 + classification: ItemClassification = ItemClassification.filler + groups: FrozenSet[Group] = frozenset() + + def as_item_data(self, counter: itertools.count) -> [ItemData]: + return [ItemData(next(counter), self.create_item_name(quantity), self.classification, + {Group.RESOURCE_PACK} | self.groups) + for quantity in self.scale_quantity.values()] + + def create_item_name(self, quantity: int) -> str: + return f"Resource Pack: {quantity} {self.name}" + + @cached_property + def scale_quantity(self) -> typing.OrderedDict[int, int]: + """Discrete scaling of the resource pack quantities. + 100 is default, 200 is double, 50 is half (if the scaling_factor allows it). + """ + levels = math.ceil(self.default_amount / self.scaling_factor) * 2 + first_level = self.default_amount % self.scaling_factor + if first_level == 0: + first_level = self.scaling_factor + quantities = sorted(set(range(first_level, self.scaling_factor * levels, self.scaling_factor)) + | {self.default_amount * 2}) + + return OrderedDict({round(quantity / self.default_amount * 100): quantity + for quantity in quantities + if quantity <= self.default_amount * 2}) + + def calculate_quantity(self, multiplier: int) -> int: + scales = list(self.scale_quantity) + left_scale = bisect.bisect_left(scales, multiplier) + closest_scale = min([scales[left_scale], scales[left_scale - 1]], + key=lambda x: abs(multiplier - x)) + return self.scale_quantity[closest_scale] + + def create_name_from_multiplier(self, multiplier: int) -> str: + return self.create_item_name(self.calculate_quantity(multiplier)) + + +class FriendshipPackData(ResourcePackData): + def create_item_name(self, quantity: int) -> str: + return f"Friendship Bonus ({quantity} <3)" + + def as_item_data(self, counter: itertools.count) -> [ItemData]: + item_datas = super().as_item_data(counter) + return [ItemData(item.code_without_offset, item.name, item.classification, {Group.FRIENDSHIP_PACK}) + for item in item_datas] + + +class StardewItemFactory(Protocol): + def __call__(self, name: Union[str, ItemData]) -> Item: + raise NotImplementedError + + +def load_item_csv(): + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + items = [] + with files(data).joinpath("items.csv").open() as file: + item_reader = csv.DictReader(file) + for item in item_reader: + id = int(item["id"]) if item["id"] else None + classification = ItemClassification[item["classification"]] + groups = {Group[group] for group in item["groups"].split(",") if group} + items.append(ItemData(id, item["name"], classification, groups)) + return items + + +def load_resource_pack_csv() -> List[ResourcePackData]: + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + resource_packs = [] + with files(data).joinpath("resource_packs.csv").open() as file: + resource_pack_reader = csv.DictReader(file) + for resource_pack in resource_pack_reader: + groups = frozenset(Group[group] for group in resource_pack["groups"].split(",") if group) + resource_packs.append(ResourcePackData(resource_pack["name"], + int(resource_pack["default_amount"]), + int(resource_pack["scaling_factor"]), + ItemClassification[resource_pack["classification"]], + groups)) + return resource_packs + + +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), +] + +all_items: List[ItemData] = load_item_csv() + events +item_table: Dict[str, ItemData] = {} +items_by_group: Dict[Group, List[ItemData]] = {} + + +def initialize_groups(): + for item in all_items: + for group in item.groups: + item_group = items_by_group.get(group, list()) + item_group.append(item) + items_by_group[group] = item_group + + +def initialize_item_table(): + item_table.update({item.name: item for item in all_items}) + + +friendship_pack = FriendshipPackData("Friendship Bonus", default_amount=2, classification=ItemClassification.useful) +all_resource_packs = load_resource_pack_csv() + +initialize_item_table() +initialize_groups() + + +def create_items(item_factory: StardewItemFactory, locations_count: int, world_options: options.StardewOptions, + random: Random) \ + -> List[Item]: + items = create_unique_items(item_factory, world_options, random) + 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") + + resource_pack_items = fill_with_resource_packs(item_factory, world_options, random, locations_count - len(items)) + items += resource_pack_items + logger.debug(f"Created {len(resource_pack_items)} resource packs") + + return items + + +def create_backpack_items(item_factory: StardewItemFactory, world_options: 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) + + +def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], random: Random): + items.append(item_factory("Rusty Sword")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_10]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_20]))) + items.append(item_factory("Slingshot")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_50]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_60]))) + items.append(item_factory("Master Slingshot")) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_80]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_90]))) + items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_110]))) + items.append(item_factory("Skull Key")) + + +def create_mine_elevators(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + if (world_options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive or + world_options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): + 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]): + 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]): + 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]) + + +def create_wizard_buildings(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Earth Obelisk")) + items.append(item_factory("Water Obelisk")) + items.append(item_factory("Desert Obelisk")) + items.append(item_factory("Island Obelisk")) + items.append(item_factory("Junimo Hut")) + items.append(item_factory("Gold Clock")) + + +def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: options.StardewOptions, + items: List[Item]): + if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, + options.BuildingProgression.option_progressive_early_shipping_bin}: + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Coop")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Progressive Barn")) + items.append(item_factory("Well")) + items.append(item_factory("Silo")) + items.append(item_factory("Mill")) + items.append(item_factory("Progressive Shed")) + items.append(item_factory("Progressive Shed")) + items.append(item_factory("Fish Pond")) + items.append(item_factory("Stable")) + items.append(item_factory("Slime Hutch")) + items.append(item_factory("Shipping Bin")) + items.append(item_factory("Progressive House")) + items.append(item_factory("Progressive House")) + items.append(item_factory("Progressive House")) + + +def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Adventurer's Guild")) + items.append(item_factory("Club Card")) + items.append(item_factory("Magnifying Glass")) + items.append(item_factory("Bear's Knowledge")) + items.append(item_factory("Iridium Snake Milk")) + + +def create_stardrops(item_factory: StardewItemFactory, items: List[Item]): + items.append(item_factory("Stardrop")) # The Mines level 100 + items.append(item_factory("Stardrop")) # Old Master Cannoli + + +def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, + items: List[Item]): + if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + items.append(item_factory("JotPK: Progressive Boots")) + items.append(item_factory("JotPK: Progressive Boots")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Gun")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Progressive Ammo")) + items.append(item_factory("JotPK: Extra Life")) + items.append(item_factory("JotPK: Extra Life")) + items.append(item_factory("JotPK: Increased Drop Rate")) + items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8) + + +def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): + number_of_buffs: int = world_options[options.NumberOfPlayerBuffs] + items.extend(item_factory(item) for item in ["Movement Speed Bonus"] * number_of_buffs) + 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_unique_items(item_factory: StardewItemFactory, world_options: 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_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 items + + +def fill_with_resource_packs(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, + required_resource_pack: int) -> List[Item]: + resource_pack_multiplier = world_options[options.ResourcePackMultiplier] + + if resource_pack_multiplier == 0: + return [item_factory(cola) for cola in ["Joja Cola"] * required_resource_pack] + + items = [] + + for i in range(required_resource_pack): + resource_pack = random.choice(all_resource_packs) + items.append(item_factory(resource_pack.create_name_from_multiplier(resource_pack_multiplier))) + + return items diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py new file mode 100644 index 00000000..a7cb70c5 --- /dev/null +++ b/worlds/stardew_valley/locations.py @@ -0,0 +1,175 @@ +import csv +import enum +from dataclasses import dataclass +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 + +LOCATION_CODE_OFFSET = 717000 + + +class LocationTags(enum.Enum): + MANDATORY = enum.auto() + BUNDLE = enum.auto() + COMMUNITY_CENTER_BUNDLE = enum.auto() + CRAFTS_ROOM_BUNDLE = enum.auto() + PANTRY_BUNDLE = enum.auto() + FISH_TANK_BUNDLE = enum.auto() + BOILER_ROOM_BUNDLE = enum.auto() + BULLETIN_BOARD_BUNDLE = enum.auto() + VAULT_BUNDLE = enum.auto() + COMMUNITY_CENTER_ROOM = enum.auto() + BACKPACK = enum.auto() + TOOL_UPGRADE = enum.auto() + HOE_UPGRADE = enum.auto() + PICKAXE_UPGRADE = enum.auto() + AXE_UPGRADE = enum.auto() + WATERING_CAN_UPGRADE = enum.auto() + TRASH_CAN_UPGRADE = enum.auto() + FISHING_ROD_UPGRADE = enum.auto() + THE_MINES_TREASURE = enum.auto() + THE_MINES_ELEVATOR = enum.auto() + SKILL_LEVEL = enum.auto() + FARMING_LEVEL = enum.auto() + FISHING_LEVEL = enum.auto() + FORAGING_LEVEL = enum.auto() + COMBAT_LEVEL = enum.auto() + MINING_LEVEL = enum.auto() + BUILDING_BLUEPRINT = enum.auto() + QUEST = enum.auto() + ARCADE_MACHINE = enum.auto() + ARCADE_MACHINE_VICTORY = enum.auto() + JOTPK = enum.auto() + JUNIMO_KART = enum.auto() + HELP_WANTED = enum.auto() + TRAVELING_MERCHANT = enum.auto() + FISHSANITY = enum.auto() + + +@dataclass(frozen=True) +class LocationData: + code_without_offset: Optional[int] + region: str + name: str + tags: FrozenSet[LocationTags] = frozenset() + + @property + def code(self) -> Optional[int]: + return LOCATION_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None + + +class StardewLocationCollector(Protocol): + def __call__(self, name: str, code: Optional[int], region: str) -> None: + raise NotImplementedError + + +def load_location_csv() -> List[LocationData]: + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files + + with files(data).joinpath("locations.csv").open() as file: + reader = csv.DictReader(file) + return [LocationData(int(location["id"]) if location["id"] else None, + location["region"], + location["name"], + frozenset(LocationTags[group] + for group in location["tags"].split(",") + if group)) + for location in reader] + + +events_locations = [ + LocationData(None, "Stardew Valley", "Succeed Grandpa's Evaluation"), + LocationData(None, "Community Center", "Complete Community Center"), + 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"), +] + +all_locations = load_location_csv() + events_locations +location_table: Dict[str, LocationData] = {location.name: location for location in all_locations} +locations_by_tag: Dict[LocationTags, List[LocationData]] = {} + + +def initialize_groups(): + for location in all_locations: + for tag in location.tags: + location_group = locations_by_tag.get(tag, list()) + location_group.append(location) + locations_by_tag[tag] = location_group + + +initialize_groups() + + +def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_number_of_quests: int): + for i in range(0, desired_number_of_quests): + batch = i // 7 + index_this_batch = i % 7 + if index_this_batch < 4: + randomized_locations.append( + location_table[f"Help Wanted: Item Delivery {(batch * 4) + index_this_batch + 1}"]) + elif index_this_batch == 4: + randomized_locations.append(location_table[f"Help Wanted: Fishing {batch + 1}"]) + elif index_this_batch == 5: + randomized_locations.append(location_table[f"Help Wanted: Slay Monsters {batch + 1}"]) + elif index_this_batch == 6: + randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"]) + + +def extend_fishsanity_locations(randomized_locations: List[LocationData], fishsanity: int, random: Random): + prefix = "Fishsanity: " + if fishsanity == options.Fishsanity.option_none: + return + elif fishsanity == options.Fishsanity.option_legendaries: + 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: + randomized_locations.extend(location_table[f"{prefix}{fish.name}"] + for fish in all_fish_items 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) + + +def create_locations(location_collector: StardewLocationCollector, + world_options: options.StardewOptions, + random: Random): + randomized_locations = [] + + randomized_locations.extend(locations_by_tag[LocationTags.MANDATORY]) + + if not world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.BACKPACK]) + + if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE]) + + if not world_options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.THE_MINES_ELEVATOR]) + + if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.SKILL_LEVEL]) + + if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + randomized_locations.extend(locations_by_tag[LocationTags.BUILDING_BLUEPRINT]) + + if not world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_disabled: + randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY]) + + if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: + randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE]) + + extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) + extend_fishsanity_locations(randomized_locations, world_options[options.Fishsanity], random) + + 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 new file mode 100644 index 00000000..79b2b63d --- /dev/null +++ b/worlds/stardew_valley/logic.py @@ -0,0 +1,1143 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List, FrozenSet + +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 .options import StardewOptions + +MISSING_ITEM = "THIS ITEM IS MISSING" + +tool_materials = { + "Copper": 1, + "Iron": 2, + "Gold": 3, + "Iridium": 4 +} + +tool_prices = { + "Copper": 2000, + "Iron": 5000, + "Gold": 10000, + "Iridium": 25000 +} + +skill_level_per_season = { + "Spring": { + "Farming": 2, + "Fishing": 2, + "Foraging": 2, + "Mining": 2, + "Combat": 2, + }, + "Summer": { + "Farming": 4, + "Fishing": 4, + "Foraging": 4, + "Mining": 4, + "Combat": 3, + }, + "Fall": { + "Farming": 7, + "Fishing": 5, + "Foraging": 5, + "Mining": 5, + "Combat": 4, + }, + "Winter": { + "Farming": 7, + "Fishing": 7, + "Foraging": 6, + "Mining": 7, + "Combat": 5, + }, + "Year Two": { + "Farming": 10, + "Fishing": 10, + "Foraging": 10, + "Mining": 10, + "Combat": 10, + }, +} +season_per_skill_level: Dict[Tuple[str, int], str] = {} +season_per_total_level: Dict[int, str] = {} + + +def initialize_season_per_skill_level(): + current_level = { + "Farming": 0, + "Fishing": 0, + "Foraging": 0, + "Mining": 0, + "Combat": 0, + } + for season, skills in skill_level_per_season.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 + 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 + + +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) +class StardewLogic: + player: int + options: StardewOptions + + item_rules: Dict[str, StardewRule] = field(default_factory=dict) + fish_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.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"), + "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"), + "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"), + "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), + "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), + "Caviar": self.has("Preserves Jar") & self.has("Sturgeon Roe"), + "Chanterelle": self.received("Fall") & self.can_reach_region("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_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(), + "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"), + "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")), + "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"), + "Cow": self.has_building("Barn"), + "Cow Milk": self.has("Milk") | self.has("Large Milk"), + "Crab": self.can_crab_pot(), + "Crab Pot": self.has_skill_level("Fishing", 3), + "Cranberries": self.received("Fall"), + "Crayfish": self.can_crab_pot(), + "Crocus": self.received("Winter"), + "Crystal Fruit": self.received("Winter"), + "Daffodil": self.received("Spring"), + "Dandelion": self.received("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"), + "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"), + "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(), + "Fried Egg": self.can_cook() & self.has("Any Egg"), + "Fried Mushroom": self.can_cook() & self.can_have_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(), + "Furnace": self.has("Stone") & self.has("Copper Ore"), + "Geode": self.can_mine_in_the_mines_floor_1_40(), + "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"), + "Green Algae": self.can_fish(), + "Green Bean": self.received("Spring"), + "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") | + (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"), + "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), + "JotPK Big Buff": self.has_jotpk_power_level(7), + "JotPK Max Buff": self.has_jotpk_power_level(9), + "Juice": self.has("Keg"), + "Junimo Kart Small Buff": self.has_junimo_kart_power_level(2), + "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"), + "Lightning Rod": self.has_skill_level("Foraging", 6), + "Lobster": self.can_crab_pot(), + "Loom": self.has_skill_level("Farming", 7) & self.has("Pine Tar"), + "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 Syrup": self.has("Tapper"), + "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"), + "Oak Resin": self.has("Tapper"), + "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_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(), + "Pale Ale": self.has("Keg") & self.has("Hops"), + "Pale Broth": self.can_cook() & self.can_have_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") & self.has("Cow Milk"), + "Peach": self.received("Summer"), + "Pepper Poppers": self.can_cook() & self.has("Cheese") & self.has( + "Hot Pepper") & self.can_have_relationship("Shane", 3), + "Periwinkle": self.can_crab_pot(), + "Pickles": self.has("Preserves Jar"), + "Pig": self.has_building("Deluxe Barn"), + "Pine Tar": self.has("Tapper"), + "Pizza": self.can_spend_money(600), + "Pomegranate": self.received("Fall"), + "Poppy": self.received("Summer"), + "Potato": self.received("Spring"), + "Preserves Jar": self.has_skill_level("Farming", 4), + "Prismatic Shard": self.received("Year Two"), + "Pumpkin": self.received("Fall"), + "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"), + "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") | + (self.has("Recycling Machine") & (self.has("Broken CD") | self.has("Broken Glasses"))), + "Rhubarb": self.received("Spring") & self.can_reach_region("The Desert"), + "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( + "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"), + "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"), + "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"), + "Staircase": self.has_skill_level("Mining", 2), + "Starfruit": (self.received("Summer") | self.received("Greenhouse")) & self.can_reach_region("The Desert"), + "Stone": self.has_tool("Pickaxe"), + "Strawberry": self.received("Spring"), + "Sturgeon Roe": self.has("Sturgeon") & self.has_building("Fish Pond"), + "Summer Spangle": self.received("Summer"), + "Sunflower": self.received("Summer") | self.received("Fall"), + "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"), + "Tapper": self.has_skill_level("Foraging", 3), + "Tomato": self.received("Summer"), + "Topaz": self.can_mine_in_the_mines_floor_1_40(), + "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"))), + "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"), + "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"), + "White Algae": self.can_fish() & self.can_mine_in_the_mines_floor_1_40(), + "Wild Horseradish": self.received("Spring"), + "Wild Plum": self.received("Fall"), + "Wilted Bouquet": self.has("Furnace") & self.has("Bouquet") & self.has("Coal"), + "Wine": self.has("Keg"), + "Winter Root": self.received("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.building_rules.update({ + "Barn": self.can_spend_money(6000) & self.has(["Wood", "Stone"]), + "Big Barn": self.can_spend_money(12000) & self.has(["Wood", "Stone"]) & self.has_building("Barn"), + "Deluxe Barn": self.can_spend_money(25000) & self.has(["Wood", "Stone"]) & self.has_building("Big Barn"), + "Coop": self.can_spend_money(4000) & self.has(["Wood", "Stone"]), + "Big Coop": self.can_spend_money(10000) & self.has(["Wood", "Stone"]) & self.has_building("Coop"), + "Deluxe Coop": self.can_spend_money(20000) & self.has(["Wood", "Stone"]) & self.has_building("Big Coop"), + "Fish Pond": self.can_spend_money(5000) & self.has(["Stone", "Seaweed", "Green Algae"]), + "Mill": self.can_spend_money(2500) & self.has(["Stone", "Wood", "Cloth"]), + "Shed": self.can_spend_money(15000) & self.has("Wood"), + "Big Shed": self.can_spend_money(20000) & self.has(["Wood", "Stone"]) & self.has_building("Shed"), + "Silo": self.can_spend_money(100) & self.has(["Stone", "Clay", "Copper Bar"]), + "Slime Hutch": self.can_spend_money(10000) & self.has(["Stone", "Refined Quartz", "Iridium Bar"]), + "Stable": self.can_spend_money(10000) & self.has(["Hardwood", "Iron Bar"]), + "Well": self.can_spend_money(1000) & self.has("Stone"), + "Shipping Bin": self.can_spend_money(250) & self.has("Wood"), + "Kitchen": self.can_spend_money(10000) & self.has("Wood") & self.has_house(0), + "Kids Room": self.can_spend_money(50000) & self.has("Hardwood") & self.has_house(1), + "Cellar": self.can_spend_money(100000) & self.has_house(2), + }) + + self.quest_rules.update({ + "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"), + "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"), + "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"), + "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( + "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( + "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"), + }) + + def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule: + if isinstance(items, str): + return _Has(items, self.item_rules) + + if count is None or count == len(items): + return _And(self.has(item) for item in items) + + if count == 1: + return _Or(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 isinstance(items, str): + return _Received(items, self.player, count) + + if count is None: + return _And(self.received(item) for item in items) + + if count == 1: + return _Or(self.received(item) for item in items) + + return _TotalReceived(count, items, self.player) + + def can_reach_region(self, spot: str) -> StardewRule: + 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) + + def can_reach_location(self, spot: str) -> StardewRule: + return _Reach(spot, "Location", self.player) + + def can_reach_entrance(self, spot: str) -> StardewRule: + 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") + + 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") + + def has_tool(self, tool: str, material: str = "Basic") -> StardewRule: + if material == "Basic": + return _True() + + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received(f"Progressive {tool}", count=tool_materials[material]) + + return self.has(f"{material} Bar") & self.can_spend_money(tool_prices[material]) + + def has_skill_level(self, skill: str, level: int) -> StardewRule: + if level == 0: + return _True() + + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.received(f"{skill} Level", count=level) + + 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)]) + + def has_total_skill_level(self, level: int) -> StardewRule: + if level == 0: + return _True() + + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + skills_items = ["Farming Level", "Mining Level", "Foraging Level", + "Fishing Level", "Combat Level"] + 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.received(season_per_total_level[level]) + + def has_building(self, building: str) -> StardewRule: + if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: + count = 1 + if building in ["Coop", "Barn", "Shed"]: + building = f"Progressive {building}" + elif building.startswith("Big"): + count = 2 + building = " ".join(["Progressive", *building.split(" ")[1:]]) + elif building.startswith("Deluxe"): + count = 3 + building = " ".join(["Progressive", *building.split(" ")[1:]]) + return self.received(f"{building}", count) + + return _Has(building, self.building_rules) + + def has_house(self, upgrade_level: int) -> StardewRule: + if upgrade_level < 1: + return _True() + + if upgrade_level > 3: + 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) + + if upgrade_level == 2: + return _Has("Kids Room", self.building_rules) + + # if upgrade_level == 3: + return _Has("Cellar", self.building_rules) + + def can_complete_quest(self, quest: str) -> StardewRule: + return _Has(quest, self.quest_rules) + + def can_get_fishing_xp(self) -> StardewRule: + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.can_fish() | self.can_crab_pot() + + return self.can_fish() + + def can_fish(self, difficulty: int = 0) -> StardewRule: + skill_required = max(0, int((difficulty / 10) - 1)) + if difficulty <= 40: + skill_required = 0 + skill_rule = self.has_skill_level("Fishing", skill_required) + number_fishing_rod_required = 1 if difficulty < 50 else 2 + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", number_fishing_rod_required) & skill_rule + + return skill_rule + + def can_catch_fish(self, fish: FishItem) -> StardewRule: + region_rule = self.can_reach_any_region(fish.locations) + season_rule = self.received(fish.seasons) + difficulty_rule = self.can_fish(fish.difficulty) + if fish.difficulty == -1: + difficulty_rule = self.can_crab_pot() + return region_rule & season_rule & difficulty_rule + + def can_catch_every_fish(self) -> StardewRule: + rules = [self.has_skill_level("Fishing", 10), self.received("Progressive Fishing Rod", 4)] + for fish in all_fish_items: + rules.append(self.can_catch_fish(fish)) + return _And(rules) + + def can_cook(self) -> StardewRule: + return self.has_house(1) or self.has_skill_level("Foraging", 9) + + def can_smelt(self, item: str) -> StardewRule: + return self.has("Furnace") & self.has(item) + + def can_crab_pot(self) -> StardewRule: + if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: + return self.has("Crab Pot") + + 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") + + def can_mine_in_the_mines_floor_41_80(self) -> StardewRule: + return self.can_reach_region("The Mines - Floor 45") + + def can_mine_in_the_mines_floor_81_120(self) -> StardewRule: + return self.can_reach_region("The 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")) + + 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")) + + def get_weapon_rule_for_floor_tier(self, tier: int): + if tier >= 4: + return self.has_galaxy_weapon() + if tier >= 3: + return self.has_great_weapon() + if tier >= 2: + return self.has_good_weapon() + if tier >= 1: + return self.has_decent_weapon() + return self.has_any_weapon() + + def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: + tier = int(floor / 40) + rules = [] + weapon_rule = self.get_weapon_rule_for_floor_tier(tier) + rules.append(weapon_rule) + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + rules.append(self.received("Progressive Pickaxe", tier)) + 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) + + def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule: + tier = int(floor / 40) + 1 + rules = [] + weapon_rule = self.get_weapon_rule_for_floor_tier(tier) + rules.append(weapon_rule) + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + rules.append(self.received("Progressive Pickaxe", count=tier)) + 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) + + def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: + if (self.options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive or + self.options[options.TheMinesElevatorsProgression] == + options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): + return self.received("Progressive Mine Elevator", count=int(floor / 5)) + return _True() + + def can_mine_to_floor(self, floor: int) -> StardewRule: + previous_elevator = max(floor - 5, 0) + previous_previous_elevator = max(floor - 10, 0) + return ((self.has_mine_elevator_to_floor(previous_elevator) & + self.can_progress_in_the_mines_from_floor(previous_elevator)) | + (self.has_mine_elevator_to_floor(previous_previous_elevator) & + self.can_progress_easily_in_the_mines_from_floor(previous_previous_elevator))) + + def has_jotpk_power_level(self, power_level: int) -> StardewRule: + if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + return _True() + jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", + "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] + return self.received(jotpk_buffs, power_level) + + def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: + if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: + return _True() + return self.received("Junimo Kart: Extra Life", power_level) + + def has_traveling_merchant(self, tier: int = 1): + traveling_merchant_days = [f"Traveling Merchant: {day}" for day in week_days] + 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) + + def can_have_relationship(self, npc: str, hearts: int) -> StardewRule: + if npc == "Leo": + return self.can_reach_region("Ginger Island") + + if npc == "Sandy": + return self.can_reach_region("The Desert") + + if npc == "Kent": + return self.received("Year Two") + + 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") + + def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule: + item_rules = [] + for bundle_item in bundle_requirements: + if bundle_item.item.item_id == -1: + return self.can_spend_money(bundle_item.amount) + else: + item_rules.append(bundle_item.item.name) + return self.has(item_rules, number_required) + + def can_complete_community_center(self) -> StardewRule: + return (self.can_reach_location("Complete Crafts Room") & + self.can_reach_location("Complete Pantry") & + self.can_reach_location("Complete Fish Tank") & + self.can_reach_location("Complete Bulletin Board") & + self.can_reach_location("Complete Vault") & + self.can_reach_location("Complete Boiler Room")) + + def can_finish_grandpa_evaluation(self) -> StardewRule: + # https://stardewvalleywiki.com/Grandpa + rules_worth_a_point = [self.can_have_earned_total_money(50000), # 50 000g + self.can_have_earned_total_money(100000), # 100 000g + self.can_have_earned_total_money(200000), # 200 000g + self.can_have_earned_total_money(300000), # 300 000g + self.can_have_earned_total_money(500000), # 500 000g + self.can_have_earned_total_money(1000000), # 1 000 000g first point + self.can_have_earned_total_money(1000000), # 1 000 000g second point + self.has_total_skill_level(30), # Total Skills: 30 + self.has_total_skill_level(50), # Total Skills: 50 + # Completing the museum not expected + # 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.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 + ] + 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) + + def has_decent_weapon(self) -> StardewRule: + return (self.has_good_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and + (Group.MINES_FLOOR_50 in item.groups or Group.MINES_FLOOR_60 in item.groups))) + + def has_good_weapon(self) -> StardewRule: + return ((self.has_great_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and + (Group.MINES_FLOOR_80 in item.groups or Group.MINES_FLOOR_90 in item.groups))) & + self.received("Adventurer's Guild")) + + def has_great_weapon(self) -> StardewRule: + return ((self.has_galaxy_weapon() | + self.received(item.name for item in all_items + if Group.WEAPON in item.groups and Group.MINES_FLOOR_110 in item.groups)) & + self.received("Adventurer's Guild")) + + def has_galaxy_weapon(self) -> StardewRule: + 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")) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py new file mode 100644 index 00000000..e7478c7d --- /dev/null +++ b/worlds/stardew_valley/options.py @@ -0,0 +1,409 @@ +from dataclasses import dataclass +from typing import Dict, Union, Protocol, runtime_checkable + +from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice + + +@runtime_checkable +class StardewOption(Protocol): + internal_name: str + + +@dataclass +class StardewOptions: + options: Dict[str, Union[bool, int]] + + def __getitem__(self, item: Union[str, StardewOption]) -> Union[bool, int]: + if isinstance(item, StardewOption): + item = item.internal_name + + return self.options.get(item, None) + + +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""" + internal_name = "goal" + display_name = "Goal" + option_community_center = 0 + option_grandpa_evaluation = 1 + option_bottom_of_the_mines = 2 + option_cryptic_note = 3 + option_master_angler = 4 + + @classmethod + def get_option_name(cls, value) -> str: + if value == cls.option_grandpa_evaluation: + return "Grandpa's Evaluation" + + return super().get_option_name(value) + + +class StartingMoney(SpecialRange): + """Amount of gold when arriving at the farm. + Set to -1 or unlimited for infinite money in this playthrough""" + internal_name = "starting_money" + display_name = "Starting Gold" + range_start = -1 + range_end = 50000 + default = 5000 + + special_range_names = { + "unlimited": -1, + "vanilla": 500, + "extra": 2000, + "rich": 5000, + "very rich": 20000, + "filthy rich": 50000, + } + + +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.""" + internal_name = "resource_pack_multiplier" + default = 100 + range_start = 0 + range_end = 200 + # step = 25 + display_name = "Resource Pack Multiplier" + + special_range_names = { + "resource packs disabled": 0, + "half packs": 50, + "normal packs": 100, + "double packs": 200, + } + + +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""" + internal_name = "bundle_randomization" + display_name = "Bundle Randomization" + default = 1 + option_vanilla = 0 + option_thematic = 1 + option_shuffled = 2 + + +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""" + internal_name = "bundle_price" + display_name = "Bundle Price" + default = 2 + option_very_cheap = 0 + option_cheap = 1 + option_normal = 2 + option_expensive = 3 + + +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 + """ + # 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! + + internal_name = "entrance_randomization" + display_name = "Entrance Randomization" + default = 0 + option_disabled = 0 + option_pelican_town = 1 + option_non_progression = 2 + # option_buildings = 3 + # option_everything = 4 + # option_chaos = 4 + + +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. + """ + internal_name = "backpack_progression" + display_name = "Backpack Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_early_progressive = 2 + + +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.""" + internal_name = "tool_progression" + display_name = "Tool Progression" + default = 1 + option_vanilla = 0 + option_progressive = 1 + + +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 + 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.""" + internal_name = "elevator_progression" + display_name = "Elevator Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_progressive_from_previous_floor = 2 + + +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.""" + internal_name = "skill_progression" + display_name = "Skill Progression" + default = 1 + option_vanilla = 0 + option_progressive = 1 + + +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, + 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. + """ + internal_name = "building_progression" + display_name = "Building Progression" + default = 2 + option_vanilla = 0 + option_progressive = 1 + option_progressive_early_shipping_bin = 2 + + +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 + player. + With 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. + """ + internal_name = "arcade_machine_locations" + display_name = "Arcade Machine Locations" + default = 3 + option_disabled = 0 + option_victories = 1 + option_victories_easy = 2 + option_full_shuffling = 3 + + +class HelpWantedLocations(SpecialRange): + """How many "Help Wanted" quests need to be completed as ArchipelagoLocations + 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" + default = 7 + range_start = 0 + range_end = 56 + # step = 7 + display_name = "Number of Help Wanted locations" + + special_range_names = { + "none": 0, + "minimum": 7, + "normal": 14, + "lots": 28, + "maximum": 56, + } + + +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 + """ + internal_name = "fishsanity" + display_name = "Fishsanity" + default = 0 + option_none = 0 + option_legendaries = 1 + option_special = 2 + option_random_selection = 3 + option_all = 4 + + +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) + and daily luck bonus (0.025 flat value per buff)""" + internal_name = "player_buff_number" + display_name = "Number of Player Buffs" + range_start = 0 + range_end = 12 + default = 4 + # step = 1 + + +class MultipleDaySleepEnabled(Toggle): + """Should you be able to sleep automatically multiple day strait?""" + 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.""" + internal_name = "multiple_day_sleep_cost" + display_name = "Multiple Day Sleep Cost" + range_start = 0 + range_end = 200 + # step = 25 + + special_range_names = { + "free": 0, + "cheap": 25, + "medium": 50, + "expensive": 100, + } + + +class ExperienceMultiplier(SpecialRange): + """How fast do you want to level up. A lower setting mean less experience. + A higher setting means more experience.""" + internal_name = "experience_multiplier" + display_name = "Experience 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 + """ + internal_name = "debris_multiplier" + display_name = "Debris Multiplier" + default = 1 + option_vanilla = 0 + option_half = 1 + option_quarter = 2 + option_none = 3 + option_start_clear = 4 + + +class QuickStart(Toggle): + """Do you want the quick start package? You will get a few items to help early game automation, + so you can use the multiple day sleep at its maximum.""" + internal_name = "quick_start" + display_name = "Quick Start" + default = 1 + + +class Gifting(Toggle): + """Do you want to enable gifting items to and from other Stardew Valley worlds?""" + internal_name = "gifting" + display_name = "Gifting" + default = 1 + + +class GiftTax(SpecialRange): + """Joja Prime will deliver gifts within one business day, for a price! + Sending a gift will cost a percentage of the item's monetary value as a tax on the sender""" + internal_name = "gift_tax" + display_name = "Gift Tax" + range_start = 0 + range_end = 400 + # step = 20 + default = 20 + + special_range_names = { + "no tax": 0, + "soft tax": 20, + "rough tax": 40, + "full tax": 100, + "oppressive tax": 200, + "nightmare tax": 400, + } + + +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, + ] +} +default_options = {option.internal_name: option.default for option in stardew_valley_options.values()} +stardew_valley_options["death_link"] = DeathLink + + +def fetch_options(world, player: int) -> StardewOptions: + return StardewOptions({option: get_option_value(world, player, option) for option in stardew_valley_options}) + + +def get_option_value(world, player: int, name: str) -> Union[bool, int]: + assert name in stardew_valley_options, f"{name} is not a valid option for Stardew Valley." + + value = getattr(world, name) + + if issubclass(stardew_valley_options[name], Toggle): + return bool(value[player].value) + return value[player].value diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py new file mode 100644 index 00000000..0979d7f8 --- /dev/null +++ b/worlds/stardew_valley/regions.py @@ -0,0 +1,291 @@ +from dataclasses import dataclass, field +from enum import IntFlag +from random import Random +from typing import Iterable, Dict, Protocol, Optional, List, Tuple + +from BaseClasses import Region, Entrance +from . import options +from .options import StardewOptions + + +class RegionFactory(Protocol): + def __call__(self, name: str, regions: Iterable[str]) -> Region: + raise NotImplementedError + + +class RandomizationFlag(IntFlag): + NOT_RANDOMIZED = 0b0 + PELICAN_TOWN = 0b11111 + NON_PROGRESSION = 0b11110 + BUILDINGS = 0b11100 + EVERYTHING = 0b11000 + CHAOS = 0b10000 + + +@dataclass(frozen=True) +class RegionData: + name: str + exits: List[str] = field(default_factory=list) + + +@dataclass(frozen=True) +class ConnectionData: + name: str + destination: str + reverse: Optional[str] = None + flag: RandomizationFlag = RandomizationFlag.NOT_RANDOMIZED + + def __post_init__(self): + if self.reverse is None and " to " in self.name: + origin, destination = self.name.split(" to ") + super().__setattr__("reverse", f"{destination} to {origin}") + + +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", + "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", + "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"), +] + +# 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"), +] + + +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} + + connections, randomized_data = randomize_connections(random, world_options) + + for connection in connections: + if connection.name not in entrances: + continue + entrances[connection.name].connect(regions[connection.destination]) + + return regions.values(), randomized_data + + +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] + 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] + random.shuffle(connections_to_randomize) + + destination_pool = list(connections_to_randomize) + random.shuffle(destination_pool) + + randomized_connections = [] + randomized_data = {} + for connection in connections_to_randomize: + destination = destination_pool.pop() + randomized_connections.append(ConnectionData(connection.name, destination.destination, destination.reverse)) + randomized_data[connection.name] = destination.name + randomized_data[destination.reverse] = connection.reverse + + return mandatory_connections, randomized_data diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt new file mode 100644 index 00000000..b0922176 --- /dev/null +++ b/worlds/stardew_valley/requirements.txt @@ -0,0 +1 @@ +importlib_resources; python_version <= '3.8' diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py new file mode 100644 index 00000000..f9ba31cc --- /dev/null +++ b/worlds/stardew_valley/rules.py @@ -0,0 +1,190 @@ +import itertools +from typing import Dict + +from BaseClasses import MultiWorld +from worlds.generic import Rules as MultiWorldRules +from . import options, locations +from .bundles import Bundle +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", +} + + +def set_rules(multi_world: MultiWorld, player: int, world_options: 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), + logic.can_mine_to_floor(floor).simplify()) + + MultiWorldRules.add_rule(multi_world.get_entrance("Enter Quarry", player), + logic.received("Bridge Repair").simplify()) + MultiWorldRules.add_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), + logic.received("Bus Repair").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Enter Skull Cavern", player), + logic.received("Skull Key").simplify()) + + MultiWorldRules.add_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), + logic.received("Island Obelisk").simplify()) + + # Those checks do not exist if ToolProgression is vanilla + if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: + MultiWorldRules.add_rule(multi_world.get_location("Purchase Fiberglass Rod", player), + (logic.has_skill_level("Fishing", 2) & logic.can_spend_money(1800)).simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Purchase Iridium Rod", player), + (logic.has_skill_level("Fishing", 6) & logic.can_spend_money(7500)).simplify()) + + materials = [None, "Copper", "Iron", "Gold", "Iridium"] + tool = ["Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can"] + for (previous, material), tool in itertools.product(zip(materials[:4], materials[1:]), tool): + if previous is None: + MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + (logic.has(f"{material} Ore") & + logic.can_spend_money(tool_prices[material])).simplify()) + else: + MultiWorldRules.add_rule(multi_world.get_location(f"{material} {tool} Upgrade", player), + (logic.has(f"{material} Ore") & logic.has_tool(tool, previous) & + logic.can_spend_money(tool_prices[material])).simplify()) + + # Skills + 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()) + 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()) + MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), + logic.received(season_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()) + MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Combat", player), + (logic.received(season_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()) + 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()) + 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()) + 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()) + 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()) + 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()) + 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()) + + # Buildings + if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: + for building in locations.locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: + MultiWorldRules.set_rule(multi_world.get_location(building.name, player), + logic.building_rules[building.name.replace(" Blueprint", "")].simplify()) + + # Story Quests + for quest in locations.locations_by_tag[LocationTags.QUEST]: + MultiWorldRules.set_rule(multi_world.get_location(quest.name, player), + logic.quest_rules[quest.name].simplify()) + + # Help Wanted Quests + desired_number_help_wanted: int = world_options[options.HelpWantedLocations] // 7 + 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)]) + fishing_rule = rule & logic.can_fish() + slay_rule = rule & logic.has_any_weapon() + for j in range(i, i + 4): + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} {delivery} {j}", player), + rule.simplify()) + + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Gathering {i}", player), + rule.simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Fishing {i}", player), + fishing_rule.simplify()) + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i}", player), + slay_rule.simplify()) + + fish_prefix = "Fishsanity: " + for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]: + if fish_location.name in all_location_names: + fish_name = fish_location.name[len(fish_prefix):] + MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player), + 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 + if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: + MultiWorldRules.add_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()) + + 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 + for day in week_days: + item_for_day = f"Traveling Merchant: {day}" + for i in range(1, 4): + location_name = f"Traveling Merchant {day} Item {i}" + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), + logic.received(item_for_day)) + + 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()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach Junimo Kart 2", player), + logic.has("Junimo Kart Medium Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach Junimo Kart 3", player), + logic.has("Junimo Kart Big Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Junimo Kart: Sunset Speedway (Victory)", player), + logic.has("Junimo Kart Max Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Play Journey of the Prairie King", player), + logic.has("JotPK Small Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach JotPK World 2", player), + logic.has("JotPK Medium Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_entrance("Reach JotPK World 3", player), + logic.has("JotPK Big Buff").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player), + logic.has("JotPK Max Buff").simplify()) diff --git a/worlds/stardew_valley/scripts/__init__.py b/worlds/stardew_valley/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worlds/stardew_valley/scripts/export_items.py b/worlds/stardew_valley/scripts/export_items.py new file mode 100644 index 00000000..6d929226 --- /dev/null +++ b/worlds/stardew_valley/scripts/export_items.py @@ -0,0 +1,26 @@ +"""Items export script +This script can be used to export all the AP items into a json file in the output folder. This file is used by the tests +of the mod to ensure it can handle all possible items. + +To run the script, use `python -m worlds.stardew_valley.scripts.export_items` from the repository root. +""" + +import json +import os.path + +from worlds.stardew_valley import item_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/stardew_valley_item_table.json", "w+") as f: + items = { + item.name: { + "code": item.code, + "classification": item.classification.name + } + for item in item_table.values() + if item.code is not None + } + json.dump({"items": items}, f) diff --git a/worlds/stardew_valley/scripts/export_locations.py b/worlds/stardew_valley/scripts/export_locations.py new file mode 100644 index 00000000..1dc60f79 --- /dev/null +++ b/worlds/stardew_valley/scripts/export_locations.py @@ -0,0 +1,26 @@ +"""Locations export script +This script can be used to export all the AP locations into a json file in the output folder. This file is used by the +tests of the mod to ensure it can handle all possible locations. + +To run the script, use `python -m worlds.stardew_valley.scripts.export_locations` from the repository root. +""" + +import json +import os + +from worlds.stardew_valley import location_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/stardew_valley_location_table.json", "w+") as f: + locations = { + location.name: { + "code": location.code, + "region": location.region, + } + for location in location_table.values() + if location.code is not None + } + json.dump({"locations": locations}, f) diff --git a/worlds/stardew_valley/scripts/update_data.py b/worlds/stardew_valley/scripts/update_data.py new file mode 100644 index 00000000..4b7b6be2 --- /dev/null +++ b/worlds/stardew_valley/scripts/update_data.py @@ -0,0 +1,88 @@ +"""Update data script +This script can be used to assign new ids for the items and locations in the CSV file. It also regenerates the items +based on the resource packs. + +To run the script, use `python -m worlds.stardew_valley.scripts.update_data` from the repository root. +""" + +import csv +import itertools +from pathlib import Path +from typing import List + +from worlds.stardew_valley import LocationData +from worlds.stardew_valley.items import load_item_csv, Group, ItemData, load_resource_pack_csv, friendship_pack +from worlds.stardew_valley.locations import load_location_csv + +RESOURCE_PACK_CODE_OFFSET = 5000 +script_folder = Path(__file__) + + +def write_item_csv(items: List[ItemData]): + with open((script_folder.parent.parent / "data/items.csv").resolve(), "w", newline="") as file: + writer = csv.DictWriter(file, ["id", "name", "classification", "groups"]) + writer.writeheader() + for item in items: + item_dict = { + "id": item.code_without_offset, + "name": item.name, + "classification": item.classification.name, + "groups": ",".join(sorted(group.name for group in item.groups)) + } + writer.writerow(item_dict) + + +def write_location_csv(locations: List[LocationData]): + with open((script_folder.parent.parent / "data/locations.csv").resolve(), "w", newline="") as file: + write = csv.DictWriter(file, ["id", "region", "name", "tags"]) + write.writeheader() + for location in locations: + location_dict = { + "id": location.code_without_offset, + "name": location.name, + "region": location.region, + "tags": ",".join(sorted(group.name for group in location.tags)) + } + write.writerow(location_dict) + + +if __name__ == "__main__": + loaded_items = load_item_csv() + + item_counter = itertools.count(max(item.code_without_offset + for item in loaded_items + if Group.RESOURCE_PACK not in item.groups + and item.code_without_offset is not None) + 1) + items_to_write = [] + for item in loaded_items: + if item.has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK): + continue + + if item.code_without_offset is None: + items_to_write.append(ItemData(next(item_counter), item.name, item.classification, item.groups)) + continue + + items_to_write.append(item) + + all_resource_packs = load_resource_pack_csv() + [friendship_pack] + resource_pack_counter = itertools.count(RESOURCE_PACK_CODE_OFFSET) + items_to_write.extend( + item for resource_pack in all_resource_packs for item in resource_pack.as_item_data(resource_pack_counter)) + + write_item_csv(items_to_write) + + loaded_locations = load_location_csv() + location_counter = itertools.count(max(location.code_without_offset + for location in loaded_locations + if location.code_without_offset is not None) + 1) + + locations_to_write = [] + for location in loaded_locations: + if location.code_without_offset is None: + locations_to_write.append( + LocationData(next(location_counter), location.region, location.name, location.tags)) + continue + + locations_to_write.append(location) + + write_location_csv(locations_to_write) diff --git a/worlds/stardew_valley/test/TestAllLogic.py b/worlds/stardew_valley/test/TestAllLogic.py new file mode 100644 index 00000000..de1c0049 --- /dev/null +++ b/worlds/stardew_valley/test/TestAllLogic.py @@ -0,0 +1,53 @@ +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 new file mode 100644 index 00000000..58017377 --- /dev/null +++ b/worlds/stardew_valley/test/TestBundles.py @@ -0,0 +1,16 @@ +import unittest + +from ..bundle_data import all_bundle_items + + +class TestBundles(unittest.TestCase): + def test_all_bundle_items_have_3_parts(self): + for bundle_item in all_bundle_items: + name = bundle_item.item.name + assert len(name) > 0 + id = bundle_item.item.item_id + assert (id > 0 or id == -1) + amount = bundle_item.amount + assert amount > 0 + quality = bundle_item.quality + assert quality >= 0 diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py new file mode 100644 index 00000000..c08cef0e --- /dev/null +++ b/worlds/stardew_valley/test/TestData.py @@ -0,0 +1,20 @@ +import unittest + +from ..items import load_item_csv +from ..locations import load_location_csv + + +class TestCsvIntegrity(unittest.TestCase): + def test_items_integrity(self): + items = load_item_csv() + + for item in items: + assert item.code_without_offset is not None, \ + "Some item do not have an id. Run the script `update_data.py` to generate them." + + def test_locations_integrity(self): + locations = load_location_csv() + + for location in locations: + assert location.code_without_offset is not None, \ + "Some location do not have an id. Run the script `update_data.py` to generate them." diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py new file mode 100644 index 00000000..840052d3 --- /dev/null +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -0,0 +1,127 @@ +from BaseClasses import ItemClassification +from . import SVTestBase +from .. import locations, items, location_table, options +from ..items import items_by_group, Group +from ..locations import LocationTags + + +class TestBaseItemGeneration(SVTestBase): + + def test_all_progression_items_are_added_to_the_pool(self): + for classification in [ItemClassification.progression, ItemClassification.useful]: + with self.subTest(classification=classification): + + all_classified_items = {self.world.create_item(item) + for item in items.items_by_group[items.Group.COMMUNITY_REWARD] + if item.classification is classification} + + for item in all_classified_items: + assert item in self.multiworld.itempool + + def test_creates_as_many_item_as_non_event_locations(self): + non_event_locations = [location for location in self.multiworld.get_locations(self.player) if + not location.event] + + assert len(non_event_locations), len(self.multiworld.itempool) + + +class TestGivenProgressiveBackpack(SVTestBase): + options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} + + def test_when_generate_world_then_two_progressive_backpack_are_added(self): + assert self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")) == 2 + + def test_when_generate_world_then_backpack_locations_are_added(self): + created_locations = {location.name for location in self.multiworld.get_locations(1)} + assert all(location.name in created_locations for location in locations.locations_by_tag[LocationTags.BACKPACK]) + + +class TestRemixedMineRewards(SVTestBase): + def test_when_generate_world_then_one_reward_is_added_per_chest(self): + # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_10]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_20]) + assert self.world.create_item("Slingshot") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_50]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_60]) + assert self.world.create_item("Master Slingshot") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_80]) + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_90]) + assert self.world.create_item("Stardrop") in self.multiworld.itempool + assert any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_110]) + assert self.world.create_item("Skull Key") in self.multiworld.itempool + + # This test as 1 over 90,000 changes to fail... Sorry in advance + def test_when_generate_world_then_rewards_are_not_all_vanilla(self): + assert not all(self.world.create_item(item) in self.multiworld.itempool + for item in + ["Leather Boots", "Steel Smallsword", "Tundra Boots", "Crystal Dagger", "Firewalker Boots", + "Obsidian Edge", "Space Boots"]) + + +class TestProgressiveElevator(SVTestBase): + options = { + options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_progressive, + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_given_access_to_floor_115_when_find_another_elevator_then_has_access_to_floor_120(self): + self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2) + self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22) + self.collect(self.multiworld.create_item("Bone Sword", self.player)) + self.collect([self.get_item_by_name("Combat Level")] * 4) + self.collect(self.get_item_by_name("Adventurer's Guild")) + + assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + self.collect(self.get_item_by_name("Progressive Mine Elevator")) + + assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + def test_given_access_to_floor_115_when_find_another_pickaxe_and_sword_then_has_access_to_floor_120(self): + self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2) + self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 22) + self.collect(self.multiworld.create_item("Bone Sword", self.player)) + self.collect([self.get_item_by_name("Combat Level")] * 4) + self.collect(self.get_item_by_name("Adventurer's Guild")) + + assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + self.collect(self.get_item_by_name("Progressive Pickaxe")) + self.collect(self.multiworld.create_item("Steel Falchion", self.player)) + self.collect(self.get_item_by_name("Combat Level")) + self.collect(self.get_item_by_name("Combat Level")) + + assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + + +class TestLocationGeneration(SVTestBase): + + def test_all_location_created_are_in_location_table(self): + for location in self.multiworld.get_locations(self.player): + if not location.event: + assert location.name in location_table + + +class TestLocationAndItemCount(SVTestBase): + options = { + 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.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, + options.HelpWantedLocations.internal_name: 0, + 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()) diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py new file mode 100644 index 00000000..98d251eb --- /dev/null +++ b/worlds/stardew_valley/test/TestItems.py @@ -0,0 +1,26 @@ +import unittest + +from BaseClasses import MultiWorld +from .. import StardewValleyWorld +from ..items import item_table + + +class TestItems(unittest.TestCase): + def test_can_create_item_of_resource_pack(self): + item_name = "Resource Pack: 500 Money" + + multi_world = MultiWorld(1) + multi_world.game[1] = "Stardew Valley" + multi_world.player_name = {1: "Tester"} + world = StardewValleyWorld(multi_world, 1) + item = world.create_item(item_name) + + assert item.name == item_name + + def test_items_table_footprint_is_between_717000_and_727000(self): + item_with_lowest_id = min((item for item in item_table.values() if item.code is not None), key=lambda x: x.code) + item_with_highest_id = max((item for item in item_table.values() if item.code is not None), + key=lambda x: x.code) + + assert item_with_lowest_id.code >= 717000 + assert item_with_highest_id.code < 727000 diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py new file mode 100644 index 00000000..83129a56 --- /dev/null +++ b/worlds/stardew_valley/test/TestLogic.py @@ -0,0 +1,293 @@ +from . import SVTestBase +from .. import options + + +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) + + +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) + + +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)}" + + +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.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) + + +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.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) + + 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/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py new file mode 100644 index 00000000..1a3d5a1d --- /dev/null +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -0,0 +1,52 @@ +import unittest + +from .. import _True +from ..logic import _Received, _Has, _False, _And, _Or + + +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_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_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_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_false_in_and(self): + rule = _And(_False(), _Received("Summer", 0, 1)) + assert rule.simplify() == _False() diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py new file mode 100644 index 00000000..3aadc7e4 --- /dev/null +++ b/worlds/stardew_valley/test/TestRegions.py @@ -0,0 +1,46 @@ +import random +import sys +import unittest + +from .. import StardewOptions, options +from ..regions import stardew_valley_regions, mandatory_connections, randomize_connections, RandomizationFlag + +connections_by_name = {connection.name for connection in mandatory_connections} +regions_by_name = {region.name for region in stardew_valley_regions} + + +class TestRegions(unittest.TestCase): + def test_region_exits_lead_somewhere(self): + for region in stardew_valley_regions: + with self.subTest(region=region): + for exit in region.exits: + assert exit in connections_by_name, f"{region.name} is leading to {exit} but it does not exist." + + def test_connection_lead_somewhere(self): + for connection in mandatory_connections: + with self.subTest(connection=connection): + assert connection.destination in regions_by_name, \ + f"{connection.name} is leading to {connection.destination} but it does not exist." + + +class TestEntranceRando(unittest.TestCase): + + def test_pelican_town_entrance_randomization(self): + for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), + (options.EntranceRandomization.option_non_progression, RandomizationFlag.NON_PROGRESSION)]: + with self.subTest(option=option, flag=flag): + seed = random.randrange(sys.maxsize) + rand = random.Random(seed) + world_options = StardewOptions({options.EntranceRandomization.internal_name: option}) + + _, randomized_connections = randomize_connections(rand, world_options) + + for connection in mandatory_connections: + if flag in connection.flag: + assert connection.name in randomized_connections, \ + f"Connection {connection.name} should be randomized but it is not in the output. Seed = {seed}" + assert connection.reverse in randomized_connections, \ + f"Connection {connection.reverse} should be randomized but it is not in the output. Seed = {seed}" + + assert len(set(randomized_connections.values())) == len( + randomized_connections.values()), f"Connections are duplicated in randomization. Seed = {seed}" diff --git a/worlds/stardew_valley/test/TestResourcePack.py b/worlds/stardew_valley/test/TestResourcePack.py new file mode 100644 index 00000000..d25505bb --- /dev/null +++ b/worlds/stardew_valley/test/TestResourcePack.py @@ -0,0 +1,76 @@ +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/__init__.py b/worlds/stardew_valley/test/__init__.py new file mode 100644 index 00000000..1ddf0376 --- /dev/null +++ b/worlds/stardew_valley/test/__init__.py @@ -0,0 +1,14 @@ +from typing import ClassVar + +from test.TestBase import WorldTestBase +from .. import StardewValleyWorld + + +class SVTestBase(WorldTestBase): + game = "Stardew Valley" + world: StardewValleyWorld + player: ClassVar[int] = 1 + + def world_setup(self, *args, **kwargs): + super().world_setup(*args, **kwargs) + self.world = self.multiworld.worlds[self.player]