Skip to content

Commit

Permalink
Stardew Valley: Refactor animals to use content packs (ArchipelagoMW#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Jouramie committed Dec 17, 2024
1 parent 42ecc61 commit 425c3f7
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 83 deletions.
21 changes: 14 additions & 7 deletions worlds/stardew_valley/content/game_content.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
from typing import Iterable, Set, Any, Mapping, Type, Tuple, Union

from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, building_progression
from ..data.animal import Animal
from ..data.building import Building
from ..data.fish_data import FishItem
from ..data.game_item import GameItem, Source, ItemTag
Expand All @@ -18,12 +19,13 @@ class StardewContent:

# regions -> To be used with can reach rule

game_items: Dict[str, GameItem] = field(default_factory=dict)
fishes: Dict[str, FishItem] = field(default_factory=dict)
villagers: Dict[str, Villager] = field(default_factory=dict)
farm_buildings: Dict[str, Building] = field(default_factory=dict)
skills: Dict[str, Skill] = field(default_factory=dict)
quests: Dict[str, Any] = field(default_factory=dict)
game_items: dict[str, GameItem] = field(default_factory=dict)
fishes: dict[str, FishItem] = field(default_factory=dict)
villagers: dict[str, Villager] = field(default_factory=dict)
farm_buildings: dict[str, Building] = field(default_factory=dict)
animals: dict[str, Animal] = field(default_factory=dict)
skills: dict[str, Skill] = field(default_factory=dict)
quests: dict[str, Any] = field(default_factory=dict)

def find_sources_of_type(self, types: Union[Type[Source], Tuple[Type[Source]]]) -> Iterable[Source]:
for item in self.game_items.values():
Expand Down Expand Up @@ -108,6 +110,11 @@ def villager_hook(self, content: StardewContent):
def farm_building_hook(self, content: StardewContent):
...

animals: Iterable[Animal] = ()

def animal_hook(self, content: StardewContent):
...

skills: Iterable[Skill] = ()

def skill_hook(self, content: StardewContent):
Expand Down
4 changes: 4 additions & 0 deletions worlds/stardew_valley/content/unpacking.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def register_pack(content: StardewContent, pack: ContentPack):
content.farm_buildings[building.name] = building
pack.farm_building_hook(content)

for animal in pack.animals:
content.animals[animal.name] = animal
pack.animal_hook(content)

for skill in pack.skills:
content.skills[skill.name] = skill
pack.skill_hook(content)
Expand Down
20 changes: 19 additions & 1 deletion worlds/stardew_valley/content/vanilla/ginger_island.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack, StardewContent
from ...data import villagers_data, fish_data
from ...data.game_item import ItemTag, Tag
from ...data.animal import Animal, AnimalName, OstrichIncubatorSource
from ...data.game_item import ItemTag, Tag, CustomRuleSource
from ...data.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
from ...data.requirement import WalnutRequirement
from ...data.shop import ShopSource
from ...strings.animal_product_names import AnimalProduct
from ...strings.book_names import Book
from ...strings.building_names import Building
from ...strings.crop_names import Fruit, Vegetable
from ...strings.fish_names import Fish
from ...strings.forageable_names import Forageable, Mushroom
from ...strings.fruit_tree_names import Sapling
from ...strings.generic_names import Generic
from ...strings.metal_names import Fossil, Mineral
from ...strings.region_names import Region, LogicRegion
from ...strings.season_names import Season
Expand Down Expand Up @@ -51,6 +55,13 @@ def harvest_source_hook(self, content: StardewContent):
Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),),
Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, seasons=(Season.summer,)),),

