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,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field 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, tool_progression from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, building_progression, tool_progression
from ..data.animal import Animal
from ..data.building import Building from ..data.building import Building
from ..data.fish_data import FishItem from ..data.fish_data import FishItem
from ..data.game_item import GameItem, Source, ItemTag from ..data.game_item import GameItem, Source, ItemTag
@@ -18,12 +19,13 @@ class StardewContent:
# regions -> To be used with can reach rule # regions -> To be used with can reach rule
game_items: Dict[str, GameItem] = field(default_factory=dict) game_items: dict[str, GameItem] = field(default_factory=dict)
fishes: Dict[str, FishItem] = field(default_factory=dict) fishes: dict[str, FishItem] = field(default_factory=dict)
villagers: Dict[str, Villager] = field(default_factory=dict) villagers: dict[str, Villager] = field(default_factory=dict)
farm_buildings: Dict[str, Building] = field(default_factory=dict) farm_buildings: dict[str, Building] = field(default_factory=dict)
skills: Dict[str, Skill] = field(default_factory=dict) animals: dict[str, Animal] = field(default_factory=dict)
quests: Dict[str, Any] = 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]: def find_sources_of_type(self, types: Union[Type[Source], Tuple[Type[Source]]]) -> Iterable[Source]:
for item in self.game_items.values(): for item in self.game_items.values():
@@ -109,6 +111,11 @@ class ContentPack:
def farm_building_hook(self, content: StardewContent): def farm_building_hook(self, content: StardewContent):
... ...
animals: Iterable[Animal] = ()
def animal_hook(self, content: StardewContent):
...
skills: Iterable[Skill] = () skills: Iterable[Skill] = ()
def skill_hook(self, content: StardewContent): def skill_hook(self, content: StardewContent):

View File

@@ -65,6 +65,10 @@ def register_pack(content: StardewContent, pack: ContentPack):
content.farm_buildings[building.name] = building content.farm_buildings[building.name] = building
pack.farm_building_hook(content) pack.farm_building_hook(content)
for animal in pack.animals:
content.animals[animal.name] = animal
pack.animal_hook(content)
for skill in pack.skills: for skill in pack.skills:
content.skills[skill.name] = skill content.skills[skill.name] = skill
pack.skill_hook(content) pack.skill_hook(content)

View File

@@ -1,15 +1,19 @@
from .pelican_town import pelican_town as pelican_town_content_pack from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack, StardewContent from ..game_content import ContentPack, StardewContent
from ...data import villagers_data, fish_data 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.harvest import ForagingSource, HarvestFruitTreeSource, HarvestCropSource
from ...data.requirement import WalnutRequirement from ...data.requirement import WalnutRequirement
from ...data.shop import ShopSource from ...data.shop import ShopSource
from ...strings.animal_product_names import AnimalProduct
from ...strings.book_names import Book from ...strings.book_names import Book
from ...strings.building_names import Building
from ...strings.crop_names import Fruit, Vegetable from ...strings.crop_names import Fruit, Vegetable
from ...strings.fish_names import Fish from ...strings.fish_names import Fish
from ...strings.forageable_names import Forageable, Mushroom from ...strings.forageable_names import Forageable, Mushroom
from ...strings.fruit_tree_names import Sapling from ...strings.fruit_tree_names import Sapling
from ...strings.generic_names import Generic
from ...strings.metal_names import Fossil, Mineral from ...strings.metal_names import Fossil, Mineral
from ...strings.region_names import Region, LogicRegion from ...strings.region_names import Region, LogicRegion
from ...strings.season_names import Season from ...strings.season_names import Season
@@ -51,6 +55,13 @@ ginger_island_content_pack = GingerIslandContentPack(
Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),), Vegetable.taro_root: (HarvestCropSource(seed=Seed.taro, seasons=(Season.summer,)),),
Fruit.pineapple: (HarvestCropSource(seed=Seed.pineapple, 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={ shop_sources={
Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),), Seed.taro: (ShopSource(items_price=((2, Fossil.bone_fragment),), shop_region=Region.island_trader),),
@@ -81,5 +92,12 @@ ginger_island_content_pack = GingerIslandContentPack(
), ),
villagers=( villagers=(
villagers_data.leo, villagers_data.leo,
),
animals=(
Animal(AnimalName.ostrich,
required_building=Building.barn,
sources=(
OstrichIncubatorSource(AnimalProduct.ostrich_egg_starter),
)),
) )
) )

View File

