Stardew valley: Fix Aurora Vineyard Tablet logic (#4512)

* - Add requirement on Aurora Vineyard tablet to start the quest

* - Add rule for using the aurora vineyard staircase

* - Added a test for the tablet

* - Add a few missing items to the test

* - Introduce a new item to split the quest from the door and avoir ER issues

* - Optimize imports

* - Forgot to generate the item

* fix Aurora mess

# Conflicts:
#	worlds/stardew_valley/rules.py
#	worlds/stardew_valley/test/mods/TestMods.py

* fix a couple errors in the cherry picked commit, added a method to improve readability and reduce chance of human error on story quest conditions

* - remove blank line

* - Code review comments

* - fixed weird assert name

* - fixed accidentally surviving line

* - Fixed imports

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
This commit is contained in:
agilbert1412
2025-03-10 18:39:35 +03:00
committed by GitHub
parent be550ff6fb
commit d83294efa7
15 changed files with 120 additions and 52 deletions

View File

@@ -928,6 +928,7 @@ id,name,classification,groups,mod_name
10518,Aurora Vineyard Tablet,progression,,Stardew Valley Expanded
10519,Scarlett's Job Offer,progression,,Stardew Valley Expanded
10520,Morgan's Schooling,progression,,Stardew Valley Expanded
10521,Aurora Vineyard Reclamation,progression,,Stardew Valley Expanded
10601,Magic Elixir Recipe,progression,"CHEFSANITY,CHEFSANITY_PURCHASE",Magic
10602,Travel Core Recipe,progression,CRAFTSANITY,Magic
10603,Haste Elixir Recipe,progression,CRAFTSANITY,Stardew Valley Expanded
1 id name classification groups mod_name
928 10518 Aurora Vineyard Tablet progression Stardew Valley Expanded
929 10519 Scarlett's Job Offer progression Stardew Valley Expanded
930 10520 Morgan's Schooling progression Stardew Valley Expanded
931 10521 Aurora Vineyard Reclamation progression Stardew Valley Expanded
932 10601 Magic Elixir Recipe progression CHEFSANITY,CHEFSANITY_PURCHASE Magic
933 10602 Travel Core Recipe progression CRAFTSANITY Magic
934 10603 Haste Elixir Recipe progression CRAFTSANITY Stardew Valley Expanded

View File

@@ -41,7 +41,7 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
if fishing is not None and content.features.skill_progression.is_progressive:
early_forced.append(fishing.level_name)
if options.quest_locations >= 0:
if options.quest_locations.has_story_quests():
early_candidates.append(Wallet.magnifying_glass)
if options.special_order_locations & stardew_options.SpecialOrderLocations.option_board:

View File

@@ -264,7 +264,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
number_progressive_raccoons = 9
if options.quest_locations < 0:
if options.quest_locations.has_no_story_quests():
number_progressive_raccoons = number_progressive_raccoons - 1
items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons)
@@ -387,7 +387,7 @@ def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValle
def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
if options.quest_locations < 0:
if options.quest_locations.has_no_story_quests():
return
# items.append(item_factory("Adventurer's Guild")) # Now unlocked always!
items.append(item_factory(Wallet.club_card))
@@ -698,7 +698,7 @@ def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewV
if not exclude_ginger_island:
items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island])
if options.quest_locations < 0:
if options.quest_locations.has_no_story_quests():
return
items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items])

View File

@@ -191,7 +191,7 @@ def extend_cropsanity_locations(randomized_locations: List[LocationData], conten
def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
if options.quest_locations < 0:
if options.quest_locations.has_no_story_quests():
return
story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST]
@@ -317,7 +317,7 @@ def extend_mandatory_locations(randomized_locations: List[LocationData], options
def extend_situational_quest_locations(randomized_locations: List[LocationData], options: StardewValleyOptions):
if options.quest_locations < 0:
if options.quest_locations.has_no_story_quests():
return
if ModNames.distant_lands in options.mods:
if ModNames.alecto in options.mods:

View File

@@ -76,7 +76,7 @@ SkillLogicMixin, QuestLogicMixin]]):
self.logic.region.can_reach_location("Complete Boiler Room"))
def can_access_raccoon_bundles(self) -> StardewRule:
if self.options.quest_locations < 0:
if self.options.quest_locations.has_no_story_quests():
return self.logic.received(CommunityUpgrade.raccoon, 1) & self.logic.quest.can_complete_quest(Quest.giant_stump)
# 1 - Break the tree

View File

@@ -48,7 +48,7 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
else:
return self.logic.crafting.received_recipe(recipe.item)
if isinstance(recipe.source, QuestSource):
if self.options.quest_locations < 0:
if self.options.quest_locations.has_no_story_quests():
return self.logic.crafting.can_learn_recipe(recipe)
else:
return self.logic.crafting.received_recipe(recipe.item)

