Stardew Valley: Refactor Animals to use Content Packs (#4320)

This commit is contained in:
Jérémie Bolduc
2025-04-20 10:17:22 -04:00
committed by GitHub
parent 33dc845de8
commit 22941168cd
15 changed files with 229 additions and 85 deletions

View File

@@ -1,25 +1,15 @@
from typing import Union
import typing
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 .logic import StardewLogic
else:
StardewLogic = object
class AnimalLogicMixin(BaseLogicMixin):
@@ -28,32 +18,19 @@ class AnimalLogicMixin(BaseLogicMixin):
self.animal = AnimalLogic(*args, **kwargs)
class AnimalLogic(BaseLogic[Union[HasLogicMixin, MoneyLogicMixin, BuildingLogicMixin]]):
class AnimalLogic(BaseLogic[StardewLogic]):
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)
def can_incubate(self, egg_item: str) -> StardewRule:
return self.logic.building.has_building(Building.coop) & self.logic.has(egg_item)
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_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_happy_animal(self, animal: str) -> StardewRule:
return self.has_animal(animal) & self.logic.has(Forageable.hay)
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_animal(self) -> StardewRule:
return self.has_any_coop_animal() | self.has_any_barn_animal()
return self.logic.source.has_access_to_any(animal.sources) & self.logic.building.has_building(animal.required_building)
def has_any_coop_animal(self) -> StardewRule:
return self.logic.has_any(*coop_animals)
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)

View File

@@ -17,6 +17,7 @@ from .skill_logic import SkillLogicMixin
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
@@ -154,18 +155,37 @@ SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, Relation
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,
@@ -173,8 +193,7 @@ SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, Relation
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)

View File

@@ -149,42 +149,37 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
# self.received("Deluxe Fertilizer Recipe") & self.has(MetalBar.iridium) & self.has(SVItem.sap),
# | (self.ability.can_cook() & self.relationship.has_hearts(NPC.emily, 3) & self.has(Forageable.leek) & self.has(Forageable.dandelion) &
# | (self.ability.can_cook() & self.relationship.has_hearts(NPC.jodi, 7) & self.has(AnimalProduct.cow_milk) & self.has(Ingredient.sugar)),
Animal.chicken: self.animal.can_buy_animal(Animal.chicken),
Animal.cow: self.animal.can_buy_animal(Animal.cow),
Animal.dinosaur: self.building.has_building(Building.big_coop) & self.has(AnimalProduct.dinosaur_egg),
Animal.duck: self.animal.can_buy_animal(Animal.duck),
Animal.goat: self.animal.can_buy_animal(Animal.goat),
Animal.ostrich: self.building.has_building(Building.barn) & self.has(AnimalProduct.ostrich_egg) & self.has(Machine.ostrich_incubator),
Animal.pig: self.animal.can_buy_animal(Animal.pig),
Animal.rabbit: self.animal.can_buy_animal(Animal.rabbit),
Animal.sheep: self.animal.can_buy_animal(Animal.sheep),
AnimalProduct.any_egg: self.has_any(AnimalProduct.chicken_egg, AnimalProduct.duck_egg),
AnimalProduct.brown_egg: self.animal.has_animal(Animal.chicken),
AnimalProduct.chicken_egg: self.has_any(AnimalProduct.egg, AnimalProduct.brown_egg, AnimalProduct.large_egg, AnimalProduct.large_brown_egg),
AnimalProduct.cow_milk: self.has_any(AnimalProduct.milk, AnimalProduct.large_milk),
AnimalProduct.duck_egg: self.animal.has_animal(Animal.duck),
AnimalProduct.duck_egg: self.animal.has_animal(Animal.duck), # Should also check starter
AnimalProduct.duck_feather: self.animal.has_happy_animal(Animal.duck),
AnimalProduct.egg: self.animal.has_animal(Animal.chicken),
AnimalProduct.goat_milk: self.has(Animal.goat),
AnimalProduct.golden_egg: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.egg: self.animal.has_animal(Animal.chicken), # Should also check starter
AnimalProduct.goat_milk: self.animal.has_animal(Animal.goat),
AnimalProduct.golden_egg: self.has(AnimalProduct.golden_egg_starter), # Should also check golden chicken if there was an alternative to obtain it without golden egg
AnimalProduct.large_brown_egg: self.animal.has_happy_animal(Animal.chicken),
AnimalProduct.large_egg: self.animal.has_happy_animal(Animal.chicken),
AnimalProduct.large_goat_milk: self.animal.has_happy_animal(Animal.goat),
AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow),
AnimalProduct.milk: self.animal.has_animal(Animal.cow),
AnimalProduct.ostrich_egg: self.tool.can_forage(Generic.any, Region.island_north, True) & self.has(Forageable.journal_scrap) & self.region.can_reach(Region.volcano_floor_5),
AnimalProduct.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit),
AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond),
AnimalProduct.squid_ink: self.mine.can_mine_in_the_mines_floor_81_120() | (self.building.has_building(Building.fish_pond) & self.has(Fish.squid)),
AnimalProduct.sturgeon_roe: self.has(Fish.sturgeon) & self.building.has_building(Building.fish_pond),
AnimalProduct.truffle: self.animal.has_animal(Animal.pig) & self.season.has_any_not_winter(),
AnimalProduct.void_egg: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
AnimalProduct.void_egg: self.has(AnimalProduct.void_egg_starter), # Should also check void chicken if there was an alternative to obtain it without void egg
AnimalProduct.wool: self.animal.has_animal(Animal.rabbit) | self.animal.has_animal(Animal.sheep),
AnimalProduct.slime_egg_green: self.has(Machine.slime_egg_press) & self.has(Loot.slime),
AnimalProduct.slime_egg_blue: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(3),
AnimalProduct.slime_egg_red: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(6),
AnimalProduct.slime_egg_purple: self.has(Machine.slime_egg_press) & self.has(Loot.slime) & self.time.has_lived_months(9),
AnimalProduct.slime_egg_tiger: self.has(Fish.lionfish) & self.building.has_building(Building.fish_pond),
AnimalProduct.duck_egg_starter: self.logic.false_, # It could be purchased at the Feast of the Winter Star, but it's random every year, so not considering it yet...
AnimalProduct.dinosaur_egg_starter: self.logic.false_, # Dinosaur eggs are also part of the museum rules, and I don't want to touch them yet.
AnimalProduct.egg_starter: self.logic.false_, # It could be purchased at the Desert Festival, but festival logic is quite a mess, so not considering it yet...
AnimalProduct.golden_egg_starter: self.received(AnimalProduct.golden_egg) & (self.money.can_spend_at(Region.ranch, 100000) | self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 100)),
AnimalProduct.void_egg_starter: self.money.can_spend_at(Region.sewer, 5000) | (self.building.has_building(Building.fish_pond) & self.has(Fish.void_salmon)),
ArtisanGood.aged_roe: self.artisan.can_preserves_jar(AnimalProduct.roe),
ArtisanGood.battery_pack: (self.has(Machine.lightning_rod) & self.season.has_any_not_winter()) | self.has(Machine.solar_panel),
ArtisanGood.caviar: self.artisan.can_preserves_jar(AnimalProduct.sturgeon_roe),

