Stardew Valley: Refactor skill progression to use new feature system (#3662)

* create a first draft of the feature

* use feature in items and locations

* add content to more places

* use feature in logic

* replace option check by feature

* remove unused code

* remove weird white space

* some import nitpicking

* flip negative if
This commit is contained in:
Jouramie 2024-11-30 21:52:07 -05:00 committed by GitHub
parent f735416bda
commit a67688749f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 244 additions and 181 deletions

View File

@ -148,7 +148,7 @@ class StardewValleyWorld(World):
region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits] region.exits = [Entrance(self.player, exit_name, region) for exit_name in exits]
return region return region
world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options) world_regions, world_entrances, self.randomized_entrances = create_regions(create_region, self.random, self.options, self.content)
self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys()) self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
self.modified_bundles = get_all_bundles(self.random, self.modified_bundles = get_all_bundles(self.random,
@ -184,7 +184,7 @@ class StardewValleyWorld(World):
self.multiworld.itempool += created_items self.multiworld.itempool += created_items
setup_early_items(self.multiworld, self.options, self.player, self.random) setup_early_items(self.multiworld, self.options, self.content, self.player, self.random)
self.setup_player_events() self.setup_player_events()
self.setup_victory() self.setup_victory()

View File

@ -3,8 +3,8 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from ..content import StardewContent from ..content import StardewContent, content_packs
from ..options import StardewValleyOptions, ExcludeGingerIsland, FestivalLocations, SkillProgression from ..options import StardewValleyOptions, FestivalLocations
from ..strings.crop_names import Fruit from ..strings.crop_names import Fruit
from ..strings.currency_names import Currency from ..strings.currency_names import Currency
from ..strings.quality_names import CropQuality, FishQuality, ForageQuality from ..strings.quality_names import CropQuality, FishQuality, ForageQuality
@ -12,34 +12,35 @@ from ..strings.quality_names import CropQuality, FishQuality, ForageQuality
class BundleItemSource(ABC): class BundleItemSource(ABC):
@abstractmethod @abstractmethod
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
... ...
class VanillaItemSource(BundleItemSource): class VanillaItemSource(BundleItemSource):
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
return True return True
class IslandItemSource(BundleItemSource): class IslandItemSource(BundleItemSource):
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
return options.exclude_ginger_island == ExcludeGingerIsland.option_false return content_packs.ginger_island_content_pack.name in content.registered_packs
class FestivalItemSource(BundleItemSource): class FestivalItemSource(BundleItemSource):
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
return options.festival_locations != FestivalLocations.option_disabled return options.festival_locations != FestivalLocations.option_disabled
# FIXME remove this once recipes are in content packs
class MasteryItemSource(BundleItemSource): class MasteryItemSource(BundleItemSource):
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
return options.skill_progression == SkillProgression.option_progressive_with_masteries return content.features.skill_progression.are_masteries_shuffled
class ContentItemSource(BundleItemSource): class ContentItemSource(BundleItemSource):
"""This is meant to be used for items that are managed by the content packs.""" """This is meant to be used for items that are managed by the content packs."""
def can_appear(self, options: StardewValleyOptions) -> bool: def can_appear(self, content: StardewContent, options: StardewValleyOptions) -> bool:
raise ValueError("This should not be called, check if the item is in the content instead.") raise ValueError("This should not be called, check if the item is in the content instead.")
@ -97,5 +98,4 @@ class BundleItem:
if isinstance(self.source, ContentItemSource): if isinstance(self.source, ContentItemSource):
return self.get_item() in content.game_items return self.get_item() in content.game_items
return self.source.can_appear(options) return self.source.can_appear(content, options)

View File

@ -1,5 +1,5 @@
from . import content_packs from . import content_packs
from .feature import cropsanity, friendsanity, fishsanity, booksanity from .feature import cropsanity, friendsanity, fishsanity, booksanity, skill_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
@ -31,7 +31,8 @@ def choose_features(player_options: options.StardewValleyOptions) -> StardewFeat
choose_booksanity(player_options.booksanity), choose_booksanity(player_options.booksanity),
choose_cropsanity(player_options.cropsanity), choose_cropsanity(player_options.cropsanity),
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),
) )
@ -105,3 +106,19 @@ def choose_friendsanity(friendsanity_option: options.Friendsanity, heart_size: o
return friendsanity.FriendsanityAllWithMarriage(heart_size.value) return friendsanity.FriendsanityAllWithMarriage(heart_size.value)
raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}") raise ValueError(f"No friendsanity feature mapped to {str(friendsanity_option.value)}")
skill_progression_by_option = {
options.SkillProgression.option_vanilla: skill_progression.SkillProgressionVanilla(),
options.SkillProgression.option_progressive: skill_progression.SkillProgressionProgressive(),
options.SkillProgression.option_progressive_with_masteries: skill_progression.SkillProgressionProgressiveWithMasteries(),
}
def choose_skill_progression(skill_progression_option: options.SkillProgression) -> skill_progression.SkillProgressionFeature:
skill_progression_feature = skill_progression_by_option.get(skill_progression_option)
if skill_progression_feature is None:
raise ValueError(f"No skill progression feature mapped to {str(skill_progression_option.value)}")
return skill_progression_feature

View File

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

View File

@ -0,0 +1,46 @@
from abc import ABC, abstractmethod
from typing import ClassVar, Iterable, Tuple
from ...data.skill import Skill
class SkillProgressionFeature(ABC):
is_progressive: ClassVar[bool]
are_masteries_shuffled: ClassVar[bool]
@abstractmethod
def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]:
...
@abstractmethod
def is_mastery_randomized(self, skill: Skill) -> bool:
...
class SkillProgressionVanilla(SkillProgressionFeature):
is_progressive = False
are_masteries_shuffled = False
def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]:
return ()
def is_mastery_randomized(self, skill: Skill) -> bool:
return False
class SkillProgressionProgressive(SkillProgressionFeature):
is_progressive = True
are_masteries_shuffled = False
def get_randomized_level_names_by_level(self, skill: Skill) -> Iterable[Tuple[int, str]]:
return skill.level_names_by_level
def is_mastery_randomized(self, skill: Skill) -> bool:
return False
class SkillProgressionProgressiveWithMasteries(SkillProgressionProgressive):
are_masteries_shuffled = True
def is_mastery_randomized(self, skill: Skill) -> bool:
return skill.has_mastery

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 from .feature import booksanity, cropsanity, fishsanity, friendsanity, skill_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
@ -53,6 +53,7 @@ class StardewFeatures:
cropsanity: cropsanity.CropsanityFeature cropsanity: cropsanity.CropsanityFeature
fishsanity: fishsanity.FishsanityFeature fishsanity: fishsanity.FishsanityFeature
friendsanity: friendsanity.FriendsanityFeature friendsanity: friendsanity.FriendsanityFeature
skill_progression: skill_progression.SkillProgressionFeature
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@ -1,7 +1,21 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property
from typing import Iterable, Tuple
@dataclass(frozen=True) @dataclass(frozen=True)
class Skill: class Skill:
name: str name: str
has_mastery: bool = field(kw_only=True) has_mastery: bool = field(kw_only=True)
@cached_property
def mastery_name(self) -> str:
return f"{self.name} Mastery"
@cached_property
def level_name(self) -> str:
return f"{self.name} Level"
@cached_property
def level_names_by_level(self) -> Iterable[Tuple[int, str]]:
return tuple((level, f"Level {level} {self.name}") for level in range(1, 11))