# Temporary animal stuff, will be moved once animal products are properly content-packed
AnimalProduct.ostrich_egg_starter: (CustomRuleSource(lambda logic: logic.tool.can_forage(Generic.any, Region.island_north, True)
& logic.has(Forageable.journal_scrap)
& logic.region.can_reach(Region.volcano_floor_5)),),
AnimalProduct.ostrich_egg: (CustomRuleSource(lambda logic: logic.has(AnimalProduct.ostrich_egg_starter)
| logic.animal.has_animal(AnimalName.ostrich)),),

},
shop_sources={
Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),),
Expand Down Expand Up @@ -81,5 +92,12 @@ def harvest_source_hook(self, content: StardewContent):
),
villagers=(
villagers_data.leo,
),
animals=(
Animal(AnimalName.ostrich,
required_building=Building.barn,
sources=(
OstrichIncubatorSource(AnimalProduct.ostrich_egg_starter),
)),
)
)
66 changes: 65 additions & 1 deletion worlds/stardew_valley/content/vanilla/the_farm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack
from ...data.animal import IncubatorSource, Animal, AnimalName
from ...data.harvest import FruitBatsSource, MushroomCaveSource
from ...data.shop import ShopSource
from ...strings.animal_product_names import AnimalProduct
from ...strings.building_names import Building
from ...strings.forageable_names import Forageable, Mushroom
from ...strings.region_names import Region

the_farm = ContentPack(
"The Farm (Vanilla)",
Expand Down Expand Up @@ -39,5 +44,64 @@
Mushroom.red: (
MushroomCaveSource(),
),
}
},
animals=(
Animal(AnimalName.chicken,
required_building=Building.coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=800),
# For now there is no way to obtain the starter item, so this adds additional rules in the system for nothing.
# IncubatorSource(AnimalProduct.egg_starter)
)),
Animal(AnimalName.cow,
required_building=Building.barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=1500),
)),
Animal(AnimalName.goat,
required_building=Building.big_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=4000),
)),
Animal(AnimalName.duck,
required_building=Building.big_coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=1200),
# For now there is no way to obtain the starter item, so this adds additional rules in the system for nothing.
# IncubatorSource(AnimalProduct.duck_egg_starter)
)),
Animal(AnimalName.sheep,
required_building=Building.deluxe_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=8000),
)),
Animal(AnimalName.rabbit,
required_building=Building.deluxe_coop,
sources=(
ShopSource(shop_region=Region.ranch, money_price=8000),
)),
Animal(AnimalName.pig,
required_building=Building.deluxe_barn,
sources=(
ShopSource(shop_region=Region.ranch, money_price=16000),
)),
Animal(AnimalName.void_chicken,
required_building=Building.big_coop,
sources=(
IncubatorSource(AnimalProduct.void_egg_starter),
)),
Animal(AnimalName.golden_chicken,
required_building=Building.big_coop,
sources=(
IncubatorSource(AnimalProduct.golden_egg_starter),
)),
Animal(AnimalName.dinosaur,
required_building=Building.big_coop,
sources=(
# We should use the starter item here, but since the dinosaur egg is also an artifact, it's part of the museum rules
# and I do not want to touch it yet.
# IncubatorSource(AnimalProduct.dinosaur_egg_starter),
IncubatorSource(AnimalProduct.dinosaur_egg),
)),
)
)
23 changes: 23 additions & 0 deletions worlds/stardew_valley/data/animal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from dataclasses import dataclass, field

from .game_item import Source
from ..strings.animal_names import Animal as AnimalName

assert AnimalName


@dataclass(frozen=True)
class Animal:
name: str
required_building: str = field(kw_only=True)
sources: tuple[Source, ...] = field(kw_only=True)


@dataclass(frozen=True)
class IncubatorSource(Source):
egg_item: str