View File

@@ -72,4 +72,16 @@ of source (Monster drop and fish can have foraging sources).
if easy logic is disabled. For instance, anything that requires money could be accessible as soon as you can sell something to someone (even wood).
Items are classified by their source. An item with a fishing or a crab pot source is considered a fish, an item dropping from a monster is a monster drop. An
item with a foraging source is a forageable. Items can fit in multiple categories.
item with a foraging source is a forageable. Items can fit in multiple categories.
## Prefer rich class to anemic list of sources
For game mechanic that might need more logic/interaction than a simple game item, prefer creating a class than just listing the sources and adding generic
requirements to them. This will simplify the implementation of more complex mechanics and increase cohesion.
For instance, `Building` can be upgraded. Instead of having a simple source for the `Big Coop` being a shop source with an additional requirement being having
the previous building, the `Building` class has knowledge of the upgrade system and know from which building it can be upgraded.
Another example is `Animal`. Instead of a shopping source with a requirement of having a `Coop`, the `Chicken` knows that a building is required. This way, a
potential source of chicken from incubating an egg would not require an additional requirement of having a coop (assuming the incubator could be obtained
without a big coop).

View File

@@ -1,6 +1,7 @@
import functools
from typing import Union, Any, Iterable
from .animal_logic import AnimalLogicMixin
from .artisan_logic import ArtisanLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic
from .grind_logic import GrindLogicMixin
@@ -11,6 +12,7 @@ from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .requirement_logic import RequirementLogicMixin
from .tool_logic import ToolLogicMixin
from ..data.animal import IncubatorSource, OstrichIncubatorSource
from ..data.artisan import MachineSource
from ..data.game_item import GenericSource, Source, GameItem, CustomRuleSource
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \
@@ -25,7 +27,7 @@ class SourceLogicMixin(BaseLogicMixin):
class SourceLogic(BaseLogic[Union[SourceLogicMixin, HasLogicMixin, ReceivedLogicMixin, HarvestingLogicMixin, MoneyLogicMixin, RegionLogicMixin,
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin, AnimalLogicMixin]]):
def has_access_to_item(self, item: GameItem):
rules = []
@@ -81,6 +83,14 @@ ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
def _(self, source: HarvestCropSource):
return self.logic.harvesting.can_harvest_crop_from(source)
@has_access_to.register
def _(self, source: IncubatorSource):
return self.logic.animal.can_incubate(source.egg_item)
@has_access_to.register
def _(self, source: OstrichIncubatorSource):
return self.logic.animal.can_ostrich_incubate(source.egg_item)
@has_access_to.register
def _(self, source: MachineSource):
return self.logic.artisan.can_produce_from(source)