Stardew Valley: Move progressive tool options handling in features (#4374)

* create tool progression feature and unwrap option

* replace option usage with calling feature

* add comment explaining why some logic is a weird place

* replace item creation logic with feature

* self review and add unit tests

* rename test cuz I named them too long

* add a test for the trash can useful stuff cuz I thought there was a bug but turns out it works

* self review again

* remove price_multiplier, turns out it's unused during generation

* damn it 3.11 why are you like this

* use blacksmith region when checking vanilla tools

* fix rule

* move can mine using in tool logic

* remove changes to performance test

* properly set the option I guess

* properly set options 2

* that's what happen when you code too late
This commit is contained in:
Jérémie Bolduc
2025-03-08 11:19:29 -05:00
committed by GitHub
parent b5269e9aa4
commit ee9bcb84b7
20 changed files with 262 additions and 121 deletions

View File

@@ -1,5 +1,5 @@
from . import content_packs from . import content_packs
from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_progression, tool_progression
from .game_content import ContentPack, StardewContent, StardewFeatures from .game_content import ContentPack, StardewContent, StardewFeatures
from .unpacking import unpack_content from .unpacking import unpack_content
from .. import options from .. import options
@@ -33,6 +33,7 @@ def choose_features(player_options: options.StardewValleyOptions) -> StardewFeat
choose_fishsanity(player_options.fishsanity), choose_fishsanity(player_options.fishsanity),
choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size), choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size),
choose_skill_progression(player_options.skill_progression), choose_skill_progression(player_options.skill_progression),
choose_tool_progression(player_options.tool_progression, player_options.skill_progression),
) )
@@ -122,3 +123,18 @@ def choose_skill_progression(skill_progression_option: options.SkillProgression)
raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}") raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}")
return skill_progression_feature return skill_progression_feature
def choose_tool_progression(tool_option: options.ToolProgression, skill_option: options.SkillProgression) -> tool_progression.ToolProgressionFeature:
if tool_option.is_vanilla:
return tool_progression.ToolProgressionVanilla()
tools_distribution = tool_progression.get_tools_distribution(
progressive_tools_enabled=True,
skill_masteries_enabled=skill_option == options.SkillProgression.option_progressive_with_masteries,
)
if tool_option.is_progressive:
return tool_progression.ToolProgressionProgressive(tools_distribution)
raise ValueError(f"No tool progression feature mapped to {str(tool_option.value)}")

View File

@@ -3,3 +3,4 @@ from . import cropsanity
from . import fishsanity from . import fishsanity
from . import friendsanity from . import friendsanity
from . import skill_progression from . import skill_progression
from . import tool_progression

View File

