Skip to content

Commit 14f5f01

Browse files
authored
Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (ArchipelagoMW#3002)
* fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) * add test to ensure fishing rod need fish shop * fishing rod should be indexed from 0 like a mentally sane person would do. * fishing rod 0 isn't real, but it definitely can hurt you. * reeeeeeeee
1 parent cf133dd commit 14f5f01

File tree

4 files changed

+75
-9
lines changed

4 files changed

+75
-9
lines changed

worlds/stardew_valley/logic/fishing_logic.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ def can_catch_quality_fish(self, fish_quality: str) -> StardewRule:
7373
return rod_rule & self.logic.skill.has_level(Skill.fishing, 4)
7474
if fish_quality == FishQuality.iridium:
7575
return rod_rule & self.logic.skill.has_level(Skill.fishing, 10)
76-
return False_()
76+
77+
raise ValueError(f"Quality {fish_quality} is unknown.")
7778

7879
def can_catch_every_fish(self) -> StardewRule:
7980
rules = [self.has_max_fishing()]

worlds/stardew_valley/logic/skill_logic.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule:
4444
tool_material = ToolMaterial.tiers[tool_level]
4545
months = max(1, level - 1)
4646
months_rule = self.logic.time.has_lived_months(months)
47-
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
47+
48+
if self.options.skill_progression != options.SkillProgression.option_vanilla:
49+
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
50+
else:
51+
previous_level_rule = True_()
4852

4953
if skill == Skill.fishing:
50-
xp_rule = self.logic.tool.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)])
54+
xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1))
5155
elif skill == Skill.farming:
5256
xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
5357
elif skill == Skill.foraging:
@@ -137,13 +141,17 @@ def can_get_fishing_xp(self) -> StardewRule:
137141
def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule:
138142
if isinstance(regions, str):
139143
regions = regions,
144+
140145
if regions is None or len(regions) == 0:
141146
regions = fishing_regions
147+
142148
skill_required = min(10, max(0, int((difficulty / 10) - 1)))
143149
if difficulty <= 40:
144150
skill_required = 0
151+
145152
skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
146153
region_rule = self.logic.region.can_reach_any(regions)
154+
# Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic.
147155
number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
148156
return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule
149157

worlds/stardew_valley/logic/tool_logic.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
from ..stardew_rule import StardewRule, True_, False_
1313
from ..strings.ap_names.skill_level_names import ModSkillLevel
1414
from ..strings.region_names import Region
15-
from ..strings.skill_names import ModSkill
1615
from ..strings.spells import MagicSpell
1716
from ..strings.tool_names import ToolMaterial, Tool
1817

18+
fishing_rod_prices = {
19+
3: 1800,
20+
4: 7500,
21+
}
22+
1923
tool_materials = {
2024
ToolMaterial.copper: 1,
2125
ToolMaterial.iron: 2,
@@ -40,27 +44,31 @@ def __init__(self, *args, **kwargs):
4044
class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]):
4145
# Should be cached
4246
def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule:
47+
assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`."
48+
4349
if material == ToolMaterial.basic or tool == Tool.scythe:
4450
return True_()
4551

4652
if self.options.tool_progression & ToolProgression.option_progressive:
4753
return self.logic.received(f"Progressive {tool}", tool_materials[material])
4854

49-
return self.logic.has(f"{material} Bar") & self.logic.money.can_spend(tool_upgrade_prices[material])
55+
return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material])
5056

5157
def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule:
5258
return self.has_tool(tool, material) & self.logic.region.can_reach(region)
5359

5460
@cache_self1
5561
def has_fishing_rod(self, level: int) -> StardewRule:
62+
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."
63+
5664
if self.options.tool_progression & ToolProgression.option_progressive:
5765
return self.logic.received(f"Progressive {Tool.fishing_rod}", level)
5866

59-
if level <= 1:
67+
if level <= 2:
68+
# We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back.
6069
return self.logic.region.can_reach(Region.beach)
61-
prices = {2: 500, 3: 1800, 4: 7500}
62-
level = min(level, 4)
63-
return self.logic.money.can_spend_at(Region.fish_shop, prices[level])
70+
71+
return self.logic.money.can_spend_at(Region.fish_shop, fishing_rod_prices[level])
6472

6573
# Should be cached
6674
def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule:

worlds/stardew_valley/test/TestRules.py

+49
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
FriendsanityHeartSize, BundleRandomization, SkillProgression
99
from ..strings.entrance_names import Entrance
1010
from ..strings.region_names import Region
11+
from ..strings.tool_names import Tool, ToolMaterial
1112

1213

1314
class TestProgressiveToolsLogic(SVTestBase):
@@ -596,6 +597,54 @@ def swap_museum_and_bathhouse(multiworld, player):
596597
bathhouse_entrance.connect(museum_region)
597598

598599

600+
class TestToolVanillaRequiresBlacksmith(SVTestBase):
601+
options = {
602+
options.EntranceRandomization: options.EntranceRandomization.option_buildings,
603+
options.ToolProgression: options.ToolProgression.option_vanilla,
604+
}
605+
seed = 4111845104987680262
606+
607+
# Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed.
608+
609+
def test_cannot_get_any_tool_without_blacksmith_access(self):
610+
railroad_item = "Railroad Boulder Removed"
611+
place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance)
612+
collect_all_except(self.multiworld, railroad_item)
613+
614+
for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
615+
for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
616+
self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
617+
618+
self.multiworld.state.collect(self.world.create_item(railroad_item), event=False)
619+
620+
for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]:
621+
for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]:
622+
self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state)
623+
624+
def test_cannot_get_fishing_rod_without_willy_access(self):
625+
railroad_item = "Railroad Boulder Removed"
626+
place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance)
627+
collect_all_except(self.multiworld, railroad_item)
628+
629+
for fishing_rod_level in [3, 4]:
630+
self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
631+
632+
self.multiworld.state.collect(self.world.create_item(railroad_item), event=False)
633+
634+
for fishing_rod_level in [3, 4]:
635+
self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state)
636+
637+
638+
def place_region_at_entrance(multiworld, player, region, entrance):
639+
region_to_place = multiworld.get_region(region, player)
640+
entrance_to_place_region = multiworld.get_entrance(entrance, player)
641+
642+
entrance_to_switch = region_to_place.entrances[0]
643+
region_to_switch = entrance_to_place_region.connected_region
644+
entrance_to_switch.connect(region_to_switch)
645+
entrance_to_place_region.connect(region_to_place)
646+
647+
599648
def collect_all_except(multiworld, item_to_not_collect: str):
600649
for item in multiworld.get_items():
601650
if item.name != item_to_not_collect:

0 commit comments

Comments
 (0)