@dataclass(frozen=True)
class OstrichIncubatorSource(Source):
egg_item: str
6 changes: 3 additions & 3 deletions worlds/stardew_valley/data/bundle_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..strings.crop_names import Fruit, Vegetable
from ..strings.currency_names import Currency
from ..strings.fertilizer_names import Fertilizer, RetainingSoil, SpeedGro
from ..strings.fish_names import Fish, WaterItem, Trash, all_fish
from ..strings.fish_names import Fish, WaterItem, Trash
from ..strings.flower_names import Flower
from ..strings.food_names import Beverage, Meal
from ..strings.forageable_names import Forageable, Mushroom
Expand Down Expand Up @@ -143,7 +143,7 @@
rabbit_foot = BundleItem(AnimalProduct.rabbit_foot)
dinosaur_egg = BundleItem(AnimalProduct.dinosaur_egg)
void_egg = BundleItem(AnimalProduct.void_egg)
ostrich_egg = BundleItem(AnimalProduct.ostrich_egg, source=BundleItem.Sources.island, )
ostrich_egg = BundleItem(AnimalProduct.ostrich_egg, source=BundleItem.Sources.content)
golden_egg = BundleItem(AnimalProduct.golden_egg)

truffle_oil = BundleItem(ArtisanGood.truffle_oil)
Expand Down Expand Up @@ -832,7 +832,7 @@
magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
strawberry_seeds.as_amount(20),
spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5),
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5),]
pumpkin_soup.as_amount(5), lucky_lunch.as_amount(5), ]
calico_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.calico, calico_items, 2, 2)

raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)
Expand Down
7 changes: 4 additions & 3 deletions worlds/stardew_valley/data/museum_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from dataclasses import dataclass
from typing import List, Tuple, Union, Optional

from ..strings.monster_names import Monster
from ..strings.animal_product_names import AnimalProduct
from ..strings.fish_names import WaterChest
from ..strings.forageable_names import Forageable
from ..strings.geode_names import Geode
from ..strings.metal_names import Mineral, Artifact, Fossil
from ..strings.monster_names import Monster
from ..strings.region_names import Region
from ..strings.geode_names import Geode


