HK: Options API updates, et al. (#3428)

* updates HK to consistently use world.random, use world.options, don't use world = self.multiworld, and remove some things from the logicMixin

* Update HK to new options dataclass

* Move completion condition helpers to Rules.py

* updates from review
This commit is contained in:
qwint 2024-07-28 16:27:39 -05:00 committed by GitHub
parent ab0903679c
commit e764da3dc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 99 deletions

View File

@ -1,10 +1,12 @@
import typing import typing
import re import re
from dataclasses import dataclass, make_dataclass
from .ExtractedData import logic_options, starts, pool_options from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms from .Rules import cost_terms
from schema import And, Schema, Optional from schema import And, Schema, Optional
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink, PerGameCommonOptions
from .Charms import vanilla_costs, names as charm_names from .Charms import vanilla_costs, names as charm_names
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -538,3 +540,5 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
}, },
**cost_sanity_weights **cost_sanity_weights
} }
HKOptions = make_dataclass("HKOptions", [(name, option) for name, option in hollow_knight_options.items()], bases=(PerGameCommonOptions,))

View File

@ -49,3 +49,42 @@ def set_rules(hk_world: World):
if term == "GEO": # No geo logic! if term == "GEO": # No geo logic!
continue continue
add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount)
def _hk_nail_combat(state, player) -> bool:
return state.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
def _hk_can_beat_thk(state, player) -> bool:
return (
state.has('Opened_Black_Egg_Temple', player)
and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
and _hk_nail_combat(state, player)
and (
state.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
or state._hk_option(player, 'ProficientCombat')
)
and state.has('FOCUS', player)
)
def _hk_siblings_ending(state, player) -> bool:
return _hk_can_beat_thk(state, player) and state.has('WHITEFRAGMENT', player, 3)
def _hk_can_beat_radiance(state, player) -> bool:
return (
state.has('Opened_Black_Egg_Temple', player)
and _hk_nail_combat(state, player)
and state.has('WHITEFRAGMENT', player, 3)
and state.has('DREAMNAIL', player)
and (
(state.has('LEFTCLAW', player) and state.has('RIGHTCLAW', player))
or state.has('WINGS', player)
)
and (state.count('FIREBALL', player) + state.count('SCREAM', player) + state.count('QUAKE', player)) > 1
and (
(state.has('LEFTDASH', player, 2) and state.has('RIGHTDASH', player, 2)) # Both Shade Cloaks
or (state._hk_option(player, 'ProficientCombat') and state.has('QUAKE', player)) # or Dive
)
)

View File

