import typing from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: # avoid import during runtime from random import Random else: Random = typing.Any locations = {"option_" + start: i for i, start in enumerate(starts)} # This way the dynamic start names are picked up by the MetaClass Choice belongs to StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations, "__doc__": "Choose your start location. " "This is currently only locked to King's Pass."}) del (locations) option_docstrings = { "RandomizeDreamers": "Allow for Dreamers to be randomized into the item pool and opens their locations for " "randomization.", "RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. " "Also opens their locations\n for receiving randomized items.", "RandomizeFocus": "Removes the ability to focus and randomizes it into the item pool.", "RandomizeSwim": "Removes the ability to swim in water and randomizes it into the item pool.", "RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for " "randomization. Includes Charms\n sold in shops.", "RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.", "RandomizeMaskShards": "Allow for Mask Shard to be randomized into the item pool and open their locations for" " randomization.", "RandomizeVesselFragments": "Allow for Vessel Fragments to be randomized into the item pool and open their " "locations for randomization.", "RandomizeCharmNotches": "Allow for Charm Notches to be randomized into the item pool. " "Includes those sold by Salubra.", "RandomizePaleOre": "Randomize Pale Ores into the item pool and open their locations for randomization.", "RandomizeGeoChests": "Allow for Geo Chests to contain randomized items, " "as well as their Geo reward being randomized into the item pool.", "RandomizeJunkPitChests": "Randomize the contents of junk pit chests into the item pool and open their locations " "for randomization.", "RandomizeRancidEggs": "Randomize Rancid Eggs into the item pool and open their locations for randomization", "RandomizeRelics": "Randomize Relics (King's Idol, et al.) into the item pool and open their locations for" " randomization.", "RandomizeWhisperingRoots": "Randomize the essence rewards from Whispering Roots into the item pool. Whispering " "Roots will now grant a randomized\n item when completed. This can be previewed by " "standing on the root.", "RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item " "pool and open their locations\n for randomization.", "RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.", "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization." "Mimic Grubs are always placed\n in your own game.", "RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see" " and buy an item\n that is randomized into that location as well.", "RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items " "on the stag station bell/toll.", "RandomizeLifebloodCocoons": "Randomize Lifeblood Cocoon grants into the item pool and open their locations" " for randomization.", "RandomizeGrimmkinFlames": "Randomize Grimmkin Flames into the item pool and open their locations for " "randomization.", "RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item " "pool, and open their locations\n for randomization. Does not include journal entries " "gained by killing enemies.", "RandomizeNail": "Removes the ability to swing the nail left, right and up, and shuffles these into the item pool.", "RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.", "RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.", "RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for" " randomization.", "RandomizeLoreTablets": "Randomize Lore items into the itempool, one per Lore Tablet, and place randomized item " "grants on the tablets themselves.\n You must still read the tablet to get the item.", "PreciseMovement": "Places skips into logic which require extremely precise player movement, possibly without " "movement skills such as\n dash or hook.", "ProficientCombat": "Places skips into logic which require proficient combat, possibly with limited items.", "BackgroundObjectPogos": "Places skips into logic for locations which are reachable via pogoing off of " "background objects.", "EnemyPogos": "Places skips into logic for locations which are reachable via pogos off of enemies.", "ObscureSkips": "Places skips into logic which are considered obscure enough that a beginner is not expected " "to know them.", "ShadeSkips": "Places shade skips into logic which utilize the player's shade for pogoing or damage boosting.", "InfectionSkips": "Places skips into logic which are only possible after the crossroads become infected.", "FireballSkips": "Places skips into logic which require the use of spells to reset fall speed while in mid-air.", "SpikeTunnels": "Places skips into logic which require the navigation of narrow tunnels filled with spikes.", "AcidSkips": "Places skips into logic which require crossing a pool of acid without Isma's Tear, or water if swim " "is disabled.", "DamageBoosts": "Places skips into logic which require you to take damage from an enemy or hazard to progress.", "DangerousSkips": "Places skips into logic which contain a high risk of taking damage.", "DarkRooms": "Places skips into logic which require navigating dark rooms without the use of the Lumafly Lantern.", "ComplexSkips": "Places skips into logic which require intense setup or are obscure even beyond advanced skip " "standards.", "DifficultSkips": "Places skips into logic which are considered more difficult than typical.", "RemoveSpellUpgrades": "Removes the second level of all spells from the item pool." } default_on = { "RandomizeDreamers", "RandomizeSkills", "RandomizeCharms", "RandomizeKeys", "RandomizeMaskShards", "RandomizeVesselFragments", "RandomizePaleOre", "RandomizeRelics" } shop_to_option = { "Seer": "SeerRewardSlots", "Grubfather": "GrubfatherRewardSlots", "Sly": "SlyShopSlots", "Sly_(Key)": "SlyKeyShopSlots", "Iselda": "IseldaShopSlots", "Salubra": "SalubraShopSlots", "Leg_Eater": "LegEaterShopSlots", "Salubra_(Requires_Charms)": "SalubraCharmShopSlots", "Egg_Shop": "EggShopSlots", } hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {} for option_name, option_data in pool_options.items(): extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]} if option_name in option_docstrings: extra_data["__doc__"] = option_docstrings[option_name] if option_name in default_on: option = type(option_name, (DefaultOnToggle,), extra_data) else: option = type(option_name, (Toggle,), extra_data) globals()[option.__name__] = option hollow_knight_randomize_options[option.__name__] = option hollow_knight_logic_options: typing.Dict[str, type(Option)] = {} for option_name in logic_options.values(): if option_name in hollow_knight_randomize_options: continue extra_data = {"__module__": __name__} if option_name in option_docstrings: extra_data["__doc__"] = option_docstrings[option_name] option = type(option_name, (Toggle,), extra_data) globals()[option.__name__] = option hollow_knight_logic_options[option.__name__] = option class RandomizeElevatorPass(Toggle): """Adds an Elevator Pass item to the item pool, which is then required to use the large elevators connecting City of Tears to the Forgotten Crossroads and Resting Grounds.""" display_name = "Randomize Elevator Pass" default = False class SplitMothwingCloak(Toggle): """Splits the Mothwing Cloak into left- and right-only versions of the item. Randomly adds a second left or right Mothwing cloak item which functions as the upgrade to Shade Cloak.""" display_name = "Split Mothwing Cloak" default = False class SplitMantisClaw(Toggle): """Splits the Mantis Claw into left- and right-only versions of the item.""" display_name = "Split Mantis Claw" default = False class SplitCrystalHeart(Toggle): """Splits the Crystal Heart into left- and right-only versions of the item.""" display_name = "Split Crystal Heart" default = False class MinimumGrubPrice(Range): """The minimum grub price in the range of prices that an item should cost from Grubfather.""" display_name = "Minimum Grub Price" range_start = 1 range_end = 46 default = 1 class MaximumGrubPrice(MinimumGrubPrice): """The maximum grub price in the range of prices that an item should cost from Grubfather.""" display_name = "Maximum Grub Price" default = 23 class MinimumEssencePrice(Range): """The minimum essence price in the range of prices that an item should cost from Seer.""" display_name = "Minimum Essence Price" range_start = 1 range_end = 2800 default = 1 class MaximumEssencePrice(MinimumEssencePrice): """The maximum essence price in the range of prices that an item should cost from Seer.""" display_name = "Maximum Essence Price" default = 1400 class MinimumEggPrice(Range): """The minimum rancid egg price in the range of prices that an item should cost from Jiji. Only takes effect if the EggSlotShops option is greater than 0.""" display_name = "Minimum Egg Price" range_start = 1 range_end = 21 default = 1 class MaximumEggPrice(MinimumEggPrice): """The maximum rancid egg price in the range of prices that an item should cost from Jiji. Only takes effect if the EggSlotShops option is greater than 0.""" display_name = "Maximum Egg Price" default = 10 class MinimumCharmPrice(Range): """The minimum charm price in the range of prices that an item should cost for Salubra's shop item which also carry a charm cost.""" display_name = "Minimum Charm Requirement" range_start = 1 range_end = 40 default = 1 class MaximumCharmPrice(MinimumCharmPrice): """The maximum charm price in the range of prices that an item should cost for Salubra's shop item which also carry a charm cost.""" display_name = "Maximum Charm Requirement" default = 20 class MinimumGeoPrice(Range): """The minimum geo price for items in geo shops.""" display_name = "Minimum Geo Price" range_start = 1 range_end = 200 default = 1 class MaximumGeoPrice(Range): """The maximum geo price for items in geo shops.""" display_name = "Maximum Geo Price" range_start = 1 range_end = 2000 default = 400 class RandomCharmCosts(NamedRange): """Total Notch Cost of all Charms together. Vanilla sums to 90. This value is distributed among all charms in a random fashion. Special Cases: Set to -1 or vanilla for vanilla costs. Set to -2 or shuffle to shuffle around the vanilla costs to different charms.""" display_name = "Randomize Charm Notch Costs" range_start = 0 range_end = 240 default = -1 vanilla_costs: typing.List[int] = vanilla_costs charm_count: int = len(vanilla_costs) special_range_names = { "vanilla": -1, "shuffle": -2 } def get_costs(self, random_source: Random) -> typing.List[int]: charms: typing.List[int] if -1 == self.value: return self.vanilla_costs.copy() elif -2 == self.value: charms = self.vanilla_costs.copy() random_source.shuffle(charms) return charms else: charms = [0]*self.charm_count for x in range(self.value): index = random_source.randint(0, self.charm_count-1) while charms[index] > 5: index = random_source.randint(0, self.charm_count-1) charms[index] += 1 return charms class PlandoCharmCosts(OptionDict): """Allows setting a Charm's Notch costs directly, mapping {name: cost}. This is set after any random Charm Notch costs, if applicable.""" display_name = "Charm Notch Cost Plando" valid_keys = frozenset(charm_names) def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]: for name, cost in self.value.items(): charm_costs[charm_names.index(name)] = cost return charm_costs class SlyShopSlots(Range): """For each extra slot, add a location to the Sly Shop and a filler item to the item pool.""" display_name = "Sly Shop Slots" default = 8 range_end = 16 class SlyKeyShopSlots(Range): """For each extra slot, add a location to the Sly Shop (requiring Shopkeeper's Key) and a filler item to the item pool.""" display_name = "Sly Key Shop Slots" default = 6 range_end = 16 class IseldaShopSlots(Range): """For each extra slot, add a location to the Iselda Shop and a filler item to the item pool.""" display_name = "Iselda Shop Slots" default = 2 range_end = 16 class SalubraShopSlots(Range): """For each extra slot, add a location to the Salubra Shop, and a filler item to the item pool.""" display_name = "Salubra Shop Slots" default = 5 range_start = 0 range_end = 16 class SalubraCharmShopSlots(Range): """For each extra slot, add a location to the Salubra Shop (requiring Charms), and a filler item to the item pool.""" display_name = "Salubra Charm Shop Slots" default = 5 range_end = 16 class LegEaterShopSlots(Range): """For each extra slot, add a location to the Leg Eater Shop and a filler item to the item pool.""" display_name = "Leg Eater Shop Slots" default = 3 range_end = 16 class GrubfatherRewardSlots(Range): """For each extra slot, add a location to the Grubfather and a filler item to the item pool.""" display_name = "Grubfather Reward Slots" default = 7 range_end = 16 class SeerRewardSlots(Range): """For each extra slot, add a location to the Seer and a filler item to the item pool.""" display_name = "Seer Reward Reward Slots" default = 8 range_end = 16 class EggShopSlots(Range): """For each slot, add a location to the Egg Shop and a filler item to the item pool.""" display_name = "Egg Shop Item Slots" range_end = 16 class ExtraShopSlots(Range): """For each extra slot, add a location to a randomly chosen shop a filler item to the item pool. The Egg Shop will be excluded from this list unless it has at least one item. Shops are capped at 16 items each. """ display_name = "Additional Shop Slots" default = 0 range_end = 9 * 16 # Number of shops x max slots per shop. class Goal(Choice): """The goal required of you in order to complete your run in Archipelago.""" display_name = "Goal" option_any = 0 option_hollowknight = 1 option_siblings = 2 option_radiance = 3 # Client support exists for this, but logic is a nightmare # option_godhome = 4 default = 0 class WhitePalace(Choice): """ Whether or not to include White Palace or not. Note: Even if excluded, the King Fragment check may still be required if charms are vanilla. """ display_name = "White Palace" option_exclude = 0 # No White Palace at all option_kingfragment = 1 # Include King Fragment check only option_nopathofpain = 2 # Exclude Path of Pain locations. option_include = 3 # Include all White Palace locations, including Path of Pain. default = 0 class ExtraPlatforms(DefaultOnToggle): """Places additional platforms to make traveling throughout Hallownest more convenient.""" class DeathLinkShade(Choice): """Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any. vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any. shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched. shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless. * This option has no effect if DeathLink is disabled. ** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override your existing shade, if any. """ option_vanilla = 0 option_shadeless = 1 option_shade = 2 default = 2 class DeathLinkBreaksFragileCharms(Toggle): """Sets if fragile charms break when you are killed by a DeathLink. * This option has no effect if DeathLink is disabled. ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it will continue to do so. """ class StartingGeo(Range): """The amount of starting geo you have.""" display_name = "Starting Geo" range_start = 0 range_end = 1000 default = 0 class CostSanity(Choice): """If enabled, most locations with costs (like stag stations) will have randomly determined costs. If set to shopsonly, CostSanity will only apply to shops (including Grubfather, Seer and Egg Shop). If set to notshops, CostSanity will only apply to non-shops (e.g. Stag stations and Cornifer locations) These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs """ option_off = 0 alias_no = 0 option_on = 1 alias_yes = 1 option_shopsonly = 2 option_notshops = 3 display_name = "Cost Sanity" class CostSanityHybridChance(Range): """The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence""" range_end = 100 default = 10 cost_sanity_weights: typing.Dict[str, type(Option)] = {} for term, cost in cost_terms.items(): option_name = f"CostSanity{cost.option}Weight" extra_data = { "__module__": __name__, "range_end": 1000, "__doc__": ( f"The likelihood of Costsanity choosing a {cost.option} cost." " Chosen as a sum of all weights from other types." ), "default": cost.weight } if cost == 'GEO': extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop." option = type(option_name, (Range,), extra_data) globals()[option.__name__] = option cost_sanity_weights[option.__name__] = option hollow_knight_options: typing.Dict[str, type(Option)] = { **hollow_knight_randomize_options, RandomizeElevatorPass.__name__: RandomizeElevatorPass, **hollow_knight_logic_options, **{ option.__name__: option for option in ( StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo, DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, MinimumEssencePrice, MaximumEssencePrice, MinimumCharmPrice, MaximumCharmPrice, RandomCharmCosts, PlandoCharmCosts, MinimumEggPrice, MaximumEggPrice, EggShopSlots, SlyShopSlots, SlyKeyShopSlots, IseldaShopSlots, SalubraShopSlots, SalubraCharmShopSlots, LegEaterShopSlots, GrubfatherRewardSlots, SeerRewardSlots, ExtraShopSlots, SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw, CostSanity, CostSanityHybridChance ) }, **cost_sanity_weights }