diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 87b6815591fd..5dbd835f943d 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -5,13 +5,14 @@ from copy import deepcopy from typing import Any, ClassVar, cast -from BaseClasses import CollectionState, ItemClassification, LocationProgressType, MultiWorld +from BaseClasses import CollectionState, EntranceType, ItemClassification, LocationProgressType, MultiWorld from Options import OptionError +from entrance_rando import disconnect_entrance_for_randomization, randomize_entrances from worlds.AutoWorld import World from .charms import charm_name_to_id, charm_names from .classes import HKClause, HKEntrance, HKItem, HKLocation, HKRegion, HKSettings, HKWeb -from .constants import gamename, randomizable_starting_items, shop_cost_types +from .constants import gamename, randomizable_starting_items, shop_cost_types, NearbySoul from .data.ids import item_name_to_id, location_name_to_id from .data.item_effects import ( affected_terms_by_item, @@ -30,20 +31,19 @@ Goal, GrubHuntGoal, HKOptions, + ShuffleEntrancesMode, StartLocation, WhitePalace, hollow_knight_options, shop_to_option, ) from .resource_state_vars import ResourceStateHandler -from .resource_state_vars.cast_spell import NearbySoul from .rules import cost_terms from .state_mixin import HKLogicMixin as HKLogicMixin from .template_world import RandomizerCoreWorld logger = logging.getLogger("Hollow Knight") - shop_locations = multi_locations event_locations = [location["name"] for location in locations if location["is_event"] and location["name"] not in ("Can_Warp_To_DG_Bench", "Can_Warp_To_Bench")] @@ -92,6 +92,7 @@ class HKWorld(RandomizerCoreWorld, World): rule_lookup: ClassVar[dict[str, str]] = {location["name"]: location["logic"] for location in hk_locations} region_lookup: ClassVar[dict[str, str]] = {location: r["name"] for r in hk_regions for location in r["locations"]} entrance_by_term: dict[str, list[str]] + entrance_pairs: dict[str, str] cached_filler_items: ClassVar[dict[int, list[str]]] = {} # per player cache grub_count: int @@ -113,6 +114,7 @@ def __init__(self, multiworld, player): self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) self.event_locations = deepcopy(event_locations) self.entrance_by_term = defaultdict(list) + self.entrance_pairs = {} def white_palace_exclusions(self) -> set[str]: exclusions = set() @@ -325,7 +327,7 @@ def generate_early(self): self.charm_names_and_costs[self.player] = {name: (charm_costs[index] if name != "Void_Heart" else 0) for name, index in charm_name_to_id.items()} - self.soul_modes[self.player] = NearbySoul.ITEMSOUL # make dynamic with ER support + self.soul_modes[self.player] = self.options.EntranceRandoType.soul_mode self.split_cloak_direction = self.random.randint(0, 1) @@ -365,7 +367,7 @@ def stage_generate_early(cls, multiworld): def validate_start(self, start_location_key: str) -> list[list[str]]: test_state = CollectionState(self.multiworld) - valid_items = ["ITEMRANDO", "2MASKS"] # TODO: properly handle these assumptions (non-er and non-cursed masks) + valid_items = ["2MASKS"] # TODO: properly handle these assumptions (non-cursed masks) if self.options.EnemyPogos: valid_items.append("ENEMYPOGOS") if test_state.has_group("Vertical", self.player): @@ -380,6 +382,9 @@ def validate_start(self, start_location_key: str) -> list[list[str]]: valid_items.append("PRECISEMOVEMENT") if self.options.DangerousSkips: valid_items.append("DANGEROUSSKIPS") + + valid_items.append(self.options.EntranceRandoType.tag) + start_location_logic = starts[start_location_key]["logic"] if not start_location_logic: # empty logic means always good @@ -406,6 +411,7 @@ def create_regions(self): for location, costs in vanilla_location_costs.items(): if self.options.AddUnshuffledLocations or getattr(self.options, location_to_option[location]): self.get_location(location).costs = costs + self.get_region("Menu").connect(self.get_region(self.start_location_region)) def get_location_map(self) -> list[tuple[str, str, Any | None]]: @@ -594,16 +600,55 @@ def can_godhome_flower(self, state: CollectionState): def can_grub_goal(self, state: CollectionState) -> bool: return all(state.has("Grub", owner, count) for owner, count in self.grub_player_count.items()) + def connect_entrances(self): + if not self.options.EntranceRandoType: + return + + transition_names = set(transitions.keys()) + + exits = [ + ex + for region in self.multiworld.get_regions(self.player) + for ex in region.exits + if ex.name in transition_names + and not ( + region.name == "Menu" + ) + ] + + if not exits: + return + + for ex in exits: + trans_data = transitions[ex.name] + if not self.options.EntranceRandoType.test_transition(trans_data): + continue + sides = trans_data["sides"] + ex.randomization_type = EntranceType.TWO_WAY if sides == "Both" else EntranceType.ONE_WAY + + disconnect_entrance_for_randomization( + ex, + None, + one_way_target_name=ex.name if ex.randomization_type == EntranceType.ONE_WAY else None + ) + + coupled = self.options.ShuffleEntrancesMode != ShuffleEntrancesMode.option_decoupled + + er_state = randomize_entrances( + world=self, + coupled=coupled, + target_group_lookup={ + 0: [0] + }, + ) + + self.entrance_pairs = dict(er_state.pairings) + def add_vanilla_connections(self): - transition_name_to_region = { - transition: region["name"] - for region in self.rc_regions - for transition in region["transitions"] - } vanilla_connections = [ - (transition_name_to_region[name], transition_name_to_region[t["vanilla_target"]], name) - for name, t in transitions.items() if t["sides"] != "OneWayOut" - ] + (transition_to_region_map[name], transition_to_region_map[t["vanilla_target"]], name) + for name, t in transitions.items() if t["vanilla_target"] is not None + ] for connection in vanilla_connections: region1 = self.get_region(connection[0]) @@ -978,6 +1023,7 @@ def fill_slot_data(self): pass slot_data["options"]["StartLocationName"] = starts[self.options.StartLocation.current_key]["logic_name"] + # slot_data["options"]["EntranceRandoTypeName"] = self.options.EntranceRandoType.current_key # 32 bit int slot_data["seed"] = self.random.randint(-2147483647, 2147483646) @@ -997,6 +1043,8 @@ def fill_slot_data(self): slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race + slot_data["entrance_pairs"] = self.entrance_pairs + return slot_data # write_spoiler @@ -1029,3 +1077,22 @@ def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle): for shop_name, locations in hk_world.created_multi_locations.items(): for loc in locations: spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}") + + def write_spoiler_end(self, spoiler_handle): + # Entrance randomization spoiler + if not self.entrance_pairs: + return + spoiler_handle.write(f"\n\n{self.player_name} Entrance Randomization:\n\nONE WAYS:") + two_ways = set() + for src, tgt in self.entrance_pairs.items(): + if (src, tgt) in two_ways: + continue # already recorded as a two_way + if self.entrance_pairs.get(tgt) == src: + two_ways.add((src, tgt)) + else: + spoiler_handle.write(f"\n{src} -> {tgt}") + + if two_ways: + spoiler_handle.write(f"\n\nTWO WAYS:") + for src, tgt in two_ways: + spoiler_handle.write(f"\n{src} <-> {tgt}") diff --git a/worlds/hk/archipelago.json b/worlds/hk/archipelago.json index 5de19401a95c..42255726548c 100644 --- a/worlds/hk/archipelago.json +++ b/worlds/hk/archipelago.json @@ -1 +1,4 @@ -{"game": "Hollow Knight", "world_version": "1.0.3"} \ No newline at end of file +{ + "game": "Hollow Knight", + "world_version": "1.0.4" +} \ No newline at end of file diff --git a/worlds/hk/classes.py b/worlds/hk/classes.py index cddef356dae7..90dec6c95a6f 100644 --- a/worlds/hk/classes.py +++ b/worlds/hk/classes.py @@ -1,4 +1,4 @@ -from typing import ClassVar, NamedTuple +from typing import ClassVar, NamedTuple, TYPE_CHECKING from BaseClasses import ( CollectionState, @@ -12,9 +12,12 @@ from worlds.AutoWorld import WebWorld from .constants import gamename -from .resource_state_vars import RCStateVariable +from .options import HKOptionGroups from .rules import cost_terms +if TYPE_CHECKING: + from .resource_state_vars import RCStateVariable + class HKSettings(Group): class DisableMapModSpoilers(Bool): @@ -59,6 +62,8 @@ class HKWeb(WebWorld): "?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title=" ) + option_groups = HKOptionGroups + class HKClause(NamedTuple): # Dict of item: count for state.has_all_counts() @@ -68,7 +73,7 @@ class HKClause(NamedTuple): hk_region_requirements: list[str] # list of resource state terms for the clause - hk_state_requirements: list[RCStateVariable] + hk_state_requirements: "list[RCStateVariable]" # default logicless rule for short circuting diff --git a/worlds/hk/constants.py b/worlds/hk/constants.py index ba45abd87f31..5e0bf1b91d60 100644 --- a/worlds/hk/constants.py +++ b/worlds/hk/constants.py @@ -1,3 +1,5 @@ +from enum import IntEnum + # Shop cost types. shop_cost_types: dict[str, tuple[str, ...]] = { "Egg_Shop": ("RANCIDEGGS",), @@ -23,3 +25,11 @@ BASE_SOUL = 12 BASE_NOTCHES = 3 BASE_HEALTH = 5 + + +class NearbySoul(IntEnum): + NONE = 1 + ITEMSOUL = 2 + MAPAREASOUL = 3 + AREASOUL = 4 + ROOMSOUL = 5 diff --git a/worlds/hk/options.py b/worlds/hk/options.py index ea097c103f59..cb21505c43a2 100644 --- a/worlds/hk/options.py +++ b/worlds/hk/options.py @@ -11,11 +11,13 @@ NamedRange, Option, OptionDict, + OptionGroup, PerGameCommonOptions, Range, Toggle, ) +from .constants import NearbySoul from .charms import charm_names, vanilla_costs from .data.option_data import logic_options, pool_options from .data.trando_data import starts @@ -27,16 +29,7 @@ else: Random = typing.Any -filtered_starts = [ - key for key, data in starts.items() - if not data["logic"] # empty logic is always valid - or any( - # strip starts that are only valid with ER as that is not implemented yet - not any(req in ("MAPAREARANDO", "FULLAREARANDO", "ROOMRANDO",) for req in clause["item_requirements"]) - for clause in data["logic"] - ) -] -locations = {"option_" + start: i for i, start in enumerate(filtered_starts)} +locations = {"option_" + start: i for i, start in enumerate(starts.keys())} # This way the dynamic start names are picked up by the MetaClass Choice belongs to StartLocation = type("StartLocation", (Choice,), { "__module__": __name__, @@ -211,6 +204,78 @@ class SplitCrystalHeart(Toggle): default = False +class EntranceRandoType(Choice): + """ + Entrance randomizer type. + + none: use vanilla transitions + map: only shuffle the entrances between map areas + full: only shuffle the entrances between Titled areas + room: shuffle all rooms entrances together + connected_area: shuffle entrances inside Titled areas but leave the connections between them vanilla + doors: shuffle all transitions through doors together + """ + display_name = "Entrance Rando Type" + option_none = 0 + option_map = 1 + option_full = 2 + option_room = 3 + option_connected_area = 4 + option_doors = 5 + default = option_none + tag_lookup = { + option_none: "ITEMRANDO", + option_map: "MAPAREARANDO", + option_full: "FULLAREARANDO", + option_room: "ROOMRANDO", + option_connected_area: "ROOMRANDO", # treated like room rando internally + option_doors: "ROOMRANDO", # treated like room rando internally + } + soul_lookup = { + option_none: NearbySoul.ITEMSOUL, + option_map: NearbySoul.MAPAREASOUL, + option_full: NearbySoul.AREASOUL, + option_room: NearbySoul.ROOMSOUL, + option_connected_area: NearbySoul.ROOMSOUL, + option_doors: NearbySoul.ROOMSOUL, + } + + @property + def tag(self) -> str: + return self.tag_lookup[self.value] + + def test_transition(self, trans_data: dict[str, typing.Any]) -> bool: + if self.value == self.option_none: + return False + elif self.value == self.option_map: + return trans_data["is_map_area_transition"] + elif self.value == self.option_full: + return trans_data["is_titled_area_transition"] + elif self.value == self.option_room: + return True + elif self.value == self.option_connected_area: + return not trans_data["is_titled_area_transition"] + elif self.value == self.option_doors: + return trans_data["direction"] == "Door" + + @property + def soul_mode(self) -> NearbySoul: + return self.soul_lookup[self.value] + + +class ShuffleEntrancesMode(Choice): + """How entrances should be shuffled when `Randomize Entrances` is enabled. + + **Coupled:** Transitions are paired so returning through an entrance takes you back. + + **Decoupled:** Any exit can lead to any entrance (not necessarily reversible). + """ + display_name = "Shuffle Entrances Mode" + option_coupled = 1 + option_decoupled = 2 + default = option_coupled + + class MinimumGrubPrice(Range): """The minimum grub price in the range of prices that an item should cost from Grubfather.""" display_name = "Minimum Grub Price" @@ -596,6 +661,7 @@ class CostSanityHybridChance(Range): for option in ( StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, + EntranceRandoType, ShuffleEntrancesMode, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, MinimumEssencePrice, MaximumEssencePrice, @@ -615,3 +681,45 @@ class CostSanityHybridChance(Range): # https://github.com/python/mypy/issues/6063 unfortunatly mypy hates this HKOptions = make_dataclass("HKOptions", list(hollow_knight_options.items()), bases=(PerGameCommonOptions,)) +HKOptionGroups: list[OptionGroup] = [ + OptionGroup("Randomize Options", [ + *hollow_knight_randomize_options.values(), + RandomizeElevatorPass + ], start_collapsed=False), + OptionGroup("Miscellaneous", [ + SplitCrystalHeart, + SplitMothwingCloak, + SplitMantisClaw, + WhitePalace, + ExtraPlatforms, + AddUnshuffledLocations, + StartingGeo, + RandomCharmCosts, + PlandoCharmCosts, + ], start_collapsed=True), + OptionGroup("Logic Options", hollow_knight_logic_options.values(), start_collapsed=False), + OptionGroup("Goal", [Goal, GrubHuntGoal], start_collapsed=False), + OptionGroup("DeathLink", [DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms], start_collapsed=True), + OptionGroup("Entrance Rando", [StartLocation, EntranceRandoType, ShuffleEntrancesMode], start_collapsed=True), + OptionGroup("Shop Slots", [ + EggShopSlots, + SlyShopSlots, + SlyKeyShopSlots, + IseldaShopSlots, + SalubraShopSlots, + SalubraCharmShopSlots, + LegEaterShopSlots, + GrubfatherRewardSlots, + SeerRewardSlots, + ExtraShopSlots + ], start_collapsed=True), + OptionGroup("CostSanity", [ + MinimumGeoPrice, MaximumGeoPrice, + MinimumGrubPrice, MaximumGrubPrice, + MinimumEssencePrice, MaximumEssencePrice, + MinimumCharmPrice, MaximumCharmPrice, + MinimumEggPrice, MaximumEggPrice, + CostSanity, CostSanityHybridChance, + *cost_sanity_weights.values() + ], start_collapsed=True), +] diff --git a/worlds/hk/resource_state_vars/cast_spell.py b/worlds/hk/resource_state_vars/cast_spell.py index e48bd2b6dc95..5bafbec8163f 100644 --- a/worlds/hk/resource_state_vars/cast_spell.py +++ b/worlds/hk/resource_state_vars/cast_spell.py @@ -1,22 +1,14 @@ from collections import Counter from collections.abc import Generator -from enum import IntEnum from itertools import chain +from ..constants import NearbySoul from ..options import HKOptions from . import RCStateVariable, cs, rs from .equip_charm import EquipCharmVariable from .soul_manager import SoulManager -class NearbySoul(IntEnum): - NONE = 1 - ITEMSOUL = 2 - MAPAREASOUL = 3 - AREASOUL = 4 - ROOMSOUL = 5 - - class CastSpellVariable(RCStateVariable): prefix: str = "$CASTSPELL" casts: list[int] diff --git a/worlds/hk/state_mixin.py b/worlds/hk/state_mixin.py index 5a66b3af4a78..3c9f15cf06df 100644 --- a/worlds/hk/state_mixin.py +++ b/worlds/hk/state_mixin.py @@ -5,11 +5,10 @@ from Utils import KeyedDefaultDict from worlds.AutoWorld import LogicMixin -from .constants import BASE_HEALTH, BASE_NOTCHES, BASE_SOUL # noqa: F401 +from .constants import BASE_HEALTH, BASE_NOTCHES, BASE_SOUL, NearbySoul # noqa: F401 if TYPE_CHECKING: from . import HKClause - from .resource_state_vars.cast_spell import NearbySoul # default_state = KeyedDefaultDict(lambda key: True if key == "NOFLOWER" else False) @@ -49,7 +48,7 @@ class HKLogicMixin(LogicMixin): _hk_charm_costs: dict[int, dict[str, int]] """mapping for charm costs per player""" - _hk_soul_modes: "dict[int, NearbySoul]" + _hk_soul_modes: dict[int, NearbySoul] """mapping of soul mode per player""" def init_mixin(self, multiworld: MultiWorld) -> None: diff --git a/worlds/hk/test/test_er.py b/worlds/hk/test/test_er.py new file mode 100644 index 000000000000..13164dc73af7 --- /dev/null +++ b/worlds/hk/test/test_er.py @@ -0,0 +1,225 @@ +import typing + +from .bases import HKTestBase + + +class TestErMapAllShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "map", + + "RandomizeDreamers": True, + "RandomizeSkills": True, + "RandomizeFocus": True, + "RandomizeSwim": True, + "RandomizeCharms": True, + "RandomizeKeys": True, + "RandomizeMaskShards": True, + "RandomizeVesselFragments": True, + "RandomizeCharmNotches": True, + "RandomizePaleOre": True, + "RandomizeGeoChests": True, + "RandomizeJunkPitChests": True, + "RandomizeRancidEggs": True, + "RandomizeRelics": True, + "RandomizeWhisperingRoots": True, + "RandomizeBossEssence": True, + "RandomizeGrubs": True, + "RandomizeMimics": True, + "RandomizeMaps": True, + "RandomizeStags": True, + "RandomizeLifebloodCocoons": True, + "RandomizeGrimmkinFlames": True, + "RandomizeJournalEntries": True, + "RandomizeNail": True, + "RandomizeGeoRocks": True, + "RandomizeBossGeo": True, + "RandomizeSoulTotems": True, + "RandomizeLoreTablets": True, + "RandomizeElevatorPass": True, + } + + +class TestErMapNoShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "map", + + "RandomizeDreamers": False, + "RandomizeSkills": False, + "RandomizeFocus": False, + "RandomizeSwim": False, + "RandomizeCharms": False, + "RandomizeKeys": False, + "RandomizeMaskShards": False, + "RandomizeVesselFragments": False, + "RandomizeCharmNotches": False, + "RandomizePaleOre": False, + "RandomizeGeoChests": False, + "RandomizeJunkPitChests": False, + "RandomizeRancidEggs": False, + "RandomizeRelics": False, + "RandomizeWhisperingRoots": False, + "RandomizeBossEssence": False, + "RandomizeGrubs": False, + "RandomizeMimics": False, + "RandomizeMaps": False, + "RandomizeStags": False, + "RandomizeLifebloodCocoons": False, + "RandomizeGrimmkinFlames": False, + "RandomizeJournalEntries": False, + "RandomizeNail": False, + "RandomizeGeoRocks": False, + "RandomizeBossGeo": False, + "RandomizeSoulTotems": False, + "RandomizeLoreTablets": False, + "RandomizeElevatorPass": False, + } + + +class TestErMFullllShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "full", + + "RandomizeDreamers": True, + "RandomizeSkills": True, + "RandomizeFocus": True, + "RandomizeSwim": True, + "RandomizeCharms": True, + "RandomizeKeys": True, + "RandomizeMaskShards": True, + "RandomizeVesselFragments": True, + "RandomizeCharmNotches": True, + "RandomizePaleOre": True, + "RandomizeGeoChests": True, + "RandomizeJunkPitChests": True, + "RandomizeRancidEggs": True, + "RandomizeRelics": True, + "RandomizeWhisperingRoots": True, + "RandomizeBossEssence": True, + "RandomizeGrubs": True, + "RandomizeMimics": True, + "RandomizeMaps": True, + "RandomizeStags": True, + "RandomizeLifebloodCocoons": True, + "RandomizeGrimmkinFlames": True, + "RandomizeJournalEntries": True, + "RandomizeNail": True, + "RandomizeGeoRocks": True, + "RandomizeBossGeo": True, + "RandomizeSoulTotems": True, + "RandomizeLoreTablets": True, + "RandomizeElevatorPass": True, + } + + +class TestErFullNoShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "full", + + "RandomizeDreamers": False, + "RandomizeSkills": False, + "RandomizeFocus": False, + "RandomizeSwim": False, + "RandomizeCharms": False, + "RandomizeKeys": False, + "RandomizeMaskShards": False, + "RandomizeVesselFragments": False, + "RandomizeCharmNotches": False, + "RandomizePaleOre": False, + "RandomizeGeoChests": False, + "RandomizeJunkPitChests": False, + "RandomizeRancidEggs": False, + "RandomizeRelics": False, + "RandomizeWhisperingRoots": False, + "RandomizeBossEssence": False, + "RandomizeGrubs": False, + "RandomizeMimics": False, + "RandomizeMaps": False, + "RandomizeStags": False, + "RandomizeLifebloodCocoons": False, + "RandomizeGrimmkinFlames": False, + "RandomizeJournalEntries": False, + "RandomizeNail": False, + "RandomizeGeoRocks": False, + "RandomizeBossGeo": False, + "RandomizeSoulTotems": False, + "RandomizeLoreTablets": False, + "RandomizeElevatorPass": False, + } + + +class TestErRoomAllShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "room", + + "RandomizeDreamers": True, + "RandomizeSkills": True, + "RandomizeFocus": True, + "RandomizeSwim": True, + "RandomizeCharms": True, + "RandomizeKeys": True, + "RandomizeMaskShards": True, + "RandomizeVesselFragments": True, + "RandomizeCharmNotches": True, + "RandomizePaleOre": True, + "RandomizeGeoChests": True, + "RandomizeJunkPitChests": True, + "RandomizeRancidEggs": True, + "RandomizeRelics": True, + "RandomizeWhisperingRoots": True, + "RandomizeBossEssence": True, + "RandomizeGrubs": True, + "RandomizeMimics": True, + "RandomizeMaps": True, + "RandomizeStags": True, + "RandomizeLifebloodCocoons": True, + "RandomizeGrimmkinFlames": True, + "RandomizeJournalEntries": True, + "RandomizeNail": True, + "RandomizeGeoRocks": True, + "RandomizeBossGeo": True, + "RandomizeSoulTotems": True, + "RandomizeLoreTablets": True, + "RandomizeElevatorPass": True, + } + + +class TestErRoomNoShuffled(HKTestBase): + options: typing.ClassVar[dict[str, str]] = { + "Goal": "any", + "EntranceRandoType": "room", + + "RandomizeDreamers": False, + "RandomizeSkills": False, + "RandomizeFocus": False, + "RandomizeSwim": False, + "RandomizeCharms": False, + "RandomizeKeys": False, + "RandomizeMaskShards": False, + "RandomizeVesselFragments": False, + "RandomizeCharmNotches": False, + "RandomizePaleOre": False, + "RandomizeGeoChests": False, + "RandomizeJunkPitChests": False, + "RandomizeRancidEggs": False, + "RandomizeRelics": False, + "RandomizeWhisperingRoots": False, + "RandomizeBossEssence": False, + "RandomizeGrubs": False, + "RandomizeMimics": False, + "RandomizeMaps": False, + "RandomizeStags": False, + "RandomizeLifebloodCocoons": False, + "RandomizeGrimmkinFlames": False, + "RandomizeJournalEntries": False, + "RandomizeNail": False, + "RandomizeGeoRocks": False, + "RandomizeBossGeo": False, + "RandomizeSoulTotems": False, + "RandomizeLoreTablets": False, + "RandomizeElevatorPass": False, + }