Stardew Valley: 5.x.x - The Allsanity Update (#2764)

Major Content update for Stardew Valley, including the following features

- Major performance improvements all across the Stardew Valley apworld, including a significant reduction in the test time
- Randomized Farm Type
- Bundles rework (Remixed Bundles and Missing Bundle!)
- New Settings:
  * Shipsanity - Shipping individual items
  * Monstersanity - Slaying monsters
  * Cooksanity - Cooking individual recipes
  * Chefsanity - Learning individual recipes
  * Craftsanity - Crafting individual items
- New Goals:
  * Protector of the Valley - Complete every monster slayer goal
  * Full Shipment - Ship every item
  * Craftmaster - Craft every item
  * Gourmet Chef - Cook every recipe
  * Legend - Earn 10 000 000g
  * Mystery of the Stardrops - Find every stardrop (Maguffin Hunt)
  * Allsanity - Complete every check in your slot
- Building Shuffle: Cheaper options
- Tool Shuffle: Cheaper options
- Money rework
- New traps
- New isolated checks and items, including the farm cave, the movie theater, etc
- Mod Support: SVE [Albrekka]
- Mod Support: Distant Lands [Albrekka]
- Mod Support: Hat Mouse Lacey [Albrekka]
- Mod Support: Boarding House [Albrekka]

Co-authored-by: Witchybun <elnendil@gmail.com>
Co-authored-by: Witchybun <96719127+Witchybun@users.noreply.github.com>
Co-authored-by: Jouramie <jouramie@hotmail.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
This commit is contained in:
agilbert1412
2024-03-15 15:05:14 +03:00
committed by GitHub
parent f7da833572
commit 52e65e208e
177 changed files with 17815 additions and 6863 deletions

View File

View File

@@ -0,0 +1,46 @@
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .mine_logic import MineLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin
from ..stardew_rule import StardewRule
from ..strings.region_names import Region
from ..strings.skill_names import Skill, ModSkill
from ..strings.tool_names import ToolMaterial, Tool
class AbilityLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ability = AbilityLogic(*args, **kwargs)
class AbilityLogic(BaseLogic[Union[AbilityLogicMixin, RegionLogicMixin, ReceivedLogicMixin, ToolLogicMixin, SkillLogicMixin, MineLogicMixin, MagicLogicMixin]]):
def can_mine_perfectly(self) -> StardewRule:
return self.logic.mine.can_progress_in_the_mines_from_floor(160)
def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule:
return (self.logic.ability.can_mine_perfectly() &
self.logic.region.can_reach(Region.skull_cavern))
def can_farm_perfectly(self) -> StardewRule:
tool_rule = self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iridium) & self.logic.tool.can_water(4)
return tool_rule & self.logic.skill.has_farming_level(10)
def can_fish_perfectly(self) -> StardewRule:
skill_rule = self.logic.skill.has_level(Skill.fishing, 10)
return skill_rule & self.logic.tool.has_fishing_rod(4)
def can_chop_trees(self) -> StardewRule:
return self.logic.tool.has_tool(Tool.axe) & self.logic.region.can_reach(Region.forest)
def can_chop_perfectly(self) -> StardewRule:
magic_rule = (self.logic.magic.can_use_clear_debris_instead_of_tool_level(3)) & self.logic.mod.skill.has_mod_level(ModSkill.magic, 10)
tool_rule = self.logic.tool.has_tool(Tool.axe, ToolMaterial.iridium)
foraging_rule = self.logic.skill.has_level(Skill.foraging, 10)
region_rule = self.logic.region.can_reach(Region.forest)
return region_rule & ((tool_rule & foraging_rule) | magic_rule)

View File

@@ -0,0 +1,40 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..stardew_rule import StardewRule, True_, Or
from ..strings.generic_names import Generic
from ..strings.geode_names import Geode
from ..strings.region_names import Region
class ActionLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.action = ActionLogic(*args, **kwargs)
class ActionLogic(BaseLogic[Union[ActionLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
def can_watch(self, channel: str = None):
tv_rule = True_()
if channel is None:
return tv_rule
return self.logic.received(channel) & tv_rule
def can_pan(self) -> StardewRule:
return self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain)
def can_pan_at(self, region: str) -> StardewRule:
return self.logic.region.can_reach(region) & self.logic.action.can_pan()
@cache_self1
def can_open_geode(self, geode: str) -> StardewRule:
blacksmith_access = self.logic.region.can_reach(Region.blacksmith)
geodes = [Geode.geode, Geode.frozen, Geode.magma, Geode.omni]
if geode == Generic.any:
return blacksmith_access & Or(*(self.logic.has(geode_type) for geode_type in geodes))
return blacksmith_access & self.logic.has(geode)

View File

@@ -0,0 +1,59 @@
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 ..strings.building_names import Building
from ..strings.forageable_names import Forageable
from ..strings.generic_names import Generic
from ..strings.region_names import Region
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)
}
class AnimalLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*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)
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 has_happy_animal(self, animal: str) -> StardewRule:
return self.has_animal(animal) & self.logic.has(Forageable.hay)
def has_any_animal(self) -> StardewRule:
return self.has_any_coop_animal() | self.has_any_barn_animal()
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)

View File

@@ -0,0 +1,34 @@
from typing import Union
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .. import options
from ..stardew_rule import StardewRule, True_
from ..strings.region_names import Region
class ArcadeLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.arcade = ArcadeLogic(*args, **kwargs)
class ArcadeLogic(BaseLogic[Union[ArcadeLogicMixin, RegionLogicMixin, ReceivedLogicMixin]]):
def has_jotpk_power_level(self, power_level: int) -> StardewRule:
if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
return True_()
jotpk_buffs = ("JotPK: Progressive Boots", "JotPK: Progressive Gun", "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate")
return self.logic.received_n(*jotpk_buffs, count=power_level)
def has_junimo_kart_power_level(self, power_level: int) -> StardewRule:
if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
return True_()
return self.logic.received("Junimo Kart: Extra Life", power_level)
def has_junimo_kart_max_level(self) -> StardewRule:
play_rule = self.logic.region.can_reach(Region.junimo_kart_3)
if self.options.arcade_machine_locations != options.ArcadeMachineLocations.option_full_shuffling:
return play_rule
return self.logic.arcade.has_junimo_kart_power_level(8)

View File

@@ -0,0 +1,53 @@
from typing import Union
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .time_logic import TimeLogicMixin
from ..stardew_rule import StardewRule
from ..strings.crop_names import all_vegetables, all_fruits, Vegetable, Fruit
from ..strings.generic_names import Generic
from ..strings.machine_names import Machine
class ArtisanLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.artisan = ArtisanLogic(*args, **kwargs)
class ArtisanLogic(BaseLogic[Union[ArtisanLogicMixin, TimeLogicMixin, HasLogicMixin]]):
def has_jelly(self) -> StardewRule:
return self.logic.artisan.can_preserves_jar(Fruit.any)
def has_pickle(self) -> StardewRule:
return self.logic.artisan.can_preserves_jar(Vegetable.any)
def can_preserves_jar(self, item: str) -> StardewRule:
machine_rule = self.logic.has(Machine.preserves_jar)
if item == Generic.any:
return machine_rule
if item == Fruit.any:
return machine_rule & self.logic.has_any(*all_fruits)
if item == Vegetable.any:
return machine_rule & self.logic.has_any(*all_vegetables)
return machine_rule & self.logic.has(item)
def has_wine(self) -> StardewRule:
return self.logic.artisan.can_keg(Fruit.any)
def has_juice(self) -> StardewRule:
return self.logic.artisan.can_keg(Vegetable.any)
def can_keg(self, item: str) -> StardewRule:
machine_rule = self.logic.has(Machine.keg)
if item == Generic.any:
return machine_rule
if item == Fruit.any:
return machine_rule & self.logic.has_any(*all_fruits)
if item == Vegetable.any:
return machine_rule & self.logic.has_any(*all_vegetables)
return machine_rule & self.logic.has(item)
def can_mayonnaise(self, item: str) -> StardewRule:
return self.logic.has(Machine.mayonnaise_machine) & self.logic.has(item)

View File

@@ -0,0 +1,50 @@
from __future__ import annotations
from typing import TypeVar, Generic, Dict, Collection
from ..options import StardewValleyOptions
from ..stardew_rule import StardewRule
class LogicRegistry:
def __init__(self):
self.item_rules: Dict[str, StardewRule] = {}
self.sapling_rules: Dict[str, StardewRule] = {}
self.tree_fruit_rules: Dict[str, StardewRule] = {}
self.seed_rules: Dict[str, StardewRule] = {}
self.cooking_rules: Dict[str, StardewRule] = {}
self.crafting_rules: Dict[str, StardewRule] = {}
self.crop_rules: Dict[str, StardewRule] = {}
self.fish_rules: Dict[str, StardewRule] = {}
self.museum_rules: Dict[str, StardewRule] = {}
self.festival_rules: Dict[str, StardewRule] = {}
self.quest_rules: Dict[str, StardewRule] = {}
self.building_rules: Dict[str, StardewRule] = {}
self.special_order_rules: Dict[str, StardewRule] = {}
self.sve_location_rules: Dict[str, StardewRule] = {}
class BaseLogicMixin:
def __init__(self, *args, **kwargs):
pass
T = TypeVar("T", bound=BaseLogicMixin)
class BaseLogic(BaseLogicMixin, Generic[T]):
player: int
registry: LogicRegistry
options: StardewValleyOptions
regions: Collection[str]
logic: T
def __init__(self, player: int, registry: LogicRegistry, options: StardewValleyOptions, regions: Collection[str], logic: T):
super().__init__(player, registry, options, regions, logic)
self.player = player
self.registry = registry
self.options = options
self.regions = regions
self.logic = logic

View File

@@ -0,0 +1,23 @@
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .received_logic import ReceivedLogicMixin
from ..stardew_rule import StardewRule
from ..strings.ap_names.buff_names import Buff
class BuffLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.buff = BuffLogic(*args, **kwargs)
class BuffLogic(BaseLogic[Union[ReceivedLogicMixin]]):
def has_max_buffs(self) -> StardewRule:
return self.has_max_speed() & self.has_max_luck()
def has_max_speed(self) -> StardewRule:
return self.logic.received(Buff.movement, self.options.movement_buff_number.value)
def has_max_luck(self) -> StardewRule:
return self.logic.received(Buff.luck, self.options.luck_buff_number.value)

View File

@@ -0,0 +1,95 @@
from typing import Dict, Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..options import BuildingProgression
from ..stardew_rule import StardewRule, True_, False_, Has
from ..strings.ap_names.event_names import Event
from ..strings.artisan_good_names import ArtisanGood
from ..strings.building_names import Building
from ..strings.fish_names import WaterItem
from ..strings.material_names import Material
from ..strings.metal_names import MetalBar
class BuildingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.building = BuildingLogic(*args, **kwargs)
class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
def initialize_rules(self):
self.registry.building_rules.update({
# @formatter:off
Building.barn: self.logic.money.can_spend(6000) & self.logic.has_all(Material.wood, Material.stone),
Building.big_barn: self.logic.money.can_spend(12000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.barn),
Building.deluxe_barn: self.logic.money.can_spend(25000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_barn),
Building.coop: self.logic.money.can_spend(4000) & self.logic.has_all(Material.wood, Material.stone),
Building.big_coop: self.logic.money.can_spend(10000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.coop),
Building.deluxe_coop: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.big_coop),
Building.fish_pond: self.logic.money.can_spend(5000) & self.logic.has_all(Material.stone, WaterItem.seaweed, WaterItem.green_algae),
Building.mill: self.logic.money.can_spend(2500) & self.logic.has_all(Material.stone, Material.wood, ArtisanGood.cloth),
Building.shed: self.logic.money.can_spend(15000) & self.logic.has(Material.wood),
Building.big_shed: self.logic.money.can_spend(20000) & self.logic.has_all(Material.wood, Material.stone) & self.logic.building.has_building(Building.shed),
Building.silo: self.logic.money.can_spend(100) & self.logic.has_all(Material.stone, Material.clay, MetalBar.copper),
Building.slime_hutch: self.logic.money.can_spend(10000) & self.logic.has_all(Material.stone, MetalBar.quartz, MetalBar.iridium),
Building.stable: self.logic.money.can_spend(10000) & self.logic.has_all(Material.hardwood, MetalBar.iron),
Building.well: self.logic.money.can_spend(1000) & self.logic.has(Material.stone),
Building.shipping_bin: self.logic.money.can_spend(250) & self.logic.has(Material.wood),
Building.kitchen: self.logic.money.can_spend(10000) & self.logic.has(Material.wood) & self.logic.building.has_house(0),
Building.kids_room: self.logic.money.can_spend(50000) & self.logic.has(Material.hardwood) & self.logic.building.has_house(1),
Building.cellar: self.logic.money.can_spend(100000) & self.logic.building.has_house(2),
# @formatter:on
})
def update_rules(self, new_rules: Dict[str, StardewRule]):
self.registry.building_rules.update(new_rules)
@cache_self1
def has_building(self, building: str) -> StardewRule:
# Shipping bin is special. The mod auto-builds it when received, no need to go to Robin.
if building is Building.shipping_bin:
if not self.options.building_progression & BuildingProgression.option_progressive:
return True_()
return self.logic.received(building)
carpenter_rule = self.logic.received(Event.can_construct_buildings)
if not self.options.building_progression & BuildingProgression.option_progressive:
return Has(building, self.registry.building_rules) & carpenter_rule
count = 1
if building in [Building.coop, Building.barn, Building.shed]:
building = f"Progressive {building}"
elif building.startswith("Big"):
count = 2
building = " ".join(["Progressive", *building.split(" ")[1:]])
elif building.startswith("Deluxe"):
count = 3
building = " ".join(["Progressive", *building.split(" ")[1:]])
return self.logic.received(building, count) & carpenter_rule
@cache_self1
def has_house(self, upgrade_level: int) -> StardewRule:
if upgrade_level < 1:
return True_()
if upgrade_level > 3:
return False_()
carpenter_rule = self.logic.received(Event.can_construct_buildings)
if self.options.building_progression & BuildingProgression.option_progressive:
return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level)
if upgrade_level == 1:
return carpenter_rule & Has(Building.kitchen, self.registry.building_rules)
if upgrade_level == 2:
return carpenter_rule & Has(Building.kids_room, self.registry.building_rules)
# if upgrade_level == 3:
return carpenter_rule & Has(Building.cellar, self.registry.building_rules)

View File

@@ -0,0 +1,66 @@
from functools import cached_property
from typing import Union, List
from .base_logic import BaseLogicMixin, BaseLogic
from .farming_logic import FarmingLogicMixin
from .fishing_logic import FishingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from ..bundles.bundle import Bundle
from ..stardew_rule import StardewRule, And, True_
from ..strings.currency_names import Currency
from ..strings.machine_names import Machine
from ..strings.quality_names import CropQuality, ForageQuality, FishQuality, ArtisanQuality
from ..strings.region_names import Region
class BundleLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.bundle = BundleLogic(*args, **kwargs)
class BundleLogic(BaseLogic[Union[HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, FarmingLogicMixin, FishingLogicMixin, SkillLogicMixin]]):
# Should be cached
def can_complete_bundle(self, bundle: Bundle) -> StardewRule:
item_rules = []
qualities = []
can_speak_junimo = self.logic.region.can_reach(Region.wizard_tower)
for bundle_item in bundle.items:
if Currency.is_currency(bundle_item.item_name):
return can_speak_junimo & self.logic.money.can_trade(bundle_item.item_name, bundle_item.amount)
item_rules.append(bundle_item.item_name)
qualities.append(bundle_item.quality)
quality_rules = self.get_quality_rules(qualities)
item_rules = self.logic.has_n(*item_rules, count=bundle.number_required)
return can_speak_junimo & item_rules & quality_rules
def get_quality_rules(self, qualities: List[str]) -> StardewRule:
crop_quality = CropQuality.get_highest(qualities)
fish_quality = FishQuality.get_highest(qualities)
forage_quality = ForageQuality.get_highest(qualities)
artisan_quality = ArtisanQuality.get_highest(qualities)
quality_rules = []
if crop_quality != CropQuality.basic:
quality_rules.append(self.logic.farming.can_grow_crop_quality(crop_quality))
if fish_quality != FishQuality.basic:
quality_rules.append(self.logic.fishing.can_catch_quality_fish(fish_quality))
if forage_quality != ForageQuality.basic:
quality_rules.append(self.logic.skill.can_forage_quality(forage_quality))
if artisan_quality != ArtisanQuality.basic:
quality_rules.append(self.logic.has(Machine.cask))
if not quality_rules:
return True_()
return And(*quality_rules)
@cached_property
def can_complete_community_center(self) -> StardewRule:
return (self.logic.region.can_reach_location("Complete Crafts Room") &
self.logic.region.can_reach_location("Complete Pantry") &
self.logic.region.can_reach_location("Complete Fish Tank") &
self.logic.region.can_reach_location("Complete Bulletin Board") &
self.logic.region.can_reach_location("Complete Vault") &
self.logic.region.can_reach_location("Complete Boiler Room"))

