mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Stardew Valley: implement new game (#1455)
* Stardew Valley Archipelago implementation * fix breaking changes * - Added and Updated Documentation for the game * Removed fun * Remove entire idea of step, due to possible inconsistency with the main AP core * Commented out the desired steps, fix renaming after rebase * Fixed wording * tests now passes on 3.8 * run flake8 * remove dependency so apworld work again * remove dependency for real * - Fix Formatting in the Game Page - Removed disabled Option Descriptions for Entrance Randomizer - Improved Game Page's description of the Arcade Machine buffs - Trimmed down the text on the Options page for Arcade Machines, so that it is smaller * - Removed blankspace * remove player field * remove None check in options * document the scripts * fix pytest warning * use importlib.resources.files * fix * add version requirement to importlib_resources * remove __init__.py from data folder * increment data version * let the __init__.py for 3.9 * use sorted() instead of list() * replace frozenset from fish_data with tuples * remove dependency on pytest * - Add a bit of text to the guide to tell them about how to redeem some received items * - Added a comment about which mod version to use * change single quotes for double quotes * Minimum client version both ways * Changed version number to be more specific. The mod will handle deciding --------- Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
This commit is contained in:
376
worlds/stardew_valley/items.py
Normal file
376
worlds/stardew_valley/items.py
Normal file
@@ -0,0 +1,376 @@
|
||||
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
|
||||
Reference in New Issue
Block a user