View File

@ -1,11 +1,13 @@
from random import Random from random import Random
from . import options as stardew_options from . import options as stardew_options
from .content import StardewContent
from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.ap_weapon_names import APWeapon
from .strings.ap_names.transport_names import Transportation from .strings.ap_names.transport_names import Transportation
from .strings.building_names import Building from .strings.building_names import Building
from .strings.region_names import Region from .strings.region_names import Region
from .strings.season_names import Season from .strings.season_names import Season
from .strings.skill_names import Skill
from .strings.tv_channel_names import Channel from .strings.tv_channel_names import Channel
from .strings.wallet_item_names import Wallet from .strings.wallet_item_names import Wallet
@ -14,7 +16,7 @@ always_early_candidates = [Region.greenhouse, Transportation.desert_obelisk, Wal
seasons = [Season.spring, Season.summer, Season.fall, Season.winter] seasons = [Season.spring, Season.summer, Season.fall, Season.winter]
def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, player: int, random: Random): def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions, content: StardewContent, player: int, random: Random):
early_forced = [] early_forced = []
early_candidates = [] early_candidates = []
early_candidates.extend(always_early_candidates) early_candidates.extend(always_early_candidates)
@ -31,12 +33,13 @@ def setup_early_items(multiworld, options: stardew_options.StardewValleyOptions,
early_forced.append("Progressive Backpack") early_forced.append("Progressive Backpack")
if options.tool_progression & stardew_options.ToolProgression.option_progressive: if options.tool_progression & stardew_options.ToolProgression.option_progressive:
if options.fishsanity != stardew_options.Fishsanity.option_none: 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")
if options.skill_progression == stardew_options.SkillProgression.option_progressive: fishing = content.skills.get(Skill.fishing)
early_forced.append("Fishing Level") if fishing is not None and content.features.skill_progression.is_progressive:
early_forced.append(fishing.level_name)
if options.quest_locations >= 0: if options.quest_locations >= 0:
early_candidates.append(Wallet.magnifying_glass) early_candidates.append(Wallet.magnifying_glass)

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, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \ BuildingProgression, ToolProgression, 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 OptionName from .strings.ap_names.ap_option_names import OptionName
from .strings.ap_names.ap_weapon_names import APWeapon from .strings.ap_names.ap_weapon_names import APWeapon
@ -226,8 +226,8 @@ 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, items) create_tools(item_factory, options, content, items)
create_skills(item_factory, options, 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)
items.append(item_factory("Railroad Boulder Removed")) items.append(item_factory("Railroad Boulder Removed"))
@ -316,7 +316,7 @@ 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, items: List[Item]): def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]):
if options.tool_progression & ToolProgression.option_progressive: if options.tool_progression & ToolProgression.option_progressive:
for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]: for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]:
name = item_data.name name = item_data.name
@ -325,28 +325,29 @@ def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions
items.append(item_factory(item_data, ItemClassification.useful)) items.append(item_factory(item_data, ItemClassification.useful))
else: else:
items.extend([item_factory(item) for item in [item_data] * 4]) items.extend([item_factory(item) for item in [item_data] * 4])
if options.skill_progression == SkillProgression.option_progressive_with_masteries:
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 Scythe"))
items.append(item_factory("Progressive Fishing Rod")) items.append(item_factory("Progressive Fishing Rod"))
# The golden scythe is always randomized
items.append(item_factory("Progressive Scythe")) items.append(item_factory("Progressive Scythe"))
def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): def create_skills(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
if options.skill_progression == SkillProgression.option_vanilla: skill_progression = content.features.skill_progression
if not skill_progression.is_progressive:
return return
for item in items_by_group[Group.SKILL_LEVEL_UP]: for skill in content.skills.values():
if item.mod_name not in options.mods and item.mod_name is not None: items.extend(item_factory(skill.level_name) for _ in skill_progression.get_randomized_level_names_by_level(skill))
continue
items.extend(item_factory(item) for item in [item.name] * 10)
if options.skill_progression != SkillProgression.option_progressive_with_masteries: if skill_progression.is_mastery_randomized(skill):
return items.append(item_factory(skill.mastery_name))
for item in items_by_group[Group.SKILL_MASTERY]: if skill_progression.are_masteries_shuffled:
if item.mod_name not in options.mods and item.mod_name is not None: items.append(item_factory(Wallet.mastery_of_the_five_ways))
continue
items.append(item_factory(item))
def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]): def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, 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, SkillProgression, BuildingProgression, ToolProgression, ElevatorProgression, BackpackProgression, FarmType FestivalLocations, BuildingProgression, ToolProgression, 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
@ -188,12 +188,12 @@ def extend_cropsanity_locations(randomized_locations: List[LocationData], conten
for item in content.find_tagged_items(ItemTag.CROPSANITY)) for item in content.find_tagged_items(ItemTag.CROPSANITY))
def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_quests_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
if options.quest_locations < 0: if options.quest_locations < 0:
return return
story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST] story_quest_locations = locations_by_tag[LocationTags.STORY_QUEST]
story_quest_locations = filter_disabled_locations(options, story_quest_locations) story_quest_locations = filter_disabled_locations(options, content, story_quest_locations)
randomized_locations.extend(story_quest_locations) randomized_locations.extend(story_quest_locations)
for i in range(0, options.quest_locations.value): for i in range(0, options.quest_locations.value):
@ -284,9 +284,9 @@ def extend_desert_festival_chef_locations(randomized_locations: List[LocationDat
randomized_locations.extend(locations_to_add) randomized_locations.extend(locations_to_add)
def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_special_order_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
if options.special_order_locations & SpecialOrderLocations.option_board: if options.special_order_locations & SpecialOrderLocations.option_board:
board_locations = filter_disabled_locations(options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD]) board_locations = filter_disabled_locations(options, content, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
randomized_locations.extend(board_locations) randomized_locations.extend(board_locations)
include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false include_island = options.exclude_ginger_island == ExcludeGingerIsland.option_false
@ -308,9 +308,9 @@ def extend_walnut_purchase_locations(randomized_locations: List[LocationData], o
randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE]) randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE])
def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_mandatory_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]] mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]]
filtered_mandatory_locations = filter_disabled_locations(options, mandatory_locations) filtered_mandatory_locations = filter_disabled_locations(options, content, mandatory_locations)
randomized_locations.extend(filtered_mandatory_locations) randomized_locations.extend(filtered_mandatory_locations)
@ -349,32 +349,32 @@ def extend_elevator_locations(randomized_locations: List[LocationData], options:
randomized_locations.extend(filtered_elevator_locations) randomized_locations.extend(filtered_elevator_locations)
def extend_monstersanity_locations(randomized_locations: List[LocationData], options): def extend_monstersanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
monstersanity = options.monstersanity monstersanity = options.monstersanity
if monstersanity == Monstersanity.option_none: if monstersanity == Monstersanity.option_none:
return return
if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals: if monstersanity == Monstersanity.option_one_per_monster or monstersanity == Monstersanity.option_split_goals:
monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]] monster_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_MONSTER]]
filtered_monster_locations = filter_disabled_locations(options, monster_locations) filtered_monster_locations = filter_disabled_locations(options, content, monster_locations)
randomized_locations.extend(filtered_monster_locations) randomized_locations.extend(filtered_monster_locations)
return return
goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]] goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_GOALS]]
filtered_goal_locations = filter_disabled_locations(options, goal_locations) filtered_goal_locations = filter_disabled_locations(options, content, goal_locations)
randomized_locations.extend(filtered_goal_locations) randomized_locations.extend(filtered_goal_locations)
if monstersanity != Monstersanity.option_progressive_goals: if monstersanity != Monstersanity.option_progressive_goals:
return return
progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]] progressive_goal_locations = [location for location in locations_by_tag[LocationTags.MONSTERSANITY_PROGRESSIVE_GOALS]]
filtered_progressive_goal_locations = filter_disabled_locations(options, progressive_goal_locations) filtered_progressive_goal_locations = filter_disabled_locations(options, content, progressive_goal_locations)
randomized_locations.extend(filtered_progressive_goal_locations) randomized_locations.extend(filtered_progressive_goal_locations)
def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_shipsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
shipsanity = options.shipsanity shipsanity = options.shipsanity
if shipsanity == Shipsanity.option_none: if shipsanity == Shipsanity.option_none:
return return
if shipsanity == Shipsanity.option_everything: if shipsanity == Shipsanity.option_everything:
ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]] ship_locations = [location for location in locations_by_tag[LocationTags.SHIPSANITY]]
filtered_ship_locations = filter_disabled_locations(options, ship_locations) filtered_ship_locations = filter_disabled_locations(options, content, ship_locations)
randomized_locations.extend(filtered_ship_locations) randomized_locations.extend(filtered_ship_locations)
return return
shipsanity_locations = set() shipsanity_locations = set()
@ -385,11 +385,11 @@ def extend_shipsanity_locations(randomized_locations: List[LocationData], option
if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish: if shipsanity == Shipsanity.option_full_shipment or shipsanity == Shipsanity.option_full_shipment_with_fish:
shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]}) shipsanity_locations = shipsanity_locations.union({location for location in locations_by_tag[LocationTags.SHIPSANITY_FULL_SHIPMENT]})
filtered_shipsanity_locations = filter_disabled_locations(options, list(shipsanity_locations)) filtered_shipsanity_locations = filter_disabled_locations(options, content, list(shipsanity_locations))
randomized_locations.extend(filtered_shipsanity_locations) randomized_locations.extend(filtered_shipsanity_locations)
def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_cooksanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
cooksanity = options.cooksanity cooksanity = options.cooksanity
if cooksanity == Cooksanity.option_none: if cooksanity == Cooksanity.option_none:
return return
@ -398,11 +398,11 @@ def extend_cooksanity_locations(randomized_locations: List[LocationData], option
else: else:
cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY]) cooksanity_locations = (location for location in locations_by_tag[LocationTags.COOKSANITY])
filtered_cooksanity_locations = filter_disabled_locations(options, cooksanity_locations) filtered_cooksanity_locations = filter_disabled_locations(options, content, cooksanity_locations)
randomized_locations.extend(filtered_cooksanity_locations) randomized_locations.extend(filtered_cooksanity_locations)
def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_chefsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
chefsanity = options.chefsanity chefsanity = options.chefsanity
if chefsanity == Chefsanity.option_none: if chefsanity == Chefsanity.option_none:
return return
@ -418,16 +418,16 @@ def extend_chefsanity_locations(randomized_locations: List[LocationData], option
if chefsanity & Chefsanity.option_skills: if chefsanity & Chefsanity.option_skills:
chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]}) chefsanity_locations_by_name.update({location.name: location for location in locations_by_tag[LocationTags.CHEFSANITY_SKILL]})
filtered_chefsanity_locations = filter_disabled_locations(options, list(chefsanity_locations_by_name.values())) filtered_chefsanity_locations = filter_disabled_locations(options, content, list(chefsanity_locations_by_name.values()))
randomized_locations.extend(filtered_chefsanity_locations) randomized_locations.extend(filtered_chefsanity_locations)
def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions): def extend_craftsanity_locations(randomized_locations: List[LocationData], options: StardewValleyOptions, content: StardewContent):
if options.craftsanity == Craftsanity.option_none: if options.craftsanity == Craftsanity.option_none:
return return
craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]] craftsanity_locations = [craft for craft in locations_by_tag[LocationTags.CRAFTSANITY]]
filtered_craftsanity_locations = filter_disabled_locations(options, craftsanity_locations) filtered_craftsanity_locations = filter_disabled_locations(options, content, craftsanity_locations)
randomized_locations.extend(filtered_craftsanity_locations) randomized_locations.extend(filtered_craftsanity_locations)
@ -467,7 +467,7 @@ def create_locations(location_collector: StardewLocationCollector,
random: Random): random: Random):
randomized_locations = [] randomized_locations = []
extend_mandatory_locations(randomized_locations, options) extend_mandatory_locations(randomized_locations, options, content)
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)
@ -476,13 +476,12 @@ def create_locations(location_collector: StardewLocationCollector,
extend_elevator_locations(randomized_locations, options) extend_elevator_locations(randomized_locations, options)
if not options.skill_progression == SkillProgression.option_vanilla: skill_progression = content.features.skill_progression
for location in locations_by_tag[LocationTags.SKILL_LEVEL]: if skill_progression.is_progressive:
if location.mod_name is not None and location.mod_name not in options.mods: for skill in content.skills.values():
continue randomized_locations.extend([location_table[location_name] for _, location_name in skill_progression.get_randomized_level_names_by_level(skill)])
if LocationTags.MASTERY_LEVEL in location.tags and options.skill_progression != SkillProgression.option_progressive_with_masteries: if skill_progression.is_mastery_randomized(skill):
continue randomized_locations.append(location_table[skill.mastery_name])
randomized_locations.append(location_table[location.name])
if options.building_progression & BuildingProgression.option_progressive: if options.building_progression & BuildingProgression.option_progressive:
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]: for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
@ -501,15 +500,15 @@ def create_locations(location_collector: StardewLocationCollector,
extend_friendsanity_locations(randomized_locations, content) extend_friendsanity_locations(randomized_locations, content)
extend_festival_locations(randomized_locations, options, random) extend_festival_locations(randomized_locations, options, random)
extend_special_order_locations(randomized_locations, options) extend_special_order_locations(randomized_locations, options, content)
extend_walnut_purchase_locations(randomized_locations, options) extend_walnut_purchase_locations(randomized_locations, options)
extend_monstersanity_locations(randomized_locations, options) extend_monstersanity_locations(randomized_locations, options, content)
extend_shipsanity_locations(randomized_locations, options) extend_shipsanity_locations(randomized_locations, options, content)
extend_cooksanity_locations(randomized_locations, options) extend_cooksanity_locations(randomized_locations, options, content)
extend_chefsanity_locations(randomized_locations, options) extend_chefsanity_locations(randomized_locations, options, content)
extend_craftsanity_locations(randomized_locations, options) extend_craftsanity_locations(randomized_locations, options, content)
extend_quests_locations(randomized_locations, options) extend_quests_locations(randomized_locations, options, content)
extend_book_locations(randomized_locations, content) extend_book_locations(randomized_locations, content)
extend_walnutsanity_locations(randomized_locations, options) extend_walnutsanity_locations(randomized_locations, options)
@ -538,19 +537,21 @@ def filter_qi_order_locations(options: StardewValleyOptions, locations: Iterable
return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags) return (location for location in locations if include_qi_orders or LocationTags.REQUIRES_QI_ORDERS not in location.tags)
def filter_masteries_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: def filter_masteries_locations(content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
include_masteries = options.skill_progression == SkillProgression.option_progressive_with_masteries # FIXME Remove once recipes are handled by the content packs
return (location for location in locations if include_masteries or LocationTags.REQUIRES_MASTERIES not in location.tags) if content.features.skill_progression.are_masteries_shuffled:
return locations
return (location for location in locations if LocationTags.REQUIRES_MASTERIES not in location.tags)
def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: def filter_modded_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]:
return (location for location in locations if location.mod_name is None or location.mod_name in options.mods) return (location for location in locations if location.mod_name is None or location.mod_name in options.mods)
def filter_disabled_locations(options: StardewValleyOptions, locations: Iterable[LocationData]) -> Iterable[LocationData]: def filter_disabled_locations(options: StardewValleyOptions, content: StardewContent, locations: Iterable[LocationData]) -> Iterable[LocationData]:
locations_farm_filter = filter_farm_type(options, locations) locations_farm_filter = filter_farm_type(options, locations)
locations_island_filter = filter_ginger_island(options, locations_farm_filter) locations_island_filter = filter_ginger_island(options, locations_farm_filter)
locations_qi_filter = filter_qi_order_locations(options, locations_island_filter) locations_qi_filter = filter_qi_order_locations(options, locations_island_filter)
locations_masteries_filter = filter_masteries_locations(options, locations_qi_filter) locations_masteries_filter = filter_masteries_locations(content, locations_qi_filter)
locations_mod_filter = filter_modded_locations(options, locations_masteries_filter) locations_mod_filter = filter_modded_locations(options, locations_masteries_filter)
return locations_mod_filter return locations_mod_filter

View File

@ -16,7 +16,7 @@ from ..data.craftable_data import CraftingRecipe, all_crafting_recipes_by_name
from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \ from ..data.recipe_source import CutsceneSource, ShopTradeSource, ArchipelagoSource, LogicSource, SpecialOrderSource, \
FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource FestivalShopSource, QuestSource, StarterSource, ShopSource, SkillSource, MasterySource, FriendshipSource, SkillCraftsanitySource
from ..locations import locations_by_tag, LocationTags from ..locations import locations_by_tag, LocationTags
from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland, SkillProgression from ..options import Craftsanity, SpecialOrderLocations, ExcludeGingerIsland
from ..stardew_rule import StardewRule, True_, False_ from ..stardew_rule import StardewRule, True_, False_
from ..strings.region_names import Region from ..strings.region_names import Region
@ -101,12 +101,13 @@ SkillLogicMixin, SpecialOrderLogicMixin, CraftingLogicMixin, QuestLogicMixin]]):
craftsanity_prefix = "Craft " craftsanity_prefix = "Craft "
all_recipes_names = [] all_recipes_names = []
exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true exclude_island = self.options.exclude_ginger_island == ExcludeGingerIsland.option_true
exclude_masteries = self.options.skill_progression != SkillProgression.option_progressive_with_masteries exclude_masteries = not self.content.features.skill_progression.are_masteries_shuffled
for location in locations_by_tag[LocationTags.CRAFTSANITY]: for location in locations_by_tag[LocationTags.CRAFTSANITY]:
if not location.name.startswith(craftsanity_prefix): if not location.name.startswith(craftsanity_prefix):
continue continue
if exclude_island and LocationTags.GINGER_ISLAND in location.tags: if exclude_island and LocationTags.GINGER_ISLAND in location.tags:
continue continue
# FIXME Remove when recipes are in content packs
if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags: if exclude_masteries and LocationTags.REQUIRES_MASTERIES in location.tags:
continue continue
if location.mod_name and location.mod_name not in self.options.mods: if location.mod_name and location.mod_name not in self.options.mods:

