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 .data.fish_data import legendary_fish, special_fish, all_fish from .data.museum_data import all_museum_items from .data.villagers_data import all_villagers LOCATION_CODE_OFFSET = 717000 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() MUSEUM_MILESTONES = enum.auto() MUSEUM_DONATIONS = enum.auto() FRIENDSANITY = 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", "Complete the Museum Collection"), LocationData(None, "Stardew Valley", "Full House"), ] 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_randomized: randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish if random.random() < 0.4) elif fishsanity == options.Fishsanity.option_all: randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish) def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random): prefix = "Museumsanity: " if museumsanity == options.Museumsanity.option_none: return elif museumsanity == options.Museumsanity.option_milestones: randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES]) elif museumsanity == options.Museumsanity.option_randomized: randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items if random.random() < 0.4) elif museumsanity == options.Museumsanity.option_all: randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items) def extend_friendsanity_locations(randomized_locations: List[LocationData], friendsanity: int): if friendsanity == options.Friendsanity.option_none: return exclude_non_bachelors = friendsanity == options.Friendsanity.option_bachelors exclude_locked_villagers = friendsanity == options.Friendsanity.option_starting_npcs or \ friendsanity == options.Friendsanity.option_bachelors exclude_post_marriage_hearts = friendsanity != options.Friendsanity.option_all_with_marriage for villager in all_villagers: if not villager.available and exclude_locked_villagers: continue if not villager.bachelor and exclude_non_bachelors: continue for heart in range(1, 15): if villager.bachelor and exclude_post_marriage_hearts and heart > 8: continue if villager.bachelor or heart < 11: randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) if not exclude_non_bachelors: for heart in range(1, 6): randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) def create_locations(location_collector: StardewLocationCollector, 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) extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random) extend_friendsanity_locations(randomized_locations, world_options[options.Friendsanity]) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region)