@@ -0,0 +1,68 @@
from abc import ABC
from collections import Counter
from collections.abc import Mapping
from dataclasses import dataclass, field
from functools import cache
from types import MappingProxyType
from typing import ClassVar
from ...strings.tool_names import Tool
def to_progressive_item(tool: str) -> str:
"""Return the name of the progressive item."""
return f"Progressive {tool}"
# The golden scythe is always randomized
VANILLA_TOOL_DISTRIBUTION = MappingProxyType({
Tool.scythe: 1,
})
PROGRESSIVE_TOOL_DISTRIBUTION = MappingProxyType({
Tool.axe: 4,
Tool.hoe: 4,
Tool.pickaxe: 4,
Tool.pan: 4,
Tool.trash_can: 4,
Tool.watering_can: 4,
Tool.fishing_rod: 4,
})
# Masteries add another tier to the scythe and the fishing rod
SKILL_MASTERIES_TOOL_DISTRIBUTION = MappingProxyType({
Tool.scythe: 1,
Tool.fishing_rod: 1,
})
@cache
def get_tools_distribution(progressive_tools_enabled: bool, skill_masteries_enabled: bool) -> Mapping[str, int]:
distribution = Counter(VANILLA_TOOL_DISTRIBUTION)
if progressive_tools_enabled:
distribution += PROGRESSIVE_TOOL_DISTRIBUTION
if skill_masteries_enabled:
distribution += SKILL_MASTERIES_TOOL_DISTRIBUTION
return MappingProxyType(distribution)
@dataclass(frozen=True)
class ToolProgressionFeature(ABC):
is_progressive: ClassVar[bool]
tool_distribution: Mapping[str, int]
to_progressive_item = staticmethod(to_progressive_item)
@dataclass(frozen=True)
class ToolProgressionVanilla(ToolProgressionFeature):
is_progressive = False
# FIXME change the default_factory to a simple default when python 3.11 is no longer supported
tool_distribution: Mapping[str, int] = field(default_factory=lambda: VANILLA_TOOL_DISTRIBUTION)
class ToolProgressionProgressive(ToolProgressionFeature):
is_progressive = True

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union from typing import Dict, Iterable, Set, Any, Mapping, Type, Tuple, Union
from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_progression, tool_progression
from ..data.fish_data import FishItem from ..data.fish_data import FishItem
from ..data.game_item import GameItem, ItemSource, ItemTag from ..data.game_item import GameItem, ItemSource, ItemTag
from ..data.skill import Skill from ..data.skill import Skill
@@ -54,6 +54,7 @@ class StardewFeatures:
fishsanity: fishsanity.FishsanityFeature fishsanity: fishsanity.FishsanityFeature
friendsanity: friendsanity.FriendsanityFeature friendsanity: friendsanity.FriendsanityFeature
skill_progression: skill_progression.SkillProgressionFeature skill_progression: skill_progression.SkillProgressionFeature
tool_progression: tool_progression.ToolProgressionFeature
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -32,7 +32,7 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive: if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive:
early_forced.append("Progressive Backpack") early_forced.append("Progressive Backpack")
if options.tool_progression & stardew_options.ToolProgression.option_progressive: if content.features.tool_progression.is_progressive:
if content.features.fishsanity.is_enabled: if content.features.fishsanity.is_enabled:
early_candidates.append("Progressive Fishing Rod") early_candidates.append("Progressive Fishing Rod")
early_forced.append("Progressive Pickaxe") early_forced.append("Progressive Pickaxe")

View File

@@ -15,7 +15,7 @@ from .data.game_item import ItemTag
from .logic.logic_event import all_events from .logic.logic_event import all_events
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \ from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ BuildingProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName
from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.ap_weapon_names import APWeapon
@@ -23,6 +23,7 @@ from .strings.ap_names.buff_names import Buff
from .strings.ap_names.community_upgrade_names import CommunityUpgrade from .strings.ap_names.community_upgrade_names import CommunityUpgrade
from .strings.ap_names.mods.mod_items import SVEQuestItem from .strings.ap_names.mods.mod_items import SVEQuestItem
from .strings.currency_names import Currency from .strings.currency_names import Currency
from .strings.tool_names import Tool
from .strings.wallet_item_names import Wallet from .strings.wallet_item_names import Wallet
ITEM_CODE_OFFSET = 717000 ITEM_CODE_OFFSET = 717000
@@ -119,11 +120,6 @@ class StardewItemFactory(Protocol):
raise NotImplementedError raise NotImplementedError
class StardewItemDeleter(Protocol):
def __call__(self, item: Item):
raise NotImplementedError
def load_item_csv(): def load_item_csv():
from importlib.resources import files from importlib.resources import files
@@ -226,7 +222,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley
create_weapons(item_factory, options, items) create_weapons(item_factory, options, items)
items.append(item_factory("Skull Key")) items.append(item_factory("Skull Key"))
create_elevators(item_factory, options, items) create_elevators(item_factory, options, items)
create_tools(item_factory, options, content, items) create_tools(item_factory, content, items)
create_skills(item_factory, content, items) create_skills(item_factory, content, items)
create_wizard_buildings(item_factory, options, items) create_wizard_buildings(item_factory, options, items)
create_carpenter_buildings(item_factory, options, items) create_carpenter_buildings(item_factory, options, items)
@@ -316,23 +312,17 @@ def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOpt
items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8]) items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])
def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): def create_tools(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
if options.tool_progression & ToolProgression.option_progressive: tool_progression = content.features.tool_progression
for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]: for tool, count in tool_progression.tool_distribution.items():
name = item_data.name item = item_table[tool_progression.to_progressive_item(tool)]
if "Trash Can" in name:
items.extend([item_factory(item) for item in [item_data] * 3])
items.append(item_factory(item_data, ItemClassification.useful))
else:
items.extend([item_factory(item) for item in [item_data] * 4])
if content.features.skill_progression.are_masteries_shuffled: # Trash can is only used in tool upgrade logic, so the last trash can is not progression because it basically does not unlock anything.
# Masteries add another tier to the scythe and the fishing rod if tool == Tool.trash_can:
items.append(item_factory("Progressive Scythe")) count -= 1
items.append(item_factory("Progressive Fishing Rod")) items.append(item_factory(item, ItemClassification.useful))
# The golden scythe is always randomized items.extend([item_factory(item) for _ in range(count)])
items.append(item_factory("Progressive Scythe"))
def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):

