798 lines
37 KiB
Python
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
|