@@ -1,7 +1,12 @@
from .pelican_town import pelican_town as pelican_town_content_pack from .pelican_town import pelican_town as pelican_town_content_pack
from ..game_content import ContentPack from ..game_content import ContentPack
from ...data.animal import IncubatorSource, Animal, AnimalName
from ...data.harvest import FruitBatsSource, MushroomCaveSource 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.forageable_names import Forageable, Mushroom
from ...strings.region_names import Region
the_farm = ContentPack( the_farm = ContentPack(
"The Farm (Vanilla)", "The Farm (Vanilla)",
@@ -39,5 +44,64 @@ the_farm = ContentPack(
Mushroom.red: ( Mushroom.red: (
MushroomCaveSource(), 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),
)),
)
) )

View File

@@ -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

View File

@@ -143,7 +143,7 @@ duck_egg = BundleItem(AnimalProduct.duck_egg)
rabbit_foot = BundleItem(AnimalProduct.rabbit_foot) rabbit_foot = BundleItem(AnimalProduct.rabbit_foot)
dinosaur_egg = BundleItem(AnimalProduct.dinosaur_egg) dinosaur_egg = BundleItem(AnimalProduct.dinosaur_egg)
void_egg = BundleItem(AnimalProduct.void_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) golden_egg = BundleItem(AnimalProduct.golden_egg)
truffle_oil = BundleItem(ArtisanGood.truffle_oil) truffle_oil = BundleItem(ArtisanGood.truffle_oil)
@@ -832,7 +832,7 @@ calico_items = [calico_egg.as_amount(200), calico_egg.as_amount(200), calico_egg
magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50), magic_rock_candy, mega_bomb.as_amount(10), mystery_box.as_amount(10), mixed_seeds.as_amount(50),
strawberry_seeds.as_amount(20), strawberry_seeds.as_amount(20),
spicy_eel.as_amount(5), crab_cakes.as_amount(5), eggplant_parmesan.as_amount(5), 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) 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) raccoon_bundle = BundleTemplate(CCRoom.bulletin_board, BundleName.raccoon, raccoon_foraging_items, 4, 4)

View File