View File

@@ -11,7 +11,7 @@ from .data.game_item import ItemTag
from .data.museum_data import all_museum_items from .data.museum_data import all_museum_items
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \ from .options import ExcludeGingerIsland, ArcadeMachineLocations, SpecialOrderLocations, Museumsanity, \
FestivalLocations, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType FestivalLocations, BuildingProgression, ElevatorProgression, BackpackProgression, FarmType
from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity from .options import StardewValleyOptions, Craftsanity, Chefsanity, Cooksanity, Shipsanity, Monstersanity
from .strings.goal_names import Goal from .strings.goal_names import Goal
from .strings.quest_names import ModQuest, Quest from .strings.quest_names import ModQuest, Quest
@@ -473,7 +473,7 @@ def create_locations(location_collector: StardewLocationCollector,
extend_bundle_locations(randomized_locations, bundle_rooms) extend_bundle_locations(randomized_locations, bundle_rooms)
extend_backpack_locations(randomized_locations, options) extend_backpack_locations(randomized_locations, options)
if options.tool_progression & ToolProgression.option_progressive: if content.features.tool_progression.is_progressive:
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE]) randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
extend_elevator_locations(randomized_locations, options) extend_elevator_locations(randomized_locations, options)

View File

@@ -10,12 +10,11 @@ from .region_logic import RegionLogicMixin
from .skill_logic import SkillLogicMixin from .skill_logic import SkillLogicMixin
from .tool_logic import ToolLogicMixin from .tool_logic import ToolLogicMixin
from .. import options from .. import options
from ..options import ToolProgression
from ..stardew_rule import StardewRule, True_ from ..stardew_rule import StardewRule, True_
from ..strings.performance_names import Performance from ..strings.performance_names import Performance
from ..strings.region_names import Region from ..strings.region_names import Region
from ..strings.skill_names import Skill from ..strings.skill_names import Skill
from ..strings.tool_names import Tool, ToolMaterial from ..strings.tool_names import ToolMaterial
class MineLogicMixin(BaseLogicMixin): class MineLogicMixin(BaseLogicMixin):
@@ -56,11 +55,12 @@ SkillLogicMixin, CookingLogicMixin]]):
def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule:
tier = floor // 40 tier = floor // 40
rules = [] rules = []
weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier) weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier)
rules.append(weapon_rule) rules.append(weapon_rule)
if self.options.tool_progression & ToolProgression.option_progressive: tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[tier])
rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) rules.append(tool_rule)
# No alternative for vanilla because we assume that you will grind the levels in the mines. # No alternative for vanilla because we assume that you will grind the levels in the mines.
if self.content.features.skill_progression.is_progressive: if self.content.features.skill_progression.is_progressive:
@@ -85,11 +85,12 @@ SkillLogicMixin, CookingLogicMixin]]):
def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule: def can_progress_in_the_skull_cavern_from_floor(self, floor: int) -> StardewRule:
tier = floor // 50 tier = floor // 50
rules = [] rules = []
weapon_rule = self.logic.combat.has_great_weapon weapon_rule = self.logic.combat.has_great_weapon
rules.append(weapon_rule) rules.append(weapon_rule)
if self.options.tool_progression & ToolProgression.option_progressive: tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[min(4, max(0, tier + 2))])
rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) rules.append(tool_rule)
# No alternative for vanilla because we assume that you will grind the levels in the mines. # No alternative for vanilla because we assume that you will grind the levels in the mines.
if self.content.features.skill_progression.is_progressive: if self.content.features.skill_progression.is_progressive:

View File

@@ -8,12 +8,11 @@ from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin from .season_logic import SeasonLogicMixin
from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin
from ..options import ToolProgression
from ..stardew_rule import StardewRule, True_, False_ from ..stardew_rule import StardewRule, True_, False_
from ..strings.ap_names.skill_level_names import ModSkillLevel from ..strings.ap_names.skill_level_names import ModSkillLevel
from ..strings.region_names import Region from ..strings.region_names import Region, LogicRegion
from ..strings.spells import MagicSpell from ..strings.spells import MagicSpell
from ..strings.tool_names import ToolMaterial, Tool from ..strings.tool_names import ToolMaterial, Tool, APTool
fishing_rod_prices = { fishing_rod_prices = {
3: 1800, 3: 1800,
@@ -57,10 +56,10 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
if material == ToolMaterial.basic or tool == Tool.scythe: if material == ToolMaterial.basic or tool == Tool.scythe:
return True_() return True_()
if self.options.tool_progression & ToolProgression.option_progressive: if self.content.features.tool_progression.is_progressive:
return self.logic.received(f"Progressive {tool}", tool_materials[material]) return self.logic.received(f"Progressive {tool}", tool_materials[material])
can_upgrade_rule = self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) can_upgrade_rule = self.logic.tool._can_purchase_upgrade(material)
if tool == Tool.pan: if tool == Tool.pan:
has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain)
if material == ToolMaterial.copper: if material == ToolMaterial.copper:
@@ -69,6 +68,20 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
return can_upgrade_rule return can_upgrade_rule
@cache_self1
def can_mine_using(self, material: str) -> StardewRule:
if material == ToolMaterial.basic:
return self.logic.true_
if self.content.features.tool_progression.is_progressive:
return self.logic.received(APTool.pickaxe, tool_materials[material])
else:
return self.logic.tool._can_purchase_upgrade(material)
@cache_self1
def _can_purchase_upgrade(self, material: str) -> StardewRule:
return self.logic.region.can_reach(LogicRegion.blacksmith_upgrade(material))
def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: 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) return self.has_tool(tool, material) & self.logic.region.can_reach(region)
@@ -76,8 +89,8 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi
def has_fishing_rod(self, level: int) -> StardewRule: def has_fishing_rod(self, level: int) -> StardewRule:
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." 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: if self.content.features.tool_progression.is_progressive:
return self.logic.received(f"Progressive {Tool.fishing_rod}", level) return self.logic.received(APTool.fishing_rod, level)
if level <= 2: 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. # We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back.

View File

@@ -1,7 +1,6 @@
from typing import Dict, Union from typing import Dict, Union
from ..mod_data import ModNames from ..mod_data import ModNames
from ... import options
from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.base_logic import BaseLogicMixin, BaseLogic
from ...logic.combat_logic import CombatLogicMixin from ...logic.combat_logic import CombatLogicMixin
from ...logic.cooking_logic import CookingLogicMixin from ...logic.cooking_logic import CookingLogicMixin
@@ -80,7 +79,7 @@ FarmingLogicMixin]]):
# Gingerbread House # Gingerbread House
} }
if self.options.tool_progression & options.ToolProgression.option_progressive: if self.content.features.tool_progression.is_progressive:
options_to_update.update({ options_to_update.update({
Ore.iridium: items[Ore.iridium] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iridium, DeepWoodsRegion.floor_50), # Iridium Tree Ore.iridium: items[Ore.iridium] | self.logic.tool.can_use_tool_at(Tool.axe, ToolMaterial.iridium, DeepWoodsRegion.floor_50), # Iridium Tree
}) })

View File

