Stardew Valley: 4.x.x - The Ginger Update (#1931)
## What is this fixing or adding? Major content update for Stardew Valley ## How was this tested? One large-scale public Beta on the archipelago server, plus several smaller private asyncs and test runs You can go to https://github.com/agilbert1412/StardewArchipelago/releases to grab the mod (latest 4.x.x version), the supported mods and the apworld, to test this PR ## New Features: - Festival Checks [Easy mode or Hard Mode] - Special Orders [Both Board and Qi] - Willy's Boat - Ginger Island Parrots - TV Channels - Trap Items [Available in various difficulty levels] - Entrance Randomizer: Buildings and Chaos - New Fishsanity options: Exclude Legendaries, Exclude Hard fish, Only easy fish - Resource Pack overhaul [Resource packs are now more enjoyable and varied] - Goal: Greatest Walnut Hunter [Find every single Golden Walnut] - Goal: Perfection [Achieve Perfection] - Option: Profit Margin [Multiplier over all earnings] - Option: Friendsanity Heart Size [Reduce clutter from friendsanity hearts] - Option: Exclude Ginger Island - will exclude many locations and items to generate a playthrough that does not go to the island - Mod Support [Curated list of mods] ## New Contributors: @Witchybun for the mod support --------- Co-authored-by: Witchybun <embenham05@gmail.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,8 @@ from . import options, data
|
||||
from .data.fish_data import legendary_fish, special_fish, all_fish
|
||||
from .data.museum_data import all_museum_items
|
||||
from .data.villagers_data import all_villagers
|
||||
from .strings.goal_names import Goal
|
||||
from .strings.region_names import Region
|
||||
|
||||
LOCATION_CODE_OFFSET = 717000
|
||||
|
||||
@@ -32,7 +34,8 @@ class LocationTags(enum.Enum):
|
||||
TRASH_CAN_UPGRADE = enum.auto()
|
||||
FISHING_ROD_UPGRADE = enum.auto()
|
||||
THE_MINES_TREASURE = enum.auto()
|
||||
THE_MINES_ELEVATOR = enum.auto()
|
||||
CROPSANITY = enum.auto()
|
||||
ELEVATOR = enum.auto()
|
||||
SKILL_LEVEL = enum.auto()
|
||||
FARMING_LEVEL = enum.auto()
|
||||
FISHING_LEVEL = enum.auto()
|
||||
@@ -51,6 +54,19 @@ class LocationTags(enum.Enum):
|
||||
MUSEUM_MILESTONES = enum.auto()
|
||||
MUSEUM_DONATIONS = enum.auto()
|
||||
FRIENDSANITY = enum.auto()
|
||||
FESTIVAL = enum.auto()
|
||||
FESTIVAL_HARD = enum.auto()
|
||||
SPECIAL_ORDER_BOARD = enum.auto()
|
||||
SPECIAL_ORDER_QI = enum.auto()
|
||||
GINGER_ISLAND = enum.auto()
|
||||
WALNUT_PURCHASE = enum.auto()
|
||||
# Skill Mods
|
||||
LUCK_LEVEL = enum.auto()
|
||||
BINNING_LEVEL = enum.auto()
|
||||
COOKING_LEVEL = enum.auto()
|
||||
SOCIALIZING_LEVEL = enum.auto()
|
||||
MAGIC_LEVEL = enum.auto()
|
||||
ARCHAEOLOGY_LEVEL = enum.auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -58,6 +74,7 @@ class LocationData:
|
||||
code_without_offset: Optional[int]
|
||||
region: str
|
||||
name: str
|
||||
mod_name: Optional[str] = None
|
||||
tags: FrozenSet[LocationTags] = frozenset()
|
||||
|
||||
@property
|
||||
@@ -81,6 +98,7 @@ def load_location_csv() -> List[LocationData]:
|
||||
return [LocationData(int(location["id"]) if location["id"] else None,
|
||||
location["region"],
|
||||
location["name"],
|
||||
str(location["mod_name"]) if location["mod_name"] else None,
|
||||
frozenset(LocationTags[group]
|
||||
for group in location["tags"].split(",")
|
||||
if group))
|
||||
@@ -88,13 +106,15 @@ def load_location_csv() -> List[LocationData]:
|
||||
|
||||
|
||||
events_locations = [
|
||||
LocationData(None, "Stardew Valley", "Succeed Grandpa's Evaluation"),
|
||||
LocationData(None, "Community Center", "Complete Community Center"),
|
||||
LocationData(None, "The Mines - Floor 120", "Reach the Bottom of The Mines"),
|
||||
LocationData(None, "Skull Cavern", "Complete Quest Cryptic Note"),
|
||||
LocationData(None, "Stardew Valley", "Catch Every Fish"),
|
||||
LocationData(None, "Stardew Valley", "Complete the Museum Collection"),
|
||||
LocationData(None, "Stardew Valley", "Full House"),
|
||||
LocationData(None, Region.farm_house, Goal.grandpa_evaluation),
|
||||
LocationData(None, Region.community_center, Goal.community_center),
|
||||
LocationData(None, Region.mines_floor_120, Goal.bottom_of_the_mines),
|
||||
LocationData(None, Region.skull_cavern_100, Goal.cryptic_note),
|
||||
LocationData(None, Region.farm, Goal.master_angler),
|
||||
LocationData(None, Region.museum, Goal.complete_museum),
|
||||
LocationData(None, Region.farm_house, Goal.full_house),
|
||||
LocationData(None, Region.island_west, Goal.greatest_walnut_hunter),
|
||||
LocationData(None, Region.qi_walnut_room, Goal.perfection),
|
||||
]
|
||||
|
||||
all_locations = load_location_csv() + events_locations
|
||||
@@ -113,6 +133,15 @@ def initialize_groups():
|
||||
initialize_groups()
|
||||
|
||||
|
||||
def extend_cropsanity_locations(randomized_locations: List[LocationData], world_options):
|
||||
if world_options[options.Cropsanity] == options.Cropsanity.option_disabled:
|
||||
return
|
||||
|
||||
cropsanity_locations = locations_by_tag[LocationTags.CROPSANITY]
|
||||
cropsanity_locations = filter_ginger_island(world_options, cropsanity_locations)
|
||||
randomized_locations.extend(cropsanity_locations)
|
||||
|
||||
|
||||
def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_number_of_quests: int):
|
||||
for i in range(0, desired_number_of_quests):
|
||||
batch = i // 7
|
||||
@@ -128,19 +157,29 @@ def extend_help_wanted_quests(randomized_locations: List[LocationData], desired_
|
||||
randomized_locations.append(location_table[f"Help Wanted: Gathering {batch + 1}"])
|
||||
|
||||
|
||||
def extend_fishsanity_locations(randomized_locations: List[LocationData], fishsanity: int, random: Random):
|
||||
def extend_fishsanity_locations(randomized_locations: List[LocationData], world_options, random: Random):
|
||||
prefix = "Fishsanity: "
|
||||
if fishsanity == options.Fishsanity.option_none:
|
||||
if world_options[options.Fishsanity] == options.Fishsanity.option_none:
|
||||
return
|
||||
elif fishsanity == options.Fishsanity.option_legendaries:
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_legendaries:
|
||||
randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish)
|
||||
elif fishsanity == options.Fishsanity.option_special:
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_special:
|
||||
randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish)
|
||||
elif fishsanity == options.Fishsanity.option_randomized:
|
||||
randomized_locations.extend(location_table[f"{prefix}{fish.name}"]
|
||||
for fish in all_fish if random.random() < 0.4)
|
||||
elif fishsanity == options.Fishsanity.option_all:
|
||||
randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish)
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_randomized:
|
||||
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if random.random() < 0.4]
|
||||
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_all:
|
||||
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish]
|
||||
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_legendaries:
|
||||
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish not in legendary_fish]
|
||||
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_exclude_hard_fish:
|
||||
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 80]
|
||||
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
|
||||
elif world_options[options.Fishsanity] == options.Fishsanity.option_only_easy_fish:
|
||||
fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in all_fish if fish.difficulty < 50]
|
||||
randomized_locations.extend(filter_ginger_island(world_options, fish_locations))
|
||||
|
||||
|
||||
def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random):
|
||||
@@ -156,26 +195,98 @@ def extend_museumsanity_locations(randomized_locations: List[LocationData], muse
|
||||
randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items)
|
||||
|
||||
|
||||
def extend_friendsanity_locations(randomized_locations: List[LocationData], friendsanity: int):
|
||||
if friendsanity == options.Friendsanity.option_none:
|
||||
def extend_friendsanity_locations(randomized_locations: List[LocationData], world_options: options.StardewOptions):
|
||||
if world_options[options.Friendsanity] == options.Friendsanity.option_none:
|
||||
return
|
||||
exclude_non_bachelors = friendsanity == options.Friendsanity.option_bachelors
|
||||
exclude_locked_villagers = friendsanity == options.Friendsanity.option_starting_npcs or \
|
||||
friendsanity == options.Friendsanity.option_bachelors
|
||||
exclude_post_marriage_hearts = friendsanity != options.Friendsanity.option_all_with_marriage
|
||||
|
||||
exclude_leo = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true
|
||||
exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
|
||||
exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \
|
||||
world_options[options.Friendsanity] == options.Friendsanity.option_bachelors
|
||||
include_post_marriage_hearts = world_options[options.Friendsanity] == options.Friendsanity.option_all_with_marriage
|
||||
heart_size = world_options[options.FriendsanityHeartSize]
|
||||
for villager in all_villagers:
|
||||
if villager.mod_name not in world_options[options.Mods] and villager.mod_name is not None:
|
||||
continue
|
||||
if not villager.available and exclude_locked_villagers:
|
||||
continue
|
||||
if not villager.bachelor and exclude_non_bachelors:
|
||||
continue
|
||||
if villager.name == "Leo" and exclude_leo:
|
||||
continue
|
||||
heart_cap = 8 if villager.bachelor else 10
|
||||
if include_post_marriage_hearts and villager.bachelor:
|
||||
heart_cap = 14
|
||||
for heart in range(1, 15):
|
||||
if villager.bachelor and exclude_post_marriage_hearts and heart > 8:
|
||||
continue
|
||||
if villager.bachelor or heart < 11:
|
||||
if heart > heart_cap:
|
||||
break
|
||||
if heart % heart_size == 0 or heart == heart_cap:
|
||||
randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"])
|
||||
if not exclude_non_bachelors:
|
||||
for heart in range(1, 6):
|
||||
randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"])
|
||||
if heart % heart_size == 0 or heart == 5:
|
||||
randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"])
|
||||
|
||||
|
||||
def extend_festival_locations(randomized_locations: List[LocationData], festival_option: int):
|
||||
if festival_option == options.FestivalLocations.option_disabled:
|
||||
return
|
||||
|
||||
festival_locations = locations_by_tag[LocationTags.FESTIVAL]
|
||||
randomized_locations.extend(festival_locations)
|
||||
extend_hard_festival_locations(randomized_locations, festival_option)
|
||||
|
||||
|
||||
def extend_hard_festival_locations(randomized_locations, festival_option: int):
|
||||
if festival_option != options.FestivalLocations.option_hard:
|
||||
return
|
||||
|
||||
hard_festival_locations = locations_by_tag[LocationTags.FESTIVAL_HARD]
|
||||
randomized_locations.extend(hard_festival_locations)
|
||||
|
||||
|
||||
def extend_special_order_locations(randomized_locations: List[LocationData], world_options):
|
||||
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_disabled:
|
||||
return
|
||||
|
||||
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
|
||||
board_locations = filter_disabled_locations(world_options, locations_by_tag[LocationTags.SPECIAL_ORDER_BOARD])
|
||||
randomized_locations.extend(board_locations)
|
||||
if world_options[options.SpecialOrderLocations] == options.SpecialOrderLocations.option_board_qi and include_island:
|
||||
include_arcade = world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled
|
||||
qi_orders = [location for location in locations_by_tag[LocationTags.SPECIAL_ORDER_QI] if include_arcade or LocationTags.JUNIMO_KART not in location.tags]
|
||||
randomized_locations.extend(qi_orders)
|
||||
|
||||
|
||||
def extend_walnut_purchase_locations(randomized_locations: List[LocationData], world_options):
|
||||
if world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_true:
|
||||
return
|
||||
randomized_locations.append(location_table["Repair Ticket Machine"])
|
||||
randomized_locations.append(location_table["Repair Boat Hull"])
|
||||
randomized_locations.append(location_table["Repair Boat Anchor"])
|
||||
randomized_locations.append(location_table["Open Professor Snail Cave"])
|
||||
randomized_locations.append(location_table["Complete Island Field Office"])
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.WALNUT_PURCHASE])
|
||||
|
||||
|
||||
def extend_mandatory_locations(randomized_locations: List[LocationData], world_options):
|
||||
mandatory_locations = [location for location in locations_by_tag[LocationTags.MANDATORY]]
|
||||
filtered_mandatory_locations = filter_disabled_locations(world_options, mandatory_locations)
|
||||
randomized_locations.extend(filtered_mandatory_locations)
|
||||
|
||||
|
||||
def extend_backpack_locations(randomized_locations: List[LocationData], world_options):
|
||||
backpack_locations = [location for location in locations_by_tag[LocationTags.BACKPACK]]
|
||||
filtered_backpack_locations = filter_modded_locations(world_options, backpack_locations)
|
||||
randomized_locations.extend(filtered_backpack_locations)
|
||||
|
||||
|
||||
def extend_elevator_locations(randomized_locations: List[LocationData], world_options):
|
||||
if world_options[options.ElevatorProgression] == options.ElevatorProgression.option_vanilla:
|
||||
return
|
||||
elevator_locations = [location for location in locations_by_tag[LocationTags.ELEVATOR]]
|
||||
filtered_elevator_locations = filter_modded_locations(world_options, elevator_locations)
|
||||
randomized_locations.extend(filtered_elevator_locations)
|
||||
|
||||
|
||||
def create_locations(location_collector: StardewLocationCollector,
|
||||
@@ -183,33 +294,55 @@ def create_locations(location_collector: StardewLocationCollector,
|
||||
random: Random):
|
||||
randomized_locations = []
|
||||
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.MANDATORY])
|
||||
|
||||
if not world_options[options.BackpackProgression] == options.BackpackProgression.option_vanilla:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.BACKPACK])
|
||||
extend_mandatory_locations(randomized_locations, world_options)
|
||||
extend_backpack_locations(randomized_locations, world_options)
|
||||
|
||||
if not world_options[options.ToolProgression] == options.ToolProgression.option_vanilla:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.TOOL_UPGRADE])
|
||||
|
||||
if not world_options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_vanilla:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.THE_MINES_ELEVATOR])
|
||||
extend_elevator_locations(randomized_locations, world_options)
|
||||
|
||||
if not world_options[options.SkillProgression] == options.SkillProgression.option_vanilla:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.SKILL_LEVEL])
|
||||
for location in locations_by_tag[LocationTags.SKILL_LEVEL]:
|
||||
if location.mod_name is None or location.mod_name in world_options[options.Mods]:
|
||||
randomized_locations.append(location_table[location.name])
|
||||
|
||||
if not world_options[options.BuildingProgression] == options.BuildingProgression.option_vanilla:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.BUILDING_BLUEPRINT])
|
||||
for location in locations_by_tag[LocationTags.BUILDING_BLUEPRINT]:
|
||||
if location.mod_name is None or location.mod_name in world_options[options.Mods]:
|
||||
randomized_locations.append(location_table[location.name])
|
||||
|
||||
if not world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_disabled:
|
||||
if world_options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_disabled:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE_VICTORY])
|
||||
|
||||
if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling:
|
||||
randomized_locations.extend(locations_by_tag[LocationTags.ARCADE_MACHINE])
|
||||
|
||||
extend_cropsanity_locations(randomized_locations, world_options)
|
||||
extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations])
|
||||
extend_fishsanity_locations(randomized_locations, world_options[options.Fishsanity], random)
|
||||
extend_fishsanity_locations(randomized_locations, world_options, random)
|
||||
extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random)
|
||||
extend_friendsanity_locations(randomized_locations, world_options[options.Friendsanity])
|
||||
extend_friendsanity_locations(randomized_locations, world_options)
|
||||
|
||||
extend_festival_locations(randomized_locations, world_options[options.FestivalLocations])
|
||||
extend_special_order_locations(randomized_locations, world_options)
|
||||
extend_walnut_purchase_locations(randomized_locations, world_options)
|
||||
|
||||
for location_data in randomized_locations:
|
||||
location_collector(location_data.name, location_data.code, location_data.region)
|
||||
|
||||
|
||||
def filter_ginger_island(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
|
||||
include_island = world_options[options.ExcludeGingerIsland] == options.ExcludeGingerIsland.option_false
|
||||
return [location for location in locations if include_island or LocationTags.GINGER_ISLAND not in location.tags]
|
||||
|
||||
|
||||
def filter_modded_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
|
||||
current_mod_names = world_options[options.Mods]
|
||||
return [location for location in locations if location.mod_name is None or location.mod_name in current_mod_names]
|
||||
|
||||
|
||||
def filter_disabled_locations(world_options: options.StardewOptions, locations: List[LocationData]) -> List[LocationData]:
|
||||
locations_first_pass = filter_ginger_island(world_options, locations)
|
||||
locations_second_pass = filter_modded_locations(world_options, locations_first_pass)
|
||||
return locations_second_pass
|
||||
|
||||
Reference in New Issue
Block a user