diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 05af275b..36e04810 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -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 diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index 5ad48912..1457c5c7 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -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: diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 1fbe012e..dcb37a8f 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -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]) diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index df86e081..c7d787e5 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -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: diff --git a/worlds/stardew_valley/logic/bundle_logic.py b/worlds/stardew_valley/logic/bundle_logic.py index 98fda1c7..8ede4de5 100644 --- a/worlds/stardew_valley/logic/bundle_logic.py +++ b/worlds/stardew_valley/logic/bundle_logic.py @@ -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 diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index 28bf0d2a..bd839707 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -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) diff --git a/worlds/stardew_valley/logic/quest_logic.py b/worlds/stardew_valley/logic/quest_logic.py index 42f401b9..8779848f 100644 --- a/worlds/stardew_valley/logic/quest_logic.py +++ b/worlds/stardew_valley/logic/quest_logic.py @@ -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) diff --git a/worlds/stardew_valley/logic/relationship_logic.py b/worlds/stardew_valley/logic/relationship_logic.py index 61e63a90..b74bdc56 100644 --- a/worlds/stardew_valley/logic/relationship_logic.py +++ b/worlds/stardew_valley/logic/relationship_logic.py @@ -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) diff --git a/worlds/stardew_valley/mods/logic/quests_logic.py b/worlds/stardew_valley/mods/logic/quests_logic.py index 2ff74523..ef969826 100644 --- a/worlds/stardew_valley/mods/logic/quests_logic.py +++ b/worlds/stardew_valley/mods/logic/quests_logic.py @@ -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 {} diff --git a/worlds/stardew_valley/mods/logic/sve_logic.py b/worlds/stardew_valley/mods/logic/sve_logic.py index fc093554..faca8d33 100644 --- a/worlds/stardew_valley/mods/logic/sve_logic.py +++ b/worlds/stardew_valley/mods/logic/sve_logic.py @@ -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) & diff --git a/worlds/stardew_valley/options/options.py b/worlds/stardew_valley/options/options.py index 5cfdfcf9..bc76c617 100644 --- a/worlds/stardew_valley/options/options.py +++ b/worlds/stardew_valley/options/options.py @@ -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? diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 01acc7b8..dc630186 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -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), diff --git a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py index 58371aeb..d87a81f5 100644 --- a/worlds/stardew_valley/strings/ap_names/mods/mod_items.py +++ b/worlds/stardew_valley/strings/ap_names/mods/mod_items.py @@ -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] - diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 1dd2ab49..dc958652 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -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) diff --git a/worlds/stardew_valley/test/mods/TestSVE.py b/worlds/stardew_valley/test/mods/TestSVE.py new file mode 100644 index 00000000..ca63dcb3 --- /dev/null +++ b/worlds/stardew_valley/test/mods/TestSVE.py @@ -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)