View File

@@ -0,0 +1,57 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin
from ..stardew_rule import StardewRule, Or, False_
from ..strings.ap_names.ap_weapon_names import APWeapon
from ..strings.performance_names import Performance
valid_weapons = (APWeapon.weapon, APWeapon.sword, APWeapon.club, APWeapon.dagger)
class CombatLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.combat = CombatLogic(*args, **kwargs)
class CombatLogic(BaseLogic[Union[CombatLogicMixin, RegionLogicMixin, ReceivedLogicMixin, MagicLogicMixin]]):
@cache_self1
def can_fight_at_level(self, level: str) -> StardewRule:
if level == Performance.basic:
return self.logic.combat.has_any_weapon | self.logic.magic.has_any_spell()
if level == Performance.decent:
return self.logic.combat.has_decent_weapon | self.logic.magic.has_decent_spells()
if level == Performance.good:
return self.logic.combat.has_good_weapon | self.logic.magic.has_good_spells()
if level == Performance.great:
return self.logic.combat.has_great_weapon | self.logic.magic.has_great_spells()
if level == Performance.galaxy:
return self.logic.combat.has_galaxy_weapon | self.logic.magic.has_amazing_spells()
if level == Performance.maximum:
return self.logic.combat.has_galaxy_weapon | self.logic.magic.has_amazing_spells() # Someday we will have the ascended weapons in AP
return False_()
@cached_property
def has_any_weapon(self) -> StardewRule:
return self.logic.received_any(*valid_weapons)
@cached_property
def has_decent_weapon(self) -> StardewRule:
return Or(*(self.logic.received(weapon, 2) for weapon in valid_weapons))
@cached_property
def has_good_weapon(self) -> StardewRule:
return Or(*(self.logic.received(weapon, 3) for weapon in valid_weapons))
@cached_property
def has_great_weapon(self) -> StardewRule:
return Or(*(self.logic.received(weapon, 4) for weapon in valid_weapons))
@cached_property
def has_galaxy_weapon(self) -> StardewRule:
return Or(*(self.logic.received(weapon, 5) for weapon in valid_weapons))

View File

@@ -0,0 +1,108 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .action_logic import ActionLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
from ..data.recipe_data import RecipeSource, StarterSource, ShopSource, SkillSource, FriendshipSource, \
QueenOfSauceSource, CookingRecipe, ShopFriendshipSource, \
all_cooking_recipes_by_name
from ..data.recipe_source import CutsceneSource, ShopTradeSource
from ..locations import locations_by_tag, LocationTags
from ..options import Chefsanity
from ..options import ExcludeGingerIsland
from ..stardew_rule import StardewRule, True_, False_, And
from ..strings.region_names import Region
from ..strings.skill_names import Skill
from ..strings.tv_channel_names import Channel
class CookingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.cooking = CookingLogic(*args, **kwargs)
class CookingLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin,
BuildingLogicMixin, RelationshipLogicMixin, SkillLogicMixin, CookingLogicMixin]]):
@cached_property
def can_cook_in_kitchen(self) -> StardewRule:
return self.logic.building.has_house(1) | self.logic.skill.has_level(Skill.foraging, 9)
# Should be cached
def can_cook(self, recipe: CookingRecipe = None) -> StardewRule:
cook_rule = self.logic.region.can_reach(Region.kitchen)
if recipe is None:
return cook_rule
recipe_rule = self.logic.cooking.knows_recipe(recipe.source, recipe.meal)
ingredients_rule = self.logic.has_all(*sorted(recipe.ingredients))
return cook_rule & recipe_rule & ingredients_rule
# Should be cached
def knows_recipe(self, source: RecipeSource, meal_name: str) -> StardewRule:
if self.options.chefsanity == Chefsanity.option_none:
return self.logic.cooking.can_learn_recipe(source)
if isinstance(source, StarterSource):
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, ShopTradeSource) and self.options.chefsanity & Chefsanity.option_purchases:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, ShopSource) and self.options.chefsanity & Chefsanity.option_purchases:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, SkillSource) and self.options.chefsanity & Chefsanity.option_skills:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, CutsceneSource) and self.options.chefsanity & Chefsanity.option_friendship:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, FriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, QueenOfSauceSource) and self.options.chefsanity & Chefsanity.option_queen_of_sauce:
return self.logic.cooking.received_recipe(meal_name)
if isinstance(source, ShopFriendshipSource) and self.options.chefsanity & Chefsanity.option_friendship:
return self.logic.cooking.received_recipe(meal_name)
return self.logic.cooking.can_learn_recipe(source)
@cache_self1
def can_learn_recipe(self, source: RecipeSource) -> StardewRule:
if isinstance(source, StarterSource):
return True_()
if isinstance(source, ShopTradeSource):
return self.logic.money.can_trade_at(source.region, source.currency, source.price)
if isinstance(source, ShopSource):
return self.logic.money.can_spend_at(source.region, source.price)
if isinstance(source, SkillSource):
return self.logic.skill.has_level(source.skill, source.level)
if isinstance(source, CutsceneSource):
return self.logic.region.can_reach(source.region) & self.logic.relationship.has_hearts(source.friend, source.hearts)
if isinstance(source, FriendshipSource):
return self.logic.relationship.has_hearts(source.friend, source.hearts)
if isinstance(source, QueenOfSauceSource):
return self.logic.action.can_watch(Channel.queen_of_sauce) & self.logic.season.has(source.season)
if isinstance(source, ShopFriendshipSource):
return self.logic.money.can_spend_at(source.region, source.price) & self.logic.relationship.has_hearts(source.friend, source.hearts)
return False_()
@cache_self1
def received_recipe(self, meal_name: str):
return self.logic.received(f"{meal_name} Recipe")
@cached_property
def can_cook_everything(self) -> StardewRule:
cooksanity_prefix = "Cook "
all_recipes_names = []
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
for location in locations_by_tag[LocationTags.COOKSANITY]:
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
continue
if location.mod_name and location.mod_name not in self.options.mods:
continue
all_recipes_names.append(location.name[len(cooksanity_prefix):])
all_recipes = [all_cooking_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
return And(*(self.logic.cooking.can_cook(recipe) for recipe in all_recipes))

View File

@@ -0,0 +1,111 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .skill_logic import SkillLogicMixin
from .special_order_logic import SpecialOrderLogicMixin
from .. import options
from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name
from ..data.recipe_data import StarterSource, ShopSource, SkillSource, FriendshipSource
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
FestivalShopSource, QuestSource
from ..locations import locations_by_tag, LocationTags
from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland
from ..stardew_rule import StardewRule, True_, False_, And
from ..strings.region_names import Region
class CraftingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.crafting = CraftingLogic(*args, **kwargs)
class CraftingLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, MoneyLogicMixin, RelationshipLogicMixin,
SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
@cache_self1
def can_craft(self, recipe: CraftingRecipe = None) -> StardewRule:
if recipe is None:
return True_()
learn_rule = self.logic.crafting.knows_recipe(recipe)
ingredients_rule = self.logic.has_all(*recipe.ingredients)
return learn_rule & ingredients_rule
@cache_self1
def knows_recipe(self, recipe: CraftingRecipe) -> StardewRule:
if isinstance(recipe.source, ArchipelagoSource):
return self.logic.received_all(*recipe.source.ap_item)
if isinstance(recipe.source, FestivalShopSource):
if self.options.festival_locations == options.FestivalLocations.option_disabled:
return self.logic.crafting.can_learn_recipe(recipe)
else:
return self.logic.crafting.received_recipe(recipe.item)
if isinstance(recipe.source, QuestSource):
if self.options.quest_locations < 0:
return self.logic.crafting.can_learn_recipe(recipe)
else:
return self.logic.crafting.received_recipe(recipe.item)
if self.options.craftsanity == Craftsanity.option_none:
return self.logic.crafting.can_learn_recipe(recipe)
if isinstance(recipe.source, StarterSource) or isinstance(recipe.source, ShopTradeSource) or isinstance(
recipe.source, ShopSource):
return self.logic.crafting.received_recipe(recipe.item)
if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations != SpecialOrderLocations.option_disabled:
return self.logic.crafting.received_recipe(recipe.item)
return self.logic.crafting.can_learn_recipe(recipe)
@cache_self1
def can_learn_recipe(self, recipe: CraftingRecipe) -> StardewRule:
if isinstance(recipe.source, StarterSource):
return True_()
if isinstance(recipe.source, ArchipelagoSource):
return self.logic.received_all(*recipe.source.ap_item)
if isinstance(recipe.source, ShopTradeSource):
return self.logic.money.can_trade_at(recipe.source.region, recipe.source.currency, recipe.source.price)
if isinstance(recipe.source, ShopSource):
return self.logic.money.can_spend_at(recipe.source.region, recipe.source.price)
if isinstance(recipe.source, SkillSource):
return self.logic.skill.has_level(recipe.source.skill, recipe.source.level)
if isinstance(recipe.source, CutsceneSource):
return self.logic.region.can_reach(recipe.source.region) & self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts)
if isinstance(recipe.source, FriendshipSource):
return self.logic.relationship.has_hearts(recipe.source.friend, recipe.source.hearts)
if isinstance(recipe.source, QuestSource):
return self.logic.quest.can_complete_quest(recipe.source.quest)
if isinstance(recipe.source, SpecialOrderSource):
if self.options.special_order_locations == SpecialOrderLocations.option_disabled:
return self.logic.special_order.can_complete_special_order(recipe.source.special_order)
return self.logic.crafting.received_recipe(recipe.item)
if isinstance(recipe.source, LogicSource):
if recipe.source.logic_rule == "Cellar":
return self.logic.region.can_reach(Region.cellar)
return False_()
@cache_self1
def received_recipe(self, item_name: str):
return self.logic.received(f"{item_name} Recipe")
@cached_property
def can_craft_everything(self) -> StardewRule:
craftsanity_prefix = "Craft "
all_recipes_names = []
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
for location in locations_by_tag[LocationTags.CRAFTSANITY]:
if not location.name.startswith(craftsanity_prefix):
continue
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
continue
if location.mod_name and location.mod_name not in self.options.mods:
continue
all_recipes_names.append(location.name[len(craftsanity_prefix):])
all_recipes = [all_crafting_recipes_by_name[recipe_name] for recipe_name in all_recipes_names]
return And(*(self.logic.crafting.can_craft(recipe) for recipe in all_recipes))

View File

@@ -0,0 +1,72 @@
from typing import Union, Iterable
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .tool_logic import ToolLogicMixin
from .traveling_merchant_logic import TravelingMerchantLogicMixin
from ..data import CropItem, SeedItem
from ..options import Cropsanity, ExcludeGingerIsland
from ..stardew_rule import StardewRule, True_, False_
from ..strings.craftable_names import Craftable
from ..strings.forageable_names import Forageable
from ..strings.machine_names import Machine
from ..strings.metal_names import Fossil
from ..strings.region_names import Region
from ..strings.seed_names import Seed
from ..strings.tool_names import Tool
class CropLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.crop = CropLogic(*args, **kwargs)
class CropLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, TravelingMerchantLogicMixin, SeasonLogicMixin, MoneyLogicMixin,
ToolLogicMixin, CropLogicMixin]]):
@cache_self1
def can_grow(self, crop: CropItem) -> StardewRule:
season_rule = self.logic.season.has_any(crop.farm_growth_seasons)
seed_rule = self.logic.has(crop.seed.name)
farm_rule = self.logic.region.can_reach(Region.farm) & season_rule
tool_rule = self.logic.tool.has_tool(Tool.hoe) & self.logic.tool.has_tool(Tool.watering_can)
region_rule = farm_rule | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm()
if crop.name == Forageable.cactus_fruit:
region_rule = self.logic.region.can_reach(Region.greenhouse) | self.logic.has(Craftable.garden_pot)
return seed_rule & region_rule & tool_rule
def can_plant_and_grow_item(self, seasons: Union[str, Iterable[str]]) -> StardewRule:
if isinstance(seasons, str):
seasons = [seasons]
season_rule = self.logic.season.has_any(seasons) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm()
farm_rule = self.logic.region.can_reach(Region.farm) | self.logic.region.can_reach(Region.greenhouse) | self.logic.crop.has_island_farm()
return season_rule & farm_rule
def has_island_farm(self) -> StardewRule:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_false:
return self.logic.region.can_reach(Region.island_west)
return False_()
@cache_self1
def can_buy_seed(self, seed: SeedItem) -> StardewRule:
if seed.requires_island and self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
if self.options.cropsanity == Cropsanity.option_disabled or seed.name == Seed.qi_bean:
item_rule = True_()
else:
item_rule = self.logic.received(seed.name)
if seed.name == Seed.coffee:
item_rule = item_rule & self.logic.traveling_merchant.has_days(3)
season_rule = self.logic.season.has_any(seed.seasons)
region_rule = self.logic.region.can_reach_all(seed.regions)
currency_rule = self.logic.money.can_spend(1000)
if seed.name == Seed.pineapple:
currency_rule = self.logic.has(Forageable.magma_cap)
if seed.name == Seed.taro:
currency_rule = self.logic.has(Fossil.bone_fragment)
return season_rule & region_rule & item_rule & currency_rule

View File

@@ -0,0 +1,41 @@
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .skill_logic import SkillLogicMixin
from ..stardew_rule import StardewRule, True_, False_
from ..strings.fertilizer_names import Fertilizer
from ..strings.quality_names import CropQuality
class FarmingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.farming = FarmingLogic(*args, **kwargs)
class FarmingLogic(BaseLogic[Union[HasLogicMixin, SkillLogicMixin, FarmingLogicMixin]]):
def has_fertilizer(self, tier: int) -> StardewRule:
if tier <= 0:
return True_()
if tier == 1:
return self.logic.has(Fertilizer.basic)
if tier == 2:
return self.logic.has(Fertilizer.quality)
if tier >= 3:
return self.logic.has(Fertilizer.deluxe)
def can_grow_crop_quality(self, quality: str) -> StardewRule:
if quality == CropQuality.basic:
return True_()
if quality == CropQuality.silver:
return self.logic.skill.has_farming_level(5) | (self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(2)) | (
self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(1)) | self.logic.farming.has_fertilizer(3)
if quality == CropQuality.gold:
return self.logic.skill.has_farming_level(10) | (
self.logic.farming.has_fertilizer(1) & self.logic.skill.has_farming_level(5)) | (
self.logic.farming.has_fertilizer(2) & self.logic.skill.has_farming_level(3)) | (
self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(2))
if quality == CropQuality.iridium:
return self.logic.farming.has_fertilizer(3) & self.logic.skill.has_farming_level(4)
return False_()

View File

