mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-02 08:43:23 -07:00
* - Improved the dynamic locations count algorithm to take into account the nature of various heavy settings in both directions * - Fixes from Code Review * - We're only testing for sunday locations, might as well only take sunday locations in the list to test * - One more slight optimization * - Added consideration for bundles per room in filler locations counting * - Registered some more IDs to handle items up to 10
830 lines
40 KiB
Python
830 lines
40 KiB
Python
import csv
|
|
import enum
|
|
import logging
|
|
import math
|
|
from dataclasses import dataclass
|
|
from random import Random
|
|
from typing import Optional, Dict, Protocol, List, Iterable
|
|
|
|
from . import data
|
|
from .bundles.bundle_room import BundleRoom
|
|
from .content.game_content import StardewContent
|
|
from .content.vanilla.ginger_island import ginger_island_content_pack
|
|
from .content.vanilla.qi_board import qi_board_content_pack
|
|
from .data.game_item import ItemTag
|
|
from .data.museum_data import all_museum_items
|
|
from .mods.mod_data import ModNames
|
|
from .options import ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
|
|
FestivalLocations, ElevatorProgression, BackpackProgression, FarmType
|
|
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
|
|
from .options.options import BackpackSize, Moviesanity, Eatsanity, IncludeEndgameLocations, Friendsanity, Fishsanity, SkillProgression, Cropsanity
|
|
from .strings.ap_names.ap_option_names import WalnutsanityOptionName, SecretsanityOptionName, EatsanityOptionName, ChefsanityOptionName, StartWithoutOptionName
|
|
from .strings.backpack_tiers import Backpack
|
|
from .strings.goal_names import Goal
|
|
from .strings.quest_names import ModQuest, Quest
|
|
from .strings.region_names import Region, LogicRegion
|
|
from .strings.villager_names import NPC
|
|
|
|
LOCATION_CODE_OFFSET = 717000
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LocationTags(enum.Enum):
|
|
MANDATORY = enum.auto()
|
|
BUNDLE = enum.auto()
|
|
TRASH_BEAR = 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()
|
|
RACCOON_BUNDLES = enum.auto()
|
|
MEME_BUNDLE = enum.auto()
|
|
BACKPACK = enum.auto()
|
|
BACKPACK_TIER = enum.auto()
|
|
SPLIT_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()
|
|
PAN_UPGRADE = enum.auto()
|
|
STARTING_TOOLS = enum.auto()
|
|
THE_MINES_TREASURE = enum.auto()
|
|
CROPSANITY = enum.auto()
|
|
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()
|
|
MASTERY_LEVEL = enum.auto()
|
|
BUILDING_BLUEPRINT = enum.auto()
|
|
STORY_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()
|
|
FESTIVAL = enum.auto()
|
|
FESTIVAL_HARD = enum.auto()
|
|
DESERT_FESTIVAL_CHEF = enum.auto()
|
|
DESERT_FESTIVAL_CHEF_MEAL = enum.auto()
|
|
SPECIAL_ORDER_BOARD = enum.auto()
|
|
SPECIAL_ORDER_QI = enum.auto()
|
|
REQUIRES_QI_ORDERS = enum.auto()
|
|
REQUIRES_MASTERIES = enum.auto()
|
|
GINGER_ISLAND = enum.auto()
|
|
WALNUT_PURCHASE = enum.auto()
|
|
WALNUTSANITY = enum.auto()
|
|
WALNUTSANITY_PUZZLE = enum.auto()
|
|
WALNUTSANITY_BUSH = enum.auto()
|
|
WALNUTSANITY_DIG = enum.auto()
|
|
WALNUTSANITY_REPEATABLE = enum.auto()
|
|
|
|
BABY = enum.auto()
|
|
MONSTERSANITY = enum.auto()
|
|
MONSTERSANITY_GOALS = enum.auto()
|
|
MONSTERSANITY_PROGRESSIVE_GOALS = enum.auto()
|
|
MONSTERSANITY_MONSTER = enum.auto()
|
|
SHIPSANITY = enum.auto()
|
|
SHIPSANITY_CROP = enum.auto()
|
|
SHIPSANITY_FISH = enum.auto()
|
|
SHIPSANITY_FULL_SHIPMENT = enum.auto()
|
|
COOKSANITY = enum.auto()
|
|
COOKSANITY_QOS = enum.auto()
|
|
CHEFSANITY = enum.auto()
|
|
CHEFSANITY_QOS = enum.auto()
|
|
CHEFSANITY_PURCHASE = enum.auto()
|
|
CHEFSANITY_FRIENDSHIP = enum.auto()
|
|
CHEFSANITY_SKILL = enum.auto()
|
|
CHEFSANITY_STARTER = enum.auto()
|
|
CRAFTSANITY = enum.auto()
|
|
CRAFTSANITY_CRAFT = enum.auto()
|
|
CRAFTSANITY_RECIPE = enum.auto()
|
|
BOOKSANITY = enum.auto()
|
|
BOOKSANITY_POWER = enum.auto()
|
|
BOOKSANITY_SKILL = enum.auto()
|
|
BOOKSANITY_LOST = enum.auto()
|
|
SECRETSANITY = enum.auto()
|
|
EASY_SECRET = enum.auto()
|
|
FISHING_SECRET = enum.auto()
|
|
DIFFICULT_SECRET = enum.auto()
|
|
SECRET_NOTE = enum.auto()
|
|
REPLACES_PREVIOUS_LOCATION = enum.auto()
|
|
ANY_MOVIE = enum.auto()
|
|
MOVIE = enum.auto()
|
|
MOVIE_SNACK = enum.auto()
|
|
HATSANITY = enum.auto()
|
|
HAT_EASY = enum.auto()
|
|
HAT_TAILORING = enum.auto()
|
|
HAT_MEDIUM = enum.auto()
|
|
HAT_DIFFICULT = enum.auto()
|
|
HAT_RNG = enum.auto()
|
|
HAT_NEAR_PERFECTION = enum.auto()
|
|
HAT_POST_PERFECTION = enum.auto()
|
|
HAT_IMPOSSIBLE = enum.auto()
|
|
EATSANITY = enum.auto()
|
|
EATSANITY_CROP = enum.auto()
|
|
EATSANITY_COOKING = enum.auto()
|
|
EATSANITY_FISH = enum.auto()
|
|
EATSANITY_ARTISAN = enum.auto()
|
|
EATSANITY_SHOP = enum.auto()
|
|
EATSANITY_POISONOUS = enum.auto()
|
|
ENDGAME_LOCATIONS = enum.auto()
|
|
REQUIRES_FRIENDSANITY = enum.auto()
|
|
REQUIRES_FRIENDSANITY_MARRIAGE = enum.auto()
|
|
|
|
BEACH_FARM = enum.auto()
|
|
# Mods
|
|
# Skill Mods
|
|
LUCK_LEVEL = enum.auto()
|
|
BINNING_LEVEL = enum.auto()
|
|
COOKING_LEVEL = enum.auto()
|
|
SOCIALIZING_LEVEL = enum.auto()
|
|
MAGIC_LEVEL = enum.auto()
|
|
ARCHAEOLOGY_LEVEL = enum.auto()
|
|
|
|
DEPRECATED = enum.auto()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LocationData:
|
|
code_without_offset: int | None
|
|
region: str
|
|
name: str
|
|
content_packs: frozenset[str] = frozenset()
|
|
"""All the content packs required for this location to be active."""
|
|
tags: frozenset[LocationTags] = frozenset()
|
|
|
|
@property
|
|
def code(self) -> int | None:
|
|
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]:
|
|
from importlib.resources import files
|
|
|
|
locations = []
|
|
with files(data).joinpath("locations.csv").open() as file:
|
|
location_reader = csv.DictReader(file)
|
|
for location in location_reader:
|
|
location_id = int(location["id"]) if location["id"] else None
|
|
location_name = location["name"]
|
|
csv_tags = [LocationTags[tag] for tag in location["tags"].split(",") if tag]
|
|
tags = frozenset(csv_tags)
|
|
csv_content_packs = [cp for cp in location["content_packs"].split(",") if cp]
|
|
content_packs = frozenset(csv_content_packs)
|
|
|
|
assert len(csv_tags) == len(tags), f"Location '{location_name}' has duplicate tags: {csv_tags}"
|
|
assert len(csv_content_packs) == len(content_packs)
|
|
|
|
if LocationTags.GINGER_ISLAND in tags:
|
|
content_packs |= {ginger_island_content_pack.name}
|
|
if LocationTags.SPECIAL_ORDER_QI in tags or LocationTags.REQUIRES_QI_ORDERS in tags:
|
|
content_packs |= {qi_board_content_pack.name}
|
|
|
|
locations.append(LocationData(location_id, location["region"], location_name, content_packs, tags))
|
|
|
|
return locations
|
|
|
|
|
|
events_locations = [
|
|
LocationData(None, Region.farm_house, Goal.grandpa_evaluation),
|
|
LocationData(None, Region.community_center, Goal.community_center),
|
|
LocationData(None, Region.mines_floor_120, Goal.bottom_of_the_mines),
|
|
LocationData(None, Region.skull_cavern_100, Goal.cryptic_note),
|
|
LocationData(None, Region.beach, Goal.master_angler),
|
|
LocationData(None, Region.museum, Goal.complete_museum),
|
|
LocationData(None, Region.farm_house, Goal.full_house),
|
|
LocationData(None, Region.island_west, Goal.greatest_walnut_hunter),
|
|
LocationData(None, Region.adventurer_guild, Goal.protector_of_the_valley),
|
|
LocationData(None, LogicRegion.shipping, Goal.full_shipment),
|
|
LocationData(None, LogicRegion.kitchen, Goal.gourmet_chef),
|
|
LocationData(None, Region.farm, Goal.craft_master),
|
|
LocationData(None, LogicRegion.shipping, Goal.legend),
|
|
LocationData(None, Region.farm, Goal.mystery_of_the_stardrops),
|
|
LocationData(None, Region.farm, Goal.mad_hatter),
|
|
LocationData(None, Region.farm, Goal.ultimate_foodie),
|
|
LocationData(None, Region.farm, Goal.allsanity),
|
|
LocationData(None, Region.qi_walnut_room, Goal.perfection),
|
|
]
|
|
|
|
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_cropsanity_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
cropsanity = content.features.cropsanity
|
|
if not cropsanity.is_enabled:
|
|
return
|
|
|
|
randomized_locations.extend(location_table[cropsanity.to_location_name(item.name)]
|
|
for item in content.find_tagged_items(ItemTag.CROPSANITY))
|
|
|
|
|
|
def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.quest_locations.has_no_story_quests():
|
|
return
|
|
|
|
story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST]
|
|
story_quest_locations = filter_disabled_locations(options, content, story_quest_locations)
|
|
randomized_locations.extend(story_quest_locations)
|
|
|
|
for i in range(0, options.quest_locations.value):
|
|
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], content: StardewContent, random: Random):
|
|
fishsanity = content.features.fishsanity
|
|
if not fishsanity.is_enabled:
|
|
return
|
|
|
|
for fish in content.fishes.values():
|
|
if not fishsanity.is_included(fish):
|
|
continue
|
|
|
|
if fishsanity.is_randomized and random.random() >= fishsanity.randomization_ratio:
|
|
continue
|
|
|
|
randomized_locations.append(location_table[fishsanity.to_location_name(fish.name)])
|
|
|
|
|
|
def extend_museumsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
|
|
prefix = "Museumsanity: "
|
|
if options.museumsanity == Museumsanity.option_none:
|
|
return
|
|
elif options.museumsanity == Museumsanity.option_milestones:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES])
|
|
elif options.museumsanity == Museumsanity.option_randomized:
|
|
randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"]
|
|
for museum_item in all_museum_items if random.random() < 0.4)
|
|
elif options.museumsanity == Museumsanity.option_all:
|
|
randomized_locations.extend(location_table[f"{prefix}{museum_item.item_name}"] for museum_item in all_museum_items)
|
|
|
|
|
|
def extend_friendsanity_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
friendsanity = content.features.friendsanity
|
|
if not friendsanity.is_enabled:
|
|
return
|
|
|
|
randomized_locations.append(location_table[f"Spouse Stardrop"])
|
|
extend_baby_locations(randomized_locations)
|
|
|
|
for villager in content.villagers.values():
|
|
for heart in friendsanity.get_randomized_hearts(villager):
|
|
randomized_locations.append(location_table[friendsanity.to_location_name(villager.name, heart)])
|
|
|
|
for heart in friendsanity.get_pet_randomized_hearts():
|
|
randomized_locations.append(location_table[friendsanity.to_location_name(NPC.pet, heart)])
|
|
|
|
|
|
def extend_baby_locations(randomized_locations: List[LocationData]):
|
|
baby_locations = [location for location in locations_by_tag[LocationTags.BABY]]
|
|
randomized_locations.extend(baby_locations)
|
|
|
|
|
|
def extend_building_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
building_progression = content.features.building_progression
|
|
if not building_progression.is_progressive:
|
|
return
|
|
|
|
for building in content.farm_buildings.values():
|
|
if building.name in building_progression.starting_buildings:
|
|
continue
|
|
|
|
location_name = building_progression.to_location_name(building.name)
|
|
randomized_locations.append(location_table[location_name])
|
|
|
|
|
|
def extend_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
|
|
if options.festival_locations == FestivalLocations.option_disabled:
|
|
return
|
|
|
|
festival_locations = locations_by_tag[LocationTags.FESTIVAL]
|
|
if not options.museumsanity:
|
|
festival_locations = [location for location in festival_locations if location.name not in ("Rarecrow #7 (Tanuki)", "Rarecrow #8 (Tribal Mask)")]
|
|
|
|
randomized_locations.extend(festival_locations)
|
|
extend_hard_festival_locations(randomized_locations, options)
|
|
extend_desert_festival_chef_locations(randomized_locations, options, random)
|
|
|
|
|
|
def extend_hard_festival_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
|
|
if options.festival_locations != FestivalLocations.option_hard:
|
|
return
|
|
|
|
hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD]
|
|
randomized_locations.extend(hard_festival_locations)
|
|
|
|
|
|
def extend_desert_festival_chef_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, random: Random):
|
|
if options.festival_locations == FestivalLocations.option_easy:
|
|
randomized_locations.append(location_table["Desert Chef"])
|
|
elif options.festival_locations == FestivalLocations.option_hard:
|
|
festival_chef_locations = locations_by_tag[LocationTags.DESERT_FESTIVAL_CHEF_MEAL]
|
|
location_to_add = random.choice(festival_chef_locations)
|
|
randomized_locations.append(location_to_add)
|
|
|
|
|
|
def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.special_order_locations & SpecialOrderLocations.option_board:
|
|
board_locations = filter_disabled_locations(options, content, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
|
|
randomized_locations.extend(board_locations)
|
|
|
|
if content.is_enabled(qi_board_content_pack):
|
|
include_arcade = options.arcade_machine_locations != ArcadeMachineLocations.option_disabled
|
|
qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if
|
|
include_arcade or LocationTags.JUNIMO_KART not in location.tags]
|
|
randomized_locations.extend(qi_orders)
|
|
|
|
|
|
def extend_walnut_purchase_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
if not content.is_enabled(ginger_island_content_pack):
|
|
return
|
|
randomized_locations.append(location_table["Repair Ticket Machine"])
|
|
randomized_locations.append(location_table["Repair Boat Hull"])
|
|
randomized_locations.append(location_table["Repair Boat Anchor"])
|
|
randomized_locations.append(location_table["Open Professor Snail Cave"])
|
|
randomized_locations.append(location_table["Complete Island Field Office"])
|
|
randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE])
|
|
|
|
|
|
def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]]
|
|
filtered_mandatory_locations = filter_disabled_locations(options, content, mandatory_locations)
|
|
randomized_locations.extend(filtered_mandatory_locations)
|
|
|
|
|
|
def extend_situational_quest_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.quest_locations.has_no_story_quests():
|
|
return
|
|
if ModNames.distant_lands in content.registered_packs:
|
|
if ModNames.alecto in content.registered_packs:
|
|
randomized_locations.append(location_table[f"Quest: {ModQuest.WitchOrder}"])
|
|
else:
|
|
randomized_locations.append(location_table[f"Quest: {ModQuest.CorruptedCropsTask}"])
|
|
|
|
|
|
def extend_bundle_locations(randomized_locations: List[LocationData], bundle_rooms: List[BundleRoom]):
|
|
for room in bundle_rooms:
|
|
room_location = f"Complete {room.name}"
|
|
if room_location in location_table:
|
|
randomized_locations.append(location_table[room_location])
|
|
for bundle in room.bundles:
|
|
randomized_locations.append(location_table[bundle.name])
|
|
|
|
|
|
def extend_trash_bear_locations(randomized_locations: List[LocationData], trash_bear_requests: Dict[str, List[str]]):
|
|
for request_type in trash_bear_requests:
|
|
randomized_locations.append(location_table[f"Trash Bear {request_type}"])
|
|
|
|
|
|
def extend_backpack_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.backpack_progression == BackpackProgression.option_vanilla:
|
|
return
|
|
|
|
no_start_backpack = StartWithoutOptionName.backpack in options.start_without
|
|
if options.backpack_size == BackpackSize.option_12:
|
|
backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK_TIER] if no_start_backpack or LocationTags.STARTING_TOOLS not in location.tags]
|
|
else:
|
|
num_per_tier = options.backpack_size.count_per_tier()
|
|
backpack_tier_names = Backpack.get_purchasable_tiers(ModNames.big_backpack in content.registered_packs, no_start_backpack)
|
|
backpack_locations = []
|
|
for tier in backpack_tier_names:
|
|
for i in range(1, num_per_tier + 1):
|
|
backpack_locations.append(location_table[f"{tier} {i}"])
|
|
|
|
filtered_backpack_locations = filter_modded_locations(backpack_locations, content)
|
|
randomized_locations.extend(filtered_backpack_locations)
|
|
|
|
|
|
def extend_elevator_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.elevator_progression == ElevatorProgression.option_vanilla:
|
|
return
|
|
elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]]
|
|
filtered_elevator_locations = filter_modded_locations(elevator_locations, content)
|
|
randomized_locations.extend(filtered_elevator_locations)
|
|
|
|
|
|
def extend_monstersanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
monstersanity = options.monstersanity
|
|
if monstersanity == Monstersanity.option_none:
|
|
return
|
|
if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals:
|
|
monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]]
|
|
filtered_monster_locations = filter_disabled_locations(options, content, monster_locations)
|
|
randomized_locations.extend(filtered_monster_locations)
|
|
return
|
|
goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]]
|
|
filtered_goal_locations = filter_disabled_locations(options, content, goal_locations)
|
|
randomized_locations.extend(filtered_goal_locations)
|
|
if monstersanity != Monstersanity.option_progressive_goals:
|
|
return
|
|
progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]]
|
|
filtered_progressive_goal_locations = filter_disabled_locations(options, content, progressive_goal_locations)
|
|
randomized_locations.extend(filtered_progressive_goal_locations)
|
|
|
|
|
|
def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
shipsanity = options.shipsanity
|
|
if shipsanity == Shipsanity.option_none:
|
|
return
|
|
if shipsanity == Shipsanity.option_everything:
|
|
ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]]
|
|
filtered_ship_locations = filter_disabled_locations(options, content, ship_locations)
|
|
randomized_locations.extend(filtered_ship_locations)
|
|
return
|
|
shipsanity_locations = set()
|
|
if shipsanity == Shipsanity.option_fish or shipsanity == Shipsanity.option_crops_and_fish or shipsanity == Shipsanity.option_full_shipment_with_fish:
|
|
shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FISH]})
|
|
if shipsanity == Shipsanity.option_crops or shipsanity == Shipsanity.option_crops_and_fish:
|
|
shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_CROP]})
|
|
if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish:
|
|
shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]})
|
|
|
|
filtered_shipsanity_locations = filter_disabled_locations(options, content, sorted(list(shipsanity_locations), key=lambda x: x.name))
|
|
randomized_locations.extend(filtered_shipsanity_locations)
|
|
|
|
|
|
def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
cooksanity = options.cooksanity
|
|
if cooksanity == Cooksanity.option_none:
|
|
return
|
|
if cooksanity == Cooksanity.option_queen_of_sauce:
|
|
cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY_QOS])
|
|
else:
|
|
cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY])
|
|
|
|
filtered_cooksanity_locations = filter_disabled_locations(options, content, cooksanity_locations)
|
|
randomized_locations.extend(filtered_cooksanity_locations)
|
|
|
|
|
|
def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
chefsanity = options.chefsanity
|
|
if chefsanity == Chefsanity.preset_none:
|
|
return
|
|
|
|
chefsanity_locations_by_name = {} # Dictionary to not make duplicates
|
|
|
|
if ChefsanityOptionName.queen_of_sauce in chefsanity:
|
|
chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_QOS]})
|
|
if ChefsanityOptionName.purchases in chefsanity:
|
|
chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_PURCHASE]})
|
|
if ChefsanityOptionName.friendship in chefsanity:
|
|
chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_FRIENDSHIP]})
|
|
if ChefsanityOptionName.skills in chefsanity:
|
|
chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]})
|
|
|
|
filtered_chefsanity_locations = filter_disabled_locations(options, content, list(chefsanity_locations_by_name.values()))
|
|
randomized_locations.extend(filtered_chefsanity_locations)
|
|
|
|
|
|
def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.craftsanity == Craftsanity.option_none:
|
|
return
|
|
|
|
craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]]
|
|
filtered_craftsanity_locations = filter_disabled_locations(options, content, craftsanity_locations)
|
|
randomized_locations.extend(filtered_craftsanity_locations)
|
|
|
|
|
|
def extend_book_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
booksanity = content.features.booksanity
|
|
if not booksanity.is_enabled:
|
|
return
|
|
|
|
book_locations = []
|
|
for book in content.find_tagged_items(ItemTag.BOOK):
|
|
if booksanity.is_included(book):
|
|
book_locations.append(location_table[booksanity.to_location_name(book.name)])
|
|
|
|
book_locations.extend(location_table[booksanity.to_location_name(book)] for book in booksanity.get_randomized_lost_books())
|
|
|
|
randomized_locations.extend(book_locations)
|
|
|
|
|
|
def extend_walnutsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
|
|
if not options.walnutsanity:
|
|
return
|
|
|
|
if WalnutsanityOptionName.puzzles in options.walnutsanity:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_PUZZLE])
|
|
if WalnutsanityOptionName.bushes in options.walnutsanity:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_BUSH])
|
|
if WalnutsanityOptionName.dig_spots in options.walnutsanity:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_DIG])
|
|
if WalnutsanityOptionName.repeatables in options.walnutsanity:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.WALNUTSANITY_REPEATABLE])
|
|
|
|
|
|
def extend_movies_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.moviesanity == Moviesanity.option_none:
|
|
return
|
|
|
|
locations = []
|
|
if options.moviesanity == Moviesanity.option_one:
|
|
locations.extend(locations_by_tag[LocationTags.ANY_MOVIE])
|
|
if options.moviesanity >= Moviesanity.option_all_movies:
|
|
locations.extend(locations_by_tag[LocationTags.MOVIE])
|
|
if options.moviesanity >= Moviesanity.option_all_movies_and_all_snacks:
|
|
locations.extend(locations_by_tag[LocationTags.MOVIE_SNACK])
|
|
filtered_locations = filter_disabled_locations(options, content, locations)
|
|
randomized_locations.extend(filtered_locations)
|
|
|
|
|
|
def extend_secrets_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if not options.secretsanity:
|
|
return
|
|
|
|
locations = []
|
|
if SecretsanityOptionName.easy in options.secretsanity:
|
|
locations.extend(locations_by_tag[LocationTags.EASY_SECRET])
|
|
if SecretsanityOptionName.fishing in options.secretsanity:
|
|
locations.extend(locations_by_tag[LocationTags.FISHING_SECRET])
|
|
if SecretsanityOptionName.difficult in options.secretsanity:
|
|
locations.extend(locations_by_tag[LocationTags.DIFFICULT_SECRET])
|
|
if SecretsanityOptionName.secret_notes in options.secretsanity:
|
|
locations.extend(locations_by_tag[LocationTags.SECRET_NOTE])
|
|
for location_dupe in locations_by_tag[LocationTags.REPLACES_PREVIOUS_LOCATION]:
|
|
second_part_of_name = location_dupe.name.split(":")[-1]
|
|
for location in randomized_locations:
|
|
second_part_of_dupe_name = location.name.split(":")[-1]
|
|
if second_part_of_name == second_part_of_dupe_name:
|
|
randomized_locations.remove(location)
|
|
filtered_locations = filter_disabled_locations(options, content, locations)
|
|
randomized_locations.extend(filtered_locations)
|
|
|
|
|
|
def extend_hats_locations(randomized_locations: List[LocationData], content: StardewContent):
|
|
hatsanity = content.features.hatsanity
|
|
if not hatsanity.is_enabled:
|
|
return
|
|
|
|
for hat in content.hats.values():
|
|
if not hatsanity.is_included(hat):
|
|
continue
|
|
|
|
randomized_locations.append(location_table[hatsanity.to_location_name(hat)])
|
|
|
|
|
|
def eatsanity_item_is_included(location: LocationData, options: StardewValleyOptions, content: StardewContent) -> bool:
|
|
eat_prefix = "Eat "
|
|
drink_prefix = "Drink "
|
|
if location.name.startswith(eat_prefix):
|
|
item_name = location.name[len(eat_prefix):]
|
|
elif location.name.startswith(drink_prefix):
|
|
item_name = location.name[len(drink_prefix):]
|
|
else:
|
|
raise Exception(f"Eatsanity Location does not have a recognized prefix: '{location.name}'")
|
|
|
|
# if not item_name in content.game_items:
|
|
# return False
|
|
if EatsanityOptionName.poisonous in options.eatsanity.value:
|
|
return True
|
|
if location in locations_by_tag[LocationTags.EATSANITY_POISONOUS]:
|
|
return False
|
|
return True
|
|
|
|
|
|
def extend_eatsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.eatsanity.value == Eatsanity.preset_none:
|
|
return
|
|
|
|
eatsanity_locations = []
|
|
if EatsanityOptionName.crops in options.eatsanity:
|
|
eatsanity_locations.extend(locations_by_tag[LocationTags.EATSANITY_CROP])
|
|
if EatsanityOptionName.cooking in options.eatsanity:
|
|
eatsanity_locations.extend(locations_by_tag[LocationTags.EATSANITY_COOKING])
|
|
if EatsanityOptionName.fish in options.eatsanity:
|
|
eatsanity_locations.extend(locations_by_tag[LocationTags.EATSANITY_FISH])
|
|
if EatsanityOptionName.artisan in options.eatsanity:
|
|
eatsanity_locations.extend(locations_by_tag[LocationTags.EATSANITY_ARTISAN])
|
|
if EatsanityOptionName.shop in options.eatsanity:
|
|
eatsanity_locations.extend(locations_by_tag[LocationTags.EATSANITY_SHOP])
|
|
|
|
eatsanity_locations = [location for location in eatsanity_locations if eatsanity_item_is_included(location, options, content)]
|
|
eatsanity_locations = filter_disabled_locations(options, content, eatsanity_locations)
|
|
randomized_locations.extend(eatsanity_locations)
|
|
|
|
|
|
def extend_endgame_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
if options.include_endgame_locations.value == IncludeEndgameLocations.option_false:
|
|
return
|
|
|
|
has_friendsanity_marriage = options.friendsanity == Friendsanity.option_all_with_marriage
|
|
has_friendsanity = (not has_friendsanity_marriage) and options.friendsanity != Friendsanity.option_none
|
|
|
|
endgame_locations = []
|
|
endgame_locations.extend(locations_by_tag[LocationTags.ENDGAME_LOCATIONS])
|
|
|
|
endgame_locations = [location for location in endgame_locations if
|
|
LocationTags.REQUIRES_FRIENDSANITY_MARRIAGE not in location.tags or has_friendsanity_marriage]
|
|
endgame_locations = [location for location in endgame_locations if LocationTags.REQUIRES_FRIENDSANITY not in location.tags or has_friendsanity]
|
|
endgame_locations = filter_disabled_locations(options, content, endgame_locations)
|
|
randomized_locations.extend(endgame_locations)
|
|
|
|
|
|
def extend_filler_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
|
|
days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
number_locations_to_add_per_day = 0
|
|
min_number_locations = 90 # Under 90 locations we can run out of rooms for the mandatory core items
|
|
if len(randomized_locations) < min_number_locations:
|
|
number_locations_to_add = min_number_locations - len(randomized_locations)
|
|
number_locations_to_add_per_day += math.ceil(number_locations_to_add / 7)
|
|
|
|
# These settings generate a lot of empty locations, so they can absorb a lot of items
|
|
filler_heavy_settings = [options.fishsanity != Fishsanity.option_none,
|
|
options.shipsanity != Shipsanity.option_none,
|
|
options.cooksanity != Cooksanity.option_none,
|
|
options.craftsanity != Craftsanity.option_none,
|
|
len(options.eatsanity.value) > 0,
|
|
options.museumsanity == Museumsanity.option_all,
|
|
options.quest_locations.value >= 0,
|
|
options.bundle_per_room >= 2]
|
|
# These settings generate orphan items and can cause too many items, if enabled without a complementary of the filler heavy settings
|
|
orphan_settings = [len(options.chefsanity.value) > 0,
|
|
options.friendsanity != Friendsanity.option_none,
|
|
options.skill_progression == SkillProgression.option_progressive_with_masteries,
|
|
options.cropsanity != Cropsanity.option_disabled,
|
|
len(options.start_without.value) > 0,
|
|
options.bundle_per_room <= -1,
|
|
options.bundle_per_room <= -2]
|
|
|
|
enabled_filler_heavy_settings = len([val for val in filler_heavy_settings if val])
|
|
enabled_orphan_settings = len([val for val in orphan_settings if val])
|
|
if enabled_orphan_settings > enabled_filler_heavy_settings:
|
|
number_locations_to_add_per_day += enabled_orphan_settings - enabled_filler_heavy_settings
|
|
|
|
if number_locations_to_add_per_day <= 0:
|
|
return
|
|
|
|
existing_traveling_merchant_locations = [location.name for location in randomized_locations if location.name.startswith("Traveling Merchant Sunday Item ")]
|
|
start_num_to_add = len(existing_traveling_merchant_locations) + 1
|
|
|
|
for i in range(start_num_to_add, start_num_to_add+number_locations_to_add_per_day):
|
|
logger.debug(f"Player too few locations, adding Traveling Merchant Items #{i}")
|
|
for day in days:
|
|
location_name = f"Traveling Merchant {day} Item {i}"
|
|
randomized_locations.append(location_table[location_name])
|
|
|
|
|
|
def create_locations(location_collector: StardewLocationCollector,
|
|
bundle_rooms: List[BundleRoom],
|
|
trash_bear_requests: Dict[str, List[str]],
|
|
options: StardewValleyOptions,
|
|
content: StardewContent,
|
|
random: Random):
|
|
randomized_locations = []
|
|
|
|
extend_mandatory_locations(randomized_locations, options, content)
|
|
extend_bundle_locations(randomized_locations, bundle_rooms)
|
|
extend_trash_bear_locations(randomized_locations, trash_bear_requests)
|
|
extend_backpack_locations(randomized_locations, options, content)
|
|
|
|
if content.features.tool_progression.is_progressive:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
|
|
|
|
extend_elevator_locations(randomized_locations, options, content)
|
|
|
|
skill_progression = content.features.skill_progression
|
|
if skill_progression.is_progressive:
|
|
for skill in content.skills.values():
|
|
randomized_locations.extend([location_table[location_name] for _, location_name in skill_progression.get_randomized_level_names_by_level(skill)])
|
|
if skill_progression.is_mastery_randomized(skill):
|
|
randomized_locations.append(location_table[skill.mastery_name])
|
|
|
|
extend_building_locations(randomized_locations, content)
|
|
|
|
if options.arcade_machine_locations != ArcadeMachineLocations.option_disabled:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
|
|
|
|
if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
|
|
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE])
|
|
|
|
extend_cropsanity_locations(randomized_locations, content)
|
|
extend_fishsanity_locations(randomized_locations, content, random)
|
|
extend_museumsanity_locations(randomized_locations, options, random)
|
|
extend_friendsanity_locations(randomized_locations, content)
|
|
|
|
extend_festival_locations(randomized_locations, options, random)
|
|
extend_special_order_locations(randomized_locations, options, content)
|
|
extend_walnut_purchase_locations(randomized_locations, content)
|
|
|
|
extend_monstersanity_locations(randomized_locations, options, content)
|
|
extend_shipsanity_locations(randomized_locations, options, content)
|
|
extend_cooksanity_locations(randomized_locations, options, content)
|
|
extend_chefsanity_locations(randomized_locations, options, content)
|
|
extend_craftsanity_locations(randomized_locations, options, content)
|
|
extend_quests_locations(randomized_locations, options, content)
|
|
extend_book_locations(randomized_locations, content)
|
|
extend_walnutsanity_locations(randomized_locations, options)
|
|
extend_movies_locations(randomized_locations, options, content)
|
|
extend_secrets_locations(randomized_locations, options, content)
|
|
extend_hats_locations(randomized_locations, content)
|
|
extend_eatsanity_locations(randomized_locations, options, content)
|
|
extend_endgame_locations(randomized_locations, options, content)
|
|
|
|
# Mods
|
|
extend_situational_quest_locations(randomized_locations, options, content)
|
|
|
|
extend_filler_locations(randomized_locations, options, content)
|
|
|
|
for location_data in randomized_locations:
|
|
location_collector(location_data.name, location_data.code, location_data.region)
|
|
|
|
|
|
def filter_deprecated_locations(locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
return [location for location in locations if LocationTags.DEPRECATED not in location.tags]
|
|
|
|
|
|
def filter_animals_quest(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
# On Meadowlands, "Feeding Animals" replaces "Raising Animals"
|
|
if options.farm_type == FarmType.option_meadowlands:
|
|
return (location for location in locations if location.name != f"Quest: {Quest.raising_animals}")
|
|
else:
|
|
return (location for location in locations if location.name != f"Quest: {Quest.feeding_animals}")
|
|
|
|
|
|
def filter_farm_exclusives(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
# Some locations are only on specific farms
|
|
if options.farm_type != FarmType.option_beach:
|
|
return (location for location in locations if LocationTags.BEACH_FARM not in location.tags)
|
|
return locations
|
|
|
|
|
|
def filter_farm_type(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
animals_filter = filter_animals_quest(options, locations)
|
|
exclusives_filter = filter_farm_exclusives(options, animals_filter)
|
|
return exclusives_filter
|
|
|
|
|
|
def filter_ginger_island(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
include_island = content.is_enabled(ginger_island_content_pack)
|
|
return (location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags)
|
|
|
|
|
|
def filter_qi_order_locations(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
include_qi_orders = content.is_enabled(qi_board_content_pack)
|
|
return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags)
|
|
|
|
|
|
def filter_masteries_locations(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
# FIXME Remove once recipes are handled by the content packs
|
|
if content.features.skill_progression.are_masteries_shuffled:
|
|
return locations
|
|
return (location for location in locations if LocationTags.REQUIRES_MASTERIES not in location.tags)
|
|
|
|
|
|
def filter_modded_locations(locations: Iterable[LocationData], content: StardewContent) -> Iterable[LocationData]:
|
|
return (location for location in locations if content.are_all_enabled(location.content_packs))
|
|
|
|
|
|
def filter_disabled_locations(options: StardewValleyOptions, content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
|
|
locations_deprecated_filter = filter_deprecated_locations(locations)
|
|
locations_farm_filter = filter_farm_type(options, locations_deprecated_filter)
|
|
locations_island_filter = filter_ginger_island(content, locations_farm_filter)
|
|
locations_qi_filter = filter_qi_order_locations(content, locations_island_filter)
|
|
locations_masteries_filter = filter_masteries_locations(content, locations_qi_filter)
|
|
locations_mod_filter = filter_modded_locations(locations_masteries_filter, content)
|
|
return locations_mod_filter
|