View File

@@ -118,25 +118,24 @@ class QuestLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, MoneyLogicMi
return Has(quest, self.registry.quest_rules, "quest")
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)
if self.options.quest_locations.has_story_quests():
return self.logic.received(Wallet.club_card)
return self.logic.quest.can_complete_quest(Quest.the_mysterious_qi)
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)
if self.options.quest_locations.has_story_quests():
return self.logic.received(Wallet.magnifying_glass)
return self.logic.quest.can_complete_quest(Quest.a_winter_mystery)
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)
if self.options.quest_locations.has_story_quests():
return self.logic.received(Wallet.dark_talisman)
return self.logic.quest.can_complete_quest(Quest.dark_talisman)
def has_raccoon_shop(self) -> StardewRule:
if self.options.quest_locations < 0:
return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump)
# 1 - Break the tree
# 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off
# 3 - Raccoon's wife opens the shop
return self.logic.received(CommunityUpgrade.raccoon, 3)
if self.options.quest_locations.has_story_quests():
# 1 - Break the tree
# 2 - Build the house, which summons the bundle racoon. This one is done manually if quests are turned off
# 3 - Raccoon's wife opens the shop
return self.logic.received(CommunityUpgrade.raccoon, 3)
return self.logic.received(CommunityUpgrade.raccoon, 2) & self.logic.quest.can_complete_quest(Quest.giant_stump)

View File

@@ -1,4 +1,5 @@
import math
import typing
from typing import Union
from Utils import cache_self1
@@ -14,13 +15,18 @@ from ..content.feature import friendsanity
from ..data.villagers_data import Villager
from ..stardew_rule import StardewRule, True_, false_, true_
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.quest_names import ModQuest
from ..strings.region_names import Region
from ..strings.season_names import Season
from ..strings.villager_names import NPC, ModNPC
if typing.TYPE_CHECKING:
from ..mods.logic.mod_logic import ModLogicMixin
else:
ModLogicMixin = object
possible_kids = ("Cute Baby", "Ugly Baby")
@@ -38,7 +44,7 @@ class RelationshipLogicMixin(BaseLogicMixin):
class RelationshipLogic(BaseLogic[Union[RelationshipLogicMixin, BuildingLogicMixin, SeasonLogicMixin, TimeLogicMixin, GiftLogicMixin, RegionLogicMixin,
ReceivedLogicMixin, HasLogicMixin]]):
ReceivedLogicMixin, HasLogicMixin, ModLogicMixin]]):
def can_date(self, npc: str) -> StardewRule:
return self.logic.relationship.has_hearts(npc, 8) & self.logic.has(Gift.bouquet)
@@ -141,7 +147,7 @@ ReceivedLogicMixin, HasLogicMixin]]):
rules.append(self.logic.region.can_reach(Region.volcano_floor_10))
elif npc == ModNPC.apples:
rules.append(self.logic.has(Fruit.starfruit))
rules.append(self.logic.mod.quest.has_completed_aurora_vineyard_bundle())
elif npc == ModNPC.scarlett:
scarlett_job = self.logic.received(SVEQuestItem.scarlett_job_offer)

View File

@@ -12,6 +12,7 @@ from ...logic.season_logic import SeasonLogicMixin
from ...logic.time_logic import TimeLogicMixin
from ...stardew_rule import StardewRule
from ...strings.animal_product_names import AnimalProduct
from ...strings.ap_names.mods.mod_items import SVEQuestItem
from ...strings.artisan_good_names import ArtisanGood
from ...strings.crop_names import Fruit, SVEFruit, SVEVegetable, Vegetable
from ...strings.fertilizer_names import Fertilizer
@@ -83,7 +84,8 @@ TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
self.logic.region.can_reach(SVERegion.grandpas_shed),
ModQuest.MarlonsBoat: self.logic.has_all(*(Loot.void_essence, Loot.solar_essence, Loot.slime, Loot.bat_wing, Loot.bug_meat)) &
self.logic.relationship.can_meet(ModNPC.lance) & self.logic.region.can_reach(SVERegion.guild_summit),
ModQuest.AuroraVineyard: self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(SVERegion.aurora_vineyard),
ModQuest.AuroraVineyard: self.logic.region.can_reach(SVERegion.aurora_vineyard) & self.logic.received(SVEQuestItem.aurora_vineyard_tablet) &
self.logic.has(Fruit.starfruit) & self.logic.region.can_reach(Region.forest),
ModQuest.MonsterCrops: self.logic.has_all(*(SVEVegetable.monster_mushroom, SVEFruit.slime_berry, SVEFruit.monster_fruit, SVEVegetable.void_root)),
ModQuest.VoidSoul: self.logic.has(ModLoot.void_soul) & self.logic.region.can_reach(Region.farm) &
self.logic.season.has_any_not_winter() & self.logic.region.can_reach(SVERegion.badlands_entrance) &
@@ -91,6 +93,12 @@ TimeLogicMixin, SeasonLogicMixin, RelationshipLogicMixin, MonsterLogicMixin]]):
self.logic.monster.can_kill_any((Monster.shadow_brute, Monster.shadow_shaman, Monster.shadow_sniper)),
}
def has_completed_aurora_vineyard_bundle(self):
if self.options.quest_locations.has_story_quests():
return self.logic.received(SVEQuestItem.aurora_vineyard_reclamation)
return self.logic.quest.can_complete_quest(ModQuest.AuroraVineyard)
def _get_distant_lands_quest_rules(self):
if ModNames.distant_lands not in self.options.mods:
return {}