View File

@ -7,7 +7,6 @@ from .has_logic import HasLogicMixin
from .received_logic import ReceivedLogicMixin from .received_logic import ReceivedLogicMixin
from .region_logic import RegionLogicMixin from .region_logic import RegionLogicMixin
from .time_logic import TimeLogicMixin from .time_logic import TimeLogicMixin
from ..options import Booksanity
from ..stardew_rule import StardewRule, HasProgressionPercent from ..stardew_rule import StardewRule, HasProgressionPercent
from ..strings.book_names import Book from ..strings.book_names import Book
from ..strings.craftable_names import Consumable from ..strings.craftable_names import Consumable
@ -39,7 +38,7 @@ class GrindLogic(BaseLogic[Union[GrindLogicMixin, HasLogicMixin, ReceivedLogicMi
opening_rule = self.logic.region.can_reach(Region.blacksmith) opening_rule = self.logic.region.can_reach(Region.blacksmith)
mystery_box_rule = self.logic.has(Consumable.mystery_box) mystery_box_rule = self.logic.has(Consumable.mystery_box)
book_of_mysteries_rule = self.logic.true_ \ book_of_mysteries_rule = self.logic.true_ \
if self.options.booksanity == Booksanity.option_none \ if not self.content.features.booksanity.is_enabled \
else self.logic.book.has_book_power(Book.book_of_mysteries) else self.logic.book.has_book_power(Book.book_of_mysteries)
# Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride. # Assuming one box per day, but halved because we don't know how many months have passed before Mr. Qi's Plane Ride.
time_rule = self.logic.time.has_lived_months(quantity // 14) time_rule = self.logic.time.has_lived_months(quantity // 14)

View File

@ -58,14 +58,19 @@ SkillLogicMixin, CookingLogicMixin]]):
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: if self.options.tool_progression & ToolProgression.option_progressive:
rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier])) rules.append(self.logic.tool.has_tool(Tool.pickaxe, ToolMaterial.tiers[tier]))
if self.options.skill_progression >= options.SkillProgression.option_progressive:
skill_tier = min(10, max(0, tier * 2)) # No alternative for vanilla because we assume that you will grind the levels in the mines.
rules.append(self.logic.skill.has_level(Skill.combat, skill_tier)) if self.content.features.skill_progression.is_progressive:
rules.append(self.logic.skill.has_level(Skill.mining, skill_tier)) skill_level = min(10, max(0, tier * 2))
rules.append(self.logic.skill.has_level(Skill.combat, skill_level))
rules.append(self.logic.skill.has_level(Skill.mining, skill_level))
if tier >= 4: if tier >= 4:
rules.append(self.logic.cooking.can_cook()) rules.append(self.logic.cooking.can_cook())
return self.logic.and_(*rules) return self.logic.and_(*rules)
@cache_self1 @cache_self1
@ -82,10 +87,14 @@ SkillLogicMixin, CookingLogicMixin]]):
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: if self.options.tool_progression & ToolProgression.option_progressive:
rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2)))) rules.append(self.logic.received("Progressive Pickaxe", min(4, max(0, tier + 2))))
if self.options.skill_progression >= options.SkillProgression.option_progressive:
skill_tier = min(10, max(0, tier * 2 + 6)) # No alternative for vanilla because we assume that you will grind the levels in the mines.
rules.extend({self.logic.skill.has_level(Skill.combat, skill_tier), if self.content.features.skill_progression.is_progressive:
self.logic.skill.has_level(Skill.mining, skill_tier)}) skill_level = min(10, max(0, tier * 2 + 6))
rules.extend((self.logic.skill.has_level(Skill.combat, skill_level),
self.logic.skill.has_level(Skill.mining, skill_level)))
return self.logic.and_(*rules) return self.logic.and_(*rules)

