mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-10-09 17:33:02 -07:00
553 lines
29 KiB
Python
553 lines
29 KiB
Python
import logging
|
|
import math
|
|
import typing
|
|
from collections import Counter
|
|
from functools import wraps
|
|
from random import Random
|
|
from typing import Dict, List, Any, ClassVar, TextIO, Optional
|
|
|
|
import entrance_rando
|
|
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
|
from NetUtils import JSONMessagePart
|
|
from Options import PerGameCommonOptions
|
|
from worlds.AutoWorld import World, WebWorld
|
|
from worlds.LauncherComponents import components, Component, icon_paths, Type
|
|
from .bundles.bundle_room import BundleRoom
|
|
from .bundles.bundles import get_all_bundles, get_trash_bear_requests
|
|
from .content import StardewContent, create_content
|
|
from .content.feature.special_order_locations import get_qi_gem_amount
|
|
from .content.feature.walnutsanity import get_walnut_amount
|
|
from .items import item_table, ItemData, Group, items_by_group, create_items, generate_filler_choice_pool, \
|
|
setup_early_items
|
|
from .items.item_data import FILLER_GROUPS
|
|
from .locations import location_table, create_locations, LocationData, locations_by_tag
|
|
from .logic.combat_logic import valid_weapons
|
|
from .logic.logic import StardewLogic
|
|
from .options import StardewValleyOptions, SeasonRandomization, Goal, BundleRandomization, EnabledFillerBuffs, \
|
|
NumberOfMovementBuffs, BuildingProgression, EntranceRandomization, ToolProgression, BackpackProgression, TrapDistribution, BundlePrice, \
|
|
BundleWhitelist, BundleBlacklist, BundlePerRoom, FarmType
|
|
from .options.forced_options import force_change_options_if_incompatible, force_change_options_if_banned
|
|
from .options.jojapocalypse_options import JojaAreYouSure
|
|
from .options.option_groups import sv_option_groups
|
|
from .options.presets import sv_options_presets
|
|
from .options.settings import StardewSettings
|
|
from .options.worlds_group import apply_most_restrictive_options
|
|
from .regions import create_regions, prepare_mod_data
|
|
from .rules import set_rules
|
|
from .stardew_rule import True_, StardewRule, HasProgressionPercent
|
|
from .stardew_rule.rule_explain import RuleExplanation
|
|
from .strings.ap_names.ap_option_names import StartWithoutOptionName
|
|
from .strings.ap_names.ap_weapon_names import APWeapon
|
|
from .strings.ap_names.event_names import Event
|
|
from .strings.goal_names import Goal as GoalName
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STARDEW_VALLEY = "Stardew Valley"
|
|
UNIVERSAL_TRACKER_SEED_PROPERTY = "ut_seed"
|
|
|
|
client_version = 0
|
|
TRACKER_ENABLED = True
|
|
|
|
|
|
class StardewLocation(Location):
|
|
game: str = STARDEW_VALLEY
|
|
|
|
|
|
class StardewItem(Item):
|
|
game: str = STARDEW_VALLEY
|
|
events_to_collect: Counter[str]
|
|
|
|
@wraps(Item.__init__)
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
super().__init__(*args, **kwargs)
|
|
self.events_to_collect = Counter()
|
|
|
|
|
|
class StardewWebWorld(WebWorld):
|
|
theme = "dirt"
|
|
bug_report_page = "https://github.com/agilbert1412/StardewArchipelago/issues/new?labels=bug&title=%5BBug%5D%3A+Brief+Description+of+bug+here"
|
|
options_presets = sv_options_presets
|
|
option_groups = sv_option_groups
|
|
|
|
setup_en = Tutorial(
|
|
"Multiworld Setup Guide",
|
|
"A guide to playing Stardew Valley with Archipelago.",
|
|
"English",
|
|
"setup_en.md",
|
|
"setup/en",
|
|
["Kaito Kid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"]
|
|
)
|
|
|
|
setup_fr = Tutorial(
|
|
"Guide de configuration MultiWorld",
|
|
"Un guide pour configurer Stardew Valley sur Archipelago",
|
|
"Français",
|
|
"setup_fr.md",
|
|
"setup/fr",
|
|
["Eindall"]
|
|
)
|
|
|
|
tutorials = [setup_en, setup_fr]
|
|
|
|
|
|
class StardewValleyWorld(World):
|
|
"""
|
|
Stardew Valley is an open-ended country-life RPG. You can farm, fish, mine, fight, complete quests,
|
|
befriend villagers, and uncover dark secrets.
|
|
"""
|
|
game = STARDEW_VALLEY
|
|
topology_present = True
|
|
|
|
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
|
location_name_to_id = {name: data.code for name, data in location_table.items()}
|
|
|
|
item_name_groups = {
|
|
group.name.replace("_", " ").title() + (" Group" if group.name.replace("_", " ").title() in item_table else ""):
|
|
[item.name for item in items] for group, items in items_by_group.items()
|
|
}
|
|
location_name_groups = {
|
|
group.name.replace("_", " ").title() + (" Group" if group.name.replace("_", " ").title() in locations_by_tag else ""):
|
|
[location.name for location in locations] for group, locations in locations_by_tag.items()
|
|
}
|
|
|
|
required_client_version = (0, 4, 0)
|
|
|
|
options_dataclass = StardewValleyOptions
|
|
options: StardewValleyOptions
|
|
settings: ClassVar[StardewSettings]
|
|
content: StardewContent
|
|
logic: StardewLogic
|
|
|
|
web = StardewWebWorld()
|
|
modified_bundles: List[BundleRoom]
|
|
randomized_entrances: Dict[str, str]
|
|
trash_bear_requests: Dict[str, List[str]]
|
|
|
|
total_progression_items: int
|
|
classifications_to_override_post_fill: list[tuple[StardewItem, ItemClassification]]
|
|
|
|
@classmethod
|
|
def create_group(cls, multiworld: MultiWorld, new_player_id: int, players: set[int]) -> World:
|
|
world_group = super().create_group(multiworld, new_player_id, players)
|
|
|
|
group_options = typing.cast(StardewValleyOptions, world_group.options)
|
|
worlds_options = [typing.cast(StardewValleyOptions, multiworld.worlds[player].options) for player in players]
|
|
apply_most_restrictive_options(group_options, worlds_options)
|
|
world_group.content = create_content(group_options)
|
|
|
|
return world_group
|
|
|
|
def __init__(self, multiworld: MultiWorld, player: int):
|
|
super().__init__(multiworld, player)
|
|
self.filler_item_pool_names = None
|
|
self.total_progression_items = 0
|
|
self.classifications_to_override_post_fill = []
|
|
|
|
# Taking the seed specified in slot data for UT, otherwise just generating the seed.
|
|
self.seed = getattr(multiworld, "re_gen_passthrough", {}).get(STARDEW_VALLEY, self.random.getrandbits(64))
|
|
self.random = Random(self.seed)
|
|
|
|
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Optional[int]:
|
|
# If the seed is not specified in the slot data, this mean the world was generated before Universal Tracker support.
|
|
seed = slot_data.get(UNIVERSAL_TRACKER_SEED_PROPERTY)
|
|
if seed is None:
|
|
logger.warning(f"World was generated before Universal Tracker support. Tracker might not be accurate.")
|
|
for option_name in slot_data:
|
|
if option_name in self.options_dataclass.type_hints:
|
|
option_value = slot_data[option_name]
|
|
option_type = self.options_dataclass.type_hints[option_name]
|
|
if isinstance(option_value, option_type):
|
|
self.options.__setattr__(option_name, option_value)
|
|
continue
|
|
parsed_option_value = option_type.from_any(option_value)
|
|
if isinstance(parsed_option_value, option_type):
|
|
self.options.__setattr__(option_name, parsed_option_value)
|
|
continue
|
|
logger.warning(f"Option {option_name} was found in slot data, but could not be automatically parsed to be used in generation.\n"
|
|
f"Slot Data Value: {option_value}"
|
|
f"Parsed Value: {parsed_option_value}"
|
|
f"Yaml Value: {self.options.__getattribute__(option_name)}")
|
|
return seed
|
|
|
|
def generate_early(self):
|
|
force_change_options_if_banned(self.options, self.settings, self.player, self.player_name)
|
|
force_change_options_if_incompatible(self.options, self.player, self.player_name)
|
|
self.content = create_content(self.options)
|
|
|
|
def create_regions(self):
|
|
def create_region(name: str) -> Region:
|
|
return Region(name, self.player, self.multiworld)
|
|
|
|
world_regions = create_regions(create_region, self.options, self.content)
|
|
|
|
self.logic = StardewLogic(self.player, self.options, self.content, world_regions.keys())
|
|
self.modified_bundles = get_all_bundles(self.random, self.logic, self.content, self.options, self.player_name)
|
|
self.trash_bear_requests = get_trash_bear_requests(self.random, self.content, self.options)
|
|
|
|
for bundle_room in self.modified_bundles:
|
|
bundle_room.special_behavior(self)
|
|
|
|
def add_location(name: str, code: Optional[int], region: str):
|
|
assert region in world_regions, f"Location {name} cannot be created in region {region}, because the region does not exist in this slot"
|
|
region: Region = world_regions[region]
|
|
location = StardewLocation(self.player, name, code, region)
|
|
region.locations.append(location)
|
|
|
|
create_locations(add_location, self.modified_bundles, self.trash_bear_requests, self.options, self.content, self.random)
|
|
self.multiworld.regions.extend(world_regions.values())
|
|
|
|
def create_items(self):
|
|
self.precollect_start_inventory_items_if_needed()
|
|
self.precollect_start_without_items()
|
|
self.precollect_starting_season()
|
|
self.precollect_building_items()
|
|
self.precollect_starting_backpacks()
|
|
items_to_exclude = [excluded_items
|
|
for excluded_items in self.multiworld.precollected_items[self.player]
|
|
if item_table[excluded_items.name].has_any_group(Group.MAXIMUM_ONE)
|
|
or not item_table[excluded_items.name].has_any_group(*FILLER_GROUPS, Group.FRIENDSHIP_PACK)]
|
|
|
|
if self.options.season_randomization == SeasonRandomization.option_disabled:
|
|
items_to_exclude = [item for item in items_to_exclude
|
|
if item_table[item.name] not in items_by_group[Group.SEASON]]
|
|
|
|
locations_count = len([location
|
|
for location in self.multiworld.get_locations(self.player)
|
|
if location.address is not None])
|
|
|
|
created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options, self.content, self.random)
|
|
|
|
self.multiworld.itempool += created_items
|
|
|
|
setup_early_items(self.multiworld, self.options, self.content, self.player, self.random)
|
|
|
|
self.setup_logic_events()
|
|
self.setup_victory()
|
|
|
|
# This is really a best-effort to get the total progression items count. It is mostly used to spread grinds across spheres are push back locations that
|
|
# only become available after months or years in game. In most cases, not having the exact count will not impact the logic.
|
|
#
|
|
# The actual total can be impacted by the start_inventory_from_pool, when items are removed from the pool but not from the total. The is also a bug
|
|
# with plando where additional progression items can be created without being accounted for, which impact the real amount of progression items. This can
|
|
# ultimately create unwinnable seeds where some items (like Blueberry seeds) are locked in Shipsanity: Blueberry, but world is deemed winnable as the
|
|
# winning rule only check the count of collected progression items.
|
|
self.total_progression_items += sum(1 for i in self.multiworld.precollected_items[self.player] if i.advancement)
|
|
self.total_progression_items += sum(1 for i in self.multiworld.get_filled_locations(self.player) if i.advancement)
|
|
self.total_progression_items += sum(1 for i in created_items if i.advancement)
|
|
self.total_progression_items -= 1 # -1 for the victory event
|
|
|
|
player_state = self.multiworld.state.prog_items[self.player]
|
|
self.update_received_progression_percent(player_state)
|
|
|
|
def precollect_start_inventory_items_if_needed(self):
|
|
# The only reason this is necessary, is because in an UT context, precollected items was not filled up, and this messes with the seeded random later
|
|
for item_name in self.options.start_inventory:
|
|
item_count = self.options.start_inventory[item_name]
|
|
precollected_count = len([precollected_item for precollected_item in self.multiworld.precollected_items[self.player]
|
|
if precollected_item.name == item_name])
|
|
while precollected_count < item_count:
|
|
self.multiworld.push_precollected(self.create_item(item_name))
|
|
precollected_count += 1
|
|
|
|
def precollect_start_without_items(self):
|
|
if StartWithoutOptionName.landslide not in self.options.start_without:
|
|
self.multiworld.push_precollected(self.create_item("Landslide Removed"))
|
|
if StartWithoutOptionName.community_center not in self.options.start_without:
|
|
self.multiworld.push_precollected(self.create_item("Community Center Key"))
|
|
self.multiworld.push_precollected(self.create_item("Forest Magic"))
|
|
self.multiworld.push_precollected(self.create_item("Wizard Invitation"))
|
|
if StartWithoutOptionName.buildings not in self.options.start_without:
|
|
self.multiworld.push_precollected(self.create_item("Shipping Bin"))
|
|
self.multiworld.push_precollected(self.create_item("Pet Bowl"))
|
|
|
|
def precollect_starting_season(self):
|
|
if self.options.season_randomization == SeasonRandomization.option_progressive:
|
|
return
|
|
|
|
season_pool = items_by_group[Group.SEASON]
|
|
|
|
if self.options.season_randomization == SeasonRandomization.option_disabled:
|
|
for season in season_pool:
|
|
self.multiworld.push_precollected(self.create_item(season))
|
|
return
|
|
|
|
if [item for item in self.multiworld.precollected_items[self.player]
|
|
if item.name in {season.name for season in items_by_group[Group.SEASON]}]:
|
|
return
|
|
|
|
if self.options.season_randomization == SeasonRandomization.option_randomized_not_winter:
|
|
season_pool = [season for season in season_pool if season.name != "Winter"]
|
|
|
|
starting_season = self.create_item(self.random.choice(season_pool))
|
|
self.multiworld.push_precollected(starting_season)
|
|
|
|
def precollect_building_items(self):
|
|
building_progression = self.content.features.building_progression
|
|
# Not adding items when building are vanilla because the buildings are already placed in the world.
|
|
if not building_progression.is_progressive:
|
|
return
|
|
|
|
# starting_buildings is a set, so sort for deterministic order.
|
|
for building in sorted(building_progression.starting_buildings):
|
|
item, quantity = building_progression.to_progressive_item(building)
|
|
for _ in range(quantity):
|
|
self.multiworld.push_precollected(self.create_item(item))
|
|
|
|
def precollect_starting_backpacks(self):
|
|
if self.options.backpack_progression != BackpackProgression.option_vanilla and StartWithoutOptionName.backpack in self.options.start_without:
|
|
minimum_start_slots = 4 if StartWithoutOptionName.tools in self.options.start_without else 6
|
|
num_starting_slots = max(minimum_start_slots, self.options.backpack_size.value)
|
|
num_starting_backpacks = math.ceil(num_starting_slots / self.options.backpack_size.value)
|
|
num_already_starting_backpacks = 0
|
|
for precollected_item in self.multiworld.precollected_items[self.player]:
|
|
if precollected_item.name == "Progressive Backpack":
|
|
num_already_starting_backpacks += 1
|
|
for i in range(num_starting_backpacks - num_already_starting_backpacks):
|
|
self.multiworld.push_precollected(self.create_item("Progressive Backpack"))
|
|
|
|
def setup_logic_events(self):
|
|
def register_event(name: str, region: str, rule: StardewRule, location_name: str | None = None) -> None:
|
|
if location_name is None:
|
|
location_name = name
|
|
event_location = LocationData(None, region, location_name)
|
|
self.create_event_location(event_location, rule, name)
|
|
|
|
self.logic.setup_events(register_event)
|
|
|
|
def setup_victory(self):
|
|
if self.options.goal == Goal.option_community_center:
|
|
self.create_event_location(location_table[GoalName.community_center],
|
|
self.logic.goal.can_complete_community_center(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_grandpa_evaluation:
|
|
self.create_event_location(location_table[GoalName.grandpa_evaluation],
|
|
self.logic.goal.can_finish_grandpa_evaluation(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_bottom_of_the_mines:
|
|
self.create_event_location(location_table[GoalName.bottom_of_the_mines],
|
|
self.logic.goal.can_complete_bottom_of_the_mines(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_cryptic_note:
|
|
self.create_event_location(location_table[GoalName.cryptic_note],
|
|
self.logic.goal.can_complete_cryptic_note(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_master_angler:
|
|
self.create_event_location(location_table[GoalName.master_angler],
|
|
self.logic.goal.can_complete_master_angler(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_complete_collection:
|
|
self.create_event_location(location_table[GoalName.complete_museum],
|
|
self.logic.goal.can_complete_complete_collection(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_full_house:
|
|
self.create_event_location(location_table[GoalName.full_house],
|
|
self.logic.goal.can_complete_full_house(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_greatest_walnut_hunter:
|
|
self.create_event_location(location_table[GoalName.greatest_walnut_hunter],
|
|
self.logic.goal.can_complete_greatest_walnut_hunter(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_protector_of_the_valley:
|
|
self.create_event_location(location_table[GoalName.protector_of_the_valley],
|
|
self.logic.goal.can_complete_protector_of_the_valley(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_full_shipment:
|
|
self.create_event_location(location_table[GoalName.full_shipment],
|
|
self.logic.goal.can_complete_full_shipment(self.get_all_location_names()),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_gourmet_chef:
|
|
self.create_event_location(location_table[GoalName.gourmet_chef],
|
|
self.logic.goal.can_complete_gourmet_chef(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_craft_master:
|
|
self.create_event_location(location_table[GoalName.craft_master],
|
|
self.logic.goal.can_complete_craft_master(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_legend:
|
|
self.create_event_location(location_table[GoalName.legend],
|
|
self.logic.goal.can_complete_legend(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_mystery_of_the_stardrops:
|
|
self.create_event_location(location_table[GoalName.mystery_of_the_stardrops],
|
|
self.logic.goal.can_complete_mystery_of_the_stardrop(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_mad_hatter:
|
|
self.create_event_location(location_table[GoalName.mad_hatter],
|
|
self.logic.goal.can_complete_mad_hatter(self.get_all_location_names()),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_ultimate_foodie:
|
|
self.create_event_location(location_table[GoalName.ultimate_foodie],
|
|
self.logic.goal.can_complete_ultimate_foodie(self.get_all_location_names()),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_allsanity:
|
|
self.create_event_location(location_table[GoalName.allsanity],
|
|
self.logic.goal.can_complete_allsanity(),
|
|
Event.victory)
|
|
elif self.options.goal == Goal.option_perfection:
|
|
self.create_event_location(location_table[GoalName.perfection],
|
|
self.logic.goal.can_complete_perfection(),
|
|
Event.victory)
|
|
|
|
self.multiworld.completion_condition[self.player] = lambda state: state.has(Event.victory, self.player)
|
|
|
|
def get_all_location_names(self) -> List[str]:
|
|
return list(location.name for location in self.multiworld.get_locations(self.player))
|
|
|
|
def create_item(self, item: str | ItemData,
|
|
classification_pre_fill: ItemClassification = None,
|
|
classification_post_fill: ItemClassification = None) -> StardewItem:
|
|
if isinstance(item, str):
|
|
item = item_table[item]
|
|
|
|
if classification_pre_fill is None:
|
|
classification_pre_fill = item.classification
|
|
|
|
stardew_item = StardewItem(item.name, classification_pre_fill, item.code, self.player)
|
|
|
|
if stardew_item.advancement:
|
|
# Progress is only counted for pre-fill progression items, so we don't count filler items later converted to progression post-fill.
|
|
stardew_item.events_to_collect[Event.received_progression_item] = 1
|
|
|
|
if (walnut_amount := get_walnut_amount(stardew_item.name)) > 0:
|
|
stardew_item.events_to_collect[Event.received_walnuts] = walnut_amount
|
|
|
|
if (qi_gem_amount := get_qi_gem_amount(stardew_item.name)) > 0:
|
|
stardew_item.events_to_collect[Event.received_qi_gems] = qi_gem_amount
|
|
|
|
if classification_post_fill is not None:
|
|
self.classifications_to_override_post_fill.append((stardew_item, classification_post_fill))
|
|
|
|
return stardew_item
|
|
|
|
def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str):
|
|
region = self.multiworld.get_region(location_data.region, self.player)
|
|
item = typing.cast(StardewItem, region.add_event(location_data.name, item, rule, StardewLocation, StardewItem))
|
|
item.events_to_collect[Event.received_progression_item] = 1
|
|
|
|
def set_rules(self):
|
|
set_rules(self)
|
|
|
|
def connect_entrances(self) -> None:
|
|
no_target_groups = {0: [0]}
|
|
placement = entrance_rando.randomize_entrances(self, coupled=True, target_group_lookup=no_target_groups)
|
|
self.randomized_entrances = prepare_mod_data(placement)
|
|
|
|
def generate_basic(self):
|
|
pass
|
|
|
|
def post_fill(self) -> None:
|
|
# Not updating the prog item count, as any change could make some locations inaccessible, which is pretty much illegal in post fill.
|
|
for item, classification in self.classifications_to_override_post_fill:
|
|
item.classification = classification
|
|
|
|
def get_filler_item_name(self) -> str:
|
|
if not self.filler_item_pool_names:
|
|
self.filler_item_pool_names = generate_filler_choice_pool(self.options, self.content)
|
|
return self.random.choice(self.filler_item_pool_names)
|
|
|
|
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
|
"""Write to the spoiler header. If individual it's right at the end of that player's options,
|
|
if as stage it's right under the common header before per-player options."""
|
|
self.add_entrances_to_spoiler_log()
|
|
|
|
def write_spoiler(self, spoiler_handle: TextIO) -> None:
|
|
"""Write to the spoiler "middle", this is after the per-player options and before locations,
|
|
meant for useful or interesting info."""
|
|
self.add_bundles_to_spoiler_log(spoiler_handle)
|
|
|
|
def add_bundles_to_spoiler_log(self, spoiler_handle: TextIO):
|
|
if self.options.bundle_randomization == BundleRandomization.option_vanilla:
|
|
return
|
|
player_name = self.multiworld.get_player_name(self.player)
|
|
spoiler_handle.write(f"\n\nCommunity Center ({player_name}):\n")
|
|
for room in self.modified_bundles:
|
|
for bundle in room.bundles:
|
|
spoiler_handle.write(f"\t[{room.name}] {bundle.name} ({bundle.number_required} required):\n")
|
|
for i, item in enumerate(bundle.items):
|
|
if "Basic" in item.quality:
|
|
quality = ""
|
|
else:
|
|
quality = f" ({item.quality.split(' ')[0]})"
|
|
spoiler_handle.write(f"\t\t{item.amount}x {item.get_item()}{quality}\n")
|
|
|
|
def add_entrances_to_spoiler_log(self):
|
|
if self.options.entrance_randomization == EntranceRandomization.option_disabled:
|
|
return
|
|
for original_entrance, replaced_entrance in self.randomized_entrances.items():
|
|
self.multiworld.spoiler.set_entrance(original_entrance, replaced_entrance, "entrance", self.player)
|
|
|
|
def fill_slot_data(self) -> Dict[str, Any]:
|
|
bundles = dict()
|
|
for room in self.modified_bundles:
|
|
bundles[room.name] = dict()
|
|
for bundle in room.bundles:
|
|
bundles[room.name][bundle.name] = {"number_required": bundle.number_required}
|
|
for i, item in enumerate(bundle.items):
|
|
bundles[room.name][bundle.name][str(i)] = f"{item.get_item()}|{item.amount}|{item.quality}"
|
|
|
|
excluded_options = [BundleRandomization, BundlePerRoom, NumberOfMovementBuffs,
|
|
EnabledFillerBuffs, TrapDistribution, BundleWhitelist, BundleBlacklist, JojaAreYouSure]
|
|
excluded_option_names = [option.internal_name for option in excluded_options]
|
|
generic_option_names = [option_name for option_name in PerGameCommonOptions.type_hints]
|
|
excluded_option_names.extend(generic_option_names)
|
|
included_option_names: List[str] = [option_name for option_name in self.options_dataclass.type_hints if option_name not in excluded_option_names]
|
|
slot_data = self.options.as_dict(*included_option_names)
|
|
slot_data.update({
|
|
UNIVERSAL_TRACKER_SEED_PROPERTY: self.seed,
|
|
"seed": self.random.randrange(1000000000), # Seed should be max 9 digits
|
|
"randomized_entrances": self.randomized_entrances,
|
|
"trash_bear_requests": self.trash_bear_requests,
|
|
"modified_bundles": bundles,
|
|
"client_version": self.world_version.as_simple_string(),
|
|
})
|
|
|
|
return slot_data
|
|
|
|
def collect(self, state: CollectionState, item: StardewItem) -> bool:
|
|
change = super().collect(state, item)
|
|
if not change:
|
|
return False
|
|
|
|
player_state = state.prog_items[self.player]
|
|
player_state.update(item.events_to_collect)
|
|
|
|
self.update_received_progression_percent(player_state)
|
|
|
|
if item.name in APWeapon.all_weapons:
|
|
player_state[Event.received_progressive_weapon] = max(player_state[Event.received_progressive_weapon], player_state[item.name])
|
|
|
|
return True
|
|
|
|
def remove(self, state: CollectionState, item: StardewItem) -> bool:
|
|
change = super().remove(state, item)
|
|
if not change:
|
|
return False
|
|
|
|
player_state = state.prog_items[self.player]
|
|
player_state.subtract(item.events_to_collect)
|
|
|
|
self.update_received_progression_percent(player_state)
|
|
|
|
if item.name in APWeapon.all_weapons:
|
|
player_state[Event.received_progressive_weapon] = max(player_state[weapon] for weapon in APWeapon.all_weapons)
|
|
|
|
return True
|
|
|
|
def update_received_progression_percent(self, player_state: Counter[str]) -> None:
|
|
if self.total_progression_items:
|
|
received_progression_count = player_state[Event.received_progression_item]
|
|
# Total progression items is not set until all items are created, but collect will be called during the item creation when an item is precollected.
|
|
# We can't update the percentage if we don't know the total progression items, can't divide by 0.
|
|
player_state[Event.received_progression_percent] = received_progression_count * 100 // self.total_progression_items
|
|
|
|
previous_explanation: RuleExplanation | None = None
|
|
|
|
def explain_rule(self, target_name: str, state: CollectionState) -> list[JSONMessagePart]:
|
|
from .client import cmd_explain
|
|
return cmd_explain(self, target_name, state)
|
|
|
|
def explain_more(self, target_name: str, state: CollectionState) -> list[JSONMessagePart]:
|
|
from .client import cmd_more
|
|
return cmd_more(self, target_name, state)
|