@@ -0,0 +1,100 @@
from typing import Union, List
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from ..data import FishItem, fish_data
from ..locations import LocationTags, locations_by_tag
from ..options import ExcludeGingerIsland, Fishsanity
from ..options import SpecialOrderLocations
from ..stardew_rule import StardewRule, True_, False_, And
from ..strings.fish_names import SVEFish
from ..strings.quality_names import FishQuality
from ..strings.region_names import Region
from ..strings.skill_names import Skill
class FishingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fishing = FishingLogic(*args, **kwargs)
class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, ToolLogicMixin, SkillLogicMixin]]):
def can_fish_in_freshwater(self) -> StardewRule:
return self.logic.skill.can_fish() & self.logic.region.can_reach_any((Region.forest, Region.town, Region.mountain))
def has_max_fishing(self) -> StardewRule:
skill_rule = self.logic.skill.has_level(Skill.fishing, 10)
return self.logic.tool.has_fishing_rod(4) & skill_rule
def can_fish_chests(self) -> StardewRule:
skill_rule = self.logic.skill.has_level(Skill.fishing, 6)
return self.logic.tool.has_fishing_rod(4) & skill_rule
def can_fish_at(self, region: str) -> StardewRule:
return self.logic.skill.can_fish() & self.logic.region.can_reach(region)
@cache_self1
def can_catch_fish(self, fish: FishItem) -> StardewRule:
quest_rule = True_()
if fish.extended_family:
quest_rule = self.logic.fishing.can_start_extended_family_quest()
region_rule = self.logic.region.can_reach_any(fish.locations)
season_rule = self.logic.season.has_any(fish.seasons)
if fish.difficulty == -1:
difficulty_rule = self.logic.skill.can_crab_pot
else:
difficulty_rule = self.logic.skill.can_fish(difficulty=(120 if fish.legendary else fish.difficulty))
if fish.name == SVEFish.kittyfish:
item_rule = self.logic.received("Kittyfish Spell")
else:
item_rule = True_()
return quest_rule & region_rule & season_rule & difficulty_rule & item_rule
def can_start_extended_family_quest(self) -> StardewRule:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
if self.options.special_order_locations != SpecialOrderLocations.option_board_qi:
return False_()
return self.logic.region.can_reach(Region.qi_walnut_room) & And(*(self.logic.fishing.can_catch_fish(fish) for fish in fish_data.legendary_fish))
def can_catch_quality_fish(self, fish_quality: str) -> StardewRule:
if fish_quality == FishQuality.basic:
return True_()
rod_rule = self.logic.tool.has_fishing_rod(2)
if fish_quality == FishQuality.silver:
return rod_rule
if fish_quality == FishQuality.gold:
return rod_rule & self.logic.skill.has_level(Skill.fishing, 4)
if fish_quality == FishQuality.iridium:
return rod_rule & self.logic.skill.has_level(Skill.fishing, 10)
return False_()
def can_catch_every_fish(self) -> StardewRule:
rules = [self.has_max_fishing()]
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_extended_family = self.options.special_order_locations != SpecialOrderLocations.option_board_qi
for fish in fish_data.get_fish_for_mods(self.options.mods.value):
if exclude_island and fish in fish_data.island_fish:
continue
if exclude_extended_family and fish in fish_data.extended_family:
continue
rules.append(self.logic.fishing.can_catch_fish(fish))
return And(*rules)
def can_catch_every_fish_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
if self.options.fishsanity == Fishsanity.option_none:
return self.can_catch_every_fish()
rules = [self.has_max_fishing()]
for fishsanity_location in locations_by_tag[LocationTags.FISHSANITY]:
if fishsanity_location.name not in all_location_names_in_slot:
continue
rules.append(self.logic.region.can_reach_location(fishsanity_location.name))
return And(*rules)

View File

@@ -0,0 +1,20 @@
from functools import cached_property
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..stardew_rule import StardewRule
from ..strings.animal_product_names import AnimalProduct
from ..strings.gift_names import Gift
class GiftLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.gifts = GiftLogic(*args, **kwargs)
class GiftLogic(BaseLogic[HasLogicMixin]):
@cached_property
def has_any_universal_love(self) -> StardewRule:
return self.logic.has_any(Gift.golden_pumpkin, Gift.pearl, "Prismatic Shard", AnimalProduct.rabbit_foot)

View File

@@ -0,0 +1,34 @@
from .base_logic import BaseLogic
from ..stardew_rule import StardewRule, And, Or, Has, Count
class HasLogicMixin(BaseLogic[None]):
# Should be cached
def has(self, item: str) -> StardewRule:
return Has(item, self.registry.item_rules)
def has_all(self, *items: str):
assert items, "Can't have all of no items."
return And(*(self.has(item) for item in items))
def has_any(self, *items: str):
assert items, "Can't have any of no items."
return Or(*(self.has(item) for item in items))
def has_n(self, *items: str, count: int):
return self.count(count, *(self.has(item) for item in items))
@staticmethod
def count(count: int, *rules: StardewRule) -> StardewRule:
assert rules, "Can't create a Count conditions without rules"
assert len(rules) >= count, "Count need at least as many rules as the count"
if count == 1:
return Or(*rules)
if count == len(rules):
return And(*rules)
return Count(list(rules), count)

View File