@ -10,9 +10,9 @@ logger = logging.getLogger("Hollow Knight")
from .Items import item_table, lookup_type_to_names, item_name_groups from .Items import item_table, lookup_type_to_names, item_name_groups
from .Regions import create_regions from .Regions import create_regions
from .Rules import set_rules, cost_terms from .Rules import set_rules, cost_terms, _hk_can_beat_thk, _hk_siblings_ending, _hk_can_beat_radiance
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \ from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
shop_to_option shop_to_option, HKOptions
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \ from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
from .Charms import names as charm_names from .Charms import names as charm_names
@ -142,7 +142,8 @@ class HKWorld(World):
As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils. As the enigmatic Knight, youll traverse the depths, unravel its mysteries and conquer its evils.
""" # from https://www.hollowknight.com """ # from https://www.hollowknight.com
game: str = "Hollow Knight" game: str = "Hollow Knight"
option_definitions = hollow_knight_options options_dataclass = HKOptions
options: HKOptions
web = HKWeb() web = HKWeb()
@ -155,8 +156,8 @@ class HKWorld(World):
charm_costs: typing.List[int] charm_costs: typing.List[int]
cached_filler_items = {} cached_filler_items = {}
def __init__(self, world, player): def __init__(self, multiworld, player):
super(HKWorld, self).__init__(world, player) super(HKWorld, self).__init__(multiworld, player)
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = { self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
location: list() for location in multi_locations location: list() for location in multi_locations
} }
@ -165,29 +166,29 @@ class HKWorld(World):
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs) self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
def generate_early(self): def generate_early(self):
world = self.multiworld options = self.options
charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) charm_costs = options.RandomCharmCosts.get_costs(self.random)
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
# world.exclude_locations[self.player].value.update(white_palace_locations) # options.exclude_locations.value.update(white_palace_locations)
for term, data in cost_terms.items(): for term, data in cost_terms.items():
mini = getattr(world, f"Minimum{data.option}Price")[self.player] mini = getattr(options, f"Minimum{data.option}Price")
maxi = getattr(world, f"Maximum{data.option}Price")[self.player] maxi = getattr(options, f"Maximum{data.option}Price")
# if minimum > maximum, set minimum to maximum # if minimum > maximum, set minimum to maximum
mini.value = min(mini.value, maxi.value) mini.value = min(mini.value, maxi.value)
self.ranges[term] = mini.value, maxi.value self.ranges[term] = mini.value, maxi.value
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key], self.multiworld.push_precollected(HKItem(starts[options.StartLocation.current_key],
True, None, "Event", self.player)) True, None, "Event", self.player))
def white_palace_exclusions(self): def white_palace_exclusions(self):
exclusions = set() exclusions = set()
wp = self.multiworld.WhitePalace[self.player] wp = self.options.WhitePalace
if wp <= WhitePalace.option_nopathofpain: if wp <= WhitePalace.option_nopathofpain:
exclusions.update(path_of_pain_locations) exclusions.update(path_of_pain_locations)
if wp <= WhitePalace.option_kingfragment: if wp <= WhitePalace.option_kingfragment:
exclusions.update(white_palace_checks) exclusions.update(white_palace_checks)
if wp == WhitePalace.option_exclude: if wp == WhitePalace.option_exclude:
exclusions.add("King_Fragment") exclusions.add("King_Fragment")
if self.multiworld.RandomizeCharms[self.player]: if self.options.RandomizeCharms:
# If charms are randomized, this will be junk-filled -- so transitions and events are not progression # If charms are randomized, this will be junk-filled -- so transitions and events are not progression
exclusions.update(white_palace_transitions) exclusions.update(white_palace_transitions)
exclusions.update(white_palace_events) exclusions.update(white_palace_events)
@ -200,7 +201,7 @@ class HKWorld(World):
# check for any goal that godhome events are relevant to # check for any goal that godhome events are relevant to
all_event_names = event_names.copy() all_event_names = event_names.copy()
if self.multiworld.Goal[self.player] in [Goal.option_godhome, Goal.option_godhome_flower]: if self.options.Goal in [Goal.option_godhome, Goal.option_godhome_flower]:
from .GodhomeData import godhome_event_names from .GodhomeData import godhome_event_names
all_event_names.update(set(godhome_event_names)) all_event_names.update(set(godhome_event_names))
@ -230,12 +231,12 @@ class HKWorld(World):
pool: typing.List[HKItem] = [] pool: typing.List[HKItem] = []
wp_exclusions = self.white_palace_exclusions() wp_exclusions = self.white_palace_exclusions()
junk_replace: typing.Set[str] = set() junk_replace: typing.Set[str] = set()
if self.multiworld.RemoveSpellUpgrades[self.player]: if self.options.RemoveSpellUpgrades:
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark")) junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
randomized_starting_items = set() randomized_starting_items = set()
for attr, items in randomizable_starting_items.items(): for attr, items in randomizable_starting_items.items():
if getattr(self.multiworld, attr)[self.player]: if getattr(self.options, attr):
randomized_starting_items.update(items) randomized_starting_items.update(items)
# noinspection PyShadowingNames # noinspection PyShadowingNames
@ -257,7 +258,7 @@ class HKWorld(World):
if item_name in junk_replace: if item_name in junk_replace:
item_name = self.get_filler_item_name() item_name = self.get_filler_item_name()
item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.multiworld.AddUnshuffledLocations[self.player] else self.create_event(item_name) item = self.create_item(item_name) if not vanilla or location_name == "Start" or self.options.AddUnshuffledLocations else self.create_event(item_name)
if location_name == "Start": if location_name == "Start":
if item_name in randomized_starting_items: if item_name in randomized_starting_items:
@ -281,55 +282,55 @@ class HKWorld(World):
location.progress_type = LocationProgressType.EXCLUDED location.progress_type = LocationProgressType.EXCLUDED
for option_key, option in hollow_knight_randomize_options.items(): for option_key, option in hollow_knight_randomize_options.items():
randomized = getattr(self.multiworld, option_key)[self.player] randomized = getattr(self.options, option_key)
if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]): if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]):
continue continue
for item_name, location_name in zip(option.items, option.locations): for item_name, location_name in zip(option.items, option.locations):
if item_name in junk_replace: if item_name in junk_replace:
item_name = self.get_filler_item_name() item_name = self.get_filler_item_name()
if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \ if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \
(item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]): (item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak):
_add("Left_" + item_name, location_name, randomized) _add("Left_" + item_name, location_name, randomized)
_add("Right_" + item_name, "Split_" + location_name, randomized) _add("Right_" + item_name, "Split_" + location_name, randomized)
continue continue
if item_name == "Mantis_Claw" and self.multiworld.SplitMantisClaw[self.player]: if item_name == "Mantis_Claw" and self.options.SplitMantisClaw:
_add("Left_" + item_name, "Left_" + location_name, randomized) _add("Left_" + item_name, "Left_" + location_name, randomized)
_add("Right_" + item_name, "Right_" + location_name, randomized) _add("Right_" + item_name, "Right_" + location_name, randomized)
continue continue
if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]: if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak:
if self.multiworld.random.randint(0, 1): if self.random.randint(0, 1):
item_name = "Left_Mothwing_Cloak" item_name = "Left_Mothwing_Cloak"
else: else:
item_name = "Right_Mothwing_Cloak" item_name = "Right_Mothwing_Cloak"
if item_name == "Grimmchild2" and self.multiworld.RandomizeGrimmkinFlames[self.player] and self.multiworld.RandomizeCharms[self.player]: if item_name == "Grimmchild2" and self.options.RandomizeGrimmkinFlames and self.options.RandomizeCharms:
_add("Grimmchild1", location_name, randomized) _add("Grimmchild1", location_name, randomized)
continue continue
_add(item_name, location_name, randomized) _add(item_name, location_name, randomized)
if self.multiworld.RandomizeElevatorPass[self.player]: if self.options.RandomizeElevatorPass:
randomized = True randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized) _add("Elevator_Pass", "Elevator_Pass", randomized)
for shop, locations in self.created_multi_locations.items(): for shop, locations in self.created_multi_locations.items():
for _ in range(len(locations), getattr(self.multiworld, shop_to_option[shop])[self.player].value): for _ in range(len(locations), getattr(self.options, shop_to_option[shop]).value):
loc = self.create_location(shop) loc = self.create_location(shop)
unfilled_locations += 1 unfilled_locations += 1
# Balance the pool # Balance the pool
item_count = len(pool) item_count = len(pool)
additional_shop_items = max(item_count - unfilled_locations, self.multiworld.ExtraShopSlots[self.player].value) additional_shop_items = max(item_count - unfilled_locations, self.options.ExtraShopSlots.value)
# Add additional shop items, as needed. # Add additional shop items, as needed.
if additional_shop_items > 0: if additional_shop_items > 0:
shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16) shops = list(shop for shop, locations in self.created_multi_locations.items() if len(locations) < 16)
if not self.multiworld.EggShopSlots[self.player].value: # No eggshop, so don't place items there if not self.options.EggShopSlots: # No eggshop, so don't place items there
shops.remove('Egg_Shop') shops.remove('Egg_Shop')
if shops: if shops:
for _ in range(additional_shop_items): for _ in range(additional_shop_items):
shop = self.multiworld.random.choice(shops) shop = self.random.choice(shops)
loc = self.create_location(shop) loc = self.create_location(shop)
unfilled_locations += 1 unfilled_locations += 1
if len(self.created_multi_locations[shop]) >= 16: if len(self.created_multi_locations[shop]) >= 16:
@ -355,7 +356,7 @@ class HKWorld(World):
loc.costs = costs loc.costs = costs
def apply_costsanity(self): def apply_costsanity(self):
setting = self.multiworld.CostSanity[self.player].value setting = self.options.CostSanity.value
if not setting: if not setting:
return # noop return # noop
@ -369,10 +370,10 @@ class HKWorld(World):
return {k: v for k, v in weights.items() if v} return {k: v for k, v in weights.items() if v}
random = self.multiworld.random random = self.random
hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
weights = { weights = {
data.term: getattr(self.multiworld, f"CostSanity{data.option}Weight")[self.player].value data.term: getattr(self.options, f"CostSanity{data.option}Weight").value
for data in cost_terms.values() for data in cost_terms.values()
} }
weights_geoless = dict(weights) weights_geoless = dict(weights)
@ -427,22 +428,22 @@ class HKWorld(World):
location.sort_costs() location.sort_costs()
def set_rules(self): def set_rules(self):
world = self.multiworld multiworld = self.multiworld
player = self.player player = self.player
goal = world.Goal[player] goal = self.options.Goal
if goal == Goal.option_hollowknight: if goal == Goal.option_hollowknight:
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player)
elif goal == Goal.option_siblings: elif goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player) multiworld.completion_condition[player] = lambda state: _hk_siblings_ending(state, player)
elif goal == Goal.option_radiance: elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player) multiworld.completion_condition[player] = lambda state: _hk_can_beat_radiance(state, player)
elif goal == Goal.option_godhome: elif goal == Goal.option_godhome:
world.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player) multiworld.completion_condition[player] = lambda state: state.count("Defeated_Pantheon_5", player)
elif goal == Goal.option_godhome_flower: elif goal == Goal.option_godhome_flower:
world.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player) multiworld.completion_condition[player] = lambda state: state.count("Godhome_Flower_Quest", player)
else: else:
# Any goal # Any goal
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player) multiworld.completion_condition[player] = lambda state: _hk_can_beat_thk(state, player) or _hk_can_beat_radiance(state, player)
set_rules(self) set_rules(self)
@ -450,8 +451,8 @@ class HKWorld(World):
slot_data = {} slot_data = {}
options = slot_data["options"] = {} options = slot_data["options"] = {}
for option_name in self.option_definitions: for option_name in hollow_knight_options:
option = getattr(self.multiworld, option_name)[self.player] option = getattr(self.options, option_name)
try: try:
optionvalue = int(option.value) optionvalue = int(option.value)
except TypeError: except TypeError:
@ -460,10 +461,10 @@ class HKWorld(World):
options[option_name] = optionvalue options[option_name] = optionvalue
# 32 bit int # 32 bit int
slot_data["seed"] = self.multiworld.per_slot_randoms[self.player].randint(-2147483647, 2147483646) slot_data["seed"] = self.random.randint(-2147483647, 2147483646)
# Backwards compatibility for shop cost data (HKAP < 0.1.0) # Backwards compatibility for shop cost data (HKAP < 0.1.0)
if not self.multiworld.CostSanity[self.player]: if not self.options.CostSanity:
for shop, terms in shop_cost_types.items(): for shop, terms in shop_cost_types.items():
unit = cost_terms[next(iter(terms))].option unit = cost_terms[next(iter(terms))].option
if unit == "Geo": if unit == "Geo":
@ -498,7 +499,7 @@ class HKWorld(World):
basename = name basename = name
if name in shop_cost_types: if name in shop_cost_types:
costs = { costs = {
term: self.multiworld.random.randint(*self.ranges[term]) term: self.random.randint(*self.ranges[term])
for term in shop_cost_types[name] for term in shop_cost_types[name]
} }
elif name in vanilla_location_costs: elif name in vanilla_location_costs:
@ -512,7 +513,7 @@ class HKWorld(World):
region = self.multiworld.get_region("Menu", self.player) region = self.multiworld.get_region("Menu", self.player)
if vanilla and not self.multiworld.AddUnshuffledLocations[self.player]: if vanilla and not self.options.AddUnshuffledLocations:
loc = HKLocation(self.player, name, loc = HKLocation(self.player, name,
None, region, costs=costs, vanilla=vanilla, None, region, costs=costs, vanilla=vanilla,
basename=basename) basename=basename)
@ -560,26 +561,26 @@ class HKWorld(World):
return change return change
@classmethod @classmethod
def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle): def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle):
hk_players = world.get_game_players(cls.game) hk_players = multiworld.get_game_players(cls.game)
spoiler_handle.write('\n\nCharm Notches:') spoiler_handle.write('\n\nCharm Notches:')
for player in hk_players: for player in hk_players:
name = world.get_player_name(player) name = multiworld.get_player_name(player)
spoiler_handle.write(f'\n{name}\n') spoiler_handle.write(f'\n{name}\n')
hk_world: HKWorld = world.worlds[player] hk_world: HKWorld = multiworld.worlds[player]
for charm_number, cost in enumerate(hk_world.charm_costs): for charm_number, cost in enumerate(hk_world.charm_costs):
spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}") spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}")
spoiler_handle.write('\n\nShop Prices:') spoiler_handle.write('\n\nShop Prices:')
for player in hk_players: for player in hk_players:
name = world.get_player_name(player) name = multiworld.get_player_name(player)
spoiler_handle.write(f'\n{name}\n') spoiler_handle.write(f'\n{name}\n')
hk_world: HKWorld = world.worlds[player] hk_world: HKWorld = multiworld.worlds[player]
if world.CostSanity[player].value: if hk_world.options.CostSanity:
for loc in sorted( for loc in sorted(
( (
loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player))) loc for loc in itertools.chain(*(region.locations for region in multiworld.get_regions(player)))
if loc.costs if loc.costs
), key=operator.attrgetter('name') ), key=operator.attrgetter('name')
): ):
@ -603,15 +604,15 @@ class HKWorld(World):
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests', 'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
'RandomizeRancidEggs' 'RandomizeRancidEggs'
): ):
if getattr(self.multiworld, group): if getattr(self.options, group):
fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in fillers.extend(item for item in hollow_knight_randomize_options[group].items if item not in
exclusions) exclusions)
self.cached_filler_items[self.player] = fillers self.cached_filler_items[self.player] = fillers
return self.multiworld.random.choice(self.cached_filler_items[self.player]) return self.random.choice(self.cached_filler_items[self.player])
def create_region(world: MultiWorld, player: int, name: str, location_names=None) -> Region: def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region:
ret = Region(name, player, world) ret = Region(name, player, multiworld)
if location_names: if location_names:
for location in location_names: for location in location_names:
loc_id = HKWorld.location_name_to_id.get(location, None) loc_id = HKWorld.location_name_to_id.get(location, None)
@ -684,42 +685,7 @@ class HKLogicMixin(LogicMixin):
return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches) return sum(self.multiworld.worlds[player].charm_costs[notch] for notch in notches)
def _hk_option(self, player: int, option_name: str) -> int: def _hk_option(self, player: int, option_name: str) -> int:
return getattr(self.multiworld, option_name)[player].value return getattr(self.multiworld.worlds[player].options, option_name).value
def _hk_start(self, player, start_location: str) -> bool: def _hk_start(self, player, start_location: str) -> bool:
return self.multiworld.StartLocation[player] == start_location return self.multiworld.worlds[player].options.StartLocation == start_location
def _hk_nail_combat(self, player: int) -> bool:
return self.has_any({'LEFTSLASH', 'RIGHTSLASH', 'UPSLASH'}, player)
def _hk_can_beat_thk(self, player: int) -> bool:
return (
self.has('Opened_Black_Egg_Temple', player)
and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
and self._hk_nail_combat(player)
and (
self.has_any({'LEFTDASH', 'RIGHTDASH'}, player)
or self._hk_option(player, 'ProficientCombat')
)
and self.has('FOCUS', player)
)
def _hk_siblings_ending(self, player: int) -> bool:
return self._hk_can_beat_thk(player) and self.has('WHITEFRAGMENT', player, 3)
def _hk_can_beat_radiance(self, player: int) -> bool:
return (
self.has('Opened_Black_Egg_Temple', player)
and self._hk_nail_combat(player)
and self.has('WHITEFRAGMENT', player, 3)
and self.has('DREAMNAIL', player)
and (
(self.has('LEFTCLAW', player) and self.has('RIGHTCLAW', player))
or self.has('WINGS', player)
)
and (self.count('FIREBALL', player) + self.count('SCREAM', player) + self.count('QUAKE', player)) > 1
and (
(self.has('LEFTDASH', player, 2) and self.has('RIGHTDASH', player, 2)) # Both Shade Cloaks
or (self._hk_option(player, 'ProficientCombat') and self.has('QUAKE', player)) # or Dive
)
)