@@ -247,6 +247,14 @@ class ToolProgression(Choice):
option_progressive_cheap = 0b011 # 3 option_progressive_cheap = 0b011 # 3
option_progressive_very_cheap = 0b101 # 5 option_progressive_very_cheap = 0b101 # 5
@property
def is_vanilla(self):
return not self.is_progressive
@property
def is_progressive(self):
return bool(self.value & self.option_progressive)
class ElevatorProgression(Choice): class ElevatorProgression(Choice):
"""Shuffle the elevator? """Shuffle the elevator?

View File

@@ -19,9 +19,8 @@ from .logic.logic import StardewLogic
from .logic.time_logic import MAX_MONTHS from .logic.time_logic import MAX_MONTHS
from .logic.tool_logic import tool_upgrade_prices from .logic.tool_logic import tool_upgrade_prices
from .mods.mod_data import ModNames from .mods.mod_data import ModNames
from .options import StardewValleyOptions, Walnutsanity from .options import BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity
from .stardew_rule import And, StardewRule, true_ from .stardew_rule import And, StardewRule, true_
from .stardew_rule.indirect_connection import look_for_indirect_connection from .stardew_rule.indirect_connection import look_for_indirect_connection
from .stardew_rule.rule_explain import explain from .stardew_rule.rule_explain import explain
@@ -69,7 +68,7 @@ def set_rules(world):
set_entrance_rules(logic, multiworld, player, world_options) set_entrance_rules(logic, multiworld, player, world_options)
set_ginger_island_rules(logic, multiworld, player, world_options) set_ginger_island_rules(logic, multiworld, player, world_options)
set_tool_rules(logic, multiworld, player, world_options) set_tool_rules(logic, multiworld, player, world_content)
set_skills_rules(logic, multiworld, player, world_content) set_skills_rules(logic, multiworld, player, world_content)
set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options) set_bundle_rules(bundle_rooms, logic, multiworld, player, world_options)
set_building_rules(logic, multiworld, player, world_options) set_building_rules(logic, multiworld, player, world_options)
@@ -111,8 +110,8 @@ def set_isolated_locations_rules(logic: StardewLogic, multiworld, player):
logic.season.has(Season.spring)) logic.season.has(Season.spring))
def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): def set_tool_rules(logic: StardewLogic, multiworld, player, content: StardewContent):
if not world_options.tool_progression & ToolProgression.option_progressive: if not content.features.tool_progression.is_progressive:
return return
MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player), MultiWorldRules.add_rule(multiworld.get_location("Purchase Fiberglass Rod", player),
@@ -281,13 +280,6 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player):
set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule) set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule)
def set_blacksmith_entrance_rules(logic, multiworld, player):
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium)
def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions): def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewValleyOptions):
set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring) set_entrance_rule(multiworld, player, LogicEntrance.grow_spring_crops, logic.farming.has_farming_tools & logic.season.has_spring)
set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer) set_entrance_rule(multiworld, player, LogicEntrance.grow_summer_crops, logic.farming.has_farming_tools & logic.season.has_summer)
@@ -306,6 +298,13 @@ def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewVa
set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp) set_entrance_rule(multiworld, player, LogicEntrance.fishing, logic.skill.can_get_fishing_xp)
def set_blacksmith_entrance_rules(logic, multiworld, player):
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_copper, MetalBar.copper, ToolMaterial.copper)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iron, MetalBar.iron, ToolMaterial.iron)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_gold, MetalBar.gold, ToolMaterial.gold)
set_blacksmith_upgrade_rule(logic, multiworld, player, LogicEntrance.blacksmith_iridium, MetalBar.iridium, ToolMaterial.iridium)
def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str): def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str):
upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material]) upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material])
set_entrance_rule(multiworld, player, entrance_name, upgrade_rule) set_entrance_rule(multiworld, player, entrance_name, upgrade_rule)

View File