@@ -0,0 +1,673 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Collection
from .ability_logic import AbilityLogicMixin
from .action_logic import ActionLogicMixin
from .animal_logic import AnimalLogicMixin
from .arcade_logic import ArcadeLogicMixin
from .artisan_logic import ArtisanLogicMixin
from .base_logic import LogicRegistry
from .buff_logic import BuffLogicMixin
from .building_logic import BuildingLogicMixin
from .bundle_logic import BundleLogicMixin
from .combat_logic import CombatLogicMixin
from .cooking_logic import CookingLogicMixin
from .crafting_logic import CraftingLogicMixin
from .crop_logic import CropLogicMixin
from .farming_logic import FarmingLogicMixin
from .fishing_logic import FishingLogicMixin
from .gift_logic import GiftLogicMixin
from .has_logic import HasLogicMixin
from .mine_logic import MineLogicMixin
from .money_logic import MoneyLogicMixin
from .monster_logic import MonsterLogicMixin
from .museum_logic import MuseumLogicMixin
from .pet_logic import PetLogicMixin
from .quest_logic import QuestLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .shipping_logic import ShippingLogicMixin
from .skill_logic import SkillLogicMixin
from .special_order_logic import SpecialOrderLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from .traveling_merchant_logic import TravelingMerchantLogicMixin
from .wallet_logic import WalletLogicMixin
from ..data import all_purchasable_seeds, all_crops
from ..data.craftable_data import all_crafting_recipes
from ..data.crops_data import crops_by_name
from ..data.fish_data import get_fish_for_mods
from ..data.museum_data import all_museum_items
from ..data.recipe_data import all_cooking_recipes
from ..mods.logic.magic_logic import MagicLogicMixin
from ..mods.logic.mod_logic import ModLogicMixin
from ..mods.mod_data import ModNames
from ..options import Cropsanity, SpecialOrderLocations, ExcludeGingerIsland, FestivalLocations, Fishsanity, Friendsanity, StardewValleyOptions
from ..stardew_rule import False_, Or, True_, And, StardewRule
from ..strings.animal_names import Animal
from ..strings.animal_product_names import AnimalProduct
from ..strings.ap_names.ap_weapon_names import APWeapon
from ..strings.ap_names.buff_names import Buff
from ..strings.ap_names.community_upgrade_names import CommunityUpgrade
from ..strings.artisan_good_names import ArtisanGood
from ..strings.building_names import Building
from ..strings.craftable_names import Consumable, Furniture, Ring, Fishing, Lighting, WildSeeds
from ..strings.crop_names import Fruit, Vegetable
from ..strings.currency_names import Currency
from ..strings.decoration_names import Decoration
from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil
from ..strings.festival_check_names import FestivalCheck
from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest
from ..strings.flower_names import Flower
from ..strings.food_names import Meal, Beverage
from ..strings.forageable_names import Forageable
from ..strings.fruit_tree_names import Sapling
from ..strings.generic_names import Generic
from ..strings.geode_names import Geode
from ..strings.gift_names import Gift
from ..strings.ingredient_names import Ingredient
from ..strings.machine_names import Machine
from ..strings.material_names import Material
from ..strings.metal_names import Ore, MetalBar, Mineral, Fossil
from ..strings.monster_drop_names import Loot
from ..strings.monster_names import Monster
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.seed_names import Seed, TreeSeed
from ..strings.skill_names import Skill
from ..strings.tool_names import Tool, ToolMaterial
from ..strings.villager_names import NPC
from ..strings.wallet_item_names import Wallet
@dataclass(frozen=False, repr=False)
class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogicMixin, TravelingMerchantLogicMixin, TimeLogicMixin,
SeasonLogicMixin, MoneyLogicMixin, ActionLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, GiftLogicMixin,
BuildingLogicMixin, ShippingLogicMixin, RelationshipLogicMixin, MuseumLogicMixin, WalletLogicMixin, AnimalLogicMixin,
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, CropLogicMixin,
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin):
player: int
options: StardewValleyOptions
regions: Collection[str]
def __init__(self, player: int, options: StardewValleyOptions, regions: Collection[str]):
self.registry = LogicRegistry()
super().__init__(player, self.registry, options, regions, self)
self.registry.fish_rules.update({fish.name: self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value)})
self.registry.museum_rules.update({donation.item_name: self.museum.can_find_museum_item(donation) for donation in all_museum_items})
for recipe in all_cooking_recipes:
if recipe.mod_name and recipe.mod_name not in self.options.mods:
continue
can_cook_rule = self.cooking.can_cook(recipe)
if recipe.meal in self.registry.cooking_rules:
can_cook_rule = can_cook_rule | self.registry.cooking_rules[recipe.meal]
self.registry.cooking_rules[recipe.meal] = can_cook_rule
for recipe in all_crafting_recipes:
if recipe.mod_name and recipe.mod_name not in self.options.mods:
continue
can_craft_rule = self.crafting.can_craft(recipe)
if recipe.item in self.registry.crafting_rules:
can_craft_rule = can_craft_rule | self.registry.crafting_rules[recipe.item]
self.registry.crafting_rules[recipe.item] = can_craft_rule
self.registry.sapling_rules.update({
Sapling.apple: self.can_buy_sapling(Fruit.apple),
Sapling.apricot: self.can_buy_sapling(Fruit.apricot),
Sapling.cherry: self.can_buy_sapling(Fruit.cherry),
Sapling.orange: self.can_buy_sapling(Fruit.orange),
Sapling.peach: self.can_buy_sapling(Fruit.peach),
Sapling.pomegranate: self.can_buy_sapling(Fruit.pomegranate),
Sapling.banana: self.can_buy_sapling(Fruit.banana),
Sapling.mango: self.can_buy_sapling(Fruit.mango),
})
self.registry.tree_fruit_rules.update({
Fruit.apple: self.crop.can_plant_and_grow_item(Season.fall),
Fruit.apricot: self.crop.can_plant_and_grow_item(Season.spring),
Fruit.cherry: self.crop.can_plant_and_grow_item(Season.spring),
Fruit.orange: self.crop.can_plant_and_grow_item(Season.summer),
Fruit.peach: self.crop.can_plant_and_grow_item(Season.summer),
Fruit.pomegranate: self.crop.can_plant_and_grow_item(Season.fall),
Fruit.banana: self.crop.can_plant_and_grow_item(Season.summer),
Fruit.mango: self.crop.can_plant_and_grow_item(Season.summer),
})
for tree_fruit in self.registry.tree_fruit_rules:
existing_rules = self.registry.tree_fruit_rules[tree_fruit]
sapling = f"{tree_fruit} Sapling"
self.registry.tree_fruit_rules[tree_fruit] = existing_rules & self.has(sapling) & self.time.has_lived_months(1)
self.registry.seed_rules.update({seed.name: self.crop.can_buy_seed(seed) for seed in all_purchasable_seeds})
self.registry.crop_rules.update({crop.name: self.crop.can_grow(crop) for crop in all_crops})
self.registry.crop_rules.update({
Seed.coffee: (self.season.has(Season.spring) | self.season.has(Season.summer)) & self.crop.can_buy_seed(crops_by_name[Seed.coffee].seed),
Fruit.ancient_fruit: (self.received("Ancient Seeds") | self.received("Ancient Seeds Recipe")) &
self.region.can_reach(Region.greenhouse) & self.has(Machine.seed_maker),
})
# @formatter:off
self.registry.item_rules.update({
"Energy Tonic": self.money.can_spend_at(Region.hospital, 1000),
WaterChest.fishing_chest: self.fishing.can_fish_chests(),
WaterChest.treasure: self.fishing.can_fish_chests(),
Ring.hot_java_ring: self.region.can_reach(Region.volcano_floor_10),
"Galaxy Soul": self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 40),
"JotPK Big Buff": self.arcade.has_jotpk_power_level(7),
"JotPK Max Buff": self.arcade.has_jotpk_power_level(9),
"JotPK Medium Buff": self.arcade.has_jotpk_power_level(4),
"JotPK Small Buff": self.arcade.has_jotpk_power_level(2),
"Junimo Kart Big Buff": self.arcade.has_junimo_kart_power_level(6),
"Junimo Kart Max Buff": self.arcade.has_junimo_kart_power_level(8),
"Junimo Kart Medium Buff": self.arcade.has_junimo_kart_power_level(4),
"Junimo Kart Small Buff": self.arcade.has_junimo_kart_power_level(2),
"Magic Rock Candy": self.region.can_reach(Region.desert) & self.has("Prismatic Shard"),
"Muscle Remedy": self.money.can_spend_at(Region.hospital, 1000),
# self.has(Ingredient.vinegar)),
# 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_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.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),
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.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),
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),
ArtisanGood.cheese: (self.has(AnimalProduct.cow_milk) & self.has(Machine.cheese_press)) | (self.region.can_reach(Region.desert) & self.has(Mineral.emerald)),
ArtisanGood.cloth: (self.has(AnimalProduct.wool) & self.has(Machine.loom)) | (self.region.can_reach(Region.desert) & self.has(Mineral.aquamarine)),
ArtisanGood.dinosaur_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.dinosaur_egg),
ArtisanGood.duck_mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.duck_egg),
ArtisanGood.goat_cheese: self.has(AnimalProduct.goat_milk) & self.has(Machine.cheese_press),
ArtisanGood.green_tea: self.artisan.can_keg(Vegetable.tea_leaves),
ArtisanGood.honey: self.money.can_spend_at(Region.oasis, 200) | (self.has(Machine.bee_house) & self.season.has_any_not_winter()),
ArtisanGood.jelly: self.artisan.has_jelly(),
ArtisanGood.juice: self.artisan.has_juice(),
ArtisanGood.maple_syrup: self.has(Machine.tapper),
ArtisanGood.mayonnaise: self.artisan.can_mayonnaise(AnimalProduct.chicken_egg),
ArtisanGood.mead: self.artisan.can_keg(ArtisanGood.honey),
ArtisanGood.oak_resin: self.has(Machine.tapper),
ArtisanGood.pale_ale: self.artisan.can_keg(Vegetable.hops),
ArtisanGood.pickles: self.artisan.has_pickle(),
ArtisanGood.pine_tar: self.has(Machine.tapper),
ArtisanGood.truffle_oil: self.has(AnimalProduct.truffle) & self.has(Machine.oil_maker),
ArtisanGood.void_mayonnaise: (self.skill.can_fish(Region.witch_swamp)) | (self.artisan.can_mayonnaise(AnimalProduct.void_egg)),
ArtisanGood.wine: self.artisan.has_wine(),
Beverage.beer: self.artisan.can_keg(Vegetable.wheat) | self.money.can_spend_at(Region.saloon, 400),
Beverage.coffee: self.artisan.can_keg(Seed.coffee) | self.has(Machine.coffee_maker) | (self.money.can_spend_at(Region.saloon, 300)) | self.has("Hot Java Ring"),
Beverage.pina_colada: self.money.can_spend_at(Region.island_resort, 600),
Beverage.triple_shot_espresso: self.has("Hot Java Ring"),
Decoration.rotten_plant: self.has(Lighting.jack_o_lantern) & self.season.has(Season.winter),
Fertilizer.basic: self.money.can_spend_at(Region.pierre_store, 100),
Fertilizer.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
Fertilizer.tree: self.skill.has_level(Skill.foraging, 7) & self.has(Material.fiber) & self.has(Material.stone),
Fish.any: Or(*(self.fishing.can_catch_fish(fish) for fish in get_fish_for_mods(self.options.mods.value))),
Fish.crab: self.skill.can_crab_pot_at(Region.beach),
Fish.crayfish: self.skill.can_crab_pot_at(Region.town),
Fish.lobster: self.skill.can_crab_pot_at(Region.beach),
Fish.mussel: self.tool.can_forage(Generic.any, Region.beach) or self.has(Fish.mussel_node),
Fish.mussel_node: self.region.can_reach(Region.island_west),
Fish.oyster: self.tool.can_forage(Generic.any, Region.beach),
Fish.periwinkle: self.skill.can_crab_pot_at(Region.town),
Fish.shrimp: self.skill.can_crab_pot_at(Region.beach),
Fish.snail: self.skill.can_crab_pot_at(Region.town),
Fishing.curiosity_lure: self.monster.can_kill(self.monster.all_monsters_by_name[Monster.mummy]),
Fishing.lead_bobber: self.skill.has_level(Skill.fishing, 6) & self.money.can_spend_at(Region.fish_shop, 200),
Forageable.blackberry: self.tool.can_forage(Season.fall) | self.has_fruit_bats(),
Forageable.cactus_fruit: self.tool.can_forage(Generic.any, Region.desert),
Forageable.cave_carrot: self.tool.can_forage(Generic.any, Region.mines_floor_10, True),
Forageable.chanterelle: self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(),
Forageable.coconut: self.tool.can_forage(Generic.any, Region.desert),
Forageable.common_mushroom: self.tool.can_forage(Season.fall) | (self.tool.can_forage(Season.spring, Region.secret_woods)) | self.has_mushroom_cave(),
Forageable.crocus: self.tool.can_forage(Season.winter),
Forageable.crystal_fruit: self.tool.can_forage(Season.winter),
Forageable.daffodil: self.tool.can_forage(Season.spring),
Forageable.dandelion: self.tool.can_forage(Season.spring),
Forageable.dragon_tooth: self.tool.can_forage(Generic.any, Region.volcano_floor_10),
Forageable.fiddlehead_fern: self.tool.can_forage(Season.summer, Region.secret_woods),
Forageable.ginger: self.tool.can_forage(Generic.any, Region.island_west, True),
Forageable.hay: self.building.has_building(Building.silo) & self.tool.has_tool(Tool.scythe),
Forageable.hazelnut: self.tool.can_forage(Season.fall),
Forageable.holly: self.tool.can_forage(Season.winter),
Forageable.journal_scrap: self.region.can_reach_all((Region.island_west, Region.island_north, Region.island_south, Region.volcano_floor_10)) & self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),
Forageable.leek: self.tool.can_forage(Season.spring),
Forageable.magma_cap: self.tool.can_forage(Generic.any, Region.volcano_floor_5),
Forageable.morel: self.tool.can_forage(Season.spring, Region.secret_woods) | self.has_mushroom_cave(),
Forageable.purple_mushroom: self.tool.can_forage(Generic.any, Region.mines_floor_95) | self.tool.can_forage(Generic.any, Region.skull_cavern_25) | self.has_mushroom_cave(),
Forageable.rainbow_shell: self.tool.can_forage(Season.summer, Region.beach),
Forageable.red_mushroom: self.tool.can_forage(Season.summer, Region.secret_woods) | self.tool.can_forage(Season.fall, Region.secret_woods) | self.has_mushroom_cave(),
Forageable.salmonberry: self.tool.can_forage(Season.spring) | self.has_fruit_bats(),
Forageable.secret_note: self.quest.has_magnifying_glass() & (self.ability.can_chop_trees() | self.mine.can_mine_in_the_mines_floor_1_40()),
Forageable.snow_yam: self.tool.can_forage(Season.winter, Region.beach, True),
Forageable.spice_berry: self.tool.can_forage(Season.summer) | self.has_fruit_bats(),
Forageable.spring_onion: self.tool.can_forage(Season.spring),
Forageable.sweet_pea: self.tool.can_forage(Season.summer),
Forageable.wild_horseradish: self.tool.can_forage(Season.spring),
Forageable.wild_plum: self.tool.can_forage(Season.fall) | self.has_fruit_bats(),
Forageable.winter_root: self.tool.can_forage(Season.winter, Region.forest, True),
Fossil.bone_fragment: (self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe)) | self.monster.can_kill(Monster.skeleton),
Fossil.fossilized_leg: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.pickaxe),
Fossil.fossilized_ribs: self.region.can_reach(Region.island_south) & self.tool.has_tool(Tool.hoe),
Fossil.fossilized_skull: self.action.can_open_geode(Geode.golden_coconut),
Fossil.fossilized_spine: self.skill.can_fish(Region.dig_site),
Fossil.fossilized_tail: self.action.can_pan_at(Region.dig_site),
Fossil.mummified_bat: self.region.can_reach(Region.volcano_floor_10),
Fossil.mummified_frog: self.region.can_reach(Region.island_east) & self.tool.has_tool(Tool.scythe),
Fossil.snake_skull: self.region.can_reach(Region.dig_site) & self.tool.has_tool(Tool.hoe),
Fossil.snake_vertebrae: self.region.can_reach(Region.island_west) & self.tool.has_tool(Tool.hoe),
Geode.artifact_trove: self.has(Geode.omni) & self.region.can_reach(Region.desert),
Geode.frozen: self.mine.can_mine_in_the_mines_floor_41_80(),
Geode.geode: self.mine.can_mine_in_the_mines_floor_1_40(),
Geode.golden_coconut: self.region.can_reach(Region.island_north),
Geode.magma: self.mine.can_mine_in_the_mines_floor_81_120() | (self.has(Fish.lava_eel) & self.building.has_building(Building.fish_pond)),
Geode.omni: self.mine.can_mine_in_the_mines_floor_41_80() | self.region.can_reach(Region.desert) | self.action.can_pan() | self.received(Wallet.rusty_key) | (self.has(Fish.octopus) & self.building.has_building(Building.fish_pond)) | self.region.can_reach(Region.volcano_floor_10),
Gift.bouquet: self.relationship.has_hearts(Generic.bachelor, 8) & self.money.can_spend_at(Region.pierre_store, 100),
Gift.golden_pumpkin: self.season.has(Season.fall) | self.action.can_open_geode(Geode.artifact_trove),
Gift.mermaid_pendant: self.region.can_reach(Region.tide_pools) & self.relationship.has_hearts(Generic.bachelor, 10) & self.building.has_house(1) & self.has(Consumable.rain_totem),
Gift.movie_ticket: self.money.can_spend_at(Region.movie_ticket_stand, 1000),
Gift.pearl: (self.has(Fish.blobfish) & self.building.has_building(Building.fish_pond)) | self.action.can_open_geode(Geode.artifact_trove),
Gift.tea_set: self.season.has(Season.winter) & self.time.has_lived_max_months,
Gift.void_ghost_pendant: self.money.can_trade_at(Region.desert, Loot.void_essence, 200) & self.relationship.has_hearts(NPC.krobus, 10),
Gift.wilted_bouquet: self.has(Machine.furnace) & self.has(Gift.bouquet) & self.has(Material.coal),
Ingredient.oil: self.money.can_spend_at(Region.pierre_store, 200) | (self.has(Machine.oil_maker) & (self.has(Vegetable.corn) | self.has(Flower.sunflower) | self.has(Seed.sunflower))),
Ingredient.qi_seasoning: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 10),
Ingredient.rice: self.money.can_spend_at(Region.pierre_store, 200) | (self.building.has_building(Building.mill) & self.has(Vegetable.unmilled_rice)),
Ingredient.sugar: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.beet)),
Ingredient.vinegar: self.money.can_spend_at(Region.pierre_store, 200),
Ingredient.wheat_flour: self.money.can_spend_at(Region.pierre_store, 100) | (self.building.has_building(Building.mill) & self.has(Vegetable.wheat)),
Loot.bat_wing: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(),
Loot.bug_meat: self.mine.can_mine_in_the_mines_floor_1_40(),
Loot.slime: self.mine.can_mine_in_the_mines_floor_1_40(),
Loot.solar_essence: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern(),
Loot.void_essence: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern(),
Machine.bee_house: self.skill.has_farming_level(3) & self.has(MetalBar.iron) & self.has(ArtisanGood.maple_syrup) & self.has(Material.coal) & self.has(Material.wood),
Machine.cask: self.building.has_house(3) & self.region.can_reach(Region.cellar) & self.has(Material.wood) & self.has(Material.hardwood),
Machine.cheese_press: self.skill.has_farming_level(6) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.hardwood) & self.has(MetalBar.copper),
Machine.coffee_maker: self.received(Machine.coffee_maker),
Machine.crab_pot: self.skill.has_level(Skill.fishing, 3) & (self.money.can_spend_at(Region.fish_shop, 1500) | (self.has(MetalBar.iron) & self.has(Material.wood))),
Machine.furnace: self.has(Material.stone) & self.has(Ore.copper),
Machine.keg: self.skill.has_farming_level(8) & self.has(Material.wood) & self.has(MetalBar.iron) & self.has(MetalBar.copper) & self.has(ArtisanGood.oak_resin),
Machine.lightning_rod: self.skill.has_level(Skill.foraging, 6) & self.has(MetalBar.iron) & self.has(MetalBar.quartz) & self.has(Loot.bat_wing),
Machine.loom: self.skill.has_farming_level(7) & self.has(Material.wood) & self.has(Material.fiber) & self.has(ArtisanGood.pine_tar),
Machine.mayonnaise_machine: self.skill.has_farming_level(2) & self.has(Material.wood) & self.has(Material.stone) & self.has("Earth Crystal") & self.has(MetalBar.copper),
Machine.ostrich_incubator: self.received("Ostrich Incubator Recipe") & self.has(Fossil.bone_fragment) & self.has(Material.hardwood) & self.has(Material.cinder_shard),
Machine.preserves_jar: self.skill.has_farming_level(4) & self.has(Material.wood) & self.has(Material.stone) & self.has(Material.coal),
Machine.recycling_machine: self.skill.has_level(Skill.fishing, 4) & self.has(Material.wood) & self.has(Material.stone) & self.has(MetalBar.iron),
Machine.seed_maker: self.skill.has_farming_level(9) & self.has(Material.wood) & self.has(MetalBar.gold) & self.has(Material.coal),
Machine.solar_panel: self.received("Solar Panel Recipe") & self.has(MetalBar.quartz) & self.has(MetalBar.iron) & self.has(MetalBar.gold),
Machine.tapper: self.skill.has_level(Skill.foraging, 3) & self.has(Material.wood) & self.has(MetalBar.copper),
Machine.worm_bin: self.skill.has_level(Skill.fishing, 8) & self.has(Material.hardwood) & self.has(MetalBar.gold) & self.has(MetalBar.iron) & self.has(Material.fiber),
Machine.enricher: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20),
Machine.pressure_nozzle: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 20),
Material.cinder_shard: self.region.can_reach(Region.volcano_floor_5),
Material.clay: self.region.can_reach_any((Region.farm, Region.beach, Region.quarry)) & self.tool.has_tool(Tool.hoe),
Material.coal: self.mine.can_mine_in_the_mines_floor_41_80() | self.action.can_pan(),
Material.fiber: True_(),
Material.hardwood: self.tool.has_tool(Tool.axe, ToolMaterial.copper) & (self.region.can_reach(Region.secret_woods) | self.region.can_reach(Region.island_west)),
Material.sap: self.ability.can_chop_trees(),
Material.stone: self.tool.has_tool(Tool.pickaxe),
Material.wood: self.tool.has_tool(Tool.axe),
Meal.bread: self.money.can_spend_at(Region.saloon, 120),
Meal.ice_cream: (self.season.has(Season.summer) & self.money.can_spend_at(Region.town, 250)) | self.money.can_spend_at(Region.oasis, 240),
Meal.pizza: self.money.can_spend_at(Region.saloon, 600),
Meal.salad: self.money.can_spend_at(Region.saloon, 220),
Meal.spaghetti: self.money.can_spend_at(Region.saloon, 240),
Meal.strange_bun: self.relationship.has_hearts(NPC.shane, 7) & self.has(Ingredient.wheat_flour) & self.has(Fish.periwinkle) & self.has(ArtisanGood.void_mayonnaise),
MetalBar.copper: self.can_smelt(Ore.copper),
MetalBar.gold: self.can_smelt(Ore.gold),
MetalBar.iridium: self.can_smelt(Ore.iridium),
MetalBar.iron: self.can_smelt(Ore.iron),
MetalBar.quartz: self.can_smelt(Mineral.quartz) | self.can_smelt("Fire Quartz") | (self.has(Machine.recycling_machine) & (self.has(Trash.broken_cd) | self.has(Trash.broken_glasses))),
MetalBar.radioactive: self.can_smelt(Ore.radioactive),
Ore.copper: self.mine.can_mine_in_the_mines_floor_1_40() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(),
Ore.gold: self.mine.can_mine_in_the_mines_floor_81_120() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(),
Ore.iridium: self.mine.can_mine_in_the_skull_cavern() | self.can_fish_pond(Fish.super_cucumber),
Ore.iron: self.mine.can_mine_in_the_mines_floor_41_80() | self.mine.can_mine_in_the_skull_cavern() | self.action.can_pan(),
Ore.radioactive: self.ability.can_mine_perfectly() & self.region.can_reach(Region.qi_walnut_room),
RetainingSoil.basic: self.money.can_spend_at(Region.pierre_store, 100),
RetainingSoil.quality: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
Sapling.tea: self.relationship.has_hearts(NPC.caroline, 2) & self.has(Material.fiber) & self.has(Material.wood),
Seed.mixed: self.tool.has_tool(Tool.scythe) & self.region.can_reach_all((Region.farm, Region.forest, Region.town)),
SpeedGro.basic: self.money.can_spend_at(Region.pierre_store, 100),
SpeedGro.deluxe: self.time.has_year_two & self.money.can_spend_at(Region.pierre_store, 150),
Trash.broken_cd: self.skill.can_crab_pot,
Trash.broken_glasses: self.skill.can_crab_pot,
Trash.driftwood: self.skill.can_crab_pot,
Trash.joja_cola: self.money.can_spend_at(Region.saloon, 75),
Trash.soggy_newspaper: self.skill.can_crab_pot,
Trash.trash: self.skill.can_crab_pot,
TreeSeed.acorn: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
TreeSeed.mahogany: self.region.can_reach(Region.secret_woods) & self.tool.has_tool(Tool.axe, ToolMaterial.iron) & self.skill.has_level(Skill.foraging, 1),
TreeSeed.maple: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
TreeSeed.mushroom: self.money.can_trade_at(Region.qi_walnut_room, Currency.qi_gem, 5),
TreeSeed.pine: self.skill.has_level(Skill.foraging, 1) & self.ability.can_chop_trees(),
Vegetable.tea_leaves: self.has(Sapling.tea) & self.time.has_lived_months(2) & self.season.has_any_not_winter(),
Fish.clam: self.tool.can_forage(Generic.any, Region.beach),
Fish.cockle: self.tool.can_forage(Generic.any, Region.beach),
WaterItem.coral: self.tool.can_forage(Generic.any, Region.tide_pools) | self.tool.can_forage(Season.summer, Region.beach),
WaterItem.green_algae: self.fishing.can_fish_in_freshwater(),
WaterItem.nautilus_shell: self.tool.can_forage(Season.winter, Region.beach),
WaterItem.sea_urchin: self.tool.can_forage(Generic.any, Region.tide_pools),
WaterItem.seaweed: self.skill.can_fish(Region.tide_pools),
WaterItem.white_algae: self.skill.can_fish(Region.mines_floor_20),
WildSeeds.grass_starter: self.money.can_spend_at(Region.pierre_store, 100),
})
# @formatter:on
self.registry.item_rules.update(self.registry.fish_rules)
self.registry.item_rules.update(self.registry.museum_rules)
self.registry.item_rules.update(self.registry.sapling_rules)
self.registry.item_rules.update(self.registry.tree_fruit_rules)
self.registry.item_rules.update(self.registry.seed_rules)
self.registry.item_rules.update(self.registry.crop_rules)
self.registry.item_rules.update(self.mod.item.get_modded_item_rules())
self.mod.item.modify_vanilla_item_rules_with_mod_additions(self.registry.item_rules) # New regions and content means new ways to obtain old items
# For some recipes, the cooked item can be obtained directly, so we either cook it or get it
for recipe in self.registry.cooking_rules:
cooking_rule = self.registry.cooking_rules[recipe]
obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
self.registry.item_rules[recipe] = obtention_rule | cooking_rule
# For some recipes, the crafted item can be obtained directly, so we either craft it or get it
for recipe in self.registry.crafting_rules:
crafting_rule = self.registry.crafting_rules[recipe]
obtention_rule = self.registry.item_rules[recipe] if recipe in self.registry.item_rules else False_()
self.registry.item_rules[recipe] = obtention_rule | crafting_rule
self.building.initialize_rules()
self.building.update_rules(self.mod.building.get_modded_building_rules())
self.quest.initialize_rules()
self.quest.update_rules(self.mod.quest.get_modded_quest_rules())
self.registry.festival_rules.update({
FestivalCheck.egg_hunt: self.can_win_egg_hunt(),
FestivalCheck.strawberry_seeds: self.money.can_spend(1000),
FestivalCheck.dance: self.relationship.has_hearts(Generic.bachelor, 4),
FestivalCheck.tub_o_flowers: self.money.can_spend(2000),
FestivalCheck.rarecrow_5: self.money.can_spend(2500),
FestivalCheck.luau_soup: self.can_succeed_luau_soup(),
FestivalCheck.moonlight_jellies: True_(),
FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800),
FestivalCheck.starport_decal: self.money.can_spend(1000),
FestivalCheck.smashing_stone: True_(),
FestivalCheck.grange_display: self.can_succeed_grange_display(),
FestivalCheck.rarecrow_1: True_(), # only cost star tokens
FestivalCheck.fair_stardrop: True_(), # only cost star tokens
FestivalCheck.spirit_eve_maze: True_(),
FestivalCheck.jack_o_lantern: self.money.can_spend(2000),
FestivalCheck.rarecrow_2: self.money.can_spend(5000),
FestivalCheck.fishing_competition: self.can_win_fishing_competition(),
FestivalCheck.rarecrow_4: self.money.can_spend(5000),
FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note),
FestivalCheck.cone_hat: self.money.can_spend(2500),
FestivalCheck.iridium_fireplace: self.money.can_spend(15000),
FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20),
FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40),
FestivalCheck.lupini_red_eagle: self.money.can_spend(1200),
FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200),
FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200),
FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200),
FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200),
FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200),
FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200),
FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200),
FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200),
FestivalCheck.secret_santa: self.gifts.has_any_universal_love,
FestivalCheck.legend_of_the_winter_star: True_(),
FestivalCheck.rarecrow_3: True_(),
FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(),
})
self.special_order.initialize_rules()
self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules())
def can_buy_sapling(self, fruit: str) -> StardewRule:
sapling_prices = {Fruit.apple: 4000, Fruit.apricot: 2000, Fruit.cherry: 3400, Fruit.orange: 4000,
Fruit.peach: 6000,
Fruit.pomegranate: 6000, Fruit.banana: 0, Fruit.mango: 0}
received_sapling = self.received(f"{fruit} Sapling")
if self.options.cropsanity == Cropsanity.option_disabled:
allowed_buy_sapling = True_()
else:
allowed_buy_sapling = received_sapling
can_buy_sapling = self.money.can_spend_at(Region.pierre_store, sapling_prices[fruit])
if fruit == Fruit.banana:
can_buy_sapling = self.has_island_trader() & self.has(Forageable.dragon_tooth)
elif fruit == Fruit.mango:
can_buy_sapling = self.has_island_trader() & self.has(Fish.mussel_node)
return allowed_buy_sapling & can_buy_sapling
def can_smelt(self, item: str) -> StardewRule:
return self.has(Machine.furnace) & self.has(item)
def can_complete_field_office(self) -> StardewRule:
field_office = self.region.can_reach(Region.field_office)
professor_snail = self.received("Open Professor Snail Cave")
tools = self.tool.has_tool(Tool.pickaxe) & self.tool.has_tool(Tool.hoe) & self.tool.has_tool(Tool.scythe)
leg_and_snake_skull = self.has_all(Fossil.fossilized_leg, Fossil.snake_skull)
ribs_and_spine = self.has_all(Fossil.fossilized_ribs, Fossil.fossilized_spine)
skull = self.has(Fossil.fossilized_skull)
tail = self.has(Fossil.fossilized_tail)
frog = self.has(Fossil.mummified_frog)
bat = self.has(Fossil.mummified_bat)
snake_vertebrae = self.has(Fossil.snake_vertebrae)
return field_office & professor_snail & tools & leg_and_snake_skull & ribs_and_spine & skull & tail & frog & bat & snake_vertebrae
def can_finish_grandpa_evaluation(self) -> StardewRule:
# https://stardewvalleywiki.com/Grandpa
rules_worth_a_point = [
self.money.can_have_earned_total(50000), # 50 000g
self.money.can_have_earned_total(100000), # 100 000g
self.money.can_have_earned_total(200000), # 200 000g
self.money.can_have_earned_total(300000), # 300 000g
self.money.can_have_earned_total(500000), # 500 000g
self.money.can_have_earned_total(1000000), # 1 000 000g first point
self.money.can_have_earned_total(1000000), # 1 000 000g second point
self.skill.has_total_level(30), # Total Skills: 30
self.skill.has_total_level(50), # Total Skills: 50
self.museum.can_complete_museum(), # Completing the museum for a point
# Catching every fish not expected
# Shipping every item not expected
self.relationship.can_get_married() & self.building.has_house(2),
self.relationship.has_hearts("5", 8), # 5 Friends
self.relationship.has_hearts("10", 8), # 10 friends
self.pet.has_hearts(5), # Max Pet
self.bundle.can_complete_community_center, # Community Center Completion
self.bundle.can_complete_community_center, # CC Ceremony first point
self.bundle.can_complete_community_center, # CC Ceremony second point
self.received(Wallet.skull_key), # Skull Key obtained
self.wallet.has_rusty_key(), # Rusty key obtained
]
return self.count(12, *rules_worth_a_point)
def can_win_egg_hunt(self) -> StardewRule:
number_of_movement_buffs = self.options.movement_buff_number
if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
return True_()
return self.received(Buff.movement, number_of_movement_buffs // 2)
def can_succeed_luau_soup(self) -> StardewRule:
if self.options.festival_locations != FestivalLocations.option_hard:
return True_()
eligible_fish = [Fish.blobfish, Fish.crimsonfish, "Ice Pip", Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish, Fish.mutant_carp,
Fish.spookfish, Fish.stingray, Fish.sturgeon, "Super Cucumber"]
fish_rule = self.has_any(*eligible_fish)
eligible_kegables = [Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
Vegetable.hops, Vegetable.wheat]
keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables]
aged_rule = self.has(Machine.cask) & Or(*keg_rules)
# There are a few other valid items, but I don't feel like coding them all
return fish_rule | aged_rule
def can_succeed_grange_display(self) -> StardewRule:
if self.options.festival_locations != FestivalLocations.option_hard:
return True_()
animal_rule = self.animal.has_animal(Generic.any)
artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any)
cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
fish_rule = self.skill.can_fish(difficulty=50)
forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough
good_fruits = [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_rule = self.has_any(*good_fruits)
good_vegetables = [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]
vegetable_rule = self.has_any(*good_vegetables)
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.skill.can_fish(difficulty=60)
def has_island_trader(self) -> StardewRule:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
return self.region.can_reach(Region.island_trader)
def has_walnut(self, number: int) -> StardewRule:
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
return False_()
if number <= 0:
return True_()
# https://stardewcommunitywiki.com/Golden_Walnut#Walnut_Locations
reach_south = self.region.can_reach(Region.island_south)
reach_north = self.region.can_reach(Region.island_north)
reach_west = self.region.can_reach(Region.island_west)
reach_hut = self.region.can_reach(Region.leo_hut)
reach_southeast = self.region.can_reach(Region.island_south_east)
reach_field_office = self.region.can_reach(Region.field_office)
reach_pirate_cove = self.region.can_reach(Region.pirate_cove)
reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut)
reach_volcano_regions = [self.region.can_reach(Region.volcano),
self.region.can_reach(Region.volcano_secret_beach),
self.region.can_reach(Region.volcano_floor_5),
self.region.can_reach(Region.volcano_floor_10)]
reach_volcano = Or(*reach_volcano_regions)
reach_all_volcano = And(*reach_volcano_regions)
reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office]
reach_caves = And(self.region.can_reach(Region.qi_walnut_room), self.region.can_reach(Region.dig_site),
self.region.can_reach(Region.gourmand_frog_cave),
self.region.can_reach(Region.colored_crystals_cave),
self.region.can_reach(Region.shipwreck), self.received(APWeapon.slingshot))
reach_entire_island = And(reach_outside_areas, reach_all_volcano,
reach_caves, reach_southeast, reach_field_office, reach_pirate_cove)
if number <= 5:
return Or(reach_south, reach_north, reach_west, reach_volcano)
if number <= 10:
return self.count(2, *reach_walnut_regions)
if number <= 15:
return self.count(3, *reach_walnut_regions)
if number <= 20:
return And(*reach_walnut_regions)
if number <= 50:
return reach_entire_island
gems = (Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz)
return reach_entire_island & self.has(Fruit.banana) & self.has_all(*gems) & self.ability.can_mine_perfectly() & \
self.ability.can_fish_perfectly() & self.has(Furniture.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \
self.can_complete_field_office()
def has_all_stardrops(self) -> StardewRule:
other_rules = []
number_of_stardrops_to_receive = 0
number_of_stardrops_to_receive += 1 # The Mines level 100
number_of_stardrops_to_receive += 1 # Old Master Cannoli
number_of_stardrops_to_receive += 1 # Museum Stardrop
number_of_stardrops_to_receive += 1 # Krobus Stardrop
if self.options.fishsanity == Fishsanity.option_none: # Master Angler Stardrop
other_rules.append(self.fishing.can_catch_every_fish())
else:
number_of_stardrops_to_receive += 1
if self.options.festival_locations == FestivalLocations.option_disabled: # Fair Stardrop
other_rules.append(self.season.has(Season.fall))
else:
number_of_stardrops_to_receive += 1
if self.options.friendsanity == Friendsanity.option_none: # Spouse Stardrop
other_rules.append(self.relationship.has_hearts(Generic.bachelor, 13))
else:
number_of_stardrops_to_receive += 1
if ModNames.deepwoods in self.options.mods: # Petting the Unicorn
number_of_stardrops_to_receive += 1
if not other_rules:
return self.received("Stardrop", number_of_stardrops_to_receive)
return self.received("Stardrop", number_of_stardrops_to_receive) & And(*other_rules)
def has_prismatic_jelly_reward_access(self) -> StardewRule:
if self.options.special_order_locations == SpecialOrderLocations.option_disabled:
return self.special_order.can_complete_special_order("Prismatic Jelly")
return self.received("Monster Musk Recipe")
def has_all_rarecrows(self) -> StardewRule:
rules = []
for rarecrow_number in range(1, 9):
rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
return And(*rules)
def has_abandoned_jojamart(self) -> StardewRule:
return self.received(CommunityUpgrade.movie_theater, 1)
def has_movie_theater(self) -> StardewRule:
return self.received(CommunityUpgrade.movie_theater, 2)
def can_use_obelisk(self, obelisk: str) -> StardewRule:
return self.region.can_reach(Region.farm) & self.received(obelisk)
def has_fruit_bats(self) -> StardewRule:
return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.fruit_bats)
def has_mushroom_cave(self) -> StardewRule:
return self.region.can_reach(Region.farm_cave) & self.received(CommunityUpgrade.mushroom_boxes)
def can_fish_pond(self, fish: str) -> StardewRule:
return self.building.has_building(Building.fish_pond) & self.has(fish)