@@ -3,12 +3,13 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Tuple, Union, Optional 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.fish_names import WaterChest
from ..strings.forageable_names import Forageable from ..strings.forageable_names import Forageable
from ..strings.metal_names import Mineral, Artifact, Fossil
from ..strings.region_names import Region
from ..strings.geode_names import Geode 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
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -105,7 +106,7 @@ class Artifact:
geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) geodes=(Geode.artifact_trove, WaterChest.fishing_chest))
ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town), ornamental_fan = create_artifact("Ornamental Fan", 7.4, (Region.beach, Region.forest, Region.town),
geodes=(Geode.artifact_trove, WaterChest.fishing_chest)) 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) monsters=Monster.pepper_rex)
rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley, rare_disc = create_artifact("Rare Disc", 5.6, Region.stardew_valley,
geodes=(Geode.artifact_trove, WaterChest.fishing_chest), geodes=(Geode.artifact_trove, WaterChest.fishing_chest),

View File

@@ -1,25 +1,15 @@
from typing import Union import typing
from .base_logic import BaseLogicMixin, BaseLogic from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin from ..stardew_rule import StardewRule
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 ..strings.building_names import Building from ..strings.building_names import Building
from ..strings.forageable_names import Forageable from ..strings.forageable_names import Forageable
from ..strings.generic_names import Generic from ..strings.machine_names import Machine
from ..strings.region_names import Region
cost_and_building_by_animal = { if typing.TYPE_CHECKING:
Animal.chicken: (800, Building.coop), from .logic import StardewLogic
Animal.cow: (1500, Building.barn), else:
Animal.goat: (4000, Building.big_barn), StardewLogic = object
Animal.duck: (1200, Building.big_coop),
Animal.sheep: (8000, Building.deluxe_barn),
Animal.rabbit: (8000, Building.deluxe_coop),
Animal.pig: (16000, Building.deluxe_barn)
}
class AnimalLogicMixin(BaseLogicMixin): class AnimalLogicMixin(BaseLogicMixin):
@@ -28,32 +18,19 @@ class AnimalLogicMixin(BaseLogicMixin):
self.animal = AnimalLogic(*args, **kwargs) self.animal = AnimalLogic(*args, **kwargs)
class AnimalLogic(BaseLogic[Union[HasLogicMixin, MoneyLogicMixin, BuildingLogicMixin]]): class AnimalLogic(BaseLogic[StardewLogic]):
def can_buy_animal(self, animal: str) -> StardewRule: def can_incubate(self, egg_item: str) -> StardewRule:
try: return self.logic.building.has_building(Building.coop) & self.logic.has(egg_item)
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 has_animal(self, animal: str) -> StardewRule: def can_ostrich_incubate(self, egg_item: str) -> StardewRule:
if animal == Generic.any: return self.logic.building.has_building(Building.barn) & self.logic.has(Machine.ostrich_incubator) & self.logic.has(egg_item)
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 has_happy_animal(self, animal: str) -> StardewRule: def has_animal(self, animal_name: str) -> StardewRule:
return self.has_animal(animal) & self.logic.has(Forageable.hay) 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.logic.source.has_access_to_any(animal.sources) & self.logic.building.has_building(animal.required_building)
return self.has_any_coop_animal() | self.has_any_barn_animal()
def has_any_coop_animal(self) -> StardewRule: def has_happy_animal(self, animal_name: str) -> StardewRule:
return self.logic.has_any(*coop_animals) return self.logic.animal.has_animal(animal_name) & self.logic.has(Forageable.hay)
def has_any_barn_animal(self) -> StardewRule:
return self.logic.has_any(*barn_animals)

View File

@@ -17,6 +17,7 @@ from .skill_logic import SkillLogicMixin
from .time_logic import TimeLogicMixin from .time_logic import TimeLogicMixin
from ..options import FestivalLocations from ..options import FestivalLocations
from ..stardew_rule import StardewRule from ..stardew_rule import StardewRule
from ..strings.animal_product_names import AnimalProduct
from ..strings.book_names import Book from ..strings.book_names import Book
from ..strings.craftable_names import Fishing from ..strings.craftable_names import Fishing
from ..strings.crop_names import Fruit, Vegetable from ..strings.crop_names import Fruit, Vegetable
@@ -154,18 +155,37 @@ SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, Relation
if self.options.festival_locations != FestivalLocations.option_hard: if self.options.festival_locations != FestivalLocations.option_hard:
return self.logic.true_ 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) 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) 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 good_fruits = (fruit
for fruit in for fruit in
(Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate, (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) Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
if fruit in self.content.game_items) if fruit in self.content.game_items)
fruit_rule = self.logic.has_any(*good_fruits) fruit_rule = self.logic.has_any(*good_fruits)
good_vegetables = (vegeteable good_vegetables = (vegeteable
for vegeteable in for vegeteable in
(Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale, (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) if vegeteable in self.content.game_items)
vegetable_rule = self.logic.has_any(*good_vegetables) vegetable_rule = self.logic.has_any(*good_vegetables)
return animal_rule & artisan_rule & cooking_rule & fish_rule & \ return animal_rule & artisan_rule & cooking_rule & fish_rule & forage_rule & fruit_rule & mineral_rule & vegetable_rule
forage_rule & fruit_rule & mineral_rule & vegetable_rule
def can_win_fishing_competition(self) -> StardewRule: def can_win_fishing_competition(self) -> StardewRule:
return self.logic.skill.can_fish(difficulty=60) 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.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.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)), # | (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.any_egg: self.has_any(AnimalProduct.chicken_egg, AnimalProduct.duck_egg),
AnimalProduct.brown_egg: self.animal.has_animal(Animal.chicken), 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.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.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.duck_feather: self.animal.has_happy_animal(Animal.duck),
AnimalProduct.egg: self.animal.has_animal(Animal.chicken), AnimalProduct.egg: self.animal.has_animal(Animal.chicken), # Should also check starter
AnimalProduct.goat_milk: self.has(Animal.goat), AnimalProduct.goat_milk: self.animal.has_animal(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.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_brown_egg: self.animal.has_happy_animal(Animal.chicken),
AnimalProduct.large_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_goat_milk: self.animal.has_happy_animal(Animal.goat),
AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow), AnimalProduct.large_milk: self.animal.has_happy_animal(Animal.cow),
AnimalProduct.milk: self.animal.has_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.rabbit_foot: self.animal.has_happy_animal(Animal.rabbit),
AnimalProduct.roe: self.skill.can_fish() & self.building.has_building(Building.fish_pond), 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.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.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.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.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_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_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_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_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.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.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.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), 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). 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 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 import functools
from typing import Union, Any, Iterable from typing import Union, Any, Iterable
from .animal_logic import AnimalLogicMixin
from .artisan_logic import ArtisanLogicMixin from .artisan_logic import ArtisanLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic from .base_logic import BaseLogicMixin, BaseLogic
from .grind_logic import GrindLogicMixin from .grind_logic import GrindLogicMixin
@@ -11,6 +12,7 @@ from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin from .region_logic import RegionLogicMixin
from .requirement_logic import RequirementLogicMixin from .requirement_logic import RequirementLogicMixin
from .tool_logic import ToolLogicMixin from .tool_logic import ToolLogicMixin
from ..data.animal import IncubatorSource, OstrichIncubatorSource
from ..data.artisan import MachineSource from ..data.artisan import MachineSource
from ..data.game_item import GenericSource, Source, GameItem, CustomRuleSource from ..data.game_item import GenericSource, Source, GameItem, CustomRuleSource
from ..data.harvest import ForagingSource, FruitBatsSource, MushroomCaveSource, SeasonalForagingSource, \ 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, 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): def has_access_to_item(self, item: GameItem):
rules = [] rules = []
@@ -81,6 +83,14 @@ ArtisanLogicMixin, ToolLogicMixin, RequirementLogicMixin, GrindLogicMixin]]):
def _(self, source: HarvestCropSource): def _(self, source: HarvestCropSource):
return self.logic.harvesting.can_harvest_crop_from(source) 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 @has_access_to.register
def _(self, source: MachineSource): def _(self, source: MachineSource):
return self.logic.artisan.can_produce_from(source) return self.logic.artisan.can_produce_from(source)

