Archipelago/worlds/stardew_valley/items.py

377 lines
16 KiB
Python

import bisect
import csv
import enum
import itertools
import logging
import math
import typing
from collections import OrderedDict
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional, FrozenSet
from BaseClasses import Item, ItemClassification
from . import options, data
ITEM_CODE_OFFSET = 717000
logger = logging.getLogger(__name__)
world_folder = Path(__file__).parent
class Group(enum.Enum):
RESOURCE_PACK = enum.auto()
FRIENDSHIP_PACK = enum.auto()
COMMUNITY_REWARD = enum.auto()
TRASH = enum.auto()
MINES_FLOOR_10 = enum.auto()
MINES_FLOOR_20 = enum.auto()
MINES_FLOOR_50 = enum.auto()
MINES_FLOOR_60 = enum.auto()
MINES_FLOOR_80 = enum.auto()
MINES_FLOOR_90 = enum.auto()
MINES_FLOOR_110 = enum.auto()
FOOTWEAR = enum.auto()
HATS = enum.auto()
RING = enum.auto()
WEAPON = enum.auto()
PROGRESSIVE_TOOLS = enum.auto()
SKILL_LEVEL_UP = enum.auto()
ARCADE_MACHINE_BUFFS = enum.auto()
GALAXY_WEAPONS = enum.auto()
BASE_RESOURCE = enum.auto()
WARP_TOTEM = enum.auto()
GEODE = enum.auto()
ORE = enum.auto()
FERTILIZER = enum.auto()
SEED = enum.auto()
FISHING_RESOURCE = enum.auto()
@dataclass(frozen=True)
class ItemData:
code_without_offset: Optional[int]
name: str
classification: ItemClassification
groups: Set[Group] = field(default_factory=frozenset)
def __post_init__(self):
if not isinstance(self.groups, frozenset):
super().__setattr__("groups", frozenset(self.groups))
@property
def code(self):
return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None
def has_any_group(self, *group: Group) -> bool:
groups = set(group)
return bool(groups.intersection(self.groups))
@dataclass(frozen=True)
class ResourcePackData:
name: str
default_amount: int = 1
scaling_factor: int = 1
classification: ItemClassification = ItemClassification.filler
groups: FrozenSet[Group] = frozenset()
def as_item_data(self, counter: itertools.count) -> [ItemData]:
return [ItemData(next(counter), self.create_item_name(quantity), self.classification,
{Group.RESOURCE_PACK} | self.groups)
for quantity in self.scale_quantity.values()]
def create_item_name(self, quantity: int) -> str:
return f"Resource Pack: {quantity} {self.name}"
@cached_property
def scale_quantity(self) -> typing.OrderedDict[int, int]:
"""Discrete scaling of the resource pack quantities.
100 is default, 200 is double, 50 is half (if the scaling_factor allows it).
"""
levels = math.ceil(self.default_amount / self.scaling_factor) * 2
first_level = self.default_amount % self.scaling_factor
if first_level == 0:
first_level = self.scaling_factor
quantities = sorted(set(range(first_level, self.scaling_factor * levels, self.scaling_factor))
| {self.default_amount * 2})
return OrderedDict({round(quantity / self.default_amount * 100): quantity
for quantity in quantities
if quantity <= self.default_amount * 2})
def calculate_quantity(self, multiplier: int) -> int:
scales = list(self.scale_quantity)
left_scale = bisect.bisect_left(scales, multiplier)
closest_scale = min([scales[left_scale], scales[left_scale - 1]],
key=lambda x: abs(multiplier - x))
return self.scale_quantity[closest_scale]
def create_name_from_multiplier(self, multiplier: int) -> str:
return self.create_item_name(self.calculate_quantity(multiplier))
class FriendshipPackData(ResourcePackData):
def create_item_name(self, quantity: int) -> str:
return f"Friendship Bonus ({quantity} <3)"
def as_item_data(self, counter: itertools.count) -> [ItemData]:
item_datas = super().as_item_data(counter)
return [ItemData(item.code_without_offset, item.name, item.classification, {Group.FRIENDSHIP_PACK})
for item in item_datas]
class StardewItemFactory(Protocol):
def __call__(self, name: Union[str, ItemData]) -> Item:
raise NotImplementedError
def load_item_csv():
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files
items = []
with files(data).joinpath("items.csv").open() as file:
item_reader = csv.DictReader(file)
for item in item_reader:
id = int(item["id"]) if item["id"] else None
classification = ItemClassification[item["classification"]]
groups = {Group[group] for group in item["groups"].split(",") if group}
items.append(ItemData(id, item["name"], classification, groups))
return items
def load_resource_pack_csv() -> List[ResourcePackData]:
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files
resource_packs = []
with files(data).joinpath("resource_packs.csv").open() as file:
resource_pack_reader = csv.DictReader(file)
for resource_pack in resource_pack_reader:
groups = frozenset(Group[group] for group in resource_pack["groups"].split(",") if group)
resource_packs.append(ResourcePackData(resource_pack["name"],
int(resource_pack["default_amount"]),
int(resource_pack["scaling_factor"]),
ItemClassification[resource_pack["classification"]],
groups))
return resource_packs
events = [
ItemData(None, "Victory", ItemClassification.progression),
ItemData(None, "Spring", ItemClassification.progression),
ItemData(None, "Summer", ItemClassification.progression),
ItemData(None, "Fall", ItemClassification.progression),
ItemData(None, "Winter", ItemClassification.progression),
ItemData(None, "Year Two", ItemClassification.progression),
]
all_items: List[ItemData] = load_item_csv() + events
item_table: Dict[str, ItemData] = {}
items_by_group: Dict[Group, List[ItemData]] = {}
def initialize_groups():
for item in all_items:
for group in item.groups:
item_group = items_by_group.get(group, list())
item_group.append(item)
items_by_group[group] = item_group
def initialize_item_table():
item_table.update({item.name: item for item in all_items})
friendship_pack = FriendshipPackData("Friendship Bonus", default_amount=2, classification=ItemClassification.useful)
all_resource_packs = load_resource_pack_csv()
initialize_item_table()
initialize_groups()
def create_items(item_factory: StardewItemFactory, locations_count: int, world_options: options.StardewOptions,
random: Random) \
-> List[Item]:
items = create_unique_items(item_factory, world_options, random)
assert len(items) <= locations_count, \
"There should be at least as many locations as there are mandatory items"
logger.debug(f"Created {len(items)} unique items")
resource_pack_items = fill_with_resource_packs(item_factory, world_options, random, locations_count - len(items))
items += resource_pack_items
logger.debug(f"Created {len(resource_pack_items)} resource packs")
return items
def create_backpack_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or
world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive):
items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2)
def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], random: Random):
items.append(item_factory("Rusty Sword"))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_10])))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_20])))
items.append(item_factory("Slingshot"))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_50])))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_60])))
items.append(item_factory("Master Slingshot"))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_80])))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_90])))
items.append(item_factory(random.choice(items_by_group[Group.MINES_FLOOR_110])))
items.append(item_factory("Skull Key"))
def create_mine_elevators(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
if (world_options[options.TheMinesElevatorsProgression] ==
options.TheMinesElevatorsProgression.option_progressive or
world_options[options.TheMinesElevatorsProgression] ==
options.TheMinesElevatorsProgression.option_progressive_from_previous_floor):
items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24])
def create_tools(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
if world_options[options.ToolProgression] == options.ToolProgression.option_progressive:
items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4)
items.append(item_factory("Golden Scythe"))
def create_skills(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
if world_options[options.SkillProgression] == options.SkillProgression.option_progressive:
items.extend([item_factory(item) for item in items_by_group[Group.SKILL_LEVEL_UP] * 10])
def create_wizard_buildings(item_factory: StardewItemFactory, items: List[Item]):
items.append(item_factory("Earth Obelisk"))
items.append(item_factory("Water Obelisk"))
items.append(item_factory("Desert Obelisk"))
items.append(item_factory("Island Obelisk"))
items.append(item_factory("Junimo Hut"))
items.append(item_factory("Gold Clock"))
def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: options.StardewOptions,
items: List[Item]):
if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive,
options.BuildingProgression.option_progressive_early_shipping_bin}:
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Coop"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Progressive Barn"))
items.append(item_factory("Well"))
items.append(item_factory("Silo"))
items.append(item_factory("Mill"))
items.append(item_factory("Progressive Shed"))
items.append(item_factory("Progressive Shed"))
items.append(item_factory("Fish Pond"))
items.append(item_factory("Stable"))
items.append(item_factory("Slime Hutch"))
items.append(item_factory("Shipping Bin"))
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
items.append(item_factory("Progressive House"))
def create_special_quest_rewards(item_factory: StardewItemFactory, items: List[Item]):
items.append(item_factory("Adventurer's Guild"))
items.append(item_factory("Club Card"))
items.append(item_factory("Magnifying Glass"))
items.append(item_factory("Bear's Knowledge"))
items.append(item_factory("Iridium Snake Milk"))
def create_stardrops(item_factory: StardewItemFactory, items: List[Item]):
items.append(item_factory("Stardrop")) # The Mines level 100
items.append(item_factory("Stardrop")) # Old Master Cannoli
def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: options.StardewOptions,
items: List[Item]):
if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Boots"))
items.append(item_factory("JotPK: Progressive Gun"))
items.append(item_factory("JotPK: Progressive Gun"))
items.append(item_factory("JotPK: Progressive Gun"))
items.append(item_factory("JotPK: Progressive Gun"))
items.append(item_factory("JotPK: Progressive Ammo"))
items.append(item_factory("JotPK: Progressive Ammo"))
items.append(item_factory("JotPK: Progressive Ammo"))
items.append(item_factory("JotPK: Extra Life"))
items.append(item_factory("JotPK: Extra Life"))
items.append(item_factory("JotPK: Increased Drop Rate"))
items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8)
def create_player_buffs(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]):
number_of_buffs: int = world_options[options.NumberOfPlayerBuffs]
items.extend(item_factory(item) for item in ["Movement Speed Bonus"] * number_of_buffs)
items.extend(item_factory(item) for item in ["Luck Bonus"] * number_of_buffs)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
items.append(item_factory("Traveling Merchant: Sunday"))
items.append(item_factory("Traveling Merchant: Monday"))
items.append(item_factory("Traveling Merchant: Tuesday"))
items.append(item_factory("Traveling Merchant: Wednesday"))
items.append(item_factory("Traveling Merchant: Thursday"))
items.append(item_factory("Traveling Merchant: Friday"))
items.append(item_factory("Traveling Merchant: Saturday"))
items.extend(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)
items.extend(item_factory(item) for item in ["Traveling Merchant Discount"] * 8)
def create_unique_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random) -> \
List[Item]:
items = []
items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD])
create_backpack_items(item_factory, world_options, items)
create_mine_rewards(item_factory, items, random)
create_mine_elevators(item_factory, world_options, items)
create_tools(item_factory, world_options, items)
create_skills(item_factory, world_options, items)
create_wizard_buildings(item_factory, items)
create_carpenter_buildings(item_factory, world_options, items)
items.append(item_factory("Beach Bridge"))
create_special_quest_rewards(item_factory, items)
create_stardrops(item_factory, items)
create_arcade_machine_items(item_factory, world_options, items)
items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS])))
items.append(
item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier])))
create_player_buffs(item_factory, world_options, items)
create_traveling_merchant_items(item_factory, items)
items.append(item_factory("Return Scepter"))
return items
def fill_with_resource_packs(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random,
required_resource_pack: int) -> List[Item]:
resource_pack_multiplier = world_options[options.ResourcePackMultiplier]
if resource_pack_multiplier == 0:
return [item_factory(cola) for cola in ["Joja Cola"] * required_resource_pack]
items = []
for i in range(required_resource_pack):
resource_pack = random.choice(all_resource_packs)
items.append(item_factory(resource_pack.create_name_from_multiplier(resource_pack_multiplier)))
return items