377 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			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
 |