View File

@@ -0,0 +1,58 @@
# Logic mixin
Mixins are used to split the logic building methods in multiple classes, so it's more scoped and easier to extend specific methods.
One single instance of Logic is necessary so mods can change the logics. This means that, when calling itself, a `Logic` class has to call its instance in
the `logic`, because it might have been overriden.
```python
class TimeLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.time = TimeLogic(*args, **kwargs)
class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]):
def has_lived_months(self, number: int) -> StardewRule:
return self.logic.received(Event.month_end, number)
def has_year_two(self) -> StardewRule:
return self.logic.time.has_lived_months(4)
def has_year_three(self) -> StardewRule:
return self.logic.time.has_lived_months(8)
```
Creating the rules for actual items has to be outside the `logic` instance. Once the vanilla logic builder is created, mods will be able to replace the logic
implementations by their own modified version. For instance, the `combat` logic can be replaced by the magic mod to extends its methods to add spells in the
combat logic.
## Logic class created on the fly (idea)
The logic class could be created dynamically, based on the `LogicMixin` provided by the content packs. This would allow replacing completely mixins, instead of
overriding their logic afterward. Might be too complicated for no real gain tho...
# Content pack (idea)
Instead of using modules to hold the data, and have each mod adding their data to existing content, each mod data should be in a `ContentPack`. Vanilla, Ginger
Island, or anything that could be disabled would be in a content pack as well.
Eventually, Vanilla content could even be disabled (a split would be required for items that are necessary to all content packs) to have a Ginger Island only
play through created without the heavy vanilla logic computation.
## Unpacking
Steps to unpack content follows the same steps has the world initialisation. Content pack however need to be unpacked in a specific order, based on their
dependencies. Vanilla would always be first, then anything that depends only on Vanilla, etc.
1. In `generate_early`, content packs are selected. The logic builders are created and content packs are unpacked so all their content is in the proper
item/npc/weapon lists.
- `ContentPack` instances are shared across players. However, some mods need to modify content of other packs. In that case, an instance of the content is
created specifically for that player (For instance, SVE changes the Wizard). This probably does not happen enough to require sharing those instances. If
necessary, a FlyWeight design pattern could be used.
2. In `create_regions`, AP regions and entrances are unpacked, and randomized if needed.
3. In `create_items`, AP items are unpacked, and randomized.
4. In `set_rules`, the rules are applied to the AP entrances and locations. Each content pack have to apply the proper rules for their entrances and locations.
- (idea) To begin this step, sphere 0 could be simplified instantly as sphere 0 regions and items are already known.
5. Nothing to do in `generate_basic`.

View File

@@ -0,0 +1,86 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .combat_logic import CombatLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin
from .. import options
from ..options import ToolProgression
from ..stardew_rule import StardewRule, And, True_
from ..strings.performance_names import Performance
from ..strings.region_names import Region
from ..strings.skill_names import Skill
from ..strings.tool_names import Tool, ToolMaterial
class MineLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mine = MineLogic(*args, **kwargs)
class MineLogic(BaseLogic[Union[MineLogicMixin, RegionLogicMixin, ReceivedLogicMixin, CombatLogicMixin, ToolLogicMixin, SkillLogicMixin]]):
# Regions
def can_mine_in_the_mines_floor_1_40(self) -> StardewRule:
return self.logic.region.can_reach(Region.mines_floor_5)
def can_mine_in_the_mines_floor_41_80(self) -> StardewRule:
return self.logic.region.can_reach(Region.mines_floor_45)
def can_mine_in_the_mines_floor_81_120(self) -> StardewRule:
return self.logic.region.can_reach(Region.mines_floor_85)
def can_mine_in_the_skull_cavern(self) -> StardewRule:
return (self.logic.mine.can_progress_in_the_mines_from_floor(120) &
self.logic.region.can_reach(Region.skull_cavern))
@cache_self1
def get_weapon_rule_for_floor_tier(self, tier: int):
if tier >= 4:
return self.logic.combat.can_fight_at_level(Performance.galaxy)
if tier >= 3:
return self.logic.combat.can_fight_at_level(Performance.great)
if tier >= 2:
return self.logic.combat.can_fight_at_level(Performance.good)
if tier >= 1:
return self.logic.combat.can_fight_at_level(Performance.decent)
return self.logic.combat.can_fight_at_level(Performance.basic)
@cache_self1
def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule:
tier = floor // 40
rules = []
weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule)
if self.options.tool_progression & ToolProgression.option_progressive:
rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
if self.options.skill_progression == options.SkillProgression.option_progressive:
skill_tier = min(10, max(0, tier * 2))
rules.append(self.logic.skill.has_level(Skill.combat, skill_tier))
rules.append(self.logic.skill.has_level(Skill.mining, skill_tier))
return And(*rules)
@cache_self1
def has_mine_elevator_to_floor(self, floor: int) -> StardewRule:
if floor < 0:
floor = 0
if self.options.elevator_progression != options.ElevatorProgression.option_vanilla:
return self.logic.received("Progressive Mine Elevator", floor // 5)
return True_()
@cache_self1
def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
tier = floor // 50
rules = []
weapon_rule = self.logic.combat.has_great_weapon
rules.append(weapon_rule)
if self.options.tool_progression & ToolProgression.option_progressive:
rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
if self.options.skill_progression == options.SkillProgression.option_progressive:
skill_tier = min(10, max(0, tier * 2 + 6))
rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier),
self.logic.skill.has_level(Skill.mining, skill_tier)})
return And(*rules)

View File