View File

@ -11,7 +11,6 @@ from .region_logic import RegionLogicMixin
from .season_logic import SeasonLogicMixin from .season_logic import SeasonLogicMixin
from .time_logic import TimeLogicMixin from .time_logic import TimeLogicMixin
from .tool_logic import ToolLogicMixin from .tool_logic import ToolLogicMixin
from .. import options
from ..data.harvest import HarvestCropSource from ..data.harvest import HarvestCropSource
from ..mods.logic.magic_logic import MagicLogicMixin from ..mods.logic.magic_logic import MagicLogicMixin
from ..mods.logic.mod_skills_levels import get_mod_skill_levels from ..mods.logic.mod_skills_levels import get_mod_skill_levels
@ -77,21 +76,21 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
if level == 0: if level == 0:
return true_ return true_
if self.options.skill_progression == options.SkillProgression.option_vanilla: if self.content.features.skill_progression.is_progressive:
return self.logic.skill.can_earn_level(skill, level) return self.logic.received(f"{skill} Level", level)
return self.logic.received(f"{skill} Level", level) return self.logic.skill.can_earn_level(skill, level)
def has_previous_level(self, skill: str, level: int) -> StardewRule: def has_previous_level(self, skill: str, level: int) -> StardewRule:
assert level > 0, f"There is no level before level 0." assert level > 0, f"There is no level before level 0."
if level == 1: if level == 1:
return true_ return true_
if self.options.skill_progression == options.SkillProgression.option_vanilla: if self.content.features.skill_progression.is_progressive:
months = max(1, level - 1) return self.logic.received(f"{skill} Level", level - 1)
return self.logic.time.has_lived_months(months)
return self.logic.received(f"{skill} Level", level - 1) months = max(1, level - 1)
return self.logic.time.has_lived_months(months)
@cache_self1 @cache_self1
def has_farming_level(self, level: int) -> StardewRule: def has_farming_level(self, level: int) -> StardewRule:
@ -102,7 +101,7 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
if level <= 0: if level <= 0:
return True_() return True_()
if self.options.skill_progression >= options.SkillProgression.option_progressive: if self.content.features.skill_progression.is_progressive:
skills_items = vanilla_skill_items skills_items = vanilla_skill_items
if allow_modded_skills: if allow_modded_skills:
skills_items += get_mod_skill_levels(self.options.mods) skills_items += get_mod_skill_levels(self.options.mods)
@ -148,7 +147,7 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
@cached_property @cached_property
def can_get_fishing_xp(self) -> StardewRule: def can_get_fishing_xp(self) -> StardewRule:
if self.options.skill_progression >= options.SkillProgression.option_progressive: if self.content.features.skill_progression.is_progressive:
return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot return self.logic.skill.can_fish() | self.logic.skill.can_crab_pot
return self.logic.skill.can_fish() return self.logic.skill.can_fish()
@ -178,7 +177,9 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
@cached_property @cached_property
def can_crab_pot(self) -> StardewRule: def can_crab_pot(self) -> StardewRule:
crab_pot_rule = self.logic.has(Fishing.bait) crab_pot_rule = self.logic.has(Fishing.bait)
if self.options.skill_progression >= options.SkillProgression.option_progressive:
# We can't use the same rule if skills are vanilla, because fishing levels are required to crab pot, which is required to get fishing levels...
if self.content.features.skill_progression.is_progressive:
crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot) crab_pot_rule = crab_pot_rule & self.logic.has(Machine.crab_pot)
else: else:
crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp crab_pot_rule = crab_pot_rule & self.logic.skill.can_get_fishing_xp
@ -200,14 +201,14 @@ CombatLogicMixin, MagicLogicMixin, HarvestingLogicMixin]]):
return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave) return self.logic.skill.can_earn_level(skill, 11) & self.logic.region.can_reach(Region.mastery_cave)
def has_mastery(self, skill: str) -> StardewRule: def has_mastery(self, skill: str) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: if self.content.features.skill_progression.are_masteries_shuffled:
return self.logic.received(f"{skill} Mastery") return self.logic.received(f"{skill} Mastery")
return self.logic.skill.can_earn_mastery(skill) return self.logic.skill.can_earn_mastery(skill)
@cached_property @cached_property
def can_enter_mastery_cave(self) -> StardewRule: def can_enter_mastery_cave(self) -> StardewRule:
if self.options.skill_progression == options.SkillProgression.option_progressive_with_masteries: if self.content.features.skill_progression.are_masteries_shuffled:
return self.logic.received(Wallet.mastery_of_the_five_ways) return self.logic.received(Wallet.mastery_of_the_five_ways)
return self.has_any_skills_maxed(included_modded_skills=False) return self.has_any_skills_maxed(included_modded_skills=False)

