From 420828de3caecd20d9b4849c6bb9035e4b573080 Mon Sep 17 00:00:00 2001 From: Jouramie Date: Wed, 20 Mar 2024 00:15:49 -0400 Subject: [PATCH 1/5] fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) --- worlds/stardew_valley/logic/skill_logic.py | 11 ++++++-- worlds/stardew_valley/logic/tool_logic.py | 6 ++-- worlds/stardew_valley/test/TestRules.py | 33 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 9134dfae40bf..328ec2a87c8a 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -44,10 +44,14 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: 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 self.options.skill_progression != options.SkillProgression.option_vanilla: + previous_level_rule = self.logic.skill.has_level(skill, level - 1) + else: + previous_level_rule = True_() if skill == Skill.fishing: - xp_rule = self.logic.tool.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)]) + xp_rule = self.logic.tool.has_fishing_rod(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: @@ -137,11 +141,14 @@ def can_get_fishing_xp(self) -> StardewRule: 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) diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index def02b35dab6..41555bb6fdc2 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -12,7 +12,6 @@ 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 @@ -40,13 +39,16 @@ def __init__(self, *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 tool == Tool.fishing_rod: + return self.logic.tool.has_fishing_rod(tool_materials[material]) + 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]) + return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, 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) diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 0d2fc38a19a3..0fbb6bf52a3f 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -8,6 +8,7 @@ FriendsanityHeartSize, BundleRandomization, SkillProgression from ..strings.entrance_names import Entrance from ..strings.region_names import Region +from ..strings.tool_names import Tool, ToolMaterial class TestProgressiveToolsLogic(SVTestBase): @@ -596,6 +597,38 @@ def swap_museum_and_bathhouse(multiworld, player): bathhouse_entrance.connect(museum_region) +class TestToolVanillaRequiresBlacksmith(SVTestBase): + options = { + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.ToolProgression: options.ToolProgression.option_vanilla, + } + + def test_cannot_make_get_any_tool_without_blacksmith_access(self): + railroad_item = "Railroad Boulder Removed" + swap_blacksmith_and_bathhouse(self.multiworld, self.player) + collect_all_except(self.multiworld, railroad_item) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + +def swap_blacksmith_and_bathhouse(multiworld, player): + blacksmith_region = multiworld.get_region(Region.blacksmith, player) + bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) + + blacksmith_entrance = blacksmith_region.entrances[0] + bathhouse_region = bathhouse_entrance.connected_region + blacksmith_entrance.connect(bathhouse_region) + bathhouse_entrance.connect(blacksmith_region) + + def collect_all_except(multiworld, item_to_not_collect: str): for item in multiworld.get_items(): if item.name != item_to_not_collect: From de07306c41397b2e84e9c058c94374bcd27ef05a Mon Sep 17 00:00:00 2001 From: Jouramie Date: Wed, 20 Mar 2024 18:25:38 -0400 Subject: [PATCH 2/5] add test to ensure fishing rod need fish shop --- worlds/stardew_valley/logic/tool_logic.py | 3 +- worlds/stardew_valley/test/TestRules.py | 34 +++++++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 41555bb6fdc2..715e285fba1f 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -39,8 +39,7 @@ def __init__(self, *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 tool == Tool.fishing_rod: - return self.logic.tool.has_fishing_rod(tool_materials[material]) + assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`." if material == ToolMaterial.basic or tool == Tool.scythe: return True_() diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 0fbb6bf52a3f..94b253d1fe06 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -602,10 +602,13 @@ class TestToolVanillaRequiresBlacksmith(SVTestBase): options.EntranceRandomization: options.EntranceRandomization.option_buildings, options.ToolProgression: options.ToolProgression.option_vanilla, } + seed = 4111845104987680262 - def test_cannot_make_get_any_tool_without_blacksmith_access(self): + # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. + + def test_cannot_get_any_tool_without_blacksmith_access(self): railroad_item = "Railroad Boulder Removed" - swap_blacksmith_and_bathhouse(self.multiworld, self.player) + place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) collect_all_except(self.multiworld, railroad_item) for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: @@ -618,15 +621,28 @@ def test_cannot_make_get_any_tool_without_blacksmith_access(self): for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + def test_cannot_get_fishing_rod_without_willy_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) + collect_all_except(self.multiworld, railroad_item) -def swap_blacksmith_and_bathhouse(multiworld, player): - blacksmith_region = multiworld.get_region(Region.blacksmith, player) - bathhouse_entrance = multiworld.get_entrance(Entrance.enter_bathhouse_entrance, player) + for fishing_rod_level in [2, 3]: + self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) + + for fishing_rod_level in [2, 3]: + self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + +def place_region_at_entrance(multiworld, player, region, entrance): + region_to_place = multiworld.get_region(region, player) + entrance_to_place_region = multiworld.get_entrance(entrance, player) - blacksmith_entrance = blacksmith_region.entrances[0] - bathhouse_region = bathhouse_entrance.connected_region - blacksmith_entrance.connect(bathhouse_region) - bathhouse_entrance.connect(blacksmith_region) + entrance_to_switch = region_to_place.entrances[0] + region_to_switch = entrance_to_place_region.connected_region + entrance_to_switch.connect(region_to_switch) + entrance_to_place_region.connect(region_to_place) def collect_all_except(multiworld, item_to_not_collect: str): From a38db3876f801e45ccad5c1b408cdf2b1bc8d782 Mon Sep 17 00:00:00 2001 From: Jouramie Date: Wed, 20 Mar 2024 18:55:02 -0400 Subject: [PATCH 3/5] fishing rod should be indexed from 0 like a mentally sane person would do. --- worlds/stardew_valley/logic/ability_logic.py | 2 +- worlds/stardew_valley/logic/fishing_logic.py | 9 +++++---- worlds/stardew_valley/logic/skill_logic.py | 5 +++-- worlds/stardew_valley/logic/tool_logic.py | 13 ++++++++++--- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index ae12ffee4742..2583807e11a7 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -33,7 +33,7 @@ def can_farm_perfectly(self) -> StardewRule: 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) + return skill_rule & self.logic.tool.has_fishing_rod(3) def can_chop_trees(self) -> StardewRule: return self.logic.tool.has_tool(Tool.axe) & self.logic.region.can_reach(Region.forest) diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index 65b3cdc2ac88..6b5ac69b4d97 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -30,11 +30,11 @@ def can_fish_in_freshwater(self) -> StardewRule: 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 + return self.logic.tool.has_fishing_rod(3) & 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 + return self.logic.tool.has_fishing_rod(3) & skill_rule def can_fish_at(self, region: str) -> StardewRule: return self.logic.skill.can_fish() & self.logic.region.can_reach(region) @@ -66,14 +66,15 @@ def can_start_extended_family_quest(self) -> StardewRule: 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) + rod_rule = self.logic.tool.has_fishing_rod(1) 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_() + + raise ValueError(f"Quality {fish_quality} is unknown.") def can_catch_every_fish(self) -> StardewRule: rules = [self.has_max_fishing()] diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 328ec2a87c8a..1b01c62d6f8f 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -51,7 +51,7 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: previous_level_rule = True_() if skill == Skill.fishing: - xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 3)) + xp_rule = self.logic.tool.has_fishing_rod(min(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: @@ -151,7 +151,8 @@ def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int 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) + # Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic. + number_fishing_rod_required = 0 if difficulty < 50 else (1 if difficulty < 80 else 3) return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule @cache_self1 diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 715e285fba1f..25a45a99042d 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -15,6 +15,11 @@ from ..strings.spells import MagicSpell from ..strings.tool_names import ToolMaterial, Tool +fishing_rod_prices = { + 2: 1800, # Fibreglass + 3: 7500, # Iridium +} + tool_materials = { ToolMaterial.copper: 1, ToolMaterial.iron: 2, @@ -54,14 +59,16 @@ def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: @cache_self1 def has_fishing_rod(self, level: int) -> StardewRule: + assert 0 <= level <= 3, "Fishing rod 0 is Training, 1 is Bamboo, 2 is Fiberglass and 3 is Iridium." + if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {Tool.fishing_rod}", level) if level <= 1: + # We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back. 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]) + + return self.logic.money.can_spend_at(Region.fish_shop, fishing_rod_prices[level]) # Should be cached def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule: From 1fc0ea59a30c685ef4b97968d94848681e0f975f Mon Sep 17 00:00:00 2001 From: Jouramie Date: Wed, 20 Mar 2024 19:02:50 -0400 Subject: [PATCH 4/5] fishing rod 0 isn't real, but it definitely can hurt you. --- worlds/stardew_valley/logic/ability_logic.py | 2 +- worlds/stardew_valley/logic/fishing_logic.py | 6 +++--- worlds/stardew_valley/logic/skill_logic.py | 4 ++-- worlds/stardew_valley/logic/tool_logic.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/worlds/stardew_valley/logic/ability_logic.py b/worlds/stardew_valley/logic/ability_logic.py index 2583807e11a7..ae12ffee4742 100644 --- a/worlds/stardew_valley/logic/ability_logic.py +++ b/worlds/stardew_valley/logic/ability_logic.py @@ -33,7 +33,7 @@ def can_farm_perfectly(self) -> StardewRule: 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(3) + 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) diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index 6b5ac69b4d97..a7399a65d99c 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -30,11 +30,11 @@ def can_fish_in_freshwater(self) -> StardewRule: def has_max_fishing(self) -> StardewRule: skill_rule = self.logic.skill.has_level(Skill.fishing, 10) - return self.logic.tool.has_fishing_rod(3) & skill_rule + 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(3) & skill_rule + 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) @@ -66,7 +66,7 @@ def can_start_extended_family_quest(self) -> StardewRule: 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(1) + rod_rule = self.logic.tool.has_fishing_rod(2) if fish_quality == FishQuality.silver: return rod_rule if fish_quality == FishQuality.gold: diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 1b01c62d6f8f..35946a0a4d36 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -51,7 +51,7 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: previous_level_rule = True_() if skill == Skill.fishing: - xp_rule = self.logic.tool.has_fishing_rod(min(tool_level, 3)) + xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1)) 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: @@ -152,7 +152,7 @@ def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required) region_rule = self.logic.region.can_reach_any(regions) # Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic. - number_fishing_rod_required = 0 if difficulty < 50 else (1 if difficulty < 80 else 3) + 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 diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index 25a45a99042d..1b1dc2a52120 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -16,8 +16,8 @@ from ..strings.tool_names import ToolMaterial, Tool fishing_rod_prices = { - 2: 1800, # Fibreglass - 3: 7500, # Iridium + 3: 1800, + 4: 7500, } tool_materials = { @@ -59,12 +59,12 @@ def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: @cache_self1 def has_fishing_rod(self, level: int) -> StardewRule: - assert 0 <= level <= 3, "Fishing rod 0 is Training, 1 is Bamboo, 2 is Fiberglass and 3 is Iridium." + assert 1 <= level <= 4, "Fishing rod 0 isn't real, it can't hurt you. Training is 1, Bamboo is 2, Fiberglass is 3 and Iridium is 4." if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {Tool.fishing_rod}", level) - if level <= 1: + if level <= 2: # We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back. return self.logic.region.can_reach(Region.beach) From 7f342cf0fda83688d3d5bd6aae61715387a40f1b Mon Sep 17 00:00:00 2001 From: Jouramie Date: Wed, 20 Mar 2024 19:04:05 -0400 Subject: [PATCH 5/5] reeeeeeeee --- worlds/stardew_valley/test/TestRules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 94b253d1fe06..787e0ce39c3e 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -626,12 +626,12 @@ def test_cannot_get_fishing_rod_without_willy_access(self): place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) collect_all_except(self.multiworld, railroad_item) - for fishing_rod_level in [2, 3]: + for fishing_rod_level in [3, 4]: self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) - for fishing_rod_level in [2, 3]: + for fishing_rod_level in [3, 4]: self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)