@@ -0,0 +1,99 @@
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .buff_logic import BuffLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .time_logic import TimeLogicMixin
from ..options import SpecialOrderLocations
from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_
from ..strings.ap_names.event_names import Event
from ..strings.currency_names import Currency
from ..strings.region_names import Region
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
"20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
class MoneyLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.money = MoneyLogic(*args, **kwargs)
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, BuffLogicMixin]]):
@cache_self1
def can_have_earned_total(self, amount: int) -> StardewRule:
if amount < 1000:
return True_()
pierre_rule = self.logic.region.can_reach_all((Region.pierre_store, Region.forest))
willy_rule = self.logic.region.can_reach_all((Region.fish_shop, Region.fishing))
clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5))
robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods))
shipping_rule = self.logic.received(Event.can_ship_items)
if amount < 2000:
selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule
return selling_any_rule
if amount < 5000:
selling_all_rule = (pierre_rule & willy_rule & clint_rule & robin_rule) | shipping_rule
return selling_all_rule
if amount < 10000:
return shipping_rule
seed_rules = self.logic.received(Event.can_shop_at_pierre)
if amount < 40000:
return shipping_rule & seed_rules
percent_progression_items_needed = min(90, amount // 20000)
return shipping_rule & seed_rules & HasProgressionPercent(self.player, percent_progression_items_needed)
@cache_self1
def can_spend(self, amount: int) -> StardewRule:
if self.options.starting_money == -1:
return True_()
return self.logic.money.can_have_earned_total(amount * 5)
# Should be cached
def can_spend_at(self, region: str, amount: int) -> StardewRule:
return self.logic.region.can_reach(region) & self.logic.money.can_spend(amount)
# Should be cached
def can_trade(self, currency: str, amount: int) -> StardewRule:
if amount == 0:
return True_()
if currency == Currency.money:
return self.can_spend(amount)
if currency == Currency.star_token:
return self.logic.region.can_reach(Region.fair)
if currency == Currency.qi_coin:
return self.logic.region.can_reach(Region.casino) & self.logic.buff.has_max_luck()
if currency == Currency.qi_gem:
if self.options.special_order_locations == SpecialOrderLocations.option_board_qi:
number_rewards = min(len(qi_gem_rewards), max(1, (amount // 10)))
return self.logic.received_n(*qi_gem_rewards, count=number_rewards)
number_rewards = 2
return self.logic.received_n(*qi_gem_rewards, count=number_rewards) & self.logic.region.can_reach(Region.qi_walnut_room) & \
self.logic.region.can_reach(Region.saloon) & self.can_have_earned_total(5000)
if currency == Currency.golden_walnut:
return self.can_spend_walnut(amount)
return self.logic.has(currency) & self.logic.time.has_lived_months(amount)
# Should be cached
def can_trade_at(self, region: str, currency: str, amount: int) -> StardewRule:
if amount == 0:
return True_()
if currency == Currency.money:
return self.logic.money.can_spend_at(region, amount)
return self.logic.region.can_reach(region) & self.can_trade(currency, amount)
def can_spend_walnut(self, amount: int) -> StardewRule:
return False_()

View File

@@ -0,0 +1,69 @@
from functools import cached_property
from typing import Iterable, Union, Hashable
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .combat_logic import CombatLogicMixin
from .region_logic import RegionLogicMixin
from .time_logic import TimeLogicMixin, MAX_MONTHS
from .. import options
from ..data import monster_data
from ..stardew_rule import StardewRule, Or, And
from ..strings.region_names import Region
class MonsterLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.monster = MonsterLogic(*args, **kwargs)
class MonsterLogic(BaseLogic[Union[MonsterLogicMixin, RegionLogicMixin, CombatLogicMixin, TimeLogicMixin]]):
@cached_property
def all_monsters_by_name(self):
return monster_data.all_monsters_by_name_given_mods(self.options.mods.value)
@cached_property
def all_monsters_by_category(self):
return monster_data.all_monsters_by_category_given_mods(self.options.mods.value)
def can_kill(self, monster: Union[str, monster_data.StardewMonster], amount_tier: int = 0) -> StardewRule:
if isinstance(monster, str):
monster = self.all_monsters_by_name[monster]
region_rule = self.logic.region.can_reach_any(monster.locations)
combat_rule = self.logic.combat.can_fight_at_level(monster.difficulty)
if amount_tier <= 0:
amount_tier = 0
time_rule = self.logic.time.has_lived_months(amount_tier)
return region_rule & combat_rule & time_rule
@cache_self1
def can_kill_many(self, monster: monster_data.StardewMonster) -> StardewRule:
return self.logic.monster.can_kill(monster, MAX_MONTHS / 3)
@cache_self1
def can_kill_max(self, monster: monster_data.StardewMonster) -> StardewRule:
return self.logic.monster.can_kill(monster, MAX_MONTHS)
# Should be cached
def can_kill_any(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule:
rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters]
return Or(*rules)
# Should be cached
def can_kill_all(self, monsters: (Iterable[monster_data.StardewMonster], Hashable), amount_tier: int = 0) -> StardewRule:
rules = [self.logic.monster.can_kill(monster, amount_tier) for monster in monsters]
return And(*rules)
def can_complete_all_monster_slaying_goals(self) -> StardewRule:
rules = [self.logic.time.has_lived_max_months]
exclude_island = self.options.exclude_ginger_island == options.ExcludeGingerIsland.option_true
island_regions = [Region.volcano_floor_5, Region.volcano_floor_10, Region.island_west, Region.dangerous_skull_cavern]
for category in self.all_monsters_by_category:
if exclude_island and all(all(location in island_regions for location in monster.locations)
for monster in self.all_monsters_by_category[category]):
continue
rules.append(self.logic.monster.can_kill_any(self.all_monsters_by_category[category]))
return And(*rules)

View File

@@ -0,0 +1,80 @@
from typing import Union
from Utils import cache_self1
from .action_logic import ActionLogicMixin
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .. import options
from ..data.museum_data import MuseumItem, all_museum_items, all_museum_artifacts, all_museum_minerals
from ..stardew_rule import StardewRule, And, False_
from ..strings.region_names import Region
class MuseumLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.museum = MuseumLogic(*args, **kwargs)
class MuseumLogic(BaseLogic[Union[ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, ActionLogicMixin, MuseumLogicMixin]]):
def can_donate_museum_items(self, number: int) -> StardewRule:
return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_items(number)
def can_donate_museum_artifacts(self, number: int) -> StardewRule:
return self.logic.region.can_reach(Region.museum) & self.logic.museum.can_find_museum_artifacts(number)
@cache_self1
def can_find_museum_item(self, item: MuseumItem) -> StardewRule:
if item.locations:
region_rule = self.logic.region.can_reach_all_except_one(item.locations)
else:
region_rule = False_()
if item.geodes:
geodes_rule = And(*(self.logic.action.can_open_geode(geode) for geode in item.geodes))
else:
geodes_rule = False_()
# monster_rule = self.can_farm_monster(item.monsters)
# extra_rule = True_()
pan_rule = False_()
if item.item_name == "Earth Crystal" or item.item_name == "Fire Quartz" or item.item_name == "Frozen Tear":
pan_rule = self.logic.action.can_pan()
return pan_rule | region_rule | geodes_rule # & monster_rule & extra_rule
def can_find_museum_artifacts(self, number: int) -> StardewRule:
rules = []
for artifact in all_museum_artifacts:
rules.append(self.logic.museum.can_find_museum_item(artifact))
return self.logic.count(number, *rules)
def can_find_museum_minerals(self, number: int) -> StardewRule:
rules = []
for mineral in all_museum_minerals:
rules.append(self.logic.museum.can_find_museum_item(mineral))
return self.logic.count(number, *rules)
def can_find_museum_items(self, number: int) -> StardewRule:
rules = []
for donation in all_museum_items:
rules.append(self.logic.museum.can_find_museum_item(donation))
return self.logic.count(number, *rules)
def can_complete_museum(self) -> StardewRule:
rules = [self.logic.region.can_reach(Region.museum)]
if self.options.museumsanity == options.Museumsanity.option_none:
rules.append(self.logic.received("Traveling Merchant Metal Detector", 2))
else:
rules.append(self.logic.received("Traveling Merchant Metal Detector", 3))
for donation in all_museum_items:
rules.append(self.logic.museum.can_find_museum_item(donation))
return And(*rules) & self.logic.region.can_reach(Region.museum)
def can_donate(self, item: str) -> StardewRule:
return self.logic.has(item) & self.logic.region.can_reach(Region.museum)

View File

@@ -0,0 +1,50 @@
import math
from typing import Union
from .base_logic import BaseLogicMixin, BaseLogic
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from ..data.villagers_data import Villager
from ..options import Friendsanity
from ..stardew_rule import StardewRule, True_
from ..strings.region_names import Region
from ..strings.villager_names import NPC
class PetLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pet = PetLogic(*args, **kwargs)
class PetLogic(BaseLogic[Union[RegionLogicMixin, ReceivedLogicMixin, TimeLogicMixin, ToolLogicMixin]]):
def has_hearts(self, hearts: int = 1) -> StardewRule:
if hearts <= 0:
return True_()
if self.options.friendsanity == Friendsanity.option_none or self.options.friendsanity == Friendsanity.option_bachelors:
return self.can_befriend_pet(hearts)
return self.received_hearts(NPC.pet, hearts)
def received_hearts(self, npc: Union[str, Villager], hearts: int) -> StardewRule:
if isinstance(npc, Villager):
return self.received_hearts(npc.name, hearts)
return self.logic.received(self.heart(npc), math.ceil(hearts / self.options.friendsanity_heart_size))
def can_befriend_pet(self, hearts: int) -> StardewRule:
if hearts <= 0:
return True_()
points = hearts * 200
points_per_month = 12 * 14
points_per_water_month = 18 * 14
farm_rule = self.logic.region.can_reach(Region.farm)
time_with_water_rule = self.logic.tool.can_water(0) & self.logic.time.has_lived_months(points // points_per_water_month)
time_without_water_rule = self.logic.time.has_lived_months(points // points_per_month)
time_rule = time_with_water_rule | time_without_water_rule
return farm_rule & time_rule
def heart(self, npc: Union[str, Villager]) -> str:
if isinstance(npc, str):
return f"{npc} <3"
return self.heart(npc.name)

View File

@@ -0,0 +1,128 @@
from typing import Dict, Union
from .base_logic import BaseLogicMixin, BaseLogic
from .building_logic import BuildingLogicMixin
from .combat_logic import CombatLogicMixin
from .cooking_logic import CookingLogicMixin
from .fishing_logic import FishingLogicMixin
from .has_logic import HasLogicMixin
from .mine_logic import MineLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .skill_logic import SkillLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from .wallet_logic import WalletLogicMixin
from ..stardew_rule import StardewRule, Has, True_
from ..strings.artisan_good_names import ArtisanGood
from ..strings.building_names import Building
from ..strings.craftable_names import Craftable
from ..strings.crop_names import Fruit, Vegetable
from ..strings.fish_names import Fish
from ..strings.food_names import Meal
from ..strings.forageable_names import Forageable
from ..strings.machine_names import Machine
from ..strings.material_names import Material
from ..strings.metal_names import MetalBar, Ore, Mineral
from ..strings.monster_drop_names import Loot
from ..strings.quest_names import Quest
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.tool_names import Tool
from ..strings.villager_names import NPC
from ..strings.wallet_item_names import Wallet
class QuestLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.quest = QuestLogic(*args, **kwargs)
class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMixin, MineLogicMixin, RegionLogicMixin, RelationshipLogicMixin, ToolLogicMixin,
FishingLogicMixin, CookingLogicMixin, CombatLogicMixin, SeasonLogicMixin, SkillLogicMixin, WalletLogicMixin, QuestLogicMixin, BuildingLogicMixin, TimeLogicMixin]]):
def initialize_rules(self):
self.update_rules({
Quest.introductions: True_(),
Quest.how_to_win_friends: self.logic.quest.can_complete_quest(Quest.introductions),
Quest.getting_started: self.logic.has(Vegetable.parsnip),
Quest.to_the_beach: self.logic.region.can_reach(Region.beach),
Quest.raising_animals: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.building.has_building(Building.coop),
Quest.advancement: self.logic.quest.can_complete_quest(Quest.getting_started) & self.logic.has(Craftable.scarecrow),
Quest.archaeology: self.logic.tool.has_tool(Tool.hoe) | self.logic.mine.can_mine_in_the_mines_floor_1_40() | self.logic.skill.can_fish(),
Quest.rat_problem: self.logic.region.can_reach_all((Region.town, Region.community_center)),
Quest.meet_the_wizard: self.logic.quest.can_complete_quest(Quest.rat_problem),
Quest.forging_ahead: self.logic.has(Ore.copper) & self.logic.has(Machine.furnace),
Quest.smelting: self.logic.has(MetalBar.copper),
Quest.initiation: self.logic.mine.can_mine_in_the_mines_floor_1_40(),
Quest.robins_lost_axe: self.logic.season.has(Season.spring) & self.logic.relationship.can_meet(NPC.robin),
Quest.jodis_request: self.logic.season.has(Season.spring) & self.logic.has(Vegetable.cauliflower) & self.logic.relationship.can_meet(NPC.jodi),
Quest.mayors_shorts: self.logic.season.has(Season.summer) & self.logic.relationship.has_hearts(NPC.marnie, 2) &
self.logic.relationship.can_meet(NPC.lewis),
Quest.blackberry_basket: self.logic.season.has(Season.fall) & self.logic.relationship.can_meet(NPC.linus),
Quest.marnies_request: self.logic.relationship.has_hearts(NPC.marnie, 3) & self.logic.has(Forageable.cave_carrot),
Quest.pam_is_thirsty: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.pale_ale) & self.logic.relationship.can_meet(NPC.pam),
Quest.a_dark_reagent: self.logic.season.has(Season.winter) & self.logic.has(Loot.void_essence) & self.logic.relationship.can_meet(NPC.wizard),
Quest.cows_delight: self.logic.season.has(Season.fall) & self.logic.has(Vegetable.amaranth) & self.logic.relationship.can_meet(NPC.marnie),
Quest.the_skull_key: self.logic.received(Wallet.skull_key),
Quest.crop_research: self.logic.season.has(Season.summer) & self.logic.has(Fruit.melon) & self.logic.relationship.can_meet(NPC.demetrius),
Quest.knee_therapy: self.logic.season.has(Season.summer) & self.logic.has(Fruit.hot_pepper) & self.logic.relationship.can_meet(NPC.george),
Quest.robins_request: self.logic.season.has(Season.winter) & self.logic.has(Material.hardwood) & self.logic.relationship.can_meet(NPC.robin),
Quest.qis_challenge: True_(), # The skull cavern floor 25 already has rules
Quest.the_mysterious_qi: (self.logic.region.can_reach_all((Region.bus_tunnel, Region.railroad, Region.mayor_house)) &
self.logic.has_all(ArtisanGood.battery_pack, Forageable.rainbow_shell, Vegetable.beet, Loot.solar_essence)),
Quest.carving_pumpkins: self.logic.season.has(Season.fall) & self.logic.has(Vegetable.pumpkin) & self.logic.relationship.can_meet(NPC.caroline),
Quest.a_winter_mystery: self.logic.season.has(Season.winter),
Quest.strange_note: self.logic.has(Forageable.secret_note) & self.logic.has(ArtisanGood.maple_syrup),
Quest.cryptic_note: self.logic.has(Forageable.secret_note),
Quest.fresh_fruit: self.logic.season.has(Season.spring) & self.logic.has(Fruit.apricot) & self.logic.relationship.can_meet(NPC.emily),
Quest.aquatic_research: self.logic.season.has(Season.summer) & self.logic.has(Fish.pufferfish) & self.logic.relationship.can_meet(NPC.demetrius),
Quest.a_soldiers_star: (self.logic.season.has(Season.summer) & self.logic.time.has_year_two & self.logic.has(Fruit.starfruit) &
self.logic.relationship.can_meet(NPC.kent)),
Quest.mayors_need: self.logic.season.has(Season.summer) & self.logic.has(ArtisanGood.truffle_oil) & self.logic.relationship.can_meet(NPC.lewis),
Quest.wanted_lobster: (self.logic.season.has(Season.fall) & self.logic.season.has(Season.fall) & self.logic.has(Fish.lobster) &
self.logic.relationship.can_meet(NPC.gus)),
Quest.pam_needs_juice: self.logic.season.has(Season.fall) & self.logic.has(ArtisanGood.battery_pack) & self.logic.relationship.can_meet(NPC.pam),
Quest.fish_casserole: self.logic.relationship.has_hearts(NPC.jodi, 4) & self.logic.has(Fish.largemouth_bass),
Quest.catch_a_squid: self.logic.season.has(Season.winter) & self.logic.has(Fish.squid) & self.logic.relationship.can_meet(NPC.willy),
Quest.fish_stew: self.logic.season.has(Season.winter) & self.logic.has(Fish.albacore) & self.logic.relationship.can_meet(NPC.gus),
Quest.pierres_notice: self.logic.season.has(Season.spring) & self.logic.has(Meal.sashimi) & self.logic.relationship.can_meet(NPC.pierre),
Quest.clints_attempt: self.logic.season.has(Season.winter) & self.logic.has(Mineral.amethyst) & self.logic.relationship.can_meet(NPC.emily),
Quest.a_favor_for_clint: self.logic.season.has(Season.winter) & self.logic.has(MetalBar.iron) & self.logic.relationship.can_meet(NPC.clint),
Quest.staff_of_power: self.logic.season.has(Season.winter) & self.logic.has(MetalBar.iridium) & self.logic.relationship.can_meet(NPC.wizard),
Quest.grannys_gift: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek) & self.logic.relationship.can_meet(NPC.evelyn),
Quest.exotic_spirits: self.logic.season.has(Season.winter) & self.logic.has(Forageable.coconut) & self.logic.relationship.can_meet(NPC.gus),
Quest.catch_a_lingcod: self.logic.season.has(Season.winter) & self.logic.has(Fish.lingcod) & self.logic.relationship.can_meet(NPC.willy),
Quest.dark_talisman: self.logic.region.can_reach(Region.railroad) & self.logic.wallet.has_rusty_key() & self.logic.relationship.can_meet(
NPC.krobus),
Quest.goblin_problem: self.logic.region.can_reach(Region.witch_swamp),
Quest.magic_ink: self.logic.relationship.can_meet(NPC.wizard),
Quest.the_pirates_wife: self.logic.relationship.can_meet(NPC.kent) & self.logic.relationship.can_meet(NPC.gus) &
self.logic.relationship.can_meet(NPC.sandy) & self.logic.relationship.can_meet(NPC.george) &
self.logic.relationship.can_meet(NPC.wizard) & self.logic.relationship.can_meet(NPC.willy),
})
def update_rules(self, new_rules: Dict[str, StardewRule]):
self.registry.quest_rules.update(new_rules)
def can_complete_quest(self, quest: str) -> StardewRule:
return Has(quest, self.registry.quest_rules)
def has_club_card(self) -> StardewRule:
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(Quest.the_mysterious_qi)
return self.logic.received(Wallet.club_card)
def has_magnifying_glass(self) -> StardewRule:
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(Quest.a_winter_mystery)
return self.logic.received(Wallet.magnifying_glass)
def has_dark_talisman(self) -> StardewRule:
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(Quest.dark_talisman)
return self.logic.received(Wallet.dark_talisman)

View File

@@ -0,0 +1,35 @@
from typing import Optional
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..stardew_rule import StardewRule, Received, And, Or, TotalReceived
class ReceivedLogicMixin(BaseLogic[HasLogicMixin], BaseLogicMixin):
# Should be cached
def received(self, item: str, count: Optional[int] = 1) -> StardewRule:
assert count >= 0, "Can't receive a negative amount of item."
return Received(item, self.player, count)
def received_all(self, *items: str):
assert items, "Can't receive all of no items."
return And(*(self.received(item) for item in items))
def received_any(self, *items: str):
assert items, "Can't receive any of no items."
return Or(*(self.received(item) for item in items))
def received_once(self, *items: str, count: int):
assert items, "Can't receive once of no items."
assert count >= 0, "Can't receive a negative amount of item."
return self.logic.count(count, *(self.received(item) for item in items))
def received_n(self, *items: str, count: int):
assert items, "Can't receive n of no items."
assert count >= 0, "Can't receive a negative amount of item."
return TotalReceived(count, items, self.player)

View File

@@ -0,0 +1,65 @@
from typing import Tuple, Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .has_logic import HasLogicMixin
from ..options import EntranceRandomization
from ..stardew_rule import StardewRule, And, Or, Reach, false_, true_
from ..strings.region_names import Region
main_outside_area = {Region.menu, Region.stardew_valley, Region.farm_house, Region.farm, Region.town, Region.beach, Region.mountain, Region.forest,
Region.bus_stop, Region.backwoods, Region.bus_tunnel, Region.tunnel_entrance}
always_accessible_regions_without_er = {*main_outside_area, Region.community_center, Region.pantry, Region.crafts_room, Region.fish_tank, Region.boiler_room,
Region.vault, Region.bulletin_board, Region.mines, Region.hospital, Region.carpenter, Region.alex_house,
Region.elliott_house, Region.ranch, Region.farm_cave, Region.wizard_tower, Region.tent, Region.pierre_store,
Region.saloon, Region.blacksmith, Region.trailer, Region.museum, Region.mayor_house, Region.haley_house,
Region.sam_house, Region.jojamart, Region.fish_shop}
always_regions_by_setting = {EntranceRandomization.option_disabled: always_accessible_regions_without_er,
EntranceRandomization.option_pelican_town: always_accessible_regions_without_er,
EntranceRandomization.option_non_progression: always_accessible_regions_without_er,
EntranceRandomization.option_buildings: main_outside_area,
EntranceRandomization.option_chaos: always_accessible_regions_without_er}
class RegionLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.region = RegionLogic(*args, **kwargs)
class RegionLogic(BaseLogic[Union[RegionLogicMixin, HasLogicMixin]]):
@cache_self1
def can_reach(self, region_name: str) -> StardewRule:
if region_name in always_regions_by_setting[self.options.entrance_randomization]:
return true_
if region_name not in self.regions:
return false_
return Reach(region_name, "Region", self.player)
@cache_self1
def can_reach_any(self, region_names: Tuple[str, ...]) -> StardewRule:
return Or(*(self.logic.region.can_reach(spot) for spot in region_names))
@cache_self1
def can_reach_all(self, region_names: Tuple[str, ...]) -> StardewRule:
return And(*(self.logic.region.can_reach(spot) for spot in region_names))
@cache_self1
def can_reach_all_except_one(self, region_names: Tuple[str, ...]) -> StardewRule:
region_names = list(region_names)
num_required = len(region_names) - 1
if num_required <= 0:
num_required = len(region_names)
return self.logic.count(num_required, *(self.logic.region.can_reach(spot) for spot in region_names))
@cache_self1
def can_reach_location(self, location_name: str) -> StardewRule:
return Reach(location_name, "Location", self.player)
# @cache_self1
# def can_reach_entrance(self, entrance_name: str) -> StardewRule:
# return Reach(entrance_name, "Entrance", self.player)

View File

@@ -0,0 +1,185 @@
import math
from functools import cached_property
from typing import Union, List
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .building_logic import BuildingLogicMixin
from .gift_logic import GiftLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .time_logic import TimeLogicMixin
from ..data.villagers_data import all_villagers_by_name, Villager, get_villagers_for_mods
from ..options import Friendsanity
from ..stardew_rule import StardewRule, True_, And, Or
from ..strings.ap_names.mods.mod_items import SVEQuestItem
from ..strings.crop_names import Fruit
from ..strings.generic_names import Generic
from ..strings.gift_names import Gift
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.villager_names import NPC, ModNPC
possible_kids = ("Cute Baby", "Ugly Baby")
def heart_item_name(npc: Union[str, Villager]) -> str:
if isinstance(npc, Villager):
npc = npc.name
return f"{npc} <3"
class RelationshipLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.relationship = RelationshipLogic(*args, **kwargs)
class RelationshipLogic(BaseLogic[Union[
RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin]]):
@cached_property
def all_villagers_given_mods(self) -> List[Villager]:
return get_villagers_for_mods(self.options.mods.value)
def can_date(self, npc: str) -> StardewRule:
return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet)
def can_marry(self, npc: str) -> StardewRule:
return self.logic.relationship.has_hearts(npc, 10) & self.logic.has(Gift.mermaid_pendant)
def can_get_married(self) -> StardewRule:
return self.logic.relationship.has_hearts(Generic.bachelor, 10) & self.logic.has(Gift.mermaid_pendant)
def has_children(self, number_children: int) -> StardewRule:
if number_children <= 0:
return True_()
if self.options.friendsanity == Friendsanity.option_none:
return self.logic.relationship.can_reproduce(number_children)
return self.logic.received_n(*possible_kids, count=number_children) & self.logic.building.has_house(2)
def can_reproduce(self, number_children: int = 1) -> StardewRule:
if number_children <= 0:
return True_()
baby_rules = [self.logic.relationship.can_get_married(), self.logic.building.has_house(2), self.logic.relationship.has_hearts(Generic.bachelor, 12),
self.logic.relationship.has_children(number_children - 1)]
return And(*baby_rules)
# Should be cached
def has_hearts(self, npc: str, hearts: int = 1) -> StardewRule:
if hearts <= 0:
return True_()
if self.options.friendsanity == Friendsanity.option_none:
return self.logic.relationship.can_earn_relationship(npc, hearts)
if npc not in all_villagers_by_name:
if npc == Generic.any or npc == Generic.bachelor:
possible_friends = []
for name in all_villagers_by_name:
if not self.npc_is_in_current_slot(name):
continue
if npc == Generic.any or all_villagers_by_name[name].bachelor:
possible_friends.append(self.logic.relationship.has_hearts(name, hearts))
return Or(*possible_friends)
if npc == Generic.all:
mandatory_friends = []
for name in all_villagers_by_name:
if not self.npc_is_in_current_slot(name):
continue
mandatory_friends.append(self.logic.relationship.has_hearts(name, hearts))
return And(*mandatory_friends)
if npc.isnumeric():
possible_friends = []
for name in all_villagers_by_name:
if not self.npc_is_in_current_slot(name):
continue
possible_friends.append(self.logic.relationship.has_hearts(name, hearts))
return self.logic.count(int(npc), *possible_friends)
return self.can_earn_relationship(npc, hearts)
if not self.npc_is_in_current_slot(npc):
return True_()
villager = all_villagers_by_name[npc]
if self.options.friendsanity == Friendsanity.option_bachelors and not villager.bachelor:
return self.logic.relationship.can_earn_relationship(npc, hearts)
if self.options.friendsanity == Friendsanity.option_starting_npcs and not villager.available:
return self.logic.relationship.can_earn_relationship(npc, hearts)
is_capped_at_8 = villager.bachelor and self.options.friendsanity != Friendsanity.option_all_with_marriage
if is_capped_at_8 and hearts > 8:
return self.logic.relationship.received_hearts(villager.name, 8) & self.logic.relationship.can_earn_relationship(npc, hearts)
return self.logic.relationship.received_hearts(villager.name, hearts)
# Should be cached
def received_hearts(self, npc: str, hearts: int) -> StardewRule:
heart_item = heart_item_name(npc)
number_required = math.ceil(hearts / self.options.friendsanity_heart_size)
return self.logic.received(heart_item, number_required)
@cache_self1
def can_meet(self, npc: str) -> StardewRule:
if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc):
return True_()
villager = all_villagers_by_name[npc]
rules = [self.logic.region.can_reach_any(villager.locations)]
if npc == NPC.kent:
rules.append(self.logic.time.has_year_two)
elif npc == NPC.leo:
rules.append(self.logic.received("Island West Turtle"))
elif npc == ModNPC.lance:
rules.append(self.logic.region.can_reach(Region.volcano_floor_10))
elif npc == ModNPC.apples:
rules.append(self.logic.has(Fruit.starfruit))
elif npc == ModNPC.scarlett:
scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer)
scarlett_spring = self.logic.season.has(Season.spring) & self.can_meet(ModNPC.andy)
scarlett_summer = self.logic.season.has(Season.summer) & self.can_meet(ModNPC.susan)
scarlett_fall = self.logic.season.has(Season.fall) & self.can_meet(ModNPC.sophia)
rules.append(scarlett_job & (scarlett_spring | scarlett_summer | scarlett_fall))
elif npc == ModNPC.morgan:
rules.append(self.logic.received(SVEQuestItem.morgan_schooling))
elif npc == ModNPC.goblin:
rules.append(self.logic.region.can_reach_all((Region.witch_hut, Region.wizard_tower)))
return And(*rules)
def can_give_loved_gifts_to_everyone(self) -> StardewRule:
rules = []
for npc in all_villagers_by_name:
if not self.npc_is_in_current_slot(npc):
continue
meet_rule = self.logic.relationship.can_meet(npc)
rules.append(meet_rule)
rules.append(self.logic.gifts.has_any_universal_love)
return And(*rules)
# Should be cached
def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule:
if hearts <= 0:
return True_()
previous_heart = hearts - self.options.friendsanity_heart_size
previous_heart_rule = self.logic.relationship.has_hearts(npc, previous_heart)
if npc not in all_villagers_by_name or not self.npc_is_in_current_slot(npc):
return previous_heart_rule
rules = [previous_heart_rule, self.logic.relationship.can_meet(npc)]
villager = all_villagers_by_name[npc]
if hearts > 2 or hearts > self.options.friendsanity_heart_size:
rules.append(self.logic.season.has(villager.birthday))
if villager.birthday == Generic.any:
rules.append(self.logic.season.has_all() | self.logic.time.has_year_three) # push logic back for any birthday-less villager
if villager.bachelor:
if hearts > 8:
rules.append(self.logic.relationship.can_date(npc))
if hearts > 10:
rules.append(self.logic.relationship.can_marry(npc))
return And(*rules)
@cache_self1
def npc_is_in_current_slot(self, name: str) -> bool:
npc = all_villagers_by_name[name]
return npc in self.all_villagers_given_mods