View File

@ -1,6 +1,5 @@
from typing import Union from typing import Union
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
@ -45,9 +44,9 @@ CookingLogicMixin]]):
self.logic.received(ModTransportation.woods_obelisk)) self.logic.received(ModTransportation.woods_obelisk))
tier = int(depth / 25) + 1 tier = int(depth / 25) + 1
if self.options.skill_progression >= options.SkillProgression.option_progressive: if self.content.features.skill_progression.is_progressive:
combat_tier = min(10, max(0, tier + 5)) combat_level = min(10, max(0, tier + 5))
rules.append(self.logic.skill.has_level(Skill.combat, combat_tier)) rules.append(self.logic.skill.has_level(Skill.combat, combat_level))
return self.logic.and_(*rules) return self.logic.and_(*rules)

View File

@ -13,7 +13,6 @@ from ...logic.region_logic import RegionLogicMixin
from ...logic.relationship_logic import RelationshipLogicMixin from ...logic.relationship_logic import RelationshipLogicMixin
from ...logic.tool_logic import ToolLogicMixin from ...logic.tool_logic import ToolLogicMixin
from ...mods.mod_data import ModNames from ...mods.mod_data import ModNames
from ...options import SkillProgression
from ...stardew_rule import StardewRule, False_, True_, And from ...stardew_rule import StardewRule, False_, True_, And
from ...strings.building_names import Building from ...strings.building_names import Building
from ...strings.craftable_names import ModCraftable, ModMachine from ...strings.craftable_names import ModCraftable, ModMachine
@ -37,7 +36,7 @@ ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicL
if level <= 0: if level <= 0:
return True_() return True_()
if self.options.skill_progression == SkillProgression.option_progressive: if self.content.features.skill_progression.is_progressive:
return self.logic.received(f"{skill} Level", level) return self.logic.received(f"{skill} Level", level)
return self.can_earn_mod_skill_level(skill, level) return self.can_earn_mod_skill_level(skill, level)
@ -85,13 +84,15 @@ ToolLogicMixin, FishingLogicMixin, CookingLogicMixin, CraftingLogicMixin, MagicL
def can_earn_archaeology_skill_level(self, level: int) -> StardewRule: def can_earn_archaeology_skill_level(self, level: int) -> StardewRule:
shifter_rule = True_() shifter_rule = True_()
preservation_rule = True_() preservation_rule = True_()
if self.options.skill_progression == self.options.skill_progression.option_progressive: if self.content.features.skill_progression.is_progressive:
shifter_rule = self.logic.has(ModCraftable.water_shifter) shifter_rule = self.logic.has(ModCraftable.water_shifter)
preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber) preservation_rule = self.logic.has(ModMachine.hardwood_preservation_chamber)
if level >= 8: if level >= 8:
return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)) & shifter_rule & preservation_rule tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.iridium) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.gold)
return tool_rule & shifter_rule & preservation_rule
if level >= 5: if level >= 5:
return (self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)) & shifter_rule tool_rule = self.logic.tool.has_tool(Tool.pan, ToolMaterial.gold) & self.logic.tool.has_tool(Tool.hoe, ToolMaterial.iron)
return tool_rule & shifter_rule
if level >= 3: if level >= 3:
return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper) return self.logic.tool.has_tool(Tool.pan, ToolMaterial.iron) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.copper)
return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic) return self.logic.tool.has_tool(Tool.pan, ToolMaterial.copper) | self.logic.tool.has_tool(Tool.hoe, ToolMaterial.basic)