@@ -194,10 +194,15 @@ class LogicEntrance:
island_cooking = "Island Cooking" island_cooking = "Island Cooking"
shipping = "Use Shipping Bin" shipping = "Use Shipping Bin"
watch_queen_of_sauce = "Watch Queen of Sauce" watch_queen_of_sauce = "Watch Queen of Sauce"
blacksmith_copper = "Upgrade Copper Tools"
blacksmith_iron = "Upgrade Iron Tools" @staticmethod
blacksmith_gold = "Upgrade Gold Tools" def blacksmith_upgrade(material: str) -> str:
blacksmith_iridium = "Upgrade Iridium Tools" return f"Upgrade {material} Tools"
blacksmith_copper = blacksmith_upgrade("Copper")
blacksmith_iron = blacksmith_upgrade("Iron")
blacksmith_gold = blacksmith_upgrade("Gold")
blacksmith_iridium = blacksmith_upgrade("Iridium")
grow_spring_crops = "Grow Spring Crops" grow_spring_crops = "Grow Spring Crops"
grow_summer_crops = "Grow Summer Crops" grow_summer_crops = "Grow Summer Crops"

View File

@@ -159,10 +159,15 @@ class LogicRegion:
kitchen = "Kitchen" kitchen = "Kitchen"
shipping = "Shipping" shipping = "Shipping"
queen_of_sauce = "The Queen of Sauce" queen_of_sauce = "The Queen of Sauce"
blacksmith_copper = "Blacksmith Copper Upgrades"
blacksmith_iron = "Blacksmith Iron Upgrades" @staticmethod
blacksmith_gold = "Blacksmith Gold Upgrades" def blacksmith_upgrade(material: str) -> str:
blacksmith_iridium = "Blacksmith Iridium Upgrades" return f"Blacksmith {material} Upgrades"
blacksmith_copper = blacksmith_upgrade("Copper")
blacksmith_iron = blacksmith_upgrade("Iron")
blacksmith_gold = blacksmith_upgrade("Gold")
blacksmith_iridium = blacksmith_upgrade("Iridium")
spring_farming = "Spring Farming" spring_farming = "Spring Farming"
summer_farming = "Summer Farming" summer_farming = "Summer Farming"

View File

@@ -5,8 +5,8 @@ from . import SVTestBase
from .. import items, location_table, options from .. import items, location_table, options
from ..items import Group from ..items import Group
from ..locations import LocationTags from ..locations import LocationTags
from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \ from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \
SkillProgression, Booksanity, Walnutsanity Booksanity, Walnutsanity
from ..strings.region_names import Region from ..strings.region_names import Region
@@ -320,7 +320,7 @@ class TestProgressiveElevator(SVTestBase):
class TestSkullCavernLogic(SVTestBase): class TestSkullCavernLogic(SVTestBase):
options = { options = {
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla, options.ElevatorProgression.internal_name: options.ElevatorProgression.option_vanilla,
ToolProgression.internal_name: ToolProgression.option_progressive, options.ToolProgression.internal_name: options.ToolProgression.option_progressive,
options.SkillProgression.internal_name: options.SkillProgression.option_progressive, options.SkillProgression.internal_name: options.SkillProgression.option_progressive,
} }

View File

@@ -9,7 +9,7 @@ class TestBitFlagsVanilla(SVTestBase):
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertFalse(tool_progressive) self.assertFalse(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -26,7 +26,7 @@ class TestBitFlagsVanillaCheap(SVTestBase):
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertFalse(tool_progressive) self.assertFalse(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -43,7 +43,7 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase):
def test_options_are_not_detected_as_progressive(self): def test_options_are_not_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertFalse(tool_progressive) self.assertFalse(tool_progressive)
self.assertFalse(building_progressive) self.assertFalse(building_progressive)
@@ -60,7 +60,7 @@ class TestBitFlagsProgressive(SVTestBase):
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertTrue(tool_progressive) self.assertTrue(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)
@@ -77,7 +77,7 @@ class TestBitFlagsProgressiveCheap(SVTestBase):
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertTrue(tool_progressive) self.assertTrue(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)
@@ -94,7 +94,7 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase):
def test_options_are_detected_as_progressive(self): def test_options_are_detected_as_progressive(self):
world_options = self.world.options world_options = self.world.options
tool_progressive = world_options.tool_progression & ToolProgression.option_progressive tool_progressive = self.world.content.features.tool_progression.is_progressive
building_progressive = world_options.building_progression & BuildingProgression.option_progressive building_progressive = world_options.building_progression & BuildingProgression.option_progressive
self.assertTrue(tool_progressive) self.assertTrue(tool_progressive)
self.assertTrue(building_progressive) self.assertTrue(building_progressive)

