From 17f03bb5f8a286e96dac496063840ab3cbdb24a2 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 11 Nov 2024 23:27:43 -0500 Subject: [PATCH] Stardew valley: Fixed furnace logic bug (#4163) --- worlds/stardew_valley/data/craftable_data.py | 11 ++++-- worlds/stardew_valley/data/recipe_source.py | 5 +++ worlds/stardew_valley/logic/crafting_logic.py | 7 ++-- .../test/assertion/rule_assert.py | 9 +++++ .../stardew_valley/test/rules/TestBundles.py | 1 + .../test/rules/TestCraftingRecipes.py | 34 +++++++++++++++++++ 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/worlds/stardew_valley/data/craftable_data.py b/worlds/stardew_valley/data/craftable_data.py index d83478a6..713db473 100644 --- a/worlds/stardew_valley/data/craftable_data.py +++ b/worlds/stardew_valley/data/craftable_data.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional from .recipe_source import RecipeSource, StarterSource, QueenOfSauceSource, ShopSource, SkillSource, FriendshipSource, ShopTradeSource, CutsceneSource, \ - ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource + ArchipelagoSource, LogicSource, SpecialOrderSource, FestivalShopSource, QuestSource, MasterySource, SkillCraftsanitySource from ..mods.mod_data import ModNames from ..strings.animal_product_names import AnimalProduct from ..strings.artisan_good_names import ArtisanGood @@ -64,6 +64,11 @@ def skill_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], return create_recipe(name, ingredients, source, mod_name) +def skill_craftsanity_recipe(name: str, skill: str, level: int, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: + source = SkillCraftsanitySource(skill, level) + return create_recipe(name, ingredients, source, mod_name) + + def mastery_recipe(name: str, skill: str, ingredients: Dict[str, int], mod_name: Optional[str] = None) -> CraftingRecipe: source = MasterySource(skill) return create_recipe(name, ingredients, source, mod_name) @@ -249,7 +254,9 @@ bait_maker = skill_recipe(Machine.bait_maker, Skill.fishing, 6, {MetalBar.iron: charcoal_kiln = skill_recipe(Machine.charcoal_kiln, Skill.foraging, 2, {Material.wood: 20, MetalBar.copper: 2}) crystalarium = skill_recipe(Machine.crystalarium, Skill.mining, 9, {Material.stone: 99, MetalBar.gold: 5, MetalBar.iridium: 2, ArtisanGood.battery_pack: 1}) -furnace = skill_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) +# In-Game, the Furnace recipe is completely unique. It is the only recipe that is obtained in a cutscene after doing a skill-related action. +# So it has a custom source that needs both the craftsanity item from AP and the skill, if craftsanity is enabled. +furnace = skill_craftsanity_recipe(Machine.furnace, Skill.mining, 1, {Ore.copper: 20, Material.stone: 25}) geode_crusher = special_order_recipe(Machine.geode_crusher, SpecialOrder.cave_patrol, {MetalBar.gold: 2, Material.stone: 50, Mineral.diamond: 1}) mushroom_log = skill_recipe(Machine.mushroom_log, Skill.foraging, 4, {Material.hardwood: 10, Material.moss: 10}) heavy_tapper = ap_recipe(Machine.heavy_tapper, {Material.hardwood: 30, MetalBar.radioactive: 1}) diff --git a/worlds/stardew_valley/data/recipe_source.py b/worlds/stardew_valley/data/recipe_source.py index 24b03bf7..ead4d62f 100644 --- a/worlds/stardew_valley/data/recipe_source.py +++ b/worlds/stardew_valley/data/recipe_source.py @@ -94,6 +94,11 @@ class SkillSource(RecipeSource): return f"SkillSource at level {self.level} {self.skill}" +class SkillCraftsanitySource(SkillSource): + def __repr__(self): + return f"SkillCraftsanitySource at level {self.level} {self.skill}" + + class MasterySource(RecipeSource): skill: str diff --git a/worlds/stardew_valley/logic/crafting_logic.py b/worlds/stardew_valley/logic/crafting_logic.py index e346e4ba..0403230e 100644 --- a/worlds/stardew_valley/logic/crafting_logic.py +++ b/worlds/stardew_valley/logic/crafting_logic.py @@ -14,7 +14,7 @@ from .special_order_logic import SpecialOrderLogicMixin from .. import options from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ - FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource + FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource from ..locations import locations_by_tag, LocationTags from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression from ..stardew_rule import StardewRule, True_, False_ @@ -54,8 +54,7 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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): + if isinstance(recipe.source, (StarterSource, ShopTradeSource, ShopSource, SkillCraftsanitySource)): return self.logic.crafting.received_recipe(recipe.item) if isinstance(recipe.source, SpecialOrderSource) and self.options.special_order_locations & SpecialOrderLocations.option_board: return self.logic.crafting.received_recipe(recipe.item) @@ -71,6 +70,8 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]): 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, SkillCraftsanitySource): + return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) & self.logic.skill.can_earn_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, SkillSource): return self.logic.skill.has_level(recipe.source.skill, recipe.source.level) if isinstance(recipe.source, MasterySource): diff --git a/worlds/stardew_valley/test/assertion/rule_assert.py b/worlds/stardew_valley/test/assertion/rule_assert.py index 5a1dad29..1031a18e 100644 --- a/worlds/stardew_valley/test/assertion/rule_assert.py +++ b/worlds/stardew_valley/test/assertion/rule_assert.py @@ -1,3 +1,4 @@ +from typing import List from unittest import TestCase from BaseClasses import CollectionState, Location @@ -14,6 +15,10 @@ class RuleAssertMixin(TestCase): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_true(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_true(rule, state) + def assert_rule_false(self, rule: StardewRule, state: CollectionState): expl = explain(rule, state, expected=False) try: @@ -22,6 +27,10 @@ class RuleAssertMixin(TestCase): raise AssertionError(f"Error while checking rule {rule}: {e}" f"\nExplanation: {expl}") + def assert_rules_false(self, rules: List[StardewRule], state: CollectionState): + for rule in rules: + self.assert_rule_false(rule, state) + def assert_rule_can_be_resolved(self, rule: StardewRule, complete_state: CollectionState): expl = explain(rule, complete_state) try: diff --git a/worlds/stardew_valley/test/rules/TestBundles.py b/worlds/stardew_valley/test/rules/TestBundles.py index ab376c90..0bc7f9bf 100644 --- a/worlds/stardew_valley/test/rules/TestBundles.py +++ b/worlds/stardew_valley/test/rules/TestBundles.py @@ -56,6 +56,7 @@ class TestRaccoonBundlesLogic(SVTestBase): self.collect("Mushroom Boxes") self.collect("Progressive Fishing Rod", 4) self.collect("Fishing Level", 10) + self.collect("Furnace Recipe") self.assertFalse(raccoon_rule_1(self.multiworld.state)) self.assertFalse(raccoon_rule_3(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py index 93c325ae..4719edea 100644 --- a/worlds/stardew_valley/test/rules/TestCraftingRecipes.py +++ b/worlds/stardew_valley/test/rules/TestCraftingRecipes.py @@ -50,6 +50,23 @@ class TestCraftsanityLogic(SVTestBase): self.multiworld.state.collect(self.create_item("Jack-O-Lantern Recipe"), prevent_sweep=False) self.assert_rule_true(rule, self.multiworld.state) + def test_require_furnace_recipe_for_smelting_checks(self): + locations = ["Craft Furnace", "Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.collect([self.create_item("Mining Level")] * 10) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.assert_rules_true(rules, self.multiworld.state) + class TestCraftsanityWithFestivalsLogic(SVTestBase): options = { @@ -101,6 +118,23 @@ class TestNoCraftsanityLogic(SVTestBase): self.collect([self.create_item("Progressive Season")] * 2) self.assert_rule_true(rule, self.multiworld.state) + def test_requires_mining_levels_for_smelting_checks(self): + locations = ["Smelting", "Copper Pickaxe Upgrade", "Gold Trash Can Upgrade"] + rules = [self.world.logic.region.can_reach_location(location) for location in locations] + self.collect([self.create_item("Progressive Pickaxe")] * 4) + self.collect([self.create_item("Progressive Fishing Rod")] * 4) + self.collect([self.create_item("Progressive Sword")] * 4) + self.collect([self.create_item("Progressive Mine Elevator")] * 24) + self.collect([self.create_item("Progressive Trash Can")] * 2) + self.multiworld.state.collect(self.create_item("Furnace Recipe"), prevent_sweep=False) + self.collect([self.create_item("Combat Level")] * 10) + self.collect([self.create_item("Fishing Level")] * 10) + self.collect_all_the_money() + self.assert_rules_false(rules, self.multiworld.state) + + self.collect([self.create_item("Mining Level")] * 10) + self.assert_rules_true(rules, self.multiworld.state) + class TestNoCraftsanityWithFestivalsLogic(SVTestBase): options = {