View File

@@ -8,6 +8,5 @@ class Animal:
rabbit = "Rabbit" rabbit = "Rabbit"
goat = "Goat" goat = "Goat"
ostrich = "Ostrich" ostrich = "Ostrich"
void_chicken = "Void Chicken"
coop_animals = [Animal.chicken, "Rabbit", "Duck", "Dinosaur"] golden_chicken = "Golden Chicken"
barn_animals = [Animal.cow, "Sheep", "Pig", "Ostrich"]

View File

@@ -3,17 +3,32 @@ class AnimalProduct:
brown_egg = "Egg (Brown)" brown_egg = "Egg (Brown)"
chicken_egg = "Chicken Egg" chicken_egg = "Chicken Egg"
cow_milk = "Cow Milk" cow_milk = "Cow Milk"
dinosaur_egg_starter = "Dinosaur Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Dinosaur-and-egg problem."""
dinosaur_egg = "Dinosaur Egg" dinosaur_egg = "Dinosaur Egg"
duck_egg_starter = "Duck Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Chicken-and-egg problem."""
duck_egg = "Duck Egg" duck_egg = "Duck Egg"
duck_feather = "Duck Feather" duck_feather = "Duck Feather"
egg_starter = "Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Chicken-and-egg problem."""
egg = "Egg" egg = "Egg"
goat_milk = "Goat Milk" goat_milk = "Goat Milk"
golden_egg_starter = "Golden Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Chicken-and-egg problem."""
golden_egg = "Golden Egg" golden_egg = "Golden Egg"
large_brown_egg = "Large Egg (Brown)" large_brown_egg = "Large Egg (Brown)"
large_egg = "Large Egg" large_egg = "Large Egg"
large_goat_milk = "Large Goat Milk" large_goat_milk = "Large Goat Milk"
large_milk = "Large Milk" large_milk = "Large Milk"
milk = "Milk" milk = "Milk"
ostrich_egg_starter = "Ostrich Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Chicken-and-egg problem."""
ostrich_egg = "Ostrich Egg" ostrich_egg = "Ostrich Egg"
rabbit_foot = "Rabbit's Foot" rabbit_foot = "Rabbit's Foot"
roe = "Roe" roe = "Roe"
@@ -25,6 +40,8 @@ class AnimalProduct:
squid_ink = "Squid Ink" squid_ink = "Squid Ink"
sturgeon_roe = "Sturgeon Roe" sturgeon_roe = "Sturgeon Roe"
truffle = "Truffle" truffle = "Truffle"
void_egg_starter = "Void Egg (Starter)"
"""This item does not really exist and should never end up being displayed.
It's there to patch the loop in logic because of the Chicken-and-egg problem."""
void_egg = "Void Egg" void_egg = "Void Egg"
wool = "Wool" wool = "Wool"

View File

@@ -142,5 +142,3 @@ class ModFossil:
pterodactyl_phalange = "Pterodactyl Phalange" pterodactyl_phalange = "Pterodactyl Phalange"
pterodactyl_vertebra = "Pterodactyl Vertebra" pterodactyl_vertebra = "Pterodactyl Vertebra"
pterodactyl_claw = "Pterodactyl Claw" pterodactyl_claw = "Pterodactyl Claw"