Archipelago/worlds/stardew_valley/items.py

798 lines
37 KiB
Python

import csv
import enum
import logging
from dataclasses import dataclass, field
from pathlib import Path
from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional
from BaseClasses import Item, ItemClassification
from . import data
from .data.villagers_data import get_villagers_for_mods
from .mods.mod_data import ModNames
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Cropsanity, \
Friendsanity, Museumsanity, \
Fishsanity, BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity
from .strings.ap_names.ap_weapon_names import APWeapon
from .strings.ap_names.buff_names import Buff
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
from .strings.ap_names.event_names import Event
from .strings.ap_names.mods.mod_items import SVEQuestItem
from .strings.villager_names import NPC, ModNPC
from .strings.wallet_item_names import Wallet
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()
FOOTWEAR = enum.auto()
HATS = enum.auto()
RING = enum.auto()
WEAPON = enum.auto()
WEAPON_GENERIC = enum.auto()
WEAPON_SWORD = enum.auto()
WEAPON_CLUB = enum.auto()
WEAPON_DAGGER = enum.auto()
WEAPON_SLINGSHOT = enum.auto()
PROGRESSIVE_TOOLS = enum.auto()
SKILL_LEVEL_UP = enum.auto()
BUILDING = enum.auto()
WIZARD_BUILDING = enum.auto()
ARCADE_MACHINE_BUFFS = enum.auto()
BASE_RESOURCE = enum.auto()
WARP_TOTEM = enum.auto()
GEODE = enum.auto()
ORE = enum.auto()
FERTILIZER = enum.auto()
SEED = enum.auto()
CROPSANITY = enum.auto()
FISHING_RESOURCE = enum.auto()
SEASON = enum.auto()
TRAVELING_MERCHANT_DAY = enum.auto()
MUSEUM = enum.auto()
FRIENDSANITY = enum.auto()
FESTIVAL = enum.auto()
RARECROW = enum.auto()
TRAP = enum.auto()
MAXIMUM_ONE = enum.auto()
EXACTLY_TWO = enum.auto()
DEPRECATED = enum.auto()
RESOURCE_PACK_USEFUL = enum.auto()
SPECIAL_ORDER_BOARD = enum.auto()
SPECIAL_ORDER_QI = enum.auto()
BABY = enum.auto()
GINGER_ISLAND = enum.auto()
WALNUT_PURCHASE = enum.auto()
TV_CHANNEL = enum.auto()
QI_CRAFTING_RECIPE = enum.auto()
CHEFSANITY = enum.auto()
CHEFSANITY_STARTER = enum.auto()
CHEFSANITY_QOS = enum.auto()
CHEFSANITY_PURCHASE = enum.auto()
CHEFSANITY_FRIENDSHIP = enum.auto()
CHEFSANITY_SKILL = enum.auto()
CRAFTSANITY = enum.auto()
# Mods
MAGIC_SPELL = enum.auto()
MOD_WARP = enum.auto()
@dataclass(frozen=True)
class ItemData:
code_without_offset: Optional[int]
name: str
classification: ItemClassification
mod_name: Optional[str] = None
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))
class StardewItemFactory(Protocol):
def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
raise NotImplementedError
class StardewItemDeleter(Protocol):
def __call__(self, item: Item):
raise NotImplementedError
def load_item_csv():
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files # noqa
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}
mod_name = str(item["mod_name"]) if item["mod_name"] else None
items.append(ItemData(id, item["name"], classification, mod_name, groups))
return items
events = [
ItemData(None, Event.victory, ItemClassification.progression),
ItemData(None, Event.can_construct_buildings, ItemClassification.progression),
ItemData(None, Event.start_dark_talisman_quest, ItemClassification.progression),
ItemData(None, Event.can_ship_items, ItemClassification.progression),
ItemData(None, Event.can_shop_at_pierre, ItemClassification.progression),
]
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})
initialize_item_table()
initialize_groups()
def get_too_many_items_error_message(locations_count: int, items_count: int) -> str:
return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]"
def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item],
options: StardewValleyOptions, random: Random) -> List[Item]:
items = []
unique_items = create_unique_items(item_factory, options, random)
remove_items(item_deleter, items_to_exclude, unique_items)
remove_items_if_no_room_for_them(item_deleter, unique_items, locations_count, random)
items += unique_items
logger.debug(f"Created {len(unique_items)} unique items")
unique_filler_items = create_unique_filler_items(item_factory, options, random, locations_count - len(items))
items += unique_filler_items
logger.debug(f"Created {len(unique_filler_items)} unique filler items")
resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count)
items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs")
return items
def remove_items(item_deleter: StardewItemDeleter, items_to_remove, items):
for item in items_to_remove:
if item in items:
items.remove(item)
item_deleter(item)
def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_items: List[Item], locations_count: int, random: Random):
if len(unique_items) <= locations_count:
return
number_of_items_to_remove = len(unique_items) - locations_count
removable_items = [item for item in unique_items if item.classification == ItemClassification.filler or item.classification == ItemClassification.trap]
if len(removable_items) < number_of_items_to_remove:
logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random non-progression items")
removable_items = [item for item in unique_items if not item.classification & ItemClassification.progression]
else:
logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random filler items")
assert len(removable_items) >= number_of_items_to_remove, get_too_many_items_error_message(locations_count, len(unique_items))
items_to_remove = random.sample(removable_items, number_of_items_to_remove)
remove_items(item_deleter, items_to_remove, unique_items)
def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random) -> List[Item]:
items = []
items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD])
items.append(item_factory(CommunityUpgrade.movie_theater)) # It is a community reward, but we need two of them
items.append(item_factory(Wallet.metal_detector)) # Always offer at least one metal detector
create_backpack_items(item_factory, options, items)
create_weapons(item_factory, options, items)
items.append(item_factory("Skull Key"))
create_elevators(item_factory, options, items)
create_tools(item_factory, options, items)
create_skills(item_factory, options, items)
create_wizard_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, options, items)
items.append(item_factory("Railroad Boulder Removed"))
items.append(item_factory(CommunityUpgrade.fruit_bats))
items.append(item_factory(CommunityUpgrade.mushroom_boxes))
items.append(item_factory("Beach Bridge"))
create_tv_channels(item_factory, options, items)
create_special_quest_rewards(item_factory, options, items)
create_stardrops(item_factory, options, items)
create_museum_items(item_factory, options, items)
create_arcade_machine_items(item_factory, options, items)
create_player_buffs(item_factory, options, items)
create_traveling_merchant_items(item_factory, items)
items.append(item_factory("Return Scepter"))
create_seasons(item_factory, options, items)
create_seeds(item_factory, options, items)
create_friendsanity_items(item_factory, options, items, random)
create_festival_rewards(item_factory, options, items)
create_special_order_board_rewards(item_factory, options, items)
create_special_order_qi_rewards(item_factory, options, items)
create_walnut_purchase_rewards(item_factory, options, items)
create_crafting_recipes(item_factory, options, items)
create_cooking_recipes(item_factory, options, items)
create_shipsanity_items(item_factory, options, items)
create_goal_items(item_factory, options, items)
items.append(item_factory("Golden Egg"))
create_magic_mod_spells(item_factory, options, items)
create_deepwoods_pendants(item_factory, options, items)
create_archaeology_items(item_factory, options, items)
return items
def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if (options.backpack_progression == BackpackProgression.option_progressive or
options.backpack_progression == BackpackProgression.option_early_progressive):
items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2)
if ModNames.big_backpack in options.mods:
items.append(item_factory("Progressive Backpack"))
def create_weapons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
weapons = weapons_count(options)
items.extend(item_factory(item) for item in [APWeapon.slingshot] * 2)
monstersanity = options.monstersanity
if monstersanity == Monstersanity.option_none: # Without monstersanity, might not be enough checks to split the weapons
items.extend(item_factory(item) for item in [APWeapon.weapon] * weapons)
items.extend(item_factory(item) for item in [APWeapon.footwear] * 3) # 1-2 | 3-4 | 6-7-8
return
items.extend(item_factory(item) for item in [APWeapon.sword] * weapons)
items.extend(item_factory(item) for item in [APWeapon.club] * weapons)
items.extend(item_factory(item) for item in [APWeapon.dagger] * weapons)
items.extend(item_factory(item) for item in [APWeapon.footwear] * 4) # 1-2 | 3-4 | 6-7-8 | 11-13
if monstersanity == Monstersanity.option_goals or monstersanity == Monstersanity.option_one_per_category or \
monstersanity == Monstersanity.option_short_goals or monstersanity == Monstersanity.option_very_short_goals:
return
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
rings_items = [item for item in items_by_group[Group.RING] if item.classification is not ItemClassification.filler]
else:
rings_items = [item for item in items_by_group[Group.RING]]
items.extend(item_factory(item) for item in rings_items)
def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.elevator_progression == ElevatorProgression.option_vanilla:
return
items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24])
if ModNames.deepwoods in options.mods:
items.extend([item_factory(item) for item in ["Progressive Woods Obelisk Sigils"] * 10])
if ModNames.skull_cavern_elevator in options.mods:
items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])
def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.tool_progression & ToolProgression.option_progressive:
for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]:
name = item_data.name
if "Trash Can" in name:
items.extend([item_factory(item) for item in [item_data] * 3])
items.append(item_factory(item_data, ItemClassification.useful))
else:
items.extend([item_factory(item) for item in [item_data] * 4])
items.append(item_factory("Golden Scythe"))
def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.skill_progression == SkillProgression.option_progressive:
for item in items_by_group[Group.SKILL_LEVEL_UP]:
if item.mod_name not in options.mods and item.mod_name is not None:
continue
items.extend(item_factory(item) for item in [item.name] * 10)
def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
useless_buildings_classification = ItemClassification.progression_skip_balancing if world_is_perfection(options) else ItemClassification.useful
items.append(item_factory("Earth Obelisk", useless_buildings_classification))
items.append(item_factory("Water Obelisk", useless_buildings_classification))
items.append(item_factory("Desert Obelisk"))
items.append(item_factory("Junimo Hut"))
items.append(item_factory("Gold Clock", useless_buildings_classification))
if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
items.append(item_factory("Island Obelisk"))
if ModNames.deepwoods in options.mods:
items.append(item_factory("Woods Obelisk"))
def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
building_option = options.building_progression
if not building_option & BuildingProgression.option_progressive:
return
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", ItemClassification.useful))
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"))
if ModNames.tractor in options.mods:
items.append(item_factory("Tractor Garage"))
def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.quest_locations < 0:
return
# items.append(item_factory("Adventurer's Guild")) # Now unlocked always!
items.append(item_factory(Wallet.club_card))
items.append(item_factory(Wallet.magnifying_glass))
if ModNames.sve in options.mods:
items.append(item_factory(Wallet.bears_knowledge))
else:
items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful)) # Not necessary outside of SVE
items.append(item_factory(Wallet.iridium_snake_milk))
items.append(item_factory("Fairy Dust Recipe"))
items.append(item_factory("Dark Talisman"))
create_special_quest_rewards_sve(item_factory, options, items)
create_distant_lands_quest_rewards(item_factory, options, items)
create_boarding_house_quest_rewards(item_factory, options, items)
def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
stardrops_classification = get_stardrop_classification(options)
items.append(item_factory("Stardrop", stardrops_classification)) # The Mines level 100
items.append(item_factory("Stardrop", stardrops_classification)) # Old Master Cannoli
items.append(item_factory("Stardrop", stardrops_classification)) # Krobus Stardrop
if options.fishsanity != Fishsanity.option_none:
items.append(item_factory("Stardrop", stardrops_classification)) # Master Angler Stardrop
if ModNames.deepwoods in options.mods:
items.append(item_factory("Stardrop", stardrops_classification)) # Petting the Unicorn
if options.friendsanity != Friendsanity.option_none:
items.append(item_factory("Stardrop", stardrops_classification)) # Spouse Stardrop
def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
items.append(item_factory(Wallet.rusty_key))
items.append(item_factory(Wallet.dwarvish_translation_guide))
items.append(item_factory("Ancient Seeds Recipe"))
items.append(item_factory("Stardrop", get_stardrop_classification(options)))
if options.museumsanity == Museumsanity.option_none:
return
items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 10)
items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5)
items.append(item_factory(Wallet.metal_detector))
def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item], random: Random):
island_villagers = [NPC.leo, ModNPC.lance]
if options.friendsanity == Friendsanity.option_none:
return
create_babies(item_factory, items, random)
exclude_non_bachelors = options.friendsanity == Friendsanity.option_bachelors
exclude_locked_villagers = options.friendsanity == Friendsanity.option_starting_npcs or \
options.friendsanity == Friendsanity.option_bachelors
include_post_marriage_hearts = options.friendsanity == Friendsanity.option_all_with_marriage
exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
mods = options.mods
heart_size = options.friendsanity_heart_size
for villager in get_villagers_for_mods(mods.value):
if not villager.available and exclude_locked_villagers:
continue
if not villager.bachelor and exclude_non_bachelors:
continue
if villager.name in island_villagers and exclude_ginger_island:
continue
heart_cap = 8 if villager.bachelor else 10
if include_post_marriage_hearts and villager.bachelor:
heart_cap = 14
classification = ItemClassification.progression
for heart in range(1, 15):
if heart > heart_cap:
break
if heart % heart_size == 0 or heart == heart_cap:
items.append(item_factory(f"{villager.name} <3", classification))
if not exclude_non_bachelors:
need_pet = options.goal == Goal.option_grandpa_evaluation
for heart in range(1, 6):
if heart % heart_size == 0 or heart == 5:
items.append(item_factory(f"Pet <3", ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful))
def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random):
baby_items = [item for item in items_by_group[Group.BABY]]
for i in range(2):
chosen_baby = random.choice(baby_items)
items.append(item_factory(chosen_baby))
def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.arcade_machine_locations == 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, options: StardewValleyOptions, items: List[Item]):
movement_buffs: int = options.movement_buff_number.value
luck_buffs: int = options.luck_buff_number.value
need_all_buffs = options.special_order_locations == SpecialOrderLocations.option_board_qi
need_half_buffs = options.festival_locations == FestivalLocations.option_easy
create_player_buff(item_factory, Buff.movement, movement_buffs, need_all_buffs, need_half_buffs, items)
create_player_buff(item_factory, Buff.luck, luck_buffs, True, need_half_buffs, items)
def create_player_buff(item_factory, buff: str, amount: int, need_all_buffs: bool, need_half_buffs: bool, items: List[Item]):
progression_buffs = amount if need_all_buffs else (amount // 2 if need_half_buffs else 0)
useful_buffs = amount - progression_buffs
items.extend(item_factory(item) for item in [buff] * progression_buffs)
items.extend(item_factory(item, ItemClassification.useful) for item in [buff] * useful_buffs)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]),
*(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6),
*(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)])
def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.season_randomization == SeasonRandomization.option_disabled:
return
if options.season_randomization == SeasonRandomization.option_progressive:
items.extend([item_factory(item) for item in ["Progressive Season"] * 3])
return
items.extend([item_factory(item) for item in items_by_group[Group.SEASON]])
def create_seeds(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.cropsanity == Cropsanity.option_disabled:
return
base_seed_items = [item for item in items_by_group[Group.CROPSANITY]]
filtered_seed_items = remove_excluded_items(base_seed_items, options)
seed_items = [item_factory(item) for item in filtered_seed_items]
items.extend(seed_items)
def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
items.append(item_factory("Deluxe Scarecrow Recipe"))
if options.festival_locations == FestivalLocations.option_disabled:
return
festival_rewards = [item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler]
items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))])
def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
items.extend([item_factory("Boat Repair"),
item_factory("Open Professor Snail Cave"),
item_factory("Ostrich Incubator Recipe"),
item_factory("Treehouse"),
*[item_factory(item) for item in items_by_group[Group.WALNUT_PURCHASE]]])
def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.special_order_locations == SpecialOrderLocations.option_disabled:
return
special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]]
items.extend([item_factory(item) for item in special_order_board_items])
def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification:
if item.classification is ItemClassification.useful:
return ItemClassification.useful
if item.name == "Special Order Board":
return ItemClassification.progression
if need_all_recipes and "Recipe" in item.name:
return ItemClassification.progression_skip_balancing
if item.name == "Monster Musk Recipe":
return ItemClassification.progression_skip_balancing
return ItemClassification.useful
def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
qi_gem_rewards = []
if options.bundle_randomization >= BundleRandomization.option_remixed:
qi_gem_rewards.append("15 Qi Gems")
qi_gem_rewards.append("15 Qi Gems")
if options.special_order_locations == SpecialOrderLocations.option_board_qi:
qi_gem_rewards.extend(["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems",
"40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"])
qi_gem_items = [item_factory(reward) for reward in qi_gem_rewards]
items.extend(qi_gem_items)
def create_tv_channels(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
channels = [channel for channel in items_by_group[Group.TV_CHANNEL]]
if options.entrance_randomization == EntranceRandomization.option_disabled:
channels = [channel for channel in channels if channel.name != "The Gateway Gazette"]
items.extend([item_factory(item) for item in channels])
def create_crafting_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
has_craftsanity = options.craftsanity == Craftsanity.option_all
crafting_recipes = []
crafting_recipes.extend([recipe for recipe in items_by_group[Group.QI_CRAFTING_RECIPE]])
if has_craftsanity:
crafting_recipes.extend([recipe for recipe in items_by_group[Group.CRAFTSANITY]])
crafting_recipes = remove_excluded_items(crafting_recipes, options)
items.extend([item_factory(item) for item in crafting_recipes])
def create_cooking_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
chefsanity = options.chefsanity
if chefsanity == Chefsanity.option_none:
return
chefsanity_recipes_by_name = {recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_STARTER]} # Dictionary to not make duplicates
if chefsanity & Chefsanity.option_queen_of_sauce:
chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_QOS]})
if chefsanity & Chefsanity.option_purchases:
chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_PURCHASE]})
if chefsanity & Chefsanity.option_friendship:
chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_FRIENDSHIP]})
if chefsanity & Chefsanity.option_skills:
chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_SKILL]})
filtered_chefsanity_recipes = remove_excluded_items(list(chefsanity_recipes_by_name.values()), options)
items.extend([item_factory(item) for item in filtered_chefsanity_recipes])
def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
shipsanity = options.shipsanity
if shipsanity != Shipsanity.option_everything:
return
items.append(item_factory(Wallet.metal_detector))
def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
goal = options.goal
if goal != Goal.option_perfection and goal != Goal.option_complete_collection:
return
items.append(item_factory(Wallet.metal_detector))
def create_archaeology_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
mods = options.mods
if ModNames.archaeology not in mods:
return
items.append(item_factory(Wallet.metal_detector))
def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]:
if options.festival_locations == FestivalLocations.option_disabled:
return []
return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if
item.classification == ItemClassification.filler]
def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if ModNames.magic not in options.mods:
return
items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]])
def create_deepwoods_pendants(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if ModNames.deepwoods not in options.mods:
return
items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]])
def create_special_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if ModNames.sve not in options.mods:
return
items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve])
if options.quest_locations < 0:
return
exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items])
if exclude_ginger_island:
return
items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island])
def create_distant_lands_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.quest_locations < 0 or ModNames.distant_lands not in options.mods:
return
items.append(item_factory("Crayfish Soup Recipe"))
if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return
items.append(item_factory("Ginger Tincture Recipe"))
def create_boarding_house_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.quest_locations < 0 or ModNames.boarding_house not in options.mods:
return
items.append(item_factory("Special Pumpkin Soup Recipe"))
def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
available_item_slots: int) -> List[Item]:
items = []
items.extend(create_filler_festival_rewards(item_factory, options))
if len(items) > available_item_slots:
items = random.sample(items, available_item_slots)
return items
def weapons_count(options: StardewValleyOptions):
weapon_count = 5
if ModNames.sve in options.mods:
weapon_count += 1
return weapon_count
def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
items_already_added: List[Item],
number_locations: int) -> List[Item]:
include_traps = options.trap_items != TrapItems.option_no_traps
items_already_added_names = [item.name for item in items_already_added]
useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
if pack.name not in items_already_added_names]
trap_items = [pack for pack in items_by_group[Group.TRAP]
if pack.name not in items_already_added_names and
(pack.mod_name is None or pack.mod_name in options.mods)]
priority_filler_items = []
priority_filler_items.extend(useful_resource_packs)
if include_traps:
priority_filler_items.extend(trap_items)
exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options)
priority_filler_items = remove_excluded_items(priority_filler_items, options)
number_priority_items = len(priority_filler_items)
required_resource_pack = number_locations - len(items_already_added)
if required_resource_pack < number_priority_items:
chosen_priority_items = [item_factory(resource_pack) for resource_pack in
random.sample(priority_filler_items, required_resource_pack)]
return chosen_priority_items
items = []
chosen_priority_items = [item_factory(resource_pack,
ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful)
for resource_pack in priority_filler_items]
items.extend(chosen_priority_items)
required_resource_pack -= number_priority_items
all_filler_packs = [filler_pack for filler_pack in all_filler_packs
if Group.MAXIMUM_ONE not in filler_pack.groups or
(filler_pack.name not in [priority_item.name for priority_item in
priority_filler_items] and filler_pack.name not in items_already_added_names)]
while required_resource_pack > 0:
resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
while exactly_2 and required_resource_pack == 1:
resource_pack = random.choice(all_filler_packs)
exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
if exactly_2:
items.append(item_factory(resource_pack, classification))
required_resource_pack -= 1
if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
all_filler_packs.remove(resource_pack)
return items
def filter_deprecated_items(items: List[ItemData]) -> List[ItemData]:
return [item for item in items if Group.DEPRECATED not in item.groups]
def filter_ginger_island_items(exclude_island: bool, items: List[ItemData]) -> List[ItemData]:
return [item for item in items if not exclude_island or Group.GINGER_ISLAND not in item.groups]
def filter_mod_items(mods: Set[str], items: List[ItemData]) -> List[ItemData]:
return [item for item in items if item.mod_name is None or item.mod_name in mods]
def remove_excluded_items(items, options: StardewValleyOptions):
return remove_excluded_items_island_mods(items, options.exclude_ginger_island == ExcludeGingerIsland.option_true, options.mods.value)
def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods: Set[str]):
deprecated_filter = filter_deprecated_items(items)
ginger_island_filter = filter_ginger_island_items(exclude_ginger_island, deprecated_filter)
mod_filter = filter_mod_items(mods, ginger_island_filter)
return mod_filter
def remove_limited_amount_packs(packs):
return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups]
def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool):
all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
all_filler_items.extend(items_by_group[Group.TRASH])
if include_traps:
all_filler_items.extend(items_by_group[Group.TRAP])
all_filler_items = remove_excluded_items_island_mods(all_filler_items, exclude_ginger_island, set())
return all_filler_items
def get_stardrop_classification(options) -> ItemClassification:
return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful
def world_is_perfection(options) -> bool:
return options.goal == Goal.option_perfection
def world_is_stardrops(options) -> bool:
return options.goal == Goal.option_mystery_of_the_stardrops