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:
Jérémie Bolduc
2023-02-26 19:19:15 -05:00
committed by GitHub
parent 0286edf20c
commit af7d0dbf37
34 changed files with 5334 additions and 1 deletions

View 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