View File

@@ -0,0 +1,44 @@
from typing import Iterable, Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from .time_logic import TimeLogicMixin
from ..options import SeasonRandomization
from ..stardew_rule import StardewRule, True_, Or, And
from ..strings.generic_names import Generic
from ..strings.season_names import Season
class SeasonLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.season = SeasonLogic(*args, **kwargs)
class SeasonLogic(BaseLogic[Union[SeasonLogicMixin, TimeLogicMixin, ReceivedLogicMixin]]):
@cache_self1
def has(self, season: str) -> StardewRule:
if season == Generic.any:
return True_()
seasons_order = [Season.spring, Season.summer, Season.fall, Season.winter]
if self.options.season_randomization == SeasonRandomization.option_progressive:
return self.logic.received(Season.progressive, seasons_order.index(season))
if self.options.season_randomization == SeasonRandomization.option_disabled:
if season == Season.spring:
return True_()
return self.logic.time.has_lived_months(1)
return self.logic.received(season)
def has_any(self, seasons: Iterable[str]):
if not seasons:
return True_()
return Or(*(self.logic.season.has(season) for season in seasons))
def has_any_not_winter(self):
return self.logic.season.has_any([Season.spring, Season.summer, Season.fall])
def has_all(self):
seasons = [Season.spring, Season.summer, Season.fall, Season.winter]
return And(*(self.logic.season.has(season) for season in seasons))

View File

@@ -0,0 +1,60 @@
from functools import cached_property
from typing import Union, List
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .building_logic import BuildingLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from ..locations import LocationTags, locations_by_tag
from ..options import ExcludeGingerIsland, Shipsanity
from ..options import SpecialOrderLocations
from ..stardew_rule import StardewRule, And
from ..strings.ap_names.event_names import Event
from ..strings.building_names import Building
class ShippingLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.shipping = ShippingLogic(*args, **kwargs)
class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, BuildingLogicMixin, RegionLogicMixin, HasLogicMixin]]):
@cached_property
def can_use_shipping_bin(self) -> StardewRule:
return self.logic.building.has_building(Building.shipping_bin)
@cache_self1
def can_ship(self, item: str) -> StardewRule:
return self.logic.received(Event.can_ship_items) & self.logic.has(item)
def can_ship_everything(self) -> StardewRule:
shipsanity_prefix = "Shipsanity: "
all_items_to_ship = []
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_qi = self.options.special_order_locations != SpecialOrderLocations.option_board_qi
mod_list = self.options.mods.value
for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]:
if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
continue
if exclude_qi and LocationTags.REQUIRES_QI_ORDERS in location.tags:
continue
if location.mod_name and location.mod_name not in mod_list:
continue
all_items_to_ship.append(location.name[len(shipsanity_prefix):])
return self.logic.building.has_building(Building.shipping_bin) & self.logic.has_all(*all_items_to_ship)
def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
if self.options.shipsanity == Shipsanity.option_none:
return self.can_ship_everything()
rules = [self.logic.building.has_building(Building.shipping_bin)]
for shipsanity_location in locations_by_tag[LocationTags.SHIPSANITY]:
if shipsanity_location.name not in all_location_names_in_slot:
continue
rules.append(self.logic.region.can_reach_location(shipsanity_location.name))
return And(*rules)