View File

@ -2,8 +2,9 @@ from random import Random
from typing import Iterable, Dict, Protocol, List, Tuple, Set from typing import Iterable, Dict, Protocol, List, Tuple, Set
from BaseClasses import Region, Entrance from BaseClasses import Region, Entrance
from .content import content_packs, StardewContent
from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod from .mods.mod_regions import ModDataList, vanilla_connections_to_remove_by_mod
from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions, SkillProgression from .options import EntranceRandomization, ExcludeGingerIsland, StardewValleyOptions
from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag from .region_classes import RegionData, ConnectionData, RandomizationFlag, ModificationFlag
from .strings.entrance_names import Entrance, LogicEntrance from .strings.entrance_names import Entrance, LogicEntrance
from .strings.region_names import Region, LogicRegion from .strings.region_names import Region, LogicRegion
@ -587,7 +588,7 @@ def modify_vanilla_regions(existing_region: RegionData, modified_region: RegionD
return updated_region return updated_region
def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions) \ def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewValleyOptions, content: StardewContent) \
-> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]: -> Tuple[Dict[str, Region], Dict[str, Entrance], Dict[str, str]]:
entrances_data, regions_data = create_final_connections_and_regions(world_options) entrances_data, regions_data = create_final_connections_and_regions(world_options)
regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data} regions_by_name: Dict[str: Region] = {region_name: region_factory(region_name, regions_data[region_name].exits) for region_name in regions_data}
@ -598,7 +599,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options:
if entrance.name in entrances_data if entrance.name in entrances_data
} }
connections, randomized_data = randomize_connections(random, world_options, regions_data, entrances_data) connections, randomized_data = randomize_connections(random, world_options, content, regions_data, entrances_data)
for connection in connections: for connection in connections:
if connection.name in entrances_by_name: if connection.name in entrances_by_name:
@ -606,7 +607,7 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options:
return regions_by_name, entrances_by_name, randomized_data return regions_by_name, entrances_by_name, randomized_data
def randomize_connections(random: Random, world_options: StardewValleyOptions, regions_by_name: Dict[str, RegionData], def randomize_connections(random: Random, world_options: StardewValleyOptions, content: StardewContent, regions_by_name: Dict[str, RegionData],
connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]: connections_by_name: Dict[str, ConnectionData]) -> Tuple[List[ConnectionData], Dict[str, str]]:
connections_to_randomize: List[ConnectionData] = [] connections_to_randomize: List[ConnectionData] = []
if world_options.entrance_randomization == EntranceRandomization.option_pelican_town: if world_options.entrance_randomization == EntranceRandomization.option_pelican_town:
@ -621,7 +622,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r
elif world_options.entrance_randomization == EntranceRandomization.option_chaos: elif world_options.entrance_randomization == EntranceRandomization.option_chaos:
connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if connections_to_randomize = [connections_by_name[connection] for connection in connections_by_name if
RandomizationFlag.BUILDINGS in connections_by_name[connection].flag] RandomizationFlag.BUILDINGS in connections_by_name[connection].flag]
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
# On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day # On Chaos, we just add the connections to randomize, unshuffled, and the client does it every day
randomized_data_for_mod = {} randomized_data_for_mod = {}
@ -630,7 +631,7 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r
randomized_data_for_mod[connection.reverse] = connection.reverse randomized_data_for_mod[connection.reverse] = connection.reverse
return list(connections_by_name.values()), randomized_data_for_mod return list(connections_by_name.values()), randomized_data_for_mod
connections_to_randomize = remove_excluded_entrances(connections_to_randomize, world_options) connections_to_randomize = remove_excluded_entrances(connections_to_randomize, content)
random.shuffle(connections_to_randomize) random.shuffle(connections_to_randomize)
destination_pool = list(connections_to_randomize) destination_pool = list(connections_to_randomize)
random.shuffle(destination_pool) random.shuffle(destination_pool)
@ -645,12 +646,11 @@ def randomize_connections(random: Random, world_options: StardewValleyOptions, r
return randomized_connections_for_generation, randomized_data_for_mod return randomized_connections_for_generation, randomized_data_for_mod
def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], world_options: StardewValleyOptions) -> List[ConnectionData]: def remove_excluded_entrances(connections_to_randomize: List[ConnectionData], content: StardewContent) -> List[ConnectionData]:
exclude_island = world_options.exclude_ginger_island == ExcludeGingerIsland.option_true # FIXME remove when regions are handled in content packs
if exclude_island: if content_packs.ginger_island_content_pack.name not in content.registered_packs:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag] connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.GINGER_ISLAND not in connection.flag]
exclude_masteries = world_options.skill_progression != SkillProgression.option_progressive_with_masteries if not content.features.skill_progression.are_masteries_shuffled:
if exclude_masteries:
connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag] connections_to_randomize = [connection for connection in connections_to_randomize if RandomizationFlag.MASTERIES not in connection.flag]
return connections_to_randomize return connections_to_randomize

