From ee9bcb84b7fd5e642b667f9b715cf2520bc18c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:19:29 -0500 Subject: [PATCH] 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 --- worlds/stardew_valley/content/__init__.py | 18 ++++- .../content/feature/__init__.py | 1 + .../content/feature/tool_progression.py | 68 +++++++++++++++++ worlds/stardew_valley/content/game_content.py | 3 +- worlds/stardew_valley/early_items.py | 2 +- worlds/stardew_valley/items.py | 34 +++------ worlds/stardew_valley/locations.py | 4 +- worlds/stardew_valley/logic/mine_logic.py | 13 ++-- worlds/stardew_valley/logic/tool_logic.py | 27 +++++-- .../stardew_valley/mods/logic/item_logic.py | 3 +- worlds/stardew_valley/options/options.py | 8 ++ worlds/stardew_valley/rules.py | 25 +++---- .../stardew_valley/strings/entrance_names.py | 13 +++- worlds/stardew_valley/strings/region_names.py | 13 +++- worlds/stardew_valley/test/TestGeneration.py | 6 +- worlds/stardew_valley/test/TestOptionFlags.py | 12 +-- worlds/stardew_valley/test/TestOptions.py | 74 +++++++------------ .../stardew_valley/test/TestWalnutsanity.py | 6 +- .../stardew_valley/test/content/__init__.py | 1 + .../content/feature/TestToolProgression.py | 52 +++++++++++++ 20 files changed, 262 insertions(+), 121 deletions(-) create mode 100644 worlds/stardew_valley/content/feature/tool_progression.py create mode 100644 worlds/stardew_valley/test/content/feature/TestToolProgression.py diff --git a/worlds/stardew_valley/content/__init__.py b/worlds/stardew_valley/content/__init__.py index 54b4d75d..53085064 100644 --- a/worlds/stardew_valley/content/__init__.py +++ b/worlds/stardew_valley/content/__init__.py @@ -1,5 +1,5 @@ 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 .unpacking import unpack_content from .. import options @@ -33,6 +33,7 @@ def choose_features(player_options: options.StardewValleyOptions) -> StardewFeat choose_fishsanity(player_options.fishsanity), choose_friendsanity(player_options.friendsanity, player_options.friendsanity_heart_size), 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)}") 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)}") diff --git a/worlds/stardew_valley/content/feature/__init__.py b/worlds/stardew_valley/content/feature/__init__.py index f3e5c673..eb23f810 100644 --- a/worlds/stardew_valley/content/feature/__init__.py +++ b/worlds/stardew_valley/content/feature/__init__.py @@ -3,3 +3,4 @@ from . import cropsanity from . import fishsanity from . import friendsanity from . import skill_progression +from . import tool_progression diff --git a/worlds/stardew_valley/content/feature/tool_progression.py b/worlds/stardew_valley/content/feature/tool_progression.py new file mode 100644 index 00000000..d5fe5cef --- /dev/null +++ b/worlds/stardew_valley/content/feature/tool_progression.py @@ -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 diff --git a/worlds/stardew_valley/content/game_content.py b/worlds/stardew_valley/content/game_content.py index 7ff3217b..3aa3350f 100644 --- a/worlds/stardew_valley/content/game_content.py +++ b/worlds/stardew_valley/content/game_content.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field 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.game_item import GameItem, ItemSource, ItemTag from ..data.skill import Skill @@ -54,6 +54,7 @@ class StardewFeatures: fishsanity: fishsanity.FishsanityFeature friendsanity: friendsanity.FriendsanityFeature skill_progression: skill_progression.SkillProgressionFeature + tool_progression: tool_progression.ToolProgressionFeature @dataclass(frozen=True) diff --git a/worlds/stardew_valley/early_items.py b/worlds/stardew_valley/early_items.py index 81e28956..5ad48912 100644 --- a/worlds/stardew_valley/early_items.py +++ b/worlds/stardew_valley/early_items.py @@ -32,7 +32,7 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, if options.backpack_progression == stardew_options.BackpackProgression.option_early_progressive: 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: early_candidates.append("Progressive Fishing Rod") early_forced.append("Progressive Pickaxe") diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 6ac827f8..056a4f6e 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -15,7 +15,7 @@ from .data.game_item import ItemTag from .logic.logic_event import all_events from .mods.mod_data import ModNames 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 from .strings.ap_names.ap_option_names import BuffOptionName, WalnutsanityOptionName 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.mods.mod_items import SVEQuestItem from .strings.currency_names import Currency +from .strings.tool_names import Tool from .strings.wallet_item_names import Wallet ITEM_CODE_OFFSET = 717000 @@ -119,11 +120,6 @@ class StardewItemFactory(Protocol): raise NotImplementedError -class StardewItemDeleter(Protocol): - def __call__(self, item: Item): - raise NotImplementedError - - def load_item_csv(): from importlib.resources import files @@ -226,7 +222,7 @@ def create_unique_items(item_factory: StardewItemFactory, options: StardewValley create_weapons(item_factory, options, items) items.append(item_factory("Skull Key")) 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_wizard_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]) -def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]): - if options.tool_progression & ToolProgression.option_progressive: - for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]: - name = item_data.name - 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]) +def create_tools(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): + tool_progression = content.features.tool_progression + for tool, count in tool_progression.tool_distribution.items(): + item = item_table[tool_progression.to_progressive_item(tool)] - if content.features.skill_progression.are_masteries_shuffled: - # Masteries add another tier to the scythe and the fishing rod - items.append(item_factory("Progressive Scythe")) - items.append(item_factory("Progressive Fishing Rod")) + # Trash can is only used in tool upgrade logic, so the last trash can is not progression because it basically does not unlock anything. + if tool == Tool.trash_can: + count -= 1 + items.append(item_factory(item, ItemClassification.useful)) - # The golden scythe is always randomized - items.append(item_factory("Progressive Scythe")) + items.extend([item_factory(item) for _ in range(count)]) def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]): diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 02c8a544..df86e081 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -11,7 +11,7 @@ from .data.game_item import ItemTag from .data.museum_data import all_museum_items from .mods.mod_data import ModNames 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 .strings.goal_names import Goal 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_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]) extend_elevator_locations(randomized_locations, options) diff --git a/worlds/stardew_valley/logic/mine_logic.py b/worlds/stardew_valley/logic/mine_logic.py index 350582ae..e332241c 100644 --- a/worlds/stardew_valley/logic/mine_logic.py +++ b/worlds/stardew_valley/logic/mine_logic.py @@ -10,12 +10,11 @@ from .region_logic import RegionLogicMixin from .skill_logic import SkillLogicMixin from .tool_logic import ToolLogicMixin from .. import options -from ..options import ToolProgression from ..stardew_rule import StardewRule, True_ from ..strings.performance_names import Performance from ..strings.region_names import Region from ..strings.skill_names import Skill -from ..strings.tool_names import Tool, ToolMaterial +from ..strings.tool_names import ToolMaterial class MineLogicMixin(BaseLogicMixin): @@ -56,11 +55,12 @@ SkillLogicMixin, CookingLogicMixin]]): def can_progress_in_the_mines_from_floor(self, floor: int) -> StardewRule: tier = floor // 40 rules = [] + weapon_rule = self.logic.mine.get_weapon_rule_for_floor_tier(tier) rules.append(weapon_rule) - if self.options.tool_progression & ToolProgression.option_progressive: - rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) + tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[tier]) + rules.append(tool_rule) # No alternative for vanilla because we assume that you will grind the levels in the mines. 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: tier = floor // 50 rules = [] + weapon_rule = self.logic.combat.has_great_weapon rules.append(weapon_rule) - if self.options.tool_progression & ToolProgression.option_progressive: - rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) + tool_rule = self.logic.tool.can_mine_using(ToolMaterial.tiers[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. if self.content.features.skill_progression.is_progressive: diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index ba593c08..8292325a 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -8,12 +8,11 @@ from .received_logic import ReceivedLogicMixin from .region_logic import RegionLogicMixin from .season_logic import SeasonLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin -from ..options import ToolProgression from ..stardew_rule import StardewRule, True_, False_ 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.tool_names import ToolMaterial, Tool +from ..strings.tool_names import ToolMaterial, Tool, APTool fishing_rod_prices = { 3: 1800, @@ -57,10 +56,10 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi if material == ToolMaterial.basic or tool == Tool.scythe: 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]) - 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: has_base_pan = self.logic.received("Glittering Boulder Removed") & self.logic.region.can_reach(Region.mountain) if material == ToolMaterial.copper: @@ -69,6 +68,20 @@ class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixi 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: 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: 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 self.content.features.tool_progression.is_progressive: + return self.logic.received(APTool.fishing_rod, level) 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. diff --git a/worlds/stardew_valley/mods/logic/item_logic.py b/worlds/stardew_valley/mods/logic/item_logic.py index 12e824d2..fd87a4a0 100644 --- a/worlds/stardew_valley/mods/logic/item_logic.py +++ b/worlds/stardew_valley/mods/logic/item_logic.py @@ -1,7 +1,6 @@ from typing import Dict, Union from ..mod_data import ModNames -from ... import options from ...logic.base_logic import BaseLogicMixin, BaseLogic from ...logic.combat_logic import CombatLogicMixin from ...logic.cooking_logic import CookingLogicMixin @@ -80,7 +79,7 @@ FarmingLogicMixin]]): # Gingerbread House } - if self.options.tool_progression & options.ToolProgression.option_progressive: + if self.content.features.tool_progression.is_progressive: 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 }) diff --git a/worlds/stardew_valley/options/options.py b/worlds/stardew_valley/options/options.py index aaeeedd1..5cfdfcf9 100644 --- a/worlds/stardew_valley/options/options.py +++ b/worlds/stardew_valley/options/options.py @@ -247,6 +247,14 @@ class ToolProgression(Choice): option_progressive_cheap = 0b011 # 3 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): """Shuffle the elevator? diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index 54afc31e..01acc7b8 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -19,9 +19,8 @@ from .logic.logic import StardewLogic from .logic.time_logic import MAX_MONTHS from .logic.tool_logic import tool_upgrade_prices from .mods.mod_data import ModNames -from .options import StardewValleyOptions, Walnutsanity -from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ - Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity +from .options import BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ + Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, StardewValleyOptions, Walnutsanity from .stardew_rule import And, StardewRule, true_ from .stardew_rule.indirect_connection import look_for_indirect_connection from .stardew_rule.rule_explain import explain @@ -69,7 +68,7 @@ def set_rules(world): set_entrance_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_bundle_rules(bundle_rooms, 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)) -def set_tool_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): - if not world_options.tool_progression & ToolProgression.option_progressive: +def set_tool_rules(logic: StardewLogic, multiworld, player, content: StardewContent): + if not content.features.tool_progression.is_progressive: return 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) -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): 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) @@ -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) +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): upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material]) set_entrance_rule(multiworld, player, entrance_name, upgrade_rule) diff --git a/worlds/stardew_valley/strings/entrance_names.py b/worlds/stardew_valley/strings/entrance_names.py index b1c84004..bad46c42 100644 --- a/worlds/stardew_valley/strings/entrance_names.py +++ b/worlds/stardew_valley/strings/entrance_names.py @@ -194,10 +194,15 @@ class LogicEntrance: island_cooking = "Island Cooking" shipping = "Use Shipping Bin" watch_queen_of_sauce = "Watch Queen of Sauce" - blacksmith_copper = "Upgrade Copper Tools" - blacksmith_iron = "Upgrade Iron Tools" - blacksmith_gold = "Upgrade Gold Tools" - blacksmith_iridium = "Upgrade Iridium Tools" + + @staticmethod + def blacksmith_upgrade(material: str) -> str: + 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_summer_crops = "Grow Summer Crops" diff --git a/worlds/stardew_valley/strings/region_names.py b/worlds/stardew_valley/strings/region_names.py index 2bbc6228..567f1315 100644 --- a/worlds/stardew_valley/strings/region_names.py +++ b/worlds/stardew_valley/strings/region_names.py @@ -159,10 +159,15 @@ class LogicRegion: kitchen = "Kitchen" shipping = "Shipping" queen_of_sauce = "The Queen of Sauce" - blacksmith_copper = "Blacksmith Copper Upgrades" - blacksmith_iron = "Blacksmith Iron Upgrades" - blacksmith_gold = "Blacksmith Gold Upgrades" - blacksmith_iridium = "Blacksmith Iridium Upgrades" + + @staticmethod + def blacksmith_upgrade(material: str) -> str: + 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" summer_farming = "Summer Farming" diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 56f338fe..38882136 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -5,8 +5,8 @@ from . import SVTestBase from .. import items, location_table, options from ..items import Group from ..locations import LocationTags -from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, ToolProgression, \ - SkillProgression, Booksanity, Walnutsanity +from ..options import Friendsanity, SpecialOrderLocations, Shipsanity, Chefsanity, SeasonRandomization, Craftsanity, ExcludeGingerIsland, SkillProgression, \ + Booksanity, Walnutsanity from ..strings.region_names import Region @@ -320,7 +320,7 @@ class TestProgressiveElevator(SVTestBase): class TestSkullCavernLogic(SVTestBase): options = { 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, } diff --git a/worlds/stardew_valley/test/TestOptionFlags.py b/worlds/stardew_valley/test/TestOptionFlags.py index 05e52b40..88f2257c 100644 --- a/worlds/stardew_valley/test/TestOptionFlags.py +++ b/worlds/stardew_valley/test/TestOptionFlags.py @@ -9,7 +9,7 @@ class TestBitFlagsVanilla(SVTestBase): def test_options_are_not_detected_as_progressive(self): 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 self.assertFalse(tool_progressive) self.assertFalse(building_progressive) @@ -26,7 +26,7 @@ class TestBitFlagsVanillaCheap(SVTestBase): def test_options_are_not_detected_as_progressive(self): 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 self.assertFalse(tool_progressive) self.assertFalse(building_progressive) @@ -43,7 +43,7 @@ class TestBitFlagsVanillaVeryCheap(SVTestBase): def test_options_are_not_detected_as_progressive(self): 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 self.assertFalse(tool_progressive) self.assertFalse(building_progressive) @@ -60,7 +60,7 @@ class TestBitFlagsProgressive(SVTestBase): def test_options_are_detected_as_progressive(self): 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 self.assertTrue(tool_progressive) self.assertTrue(building_progressive) @@ -77,7 +77,7 @@ class TestBitFlagsProgressiveCheap(SVTestBase): def test_options_are_detected_as_progressive(self): 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 self.assertTrue(tool_progressive) self.assertTrue(building_progressive) @@ -94,7 +94,7 @@ class TestBitFlagsProgressiveVeryCheap(SVTestBase): def test_options_are_detected_as_progressive(self): 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 self.assertTrue(tool_progressive) self.assertTrue(building_progressive) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 2cd83f01..06bbfd45 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,17 +1,17 @@ import itertools +from BaseClasses import ItemClassification 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 .long.option_names import all_option_choices from .. import items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table -from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations, \ - SkillProgression +from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations from ..strings.goal_names import Goal as GoalName from ..strings.season_names import Season 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} TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} @@ -77,52 +77,30 @@ class TestSeasonRandomization(SVTestCase): self.assertEqual(items.count(Season.progressive), 3) -class TestToolProgression(SVTestCase): - def test_given_vanilla_when_generate_then_no_tool_in_pool(self): - world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} - 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") +class TestToolProgression(SVTestBase): + options = { + ToolProgression.internal_name: ToolProgression.option_progressive, + } def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): - world_options = {ToolProgression.internal_name: ToolProgression.option_progressive} - with solo_multiworld(world_options) as (multi_world, _): - locations = {locations.name for locations in multi_world.get_locations(1)} - for material, tool in itertools.product(ToolMaterial.tiers.values(), - [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): - if material == ToolMaterial.basic: - continue - self.assertIn(f"{material} {tool} Upgrade", locations) - self.assertIn("Purchase Training Rod", locations) - self.assertIn("Bamboo Pole Cutscene", locations) - self.assertIn("Purchase Fiberglass Rod", locations) - self.assertIn("Purchase Iridium Rod", locations) + locations = set(self.get_real_location_names()) + for material, tool in itertools.product(ToolMaterial.tiers.values(), + [Tool.hoe, Tool.pickaxe, Tool.axe, Tool.watering_can, Tool.trash_can]): + if material == ToolMaterial.basic: + continue + self.assertIn(f"{material} {tool} Upgrade", locations) + self.assertIn("Purchase Training Rod", locations) + self.assertIn("Bamboo Pole Cutscene", 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): diff --git a/worlds/stardew_valley/test/TestWalnutsanity.py b/worlds/stardew_valley/test/TestWalnutsanity.py index 862553de..e3f06bf1 100644 --- a/worlds/stardew_valley/test/TestWalnutsanity.py +++ b/worlds/stardew_valley/test/TestWalnutsanity.py @@ -1,5 +1,5 @@ from . import SVTestBase -from ..options import ExcludeGingerIsland, Walnutsanity +from ..options import ExcludeGingerIsland, Walnutsanity, ToolProgression, SkillProgression from ..strings.ap_names.ap_option_names import WalnutsanityOptionName @@ -7,6 +7,8 @@ class TestWalnutsanityNone(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: Walnutsanity.preset_none, + SkillProgression: ToolProgression.option_progressive, + ToolProgression: ToolProgression.option_progressive, } def test_no_walnut_locations(self): @@ -50,6 +52,8 @@ class TestWalnutsanityPuzzles(SVTestBase): options = { ExcludeGingerIsland: ExcludeGingerIsland.option_false, Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}), + SkillProgression: ToolProgression.option_progressive, + ToolProgression: ToolProgression.option_progressive, } def test_only_puzzle_walnut_locations(self): diff --git a/worlds/stardew_valley/test/content/__init__.py b/worlds/stardew_valley/test/content/__init__.py index c666a3aa..0832c2e3 100644 --- a/worlds/stardew_valley/test/content/__init__.py +++ b/worlds/stardew_valley/test/content/__init__.py @@ -9,6 +9,7 @@ default_features = StardewFeatures( feature.fishsanity.FishsanityNone(), feature.friendsanity.FriendsanityNone(), feature.skill_progression.SkillProgressionVanilla(), + feature.tool_progression.ToolProgressionVanilla() ) diff --git a/worlds/stardew_valley/test/content/feature/TestToolProgression.py b/worlds/stardew_valley/test/content/feature/TestToolProgression.py new file mode 100644 index 00000000..618c78dd --- /dev/null +++ b/worlds/stardew_valley/test/content/feature/TestToolProgression.py @@ -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, + })