View File

@@ -0,0 +1,172 @@
from functools import cached_property
from typing import Union, Tuple
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .combat_logic import CombatLogicMixin
from .crop_logic import CropLogicMixin
from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from .. import options
from ..data import all_crops
from ..mods.logic.magic_logic import MagicLogicMixin
from ..mods.logic.mod_skills_levels import get_mod_skill_levels
from ..stardew_rule import StardewRule, True_, Or, False_
from ..strings.craftable_names import Fishing
from ..strings.machine_names import Machine
from ..strings.performance_names import Performance
from ..strings.quality_names import ForageQuality
from ..strings.region_names import Region
from ..strings.skill_names import Skill, all_mod_skills
from ..strings.tool_names import ToolMaterial, Tool
fishing_regions = (Region.beach, Region.town, Region.forest, Region.mountain, Region.island_south, Region.island_west)
class SkillLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.skill = SkillLogic(*args, **kwargs)
class SkillLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, ToolLogicMixin, SkillLogicMixin,
CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]):
# Should be cached
def can_earn_level(self, skill: str, level: int) -> StardewRule:
if level <= 0:
return True_()
tool_level = (level - 1) // 2
tool_material = ToolMaterial.tiers[tool_level]
months = max(1, level - 1)
months_rule = self.logic.time.has_lived_months(months)
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
if skill == Skill.fishing:
xp_rule = self.logic.tool.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)])
elif skill == Skill.farming:
xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
elif skill == Skill.foraging:
xp_rule = self.logic.tool.has_tool(Tool.axe, tool_material) | self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
elif skill == Skill.mining:
xp_rule = self.logic.tool.has_tool(Tool.pickaxe, tool_material) | \
self.logic.magic.can_use_clear_debris_instead_of_tool_level(tool_level)
xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
elif skill == Skill.combat:
combat_tier = Performance.tiers[tool_level]
xp_rule = self.logic.combat.can_fight_at_level(combat_tier)
xp_rule = xp_rule & self.logic.region.can_reach(Region.mines_floor_5)
elif skill in all_mod_skills:
# Ideal solution would be to add a logic registry, but I'm too lazy.
return self.logic.mod.skill.can_earn_mod_skill_level(skill, level)
else:
raise Exception(f"Unknown skill: {skill}")
return previous_level_rule & months_rule & xp_rule
# Should be cached
def has_level(self, skill: str, level: int) -> StardewRule:
if level <= 0:
return True_()
if self.options.skill_progression == options.SkillProgression.option_progressive:
return self.logic.received(f"{skill} Level", level)
return self.logic.skill.can_earn_level(skill, level)
@cache_self1
def has_farming_level(self, level: int) -> StardewRule:
return self.logic.skill.has_level(Skill.farming, level)
# Should be cached
def has_total_level(self, level: int, allow_modded_skills: bool = False) -> StardewRule:
if level <= 0:
return True_()
if self.options.skill_progression == options.SkillProgression.option_progressive:
skills_items = ("Farming Level", "Mining Level", "Foraging Level", "Fishing Level", "Combat Level")
if allow_modded_skills:
skills_items += get_mod_skill_levels(self.options.mods)
return self.logic.received_n(*skills_items, count=level)
months_with_4_skills = max(1, (level // 4) - 1)
months_with_5_skills = max(1, (level // 5) - 1)
rule_with_fishing = self.logic.time.has_lived_months(months_with_5_skills) & self.logic.skill.can_get_fishing_xp
if level > 40:
return rule_with_fishing
return self.logic.time.has_lived_months(months_with_4_skills) | rule_with_fishing
@cached_property
def can_get_farming_xp(self) -> StardewRule:
crop_rules = []
for crop in all_crops:
crop_rules.append(self.logic.crop.can_grow(crop))
return Or(*crop_rules)
@cached_property
def can_get_foraging_xp(self) -> StardewRule:
tool_rule = self.logic.tool.has_tool(Tool.axe)
tree_rule = self.logic.region.can_reach(Region.forest) & self.logic.season.has_any_not_winter()
stump_rule = self.logic.region.can_reach(Region.secret_woods) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.copper)
return tool_rule & (tree_rule | stump_rule)
@cached_property
def can_get_mining_xp(self) -> StardewRule:
tool_rule = self.logic.tool.has_tool(Tool.pickaxe)
stone_rule = self.logic.region.can_reach_any((Region.mines_floor_5, Region.quarry, Region.skull_cavern_25, Region.volcano_floor_5))
return tool_rule & stone_rule
@cached_property
def can_get_combat_xp(self) -> StardewRule:
tool_rule = self.logic.combat.has_any_weapon()
enemy_rule = self.logic.region.can_reach_any((Region.mines_floor_5, Region.skull_cavern_25, Region.volcano_floor_5))
return tool_rule & enemy_rule
@cached_property
def can_get_fishing_xp(self) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_progressive:
return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot
return self.logic.skill.can_fish()
# Should be cached
def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule:
if isinstance(regions, str):
regions = regions,
if regions is None or len(regions) == 0:
regions = fishing_regions
skill_required = min(10, max(0, int((difficulty / 10) - 1)))
if difficulty <= 40:
skill_required = 0
skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
region_rule = self.logic.region.can_reach_any(regions)
number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule
@cache_self1
def can_crab_pot_at(self, region: str) -> StardewRule:
return self.logic.skill.can_crab_pot & self.logic.region.can_reach(region)
@cached_property
def can_crab_pot(self) -> StardewRule:
crab_pot_rule = self.logic.has(Fishing.bait)
if self.options.skill_progression == options.SkillProgression.option_progressive:
crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot)
else:
crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp
water_region_rules = self.logic.region.can_reach_any(fishing_regions)
return crab_pot_rule & water_region_rules
def can_forage_quality(self, quality: str) -> StardewRule:
if quality == ForageQuality.basic:
return True_()
if quality == ForageQuality.silver:
return self.has_level(Skill.foraging, 5)
if quality == ForageQuality.gold:
return self.has_level(Skill.foraging, 9)
return False_()

View File

@@ -0,0 +1,111 @@
from typing import Dict, Union
from .ability_logic import AbilityLogicMixin
from .arcade_logic import ArcadeLogicMixin
from .artisan_logic import ArtisanLogicMixin
from .base_logic import BaseLogicMixin, BaseLogic
from .buff_logic import BuffLogicMixin
from .cooking_logic import CookingLogicMixin
from .has_logic import HasLogicMixin
from .mine_logic import MineLogicMixin
from .money_logic import MoneyLogicMixin
from .monster_logic import MonsterLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .relationship_logic import RelationshipLogicMixin
from .season_logic import SeasonLogicMixin
from .shipping_logic import ShippingLogicMixin
from .skill_logic import SkillLogicMixin
from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin
from ..stardew_rule import StardewRule, Has
from ..strings.animal_product_names import AnimalProduct
from ..strings.ap_names.event_names import Event
from ..strings.ap_names.transport_names import Transportation
from ..strings.artisan_good_names import ArtisanGood
from ..strings.crop_names import Vegetable, Fruit
from ..strings.fertilizer_names import Fertilizer
from ..strings.fish_names import Fish
from ..strings.forageable_names import Forageable
from ..strings.machine_names import Machine
from ..strings.material_names import Material
from ..strings.metal_names import Mineral
from ..strings.monster_drop_names import Loot
from ..strings.monster_names import Monster
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import Tool
from ..strings.villager_names import NPC
class SpecialOrderLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.special_order = SpecialOrderLogic(*args, **kwargs)
class SpecialOrderLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, TimeLogicMixin, MoneyLogicMixin,
ShippingLogicMixin, ArcadeLogicMixin, ArtisanLogicMixin, RelationshipLogicMixin, ToolLogicMixin, SkillLogicMixin,
MineLogicMixin, CookingLogicMixin, BuffLogicMixin,
AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]):
def initialize_rules(self):
self.update_rules({
SpecialOrder.island_ingredients: self.logic.relationship.can_meet(NPC.caroline) & self.logic.special_order.has_island_transport() &
self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_ship(Vegetable.taro_root) &
self.logic.shipping.can_ship(Fruit.pineapple) & self.logic.shipping.can_ship(Forageable.ginger),
SpecialOrder.cave_patrol: self.logic.relationship.can_meet(NPC.clint),
SpecialOrder.aquatic_overpopulation: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(),
SpecialOrder.biome_balance: self.logic.relationship.can_meet(NPC.demetrius) & self.logic.ability.can_fish_perfectly(),
SpecialOrder.rock_rejuivenation: (self.logic.relationship.has_hearts(NPC.emily, 4) &
self.logic.has_all(Mineral.ruby, Mineral.topaz, Mineral.emerald, Mineral.jade, Mineral.amethyst,
ArtisanGood.cloth)),
SpecialOrder.gifts_for_george: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek),
SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton),
SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg),
SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items),
SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot,
SpecialOrder.the_strong_stuff: self.logic.artisan.can_keg(Vegetable.potato),
SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(),
SpecialOrder.robins_project: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() &
self.logic.has(Material.hardwood),
SpecialOrder.robins_resource_rush: self.logic.relationship.can_meet(NPC.robin) & self.logic.ability.can_chop_perfectly() &
self.logic.has(Fertilizer.tree) & self.logic.ability.can_mine_perfectly(),
SpecialOrder.juicy_bugs_wanted: self.logic.has(Loot.bug_meat),
SpecialOrder.tropical_fish: self.logic.relationship.can_meet(NPC.willy) & self.logic.received("Island Resort") &
self.logic.special_order.has_island_transport() &
self.logic.has(Fish.stingray) & self.logic.has(Fish.blue_discus) & self.logic.has(Fish.lionfish),
SpecialOrder.a_curious_substance: self.logic.region.can_reach(Region.wizard_tower),
SpecialOrder.prismatic_jelly: self.logic.region.can_reach(Region.wizard_tower),
SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) &
self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) &
self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items),
SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(),
SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") &
self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(),
SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) &
(self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)),
SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(),
SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) &
self.logic.has(Fish.crimsonfish) & self.logic.has(Fish.mutant_carp) & self.logic.has(Fish.legend),
SpecialOrder.danger_in_the_deep: self.logic.ability.can_mine_perfectly() & self.logic.mine.has_mine_elevator_to_floor(120),
SpecialOrder.skull_cavern_invasion: self.logic.ability.can_mine_perfectly_in_the_skull_cavern() & self.logic.buff.has_max_buffs(),
SpecialOrder.qis_prismatic_grange: self.logic.has(Loot.bug_meat) & # 100 Bug Meat
self.logic.money.can_spend_at(Region.saloon, 24000) & # 100 Spaghetti
self.logic.money.can_spend_at(Region.blacksmith, 15000) & # 100 Copper Ore
self.logic.money.can_spend_at(Region.ranch, 5000) & # 100 Hay
self.logic.money.can_spend_at(Region.saloon, 22000) & # 100 Salads
self.logic.money.can_spend_at(Region.saloon, 7500) & # 100 Joja Cola
self.logic.money.can_spend(80000), # I need this extra rule because money rules aren't additive...
})
def update_rules(self, new_rules: Dict[str, StardewRule]):
self.registry.special_order_rules.update(new_rules)
def can_complete_special_order(self, special_order: str) -> StardewRule:
return Has(special_order, self.registry.special_order_rules)
def has_island_transport(self) -> StardewRule:
return self.logic.received(Transportation.island_obelisk) | self.logic.received(Transportation.boat_repair)

View File

@@ -0,0 +1,38 @@
from functools import cached_property
from typing import Union
from Utils import cache_self1
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from ..stardew_rule import StardewRule, HasProgressionPercent, True_
MAX_MONTHS = 12
MONTH_COEFFICIENT = 24 // MAX_MONTHS
class TimeLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.time = TimeLogic(*args, **kwargs)
class TimeLogic(BaseLogic[Union[TimeLogicMixin, ReceivedLogicMixin]]):
@cache_self1
def has_lived_months(self, number: int) -> StardewRule:
if number <= 0:
return True_()
number = min(number, MAX_MONTHS)
return HasProgressionPercent(self.player, number * MONTH_COEFFICIENT)
@cached_property
def has_lived_max_months(self) -> StardewRule:
return self.logic.time.has_lived_months(MAX_MONTHS)
@cached_property
def has_year_two(self) -> StardewRule:
return self.logic.time.has_lived_months(4)
@cached_property
def has_year_three(self) -> StardewRule:
return self.logic.time.has_lived_months(8)

View File

@@ -0,0 +1,81 @@
from typing import Union, Iterable
from Utils import cache_self1
from .base_logic import BaseLogicMixin, BaseLogic
from .has_logic import HasLogicMixin
from .money_logic import MoneyLogicMixin
from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin
from ..options import ToolProgression
from ..stardew_rule import StardewRule, True_, False_
from ..strings.ap_names.skill_level_names import ModSkillLevel
from ..strings.region_names import Region
from ..strings.skill_names import ModSkill
from ..strings.spells import MagicSpell
from ..strings.tool_names import ToolMaterial, Tool
tool_materials = {
ToolMaterial.copper: 1,
ToolMaterial.iron: 2,
ToolMaterial.gold: 3,
ToolMaterial.iridium: 4
}
tool_upgrade_prices = {
ToolMaterial.copper: 2000,
ToolMaterial.iron: 5000,
ToolMaterial.gold: 10000,
ToolMaterial.iridium: 25000
}
class ToolLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tool = ToolLogic(*args, **kwargs)
class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]):
# Should be cached
def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule:
if material == ToolMaterial.basic or tool == Tool.scythe:
return True_()
if self.options.tool_progression & ToolProgression.option_progressive:
return self.logic.received(f"Progressive {tool}", tool_materials[material])
return self.logic.has(f"{material} Bar") & self.logic.money.can_spend(tool_upgrade_prices[material])
def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule:
return self.has_tool(tool, material) & self.logic.region.can_reach(region)
@cache_self1
def has_fishing_rod(self, level: int) -> StardewRule:
if self.options.tool_progression & ToolProgression.option_progressive:
return self.logic.received(f"Progressive {Tool.fishing_rod}", level)
if level <= 1:
return self.logic.region.can_reach(Region.beach)
prices = {2: 500, 3: 1800, 4: 7500}
level = min(level, 4)
return self.logic.money.can_spend_at(Region.fish_shop, prices[level])
# Should be cached
def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule:
season_rule = False_()
if isinstance(season, str):
season_rule = self.logic.season.has(season)
elif isinstance(season, Iterable):
season_rule = self.logic.season.has_any(season)
region_rule = self.logic.region.can_reach(region)
if need_hoe:
return season_rule & region_rule & self.logic.tool.has_tool(Tool.hoe)
return season_rule & region_rule
@cache_self1
def can_water(self, level: int) -> StardewRule:
tool_rule = self.logic.tool.has_tool(Tool.watering_can, ToolMaterial.tiers[level])
spell_rule = self.logic.received(MagicSpell.water) & self.logic.magic.can_use_altar() & self.logic.received(ModSkillLevel.magic_level, level)
return tool_rule | spell_rule

View File

@@ -0,0 +1,26 @@
from typing import Union
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from ..stardew_rule import True_
from ..strings.calendar_names import Weekday
class TravelingMerchantLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.traveling_merchant = TravelingMerchantLogic(*args, **kwargs)
class TravelingMerchantLogic(BaseLogic[Union[TravelingMerchantLogicMixin, ReceivedLogicMixin]]):
def has_days(self, number_days: int = 1):
if number_days <= 0:
return True_()
traveling_merchant_days = tuple(f"Traveling Merchant: {day}" for day in Weekday.all_days)
if number_days == 1:
return self.logic.received_any(*traveling_merchant_days)
tier = min(7, max(1, number_days))
return self.logic.received_n(*traveling_merchant_days, count=tier)

View File

@@ -0,0 +1,19 @@
from .base_logic import BaseLogic, BaseLogicMixin
from .received_logic import ReceivedLogicMixin
from ..stardew_rule import StardewRule
from ..strings.wallet_item_names import Wallet
class WalletLogicMixin(BaseLogicMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.wallet = WalletLogic(*args, **kwargs)
class WalletLogic(BaseLogic[ReceivedLogicMixin]):
def can_speak_dwarf(self) -> StardewRule:
return self.logic.received(Wallet.dwarvish_translation_guide)
def has_rusty_key(self) -> StardewRule:
return self.logic.received(Wallet.rusty_key)