View File

@@ -41,24 +41,24 @@ class SVELogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, QuestLogicMixi
return self.logic.or_(*(self.logic.received(rune) for rune in rune_list))
def has_iridium_bomb(self):
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder)
return self.logic.received(SVEQuestItem.iridium_bomb)
if self.options.quest_locations.has_story_quests():
return self.logic.received(SVEQuestItem.iridium_bomb)
return self.logic.quest.can_complete_quest(ModQuest.RailroadBoulder)
def has_marlon_boat(self):
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat)
return self.logic.received(SVEQuestItem.marlon_boat_paddle)
if self.options.quest_locations.has_story_quests():
return self.logic.received(SVEQuestItem.marlon_boat_paddle)
return self.logic.quest.can_complete_quest(ModQuest.MarlonsBoat)
def has_grandpa_shed_repaired(self):
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed)
return self.logic.received(SVEQuestItem.grandpa_shed)
if self.options.quest_locations.has_story_quests():
return self.logic.received(SVEQuestItem.grandpa_shed)
return self.logic.quest.can_complete_quest(ModQuest.GrandpasShed)
def has_bear_knowledge(self):
if self.options.quest_locations < 0:
return self.logic.quest.can_complete_quest(Quest.strange_note)
return self.logic.received(Wallet.bears_knowledge)
if self.options.quest_locations.has_story_quests():
return self.logic.received(Wallet.bears_knowledge)
return self.logic.quest.can_complete_quest(Quest.strange_note)
def can_buy_bear_recipe(self):
access_rule = (self.logic.quest.can_complete_quest(Quest.strange_note) & self.logic.tool.has_tool(Tool.axe, ToolMaterial.basic) &

View File

@@ -384,6 +384,12 @@ class QuestLocations(NamedRange):
"maximum": 56,
}
def has_story_quests(self) -> bool:
return self.value >= 0
def has_no_story_quests(self) -> bool:
return not self.has_story_quests()
class Fishsanity(Choice):
"""Locations for catching each fish the first time?

View File

@@ -149,7 +149,7 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw
bundle_rules = logic.bundle.can_complete_bundle(bundle)
if bundle_room.name == CCRoom.raccoon_requests:
num = int(bundle.name[-1])
extra_raccoons = 1 if world_options.quest_locations >= 0 else 0
extra_raccoons = 1 if world_options.quest_locations.has_story_quests() else 0
extra_raccoons = extra_raccoons + num
bundle_rules = logic.received(CommunityUpgrade.raccoon, extra_raccoons) & bundle_rules
if num > 1:
@@ -505,7 +505,7 @@ def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content:
def set_story_quests_rules(all_location_names: Set[str], logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
if world_options.quest_locations < 0:
if world_options.quest_locations.has_no_story_quests():
return
for quest in locations.locations_by_tag[LocationTags.STORY_QUEST]:
if quest.name in all_location_names and (quest.mod_name is None or quest.mod_name in world_options.mods):
@@ -540,9 +540,9 @@ slay_monsters = "Slay Monsters"
def set_help_wanted_quests_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):
help_wanted_number = world_options.quest_locations.value
if help_wanted_number < 0:
if world_options.quest_locations.has_no_story_quests():
return
help_wanted_number = world_options.quest_locations.value
for i in range(0, help_wanted_number):
set_number = i // 7
month_rule = logic.time.has_lived_months(set_number)
@@ -973,6 +973,7 @@ def set_sve_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, worl
set_entrance_rule(multiworld, player, SVEEntrance.use_bear_shop, (logic.mod.sve.can_buy_bear_recipe()))
set_entrance_rule(multiworld, player, SVEEntrance.railroad_to_grampleton_station, logic.received(SVEQuestItem.scarlett_job_offer))
set_entrance_rule(multiworld, player, SVEEntrance.museum_to_gunther_bedroom, logic.relationship.has_hearts(ModNPC.gunther, 2))
set_entrance_rule(multiworld, player, SVEEntrance.to_aurora_basement, logic.mod.quest.has_completed_aurora_vineyard_bundle())
logic.mod.sve.initialize_rules()
for location in logic.registry.sve_location_rules:
MultiWorldRules.set_rule(multiworld.get_location(location, player),

View File

@@ -19,6 +19,12 @@ class SkillLevel:
class SVEQuestItem:
aurora_vineyard_tablet = "Aurora Vineyard Tablet"
"""Triggers the apparition of the bundle tablet in the Aurora Vineyard, so you can do the Aurora Vineyard quest.
This aim to break dependencies on completing the Community Center.
"""
aurora_vineyard_reclamation = "Aurora Vineyard Reclamation"
"""Triggers the unlock of the Aurora Vineyard basement, so you can meet Apples.
"""
iridium_bomb = "Iridium Bomb"
void_soul = "Void Spirit Peace Agreement"
kittyfish_spell = "Kittyfish Spell"
@@ -29,10 +35,10 @@ class SVEQuestItem:
fable_reef_portal = "Fable Reef Portal"
grandpa_shed = "Grandpa's Shed"
sve_always_quest_items: List[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling]
sve_always_quest_items_ginger_island: List[str] = [fable_reef_portal]
sve_quest_items: List[str] = [aurora_vineyard_tablet, iridium_bomb, void_soul, grandpa_shed]
sve_quest_items_ginger_island: List[str] = [marlon_boat_paddle]
sve_always_quest_items: list[str] = [kittyfish_spell, scarlett_job_offer, morgan_schooling, aurora_vineyard_tablet, ]
sve_always_quest_items_ginger_island: list[str] = [fable_reef_portal, ]
sve_quest_items: list[str] = [iridium_bomb, void_soul, grandpa_shed, aurora_vineyard_reclamation, ]
sve_quest_items_ginger_island: list[str] = [marlon_boat_paddle, ]
class SVELocation:
@@ -53,4 +59,3 @@ class SVERunes:
nexus_wizard = "Nexus: Wizard Runes"
nexus_items: List[str] = [nexus_farm, nexus_wizard, nexus_spring, nexus_aurora, nexus_guild, nexus_junimo, nexus_outpost]

View File

@@ -1,10 +1,9 @@
import random
from BaseClasses import get_seed
from BaseClasses import get_seed, ItemClassification
from .. import SVTestBase, SVTestCase, allsanity_mods_6_x_x, fill_dataclass_with_default
from ..assertion import ModAssertMixin, WorldAssertMixin
from ... import items, Group, ItemClassification, create_content
from ... import options
from ... import options, items, Group, create_content
from ...mods.mod_data import ModNames
from ...options import SkillProgression, Walnutsanity
from ...options.options import all_mods
@@ -188,3 +187,17 @@ class TestModEntranceRando(SVTestCase):
self.assertEqual(len(set(randomized_connections.values())), len(randomized_connections.values()),
f"Connections are duplicated in randomization.")
class TestVanillaLogicAlternativeWhenQuestsAreNotRandomized(WorldAssertMixin, SVTestBase):
"""We often forget to add an alternative rule that works when quests are not randomized. When this happens, some
Location are not reachable because they depend on items that are only added to the pool when quests are randomized.
"""
options = allsanity_mods_6_x_x() | {
options.QuestLocations.internal_name: options.QuestLocations.special_range_names["none"],
options.Goal.internal_name: options.Goal.option_perfection,
}
def test_given_no_quest_all_mods_when_generate_then_can_reach_everything(self):
self.collect_everything()
self.assert_can_reach_everything(self.multiworld)

View File

@@ -0,0 +1,29 @@
from .. import SVTestBase
from ... import options
from ...mods.mod_data import ModNames
from ...strings.ap_names.mods.mod_items import SVEQuestItem
from ...strings.quest_names import ModQuest
from ...strings.region_names import SVERegion
class TestAuroraVineyard(SVTestBase):
options = {
options.Cropsanity.internal_name: options.Cropsanity.option_enabled,
options.Mods.internal_name: frozenset({ModNames.sve})
}
def test_need_tablet_to_do_quest(self):
self.collect("Starfruit Seeds")
self.collect("Bus Repair")
self.collect("Shipping Bin")
self.collect("Summer")
location_name = ModQuest.AuroraVineyard
self.assert_cannot_reach_location(location_name, self.multiworld.state)
self.collect(SVEQuestItem.aurora_vineyard_tablet)
self.assert_can_reach_location(location_name, self.multiworld.state)
def test_need_reclamation_to_go_downstairs(self):
region_name = SVERegion.aurora_vineyard_basement
self.assert_cannot_reach_region(region_name, self.multiworld.state)
self.collect(SVEQuestItem.aurora_vineyard_reclamation, 1)
self.assert_can_reach_region(region_name, self.multiworld.state)