View File

@@ -1,17 +1,17 @@
import itertools import itertools
from BaseClasses import ItemClassification
from Options import NamedRange from Options import NamedRange
from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld from . import SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, solo_multiworld, SVTestBase
from .assertion import WorldAssertMixin from .assertion import WorldAssertMixin
from .long.option_names import all_option_choices from .long.option_names import all_option_choices
from .. import items_by_group, Group, StardewValleyWorld from .. import items_by_group, Group, StardewValleyWorld
from ..locations import locations_by_tag, LocationTags, location_table from ..locations import locations_by_tag, LocationTags, location_table
from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \ from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations
SkillProgression
from ..strings.goal_names import Goal as GoalName from ..strings.goal_names import Goal as GoalName
from ..strings.season_names import Season from ..strings.season_names import Season
from ..strings.special_order_names import SpecialOrder from ..strings.special_order_names import SpecialOrder
from ..strings.tool_names import ToolMaterial, Tool from ..strings.tool_names import ToolMaterial, Tool, APTool
SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter} SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter}
TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"}
@@ -77,52 +77,30 @@ class TestSeasonRandomization(SVTestCase):
self.assertEqual(items.count(Season.progressive), 3) self.assertEqual(items.count(Season.progressive), 3)
class TestToolProgression(SVTestCase): class TestToolProgression(SVTestBase):
def test_given_vanilla_when_generate_then_no_tool_in_pool(self): options = {
world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} ToolProgression.internal_name: ToolProgression.option_progressive,
with solo_multiworld(world_options) as (multi_world, _): }
items = {item.name for item in multi_world.get_items()}
for tool in TOOLS:
self.assertNotIn(tool, items)
def test_given_progressive_when_generate_then_each_tool_is_in_pool_4_times(self):
world_options = {ToolProgression.internal_name: ToolProgression.option_progressive,
SkillProgression.internal_name: SkillProgression.option_progressive}
with solo_multiworld(world_options) as (multi_world, _):
items = [item.name for item in multi_world.get_items()]
for tool in TOOLS:
count = items.count("Progressive " + tool)
self.assertEqual(count, 4, f"Progressive {tool} was there {count} times")
scythe_count = items.count("Progressive Scythe")
self.assertEqual(scythe_count, 1, f"Progressive Scythe was there {scythe_count} times")
self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated")
def test_given_progressive_with_masteries_when_generate_then_fishing_rod_is_in_the_pool_5_times(self):
world_options = {ToolProgression.internal_name: ToolProgression.option_progressive,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries}
with solo_multiworld(world_options) as (multi_world, _):
items = [item.name for item in multi_world.get_items()]
for tool in TOOLS:
count = items.count("Progressive " + tool)
expected_count = 5 if tool == "Fishing Rod" else 4
self.assertEqual(count, expected_count, f"Progressive {tool} was there {count} times")
scythe_count = items.count("Progressive Scythe")
self.assertEqual(scythe_count, 2, f"Progressive Scythe was there {scythe_count} times")
self.assertEqual(items.count("Golden Scythe"), 0, f"Golden Scythe is deprecated")
def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self):
world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} locations = set(self.get_real_location_names())
with solo_multiworld(world_options) as (multi_world, _): for material, tool in itertools.product(ToolMaterial.tiers.values(),
locations = {locations.name for locations in multi_world.get_locations(1)} [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]):
for material, tool in itertools.product(ToolMaterial.tiers.values(), if material == ToolMaterial.basic:
[Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): continue
if material == ToolMaterial.basic: self.assertIn(f"{material} {tool} Upgrade", locations)
continue self.assertIn("Purchase Training Rod", locations)
self.assertIn(f"{material} {tool} Upgrade", locations) self.assertIn("Bamboo Pole Cutscene", locations)
self.assertIn("Purchase Training Rod", locations) self.assertIn("Purchase Fiberglass Rod", locations)
self.assertIn("Bamboo Pole Cutscene", locations) self.assertIn("Purchase Iridium Rod", locations)
self.assertIn("Purchase Fiberglass Rod", locations)
self.assertIn("Purchase Iridium Rod", locations) def test_given_progressive_when_generate_then_only_3_trash_can_are_progressive(self):
trash_cans = self.get_items_by_name(APTool.trash_can)
progressive_count = sum([1 for item in trash_cans if item.classification == ItemClassification.progression])
useful_count = sum([1 for item in trash_cans if item.classification == ItemClassification.useful])
self.assertEqual(progressive_count, 3)
self.assertEqual(useful_count, 1)
class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase): class TestGenerateAllOptionsWithExcludeGingerIsland(WorldAssertMixin, SVTestCase):