@dataclass(frozen=True)
Expand Down Expand Up @@ -105,7 +106,7 @@ class Artifact:
geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town),
geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
dinosaur_egg = create_artifact("Dinosaur Egg", 11.4, (Region.skull_cavern),
dinosaur_egg = create_artifact(AnimalProduct.dinosaur_egg, 11.4, (Region.skull_cavern),
monsters=Monster.pepper_rex)
rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley,
geodes=(Geode.artifact_trove, WaterChest.fishing_chest),
Expand Down
56 changes: 18 additions & 38 deletions worlds/stardew_valley/logic/animal_logic.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import typing
from typing import Union

from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from ..stardew_rule import StardewRule, true_
from ..strings.animal_names import Animal, coop_animals, barn_animals
from ..stardew_rule import StardewRule
from ..strings.building_names import Building
from ..strings.forageable_names import Forageable
from ..strings.generic_names import Generic
from ..strings.region_names import Region
from ..strings.machine_names import Machine

cost_and_building_by_animal = {
Animal.chicken: (800, Building.coop),
Animal.cow: (1500, Building.barn),
Animal.goat: (4000, Building.big_barn),
Animal.duck: (1200, Building.big_coop),
Animal.sheep: (8000, Building.deluxe_barn),
Animal.rabbit: (8000, Building.deluxe_coop),
Animal.pig: (16000, Building.deluxe_barn)
}
if typing.TYPE_CHECKING:
from .source_logic import SourceLogicMixin

assert SourceLogicMixin


class AnimalLogicMixin(BaseLogicMixin):
Expand All @@ -28,32 +21,19 @@ def __init__(self, *args, **kwargs):
self.animal = AnimalLogic(*args, **kwargs)


class AnimalLogic(BaseLogic[Union[HasLogicMixin, MoneyLogicMixin, BuildingLogicMixin]]):

def can_buy_animal(self, animal: str) -> StardewRule:
try:
price, building = cost_and_building_by_animal[animal]
except KeyError:
return true_
return self.logic.money.can_spend_at(Region.ranch, price) & self.logic.building.has_building(building)
class AnimalLogic(BaseLogic[Union[AnimalLogicMixin, HasLogicMixin, BuildingLogicMixin, 'SourceLogicMixin']]):

def has_animal(self, animal: str) -> StardewRule:
if animal == Generic.any:
return self.has_any_animal()
elif animal == Building.coop:
return self.has_any_coop_animal()
elif animal == Building.barn:
return self.has_any_barn_animal()
return self.logic.has(animal)
def can_incubate(self, egg_item: str) -> StardewRule:
return self.logic.building.has_building(Building.coop) & self.logic.has(egg_item)

def has_happy_animal(self, animal: str) -> StardewRule:
return self.has_animal(animal) & self.logic.has(Forageable.hay)
def can_ostrich_incubate(self, egg_item: str) -> StardewRule:
return self.logic.building.has_building(Building.barn) & self.logic.has(Machine.ostrich_incubator) & self.logic.has(egg_item)

def has_any_animal(self) -> StardewRule:
return self.has_any_coop_animal() | self.has_any_barn_animal()
def has_animal(self, animal_name: str) -> StardewRule:
animal = self.content.animals.get(animal_name)
assert animal is not None, f"Animal {animal_name} not found."

def has_any_coop_animal(self) -> StardewRule:
return self.logic.has_any(*coop_animals)
return self.logic.source.has_access_to_any(animal.sources) & self.logic.building.has_building(animal.required_building)

def has_any_barn_animal(self) -> StardewRule:
return self.logic.has_any(*barn_animals)
def has_happy_animal(self, animal_name: str) -> StardewRule:
return self.logic.animal.has_animal(animal_name) & self.logic.has(Forageable.hay)
31 changes: 25 additions & 6 deletions worlds/stardew_valley/logic/festival_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .time_logic import TimeLogicMixin
from ..options import FestivalLocations
from ..stardew_rule import StardewRule
from ..strings.animal_product_names import AnimalProduct
from ..strings.book_names import Book
from ..strings.craftable_names import Fishing
from ..strings.crop_names import Fruit, Vegetable
Expand Down Expand Up @@ -154,27 +155,45 @@ def can_succeed_grange_display(self) -> StardewRule:
if self.options.festival_locations != FestivalLocations.option_hard:
return self.logic.true_

animal_rule = self.logic.animal.has_animal(Generic.any)
# Other animal products are not counted in the animal product category
good_animal_products = [
AnimalProduct.duck_egg, AnimalProduct.duck_feather, AnimalProduct.egg, AnimalProduct.goat_milk, AnimalProduct.golden_egg, AnimalProduct.large_egg,
AnimalProduct.large_goat_milk, AnimalProduct.large_milk, AnimalProduct.milk, AnimalProduct.ostrich_egg, AnimalProduct.rabbit_foot,
AnimalProduct.void_egg, AnimalProduct.wool
]
if AnimalProduct.ostrich_egg not in self.content.game_items:
# When ginger island is excluded, ostrich egg is not available
good_animal_products.remove(AnimalProduct.ostrich_egg)
animal_rule = self.logic.has_any(*good_animal_products)

artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any)
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough

# Salads at the bar are good enough
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220)

fish_rule = self.logic.skill.can_fish(difficulty=50)
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough

# Hazelnut always available since the grange display is in fall
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods))

# More than half the minerals are good enough
mineral_rule = self.logic.action.can_open_geode(Generic.any)

good_fruits = (fruit
for fruit in
(Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
if fruit in self.content.game_items)
fruit_rule = self.logic.has_any(*good_fruits)

good_vegetables = (vegeteable
for vegeteable in
(Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
if vegeteable in self.content.game_items)
vegetable_rule = self.logic.has_any(*good_vegetables)

return animal_rule & artisan_rule & cooking_rule & fish_rule & \
forage_rule & fruit_rule & mineral_rule & vegetable_rule
return animal_rule & artisan_rule & cooking_rule & fish_rule & forage_rule & fruit_rule & mineral_rule & vegetable_rule

def can_win_fishing_competition(self) -> StardewRule:
return self.logic.skill.can_fish(difficulty=60)
Expand Down
Loading

0 comments on commit 425c3f7

Please sign in to comment.