View File

@ -21,7 +21,7 @@ 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 StardewValleyOptions, Walnutsanity
from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \ from .options import ToolProgression, BuildingProgression, ExcludeGingerIsland, SpecialOrderLocations, Museumsanity, BackpackProgression, Shipsanity, \
Monstersanity, Chefsanity, Craftsanity, ArcadeMachineLocations, Cooksanity, SkillProgression 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
@ -47,7 +47,7 @@ from .strings.performance_names import Performance
from .strings.quest_names import Quest from .strings.quest_names import Quest
from .strings.region_names import Region from .strings.region_names import Region
from .strings.season_names import Season from .strings.season_names import Season
from .strings.skill_names import ModSkill, Skill from .strings.skill_names import Skill
from .strings.tool_names import Tool, ToolMaterial from .strings.tool_names import Tool, ToolMaterial
from .strings.tv_channel_names import Channel from .strings.tv_channel_names import Channel
from .strings.villager_names import NPC, ModNPC from .strings.villager_names import NPC, ModNPC
@ -70,7 +70,7 @@ def set_rules(world):
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_options)
set_skills_rules(logic, multiworld, player, world_options) 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)
set_cropsanity_rules(logic, multiworld, player, world_content) set_cropsanity_rules(logic, multiworld, player, world_content)
@ -164,58 +164,21 @@ def set_bundle_rules(bundle_rooms: List[BundleRoom], logic: StardewLogic, multiw
MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules)) MultiWorldRules.add_rule(multiworld.get_location(room_location, player), And(*room_rules))
def set_skills_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): def set_skills_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, content: StardewContent):
mods = world_options.mods skill_progression = content.features.skill_progression
if world_options.skill_progression == SkillProgression.option_vanilla: if not skill_progression.is_progressive:
return return
for i in range(1, 11): for skill in content.skills.values():
set_vanilla_skill_rule_for_level(logic, multiworld, player, i) for level, level_name in skill_progression.get_randomized_level_names_by_level(skill):
set_modded_skill_rule_for_level(logic, multiworld, player, mods, i) rule = logic.skill.can_earn_level(skill.name, level)
location = multiworld.get_location(level_name, player)
MultiWorldRules.set_rule(location, rule)
if world_options.skill_progression == SkillProgression.option_progressive: if skill_progression.is_mastery_randomized(skill):
return rule = logic.skill.can_earn_mastery(skill.name)
location = multiworld.get_location(skill.mastery_name, player)
for skill in [Skill.farming, Skill.fishing, Skill.foraging, Skill.mining, Skill.combat]: MultiWorldRules.set_rule(location, rule)
MultiWorldRules.set_rule(multiworld.get_location(f"{skill} Mastery", player), logic.skill.can_earn_mastery(skill))
def set_vanilla_skill_rule_for_level(logic: StardewLogic, multiworld, player, level: int):
set_vanilla_skill_rule(logic, multiworld, player, Skill.farming, level)
set_vanilla_skill_rule(logic, multiworld, player, Skill.fishing, level)
set_vanilla_skill_rule(logic, multiworld, player, Skill.foraging, level)
set_vanilla_skill_rule(logic, multiworld, player, Skill.mining, level)
set_vanilla_skill_rule(logic, multiworld, player, Skill.combat, level)
def set_modded_skill_rule_for_level(logic: StardewLogic, multiworld, player, mods, level: int):
if ModNames.luck_skill in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.luck, level)
if ModNames.magic in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.magic, level)
if ModNames.binning_skill in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.binning, level)
if ModNames.cooking_skill in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.cooking, level)
if ModNames.socializing_skill in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.socializing, level)
if ModNames.archaeology in mods:
set_modded_skill_rule(logic, multiworld, player, ModSkill.archaeology, level)
def get_skill_level_location(multiworld, player, skill: str, level: int):
location_name = f"Level {level} {skill}"
return multiworld.get_location(location_name, player)
def set_vanilla_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int):
rule = logic.skill.can_earn_level(skill, level)
MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule)
def set_modded_skill_rule(logic: StardewLogic, multiworld, player, skill: str, level: int):
rule = logic.skill.can_earn_level(skill, level)
MultiWorldRules.set_rule(get_skill_level_location(multiworld, player, skill, level), rule)
def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions): def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: StardewValleyOptions):