View File

@@ -1,5 +1,5 @@
from . import SVTestBase from . import SVTestBase
from ..options import ExcludeGingerIsland, Walnutsanity from ..options import ExcludeGingerIsland, Walnutsanity, ToolProgression, SkillProgression
from ..strings.ap_names.ap_option_names import WalnutsanityOptionName from ..strings.ap_names.ap_option_names import WalnutsanityOptionName
@@ -7,6 +7,8 @@ class TestWalnutsanityNone(SVTestBase):
options = { options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false, ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: Walnutsanity.preset_none, Walnutsanity: Walnutsanity.preset_none,
SkillProgression: ToolProgression.option_progressive,
ToolProgression: ToolProgression.option_progressive,
} }
def test_no_walnut_locations(self): def test_no_walnut_locations(self):
@@ -50,6 +52,8 @@ class TestWalnutsanityPuzzles(SVTestBase):
options = { options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false, ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}), Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}),
SkillProgression: ToolProgression.option_progressive,
ToolProgression: ToolProgression.option_progressive,
} }
def test_only_puzzle_walnut_locations(self): def test_only_puzzle_walnut_locations(self):

View File

@@ -9,6 +9,7 @@ default_features = StardewFeatures(
feature.fishsanity.FishsanityNone(), feature.fishsanity.FishsanityNone(),
feature.friendsanity.FriendsanityNone(), feature.friendsanity.FriendsanityNone(),
feature.skill_progression.SkillProgressionVanilla(), feature.skill_progression.SkillProgressionVanilla(),
feature.tool_progression.ToolProgressionVanilla()
) )

View File

@@ -0,0 +1,52 @@
import unittest
from ....content import choose_tool_progression
from ....options import ToolProgression, SkillProgression
from ....strings.tool_names import Tool
class TestToolDistribution(unittest.TestCase):
def test_given_vanilla_tool_progression_when_create_feature_then_only_one_scythe_is_randomized(self):
tool_progression = ToolProgression(ToolProgression.option_vanilla)
skill_progression = SkillProgression.from_text("random")
feature = choose_tool_progression(tool_progression, skill_progression)
self.assertEqual(feature.tool_distribution, {
Tool.scythe: 1,
})
def test_given_progressive_tool_when_create_feature_then_all_tool_upgrades_are_randomized(self):
tool_progression = ToolProgression(ToolProgression.option_progressive)
skill_progression = SkillProgression(SkillProgression.option_progressive)
feature = choose_tool_progression(tool_progression, skill_progression)
self.assertEqual(feature.tool_distribution, {
Tool.scythe: 1,
Tool.pickaxe: 4,
Tool.axe: 4,
Tool.hoe: 4,
Tool.watering_can: 4,
Tool.trash_can: 4,
Tool.pan: 4,
Tool.fishing_rod: 4,
})
def test_given_progressive_tool_and_skill_masteries_when_create_feature_then_additional_scythe_and_fishing_rod_are_randomized(self):
tool_progression = ToolProgression(ToolProgression.option_progressive)
skill_progression = SkillProgression(SkillProgression.option_progressive_with_masteries)
feature = choose_tool_progression(tool_progression, skill_progression)
self.assertEqual(feature.tool_distribution, {
Tool.scythe: 2,
Tool.pickaxe: 4,
Tool.axe: 4,
Tool.hoe: 4,
Tool.watering_can: 4,
Tool.trash_can: 4,
Tool.pan: 4,
Tool.fishing_rod: 5,
})