diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py index 8a72a481..c3f8e9f8 100644 --- a/worlds/stardew_valley/content/game_content.py +++ b/worlds/stardew_valley/content/game_content.py @@ -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, tool_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 @@ -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(): @@ -109,6 +111,11 @@ class ContentPack: 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): diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py index 2d50f771..faa7cb53 100644 --- a/worlds/stardew_valley/content/unpacking.py +++ b/worlds/stardew_valley/content/unpacking.py @@ -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) diff --git a/worlds/stardew_valley/content/vanilla/ginger_island.py b/worlds/stardew_valley/content/vanilla/ginger_island.py index 2fbcb032..edb135ea 100644 --- a/worlds/stardew_valley/content/vanilla/ginger_island.py +++ b/worlds/stardew_valley/content/vanilla/ginger_island.py @@ -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 @@ -51,6 +55,13 @@ ginger_island_content_pack = GingerIslandContentPack( 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),), @@ -81,5 +92,12 @@ ginger_island_content_pack = GingerIslandContentPack( ), villagers=( villagers_data.leo, + ), + animals=( + Animal(AnimalName.ostrich, + required_building=Building.barn, + sources=( + OstrichIncubatorSource(AnimalProduct.ostrich_egg_starter), + )), ) ) diff --git a/worlds/stardew_valley/content/vanilla/the_farm.py b/worlds/stardew_valley/content/vanilla/the_farm.py index 68d0bf10..183025e4 100644 --- a/worlds/stardew_valley/content/vanilla/the_farm.py +++ b/worlds/stardew_valley/content/vanilla/the_farm.py @@ -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)", @@ -39,5 +44,64 @@ the_farm = ContentPack( 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), + )), + ) ) diff --git a/worlds/stardew_valley/data/animal.py b/worlds/stardew_valley/data/animal.py new file mode 100644 index 00000000..8121b6f6 --- /dev/null +++ b/worlds/stardew_valley/data/animal.py @@ -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 diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index 75f0f75a..3a5523ec 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -143,7 +143,7 @@ duck_egg = BundleItem(AnimalProduct.duck_egg) 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) @@ -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), 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) diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py index b81c518a..0607261e 100644 --- a/worlds/stardew_valley/data/museum_data.py +++ b/worlds/stardew_valley/data/museum_data.py @@ -3,12 +3,13 @@ from __future__ import annotations 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.metal_names import Mineral, Artifact, Fossil -from ..strings.region_names import Region 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) @@ -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), diff --git a/worlds/stardew_valley/logic/animal_logic.py b/worlds/stardew_valley/logic/animal_logic.py index eb1ebeee..071133d5 100644 --- a/worlds/stardew_valley/logic/animal_logic.py +++ b/worlds/stardew_valley/logic/animal_logic.py @@ -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) diff --git a/worlds/stardew_valley/logic/festival_logic.py b/worlds/stardew_valley/logic/festival_logic.py index 2b226172..939e9049 100644 --- a/worlds/stardew_valley/logic/festival_logic.py +++ b/worlds/stardew_valley/logic/festival_logic.py @@ -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) diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index aa4cd075..3848e393 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -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), diff --git a/worlds/stardew_valley/logic/logic_and_mods_design.md b/worlds/stardew_valley/logic/logic_and_mods_design.md index 87631175..bf6684a3 100644 --- a/worlds/stardew_valley/logic/logic_and_mods_design.md +++ b/worlds/stardew_valley/logic/logic_and_mods_design.md @@ -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). diff --git a/worlds/stardew_valley/logic/source_logic.py b/worlds/stardew_valley/logic/source_logic.py index f1c6fe3d..67ce5517 100644 --- a/worlds/stardew_valley/logic/source_logic.py +++ b/worlds/stardew_valley/logic/source_logic.py @@ -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) diff --git a/worlds/stardew_valley/strings/animal_names.py b/worlds/stardew_valley/strings/animal_names.py index ecae0d76..59f2f3c9 100644 --- a/worlds/stardew_valley/strings/animal_names.py +++ b/worlds/stardew_valley/strings/animal_names.py @@ -8,6 +8,5 @@ class Animal: rabbit = "Rabbit" goat = "Goat" ostrich = "Ostrich" - -coop_animals = [Animal.chicken, "Rabbit", "Duck", "Dinosaur"] -barn_animals = [Animal.cow, "Sheep", "Pig", "Ostrich"] \ No newline at end of file + void_chicken = "Void Chicken" + golden_chicken = "Golden Chicken" diff --git a/worlds/stardew_valley/strings/animal_product_names.py b/worlds/stardew_valley/strings/animal_product_names.py index f89b610a..1b7490a6 100644 --- a/worlds/stardew_valley/strings/animal_product_names.py +++ b/worlds/stardew_valley/strings/animal_product_names.py @@ -3,17 +3,32 @@ class AnimalProduct: brown_egg = "Egg (Brown)" chicken_egg = "Chicken Egg" 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" + 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_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" 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" large_brown_egg = "Large Egg (Brown)" large_egg = "Large Egg" large_goat_milk = "Large Goat Milk" large_milk = "Large 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" rabbit_foot = "Rabbit's Foot" roe = "Roe" @@ -25,6 +40,8 @@ class AnimalProduct: squid_ink = "Squid Ink" sturgeon_roe = "Sturgeon Roe" 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" wool = "Wool" - diff --git a/worlds/stardew_valley/strings/metal_names.py b/worlds/stardew_valley/strings/metal_names.py index 7798c06d..7efdc7ed 100644 --- a/worlds/stardew_valley/strings/metal_names.py +++ b/worlds/stardew_valley/strings/metal_names.py @@ -142,5 +142,3 @@ class ModFossil: pterodactyl_phalange = "Pterodactyl Phalange" pterodactyl_vertebra = "Pterodactyl Vertebra" pterodactyl_claw = "Pterodactyl Claw" - -