View File

@ -4,6 +4,7 @@ from typing import Set
from BaseClasses import get_seed from BaseClasses import get_seed
from . import SVTestCase, complete_options_with_default from . import SVTestCase, complete_options_with_default
from .. import create_content
from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression from ..options import EntranceRandomization, ExcludeGingerIsland, SkillProgression
from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag, create_final_connections_and_regions
from ..strings.entrance_names import Entrance as EntranceName from ..strings.entrance_names import Entrance as EntranceName
@ -63,11 +64,12 @@ class TestEntranceRando(SVTestCase):
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
}) })
content = create_content(sv_options)
seed = get_seed() seed = get_seed()
rand = random.Random(seed) rand = random.Random(seed)
with self.subTest(flag=flag, msg=f"Seed: {seed}"): with self.subTest(flag=flag, msg=f"Seed: {seed}"):
entrances, regions = create_final_connections_and_regions(sv_options) entrances, regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
for connection in vanilla_connections: for connection in vanilla_connections:
if flag in connection.flag: if flag in connection.flag:
@ -90,11 +92,12 @@ class TestEntranceRando(SVTestCase):
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_true,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
}) })
content = create_content(sv_options)
seed = get_seed() seed = get_seed()
rand = random.Random(seed) rand = random.Random(seed)
with self.subTest(option=option, flag=flag, seed=seed): with self.subTest(option=option, flag=flag, seed=seed):
entrances, regions = create_final_connections_and_regions(sv_options) entrances, regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, regions, entrances) _, randomized_connections = randomize_connections(rand, sv_options, content, regions, entrances)
for connection in vanilla_connections: for connection in vanilla_connections:
if flag in connection.flag: if flag in connection.flag:
@ -118,13 +121,14 @@ class TestEntranceRando(SVTestCase):
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
}) })
content = create_content(sv_options)
for i in range(0, 100 if self.skip_long_tests else 10000): for i in range(0, 100 if self.skip_long_tests else 10000):
seed = get_seed() seed = get_seed()
rand = random.Random(seed) rand = random.Random(seed)
with self.subTest(msg=f"Seed: {seed}"): with self.subTest(msg=f"Seed: {seed}"):
entrances, regions = create_final_connections_and_regions(sv_options) entrances, regions = create_final_connections_and_regions(sv_options)
randomized_connections, randomized_data = randomize_connections(rand, sv_options, regions, entrances) randomized_connections, randomized_data = randomize_connections(rand, sv_options, content, regions, entrances)
connections_by_name = {connection.name: connection for connection in randomized_connections} connections_by_name = {connection.name: connection for connection in randomized_connections}
blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island} blocked_entrances = {EntranceName.use_island_obelisk, EntranceName.boat_to_ginger_island}

View File

@ -7,7 +7,8 @@ default_features = StardewFeatures(
feature.booksanity.BooksanityDisabled(), feature.booksanity.BooksanityDisabled(),
feature.cropsanity.CropsanityDisabled(), feature.cropsanity.CropsanityDisabled(),
feature.fishsanity.FishsanityNone(), feature.fishsanity.FishsanityNone(),
feature.friendsanity.FriendsanityNone() feature.friendsanity.FriendsanityNone(),
feature.skill_progression.SkillProgressionVanilla(),
) )

View File

@ -3,7 +3,7 @@ import random
from BaseClasses import get_seed from BaseClasses import get_seed
from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld from .. import SVTestBase, SVTestCase, allsanity_no_mods_6_x_x, allsanity_mods_6_x_x, complete_options_with_default, solo_multiworld
from ..assertion import ModAssertMixin, WorldAssertMixin from ..assertion import ModAssertMixin, WorldAssertMixin
from ... import items, Group, ItemClassification from ... import items, Group, ItemClassification, create_content
from ... import options from ... import options
from ...items import items_by_group from ...items import items_by_group
from ...options import SkillProgression, Walnutsanity from ...options import SkillProgression, Walnutsanity
@ -128,12 +128,13 @@ class TestModEntranceRando(SVTestCase):
SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries, SkillProgression.internal_name: SkillProgression.option_progressive_with_masteries,
options.Mods.internal_name: frozenset(options.Mods.valid_keys) options.Mods.internal_name: frozenset(options.Mods.valid_keys)
}) })
content = create_content(sv_options)
seed = get_seed() seed = get_seed()
rand = random.Random(seed) rand = random.Random(seed)
with self.subTest(option=option, flag=flag, seed=seed): with self.subTest(option=option, flag=flag, seed=seed):
final_connections, final_regions = create_final_connections_and_regions(sv_options) final_connections, final_regions = create_final_connections_and_regions(sv_options)
_, randomized_connections = randomize_connections(rand, sv_options, final_regions, final_connections) _, randomized_connections = randomize_connections(rand, sv_options, content, final_regions, final_connections)
for connection_name in final_connections: for connection_name in final_connections:
connection = final_connections[connection_name] connection = final_connections[connection_name]