Hollow Knight updates (goals, WP/POP, etc.) (#438)

* Hollow Knight updates:

- Add configurable goals (Any, THK, Siblings, Radiance)
  - Change base logic to require Opened_Black_Egg_Temple instead of
    requiring 3 dreamers.  This is future-proof for transition rando,
    where Black Egg might not have been located yet.
  - Add combat logic for THK and Radiance on par with Rando4's boss logic,
    so itemless HK shouldn't be required.
- Existing completion logic now uses Black_Egg_te

- Add White Palace options
  (Exclude, King Fragment Only, No Path of Pain, Include)
  - Excluded WP may still be required for King Fragment if Charms are
    not randomized
  - Simply don't place WP locations that are excluded
  - Distinguish between POP locations (required for POP), WP checks (
    actual item locations), WP transitions (relevant for future transition
    rando), and WP events (logically required to reach King Fragment)
  - Many transitions were listed twice.  Remove duplicates.
  - Sort transitions by scene

- For randomizable locations that have no logical significance when not
    randomized, simply skip adding them to the pool entirely for
    theoretically faster generation.

* Hollow Knight updates

  - Support random starting geo up to 1000 geo.
  - Always include locations rather than dropping unrandomized "logicless"
    ones, as it is required to best support same-slot coop.
This commit is contained in:
Daniel Grace 2022-06-12 23:23:03 -07:00 committed by GitHub
parent 8c64f6221e
commit e5a1052089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 180 additions and 60 deletions

View File

@ -22,3 +22,15 @@ for item, item_data in item_table.items():
item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel", item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel",
"Relic", "Root", "Map", "Stag", "Cocoon", "Relic", "Root", "Map", "Stag", "Cocoon",
"Soul", "DreamWarrior", "DreamBoss")} "Soul", "DreamWarrior", "DreamBoss")}
directionals = ('', 'Left_', 'Right_')
item_name_groups.update({
"Dreamer": {"Herrah", "Monomon", "Lurien"},
"Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'},
"Claw": {x + 'Mantis_Claw' for x in directionals},
"CDash": {x + 'Crystal_Heart' for x in directionals},
"Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"},
})
item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash']
item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'}

View File

@ -263,6 +263,39 @@ class EggShopSlots(Range):
range_end = 16 range_end = 16
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 StartingGeo(Range):
"""The amount of starting geo you have."""
display_name = "Starting Geo"
range_start = 0
range_end = 1000
default = 0
hollow_knight_options: typing.Dict[str, type(Option)] = { hollow_knight_options: typing.Dict[str, type(Option)] = {
**hollow_knight_randomize_options, **hollow_knight_randomize_options,
**hollow_knight_logic_options, **hollow_knight_logic_options,
@ -278,4 +311,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
MinimumEggPrice.__name__: MinimumEggPrice, MinimumEggPrice.__name__: MinimumEggPrice,
MaximumEggPrice.__name__: MaximumEggPrice, MaximumEggPrice.__name__: MaximumEggPrice,
EggShopSlots.__name__: EggShopSlots, EggShopSlots.__name__: EggShopSlots,
Goal.__name__: Goal,
WhitePalace.__name__: WhitePalace,
StartingGeo.__name__: StartingGeo,
} }

View File

@ -9,77 +9,80 @@ 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 from .Rules import set_rules
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled, Goal, WhitePalace
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 event_names, item_effects, connectors, one_ways
from .Charms import names as charm_names from .Charms import names as charm_names
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, Tutorial from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial
from ..AutoWorld import World, LogicMixin, WebWorld from ..AutoWorld import World, LogicMixin, WebWorld
white_palace_locations = { path_of_pain_locations = {
"Soul_Totem-Path_of_Pain_Below_Thornskip", "Soul_Totem-Path_of_Pain_Below_Thornskip",
"Soul_Totem-White_Palace_Final",
"Lore_Tablet-Path_of_Pain_Entrance", "Lore_Tablet-Path_of_Pain_Entrance",
"Soul_Totem-Path_of_Pain_Left_of_Lever", "Soul_Totem-Path_of_Pain_Left_of_Lever",
"Soul_Totem-Path_of_Pain_Hidden", "Soul_Totem-Path_of_Pain_Hidden",
"Soul_Totem-Path_of_Pain_Entrance", "Soul_Totem-Path_of_Pain_Entrance",
"Soul_Totem-Path_of_Pain_Final", "Soul_Totem-Path_of_Pain_Final",
"Soul_Totem-White_Palace_Entrance",
"Soul_Totem-Path_of_Pain_Below_Lever", "Soul_Totem-Path_of_Pain_Below_Lever",
"Lore_Tablet-Palace_Throne",
"Soul_Totem-Path_of_Pain_Second", "Soul_Totem-Path_of_Pain_Second",
"Journal_Entry-Seal_of_Binding",
"Warp-Path_of_Pain_Complete",
"Defeated_Path_of_Pain_Arena",
"Completed_Path_of_Pain",
# Path of Pain transitions
"White_Palace_17[right1]", "White_Palace_17[bot1]",
"White_Palace_18[top1]", "White_Palace_18[right1]",
"White_Palace_19[left1]", "White_Palace_19[top1]",
"White_Palace_20[bot1]",
}
white_palace_transitions = {
# Event-Transitions:
# "Grubfather_2",
"White_Palace_01[left1]", "White_Palace_01[right1]", "White_Palace_01[top1]",
"White_Palace_02[left1]",
"White_Palace_03_hub[bot1]", "White_Palace_03_hub[left1]", "White_Palace_03_hub[left2]",
"White_Palace_03_hub[right1]", "White_Palace_03_hub[top1]",
"White_Palace_04[right2]", "White_Palace_04[top1]",
"White_Palace_05[left1]", "White_Palace_05[left2]", "White_Palace_05[right1]", "White_Palace_05[right2]",
"White_Palace_06[bot1]", "White_Palace_06[left1]", "White_Palace_06[top1]", "White_Palace_07[bot1]",
"White_Palace_07[top1]", "White_Palace_08[left1]", "White_Palace_08[right1]",
"White_Palace_09[right1]",
"White_Palace_11[door2]",
"White_Palace_12[bot1]", "White_Palace_12[right1]",
"White_Palace_13[left1]", "White_Palace_13[left2]", "White_Palace_13[left3]", "White_Palace_13[right1]",
"White_Palace_14[bot1]", "White_Palace_14[right1]",
"White_Palace_15[left1]", "White_Palace_15[right1]", "White_Palace_15[right2]",
"White_Palace_16[left1]", "White_Palace_16[left2]",
}
white_palace_checks = {
"Soul_Totem-White_Palace_Final",
"Soul_Totem-White_Palace_Entrance",
"Lore_Tablet-Palace_Throne",
"Soul_Totem-White_Palace_Left", "Soul_Totem-White_Palace_Left",
"Lore_Tablet-Palace_Workshop", "Lore_Tablet-Palace_Workshop",
"Soul_Totem-White_Palace_Hub", "Soul_Totem-White_Palace_Hub",
"Journal_Entry-Seal_of_Binding", "Soul_Totem-White_Palace_Right"
"Soul_Totem-White_Palace_Right", }
"King_Fragment",
# Events: white_palace_events = {
"Palace_Entrance_Lantern_Lit",
"Palace_Left_Lantern_Lit",
"Palace_Right_Lantern_Lit",
"Warp-Path_of_Pain_Complete",
"Defeated_Path_of_Pain_Arena",
"Palace_Atrium_Gates_Opened",
"Completed_Path_of_Pain",
"Warp-White_Palace_Atrium_to_Palace_Grounds",
"Warp-White_Palace_Entrance_to_Palace_Grounds",
# Event-Regions:
"White_Palace_03_hub", "White_Palace_03_hub",
"White_Palace_13", "White_Palace_13",
"White_Palace_01", "White_Palace_01",
# Event-Transitions: "Palace_Entrance_Lantern_Lit",
"White_Palace_12[bot1]", "White_Palace_12[bot1]", "White_Palace_03_hub[bot1]", "White_Palace_16[left2]", "Palace_Left_Lantern_Lit",
"White_Palace_16[left2]", "White_Palace_11[door2]", "White_Palace_11[door2]", "White_Palace_18[top1]", "Palace_Right_Lantern_Lit",
"White_Palace_18[top1]", "White_Palace_15[left1]", "White_Palace_15[left1]", "White_Palace_05[left2]", "Palace_Atrium_Gates_Opened",
"White_Palace_05[left2]", "White_Palace_14[bot1]", "White_Palace_14[bot1]", "White_Palace_13[left2]", "Warp-White_Palace_Atrium_to_Palace_Grounds",
"White_Palace_13[left2]", "White_Palace_03_hub[left1]", "White_Palace_03_hub[left1]", "White_Palace_15[right2]", "Warp-White_Palace_Entrance_to_Palace_Grounds",
"White_Palace_15[right2]", "White_Palace_06[top1]", "White_Palace_06[top1]", "White_Palace_03_hub[bot1]",
"White_Palace_08[right1]", "White_Palace_08[right1]", "White_Palace_03_hub[right1]", "White_Palace_03_hub[right1]",
"White_Palace_01[right1]", "White_Palace_01[right1]", "White_Palace_08[left1]", "White_Palace_08[left1]",
"White_Palace_19[left1]", "White_Palace_19[left1]", "White_Palace_04[right2]", "White_Palace_04[right2]",
"White_Palace_01[left1]", "White_Palace_01[left1]", "White_Palace_17[right1]", "White_Palace_17[right1]",
"White_Palace_07[bot1]", "White_Palace_07[bot1]", "White_Palace_20[bot1]", "White_Palace_20[bot1]",
"White_Palace_03_hub[left2]", "White_Palace_03_hub[left2]", "White_Palace_18[right1]", "White_Palace_18[right1]",
"White_Palace_05[right1]", "White_Palace_05[right1]", "White_Palace_17[bot1]", "White_Palace_17[bot1]",
"White_Palace_09[right1]", "White_Palace_09[right1]", "White_Palace_16[left1]", "White_Palace_16[left1]",
"White_Palace_13[left1]", "White_Palace_13[left1]", "White_Palace_06[bot1]", "White_Palace_06[bot1]",
"White_Palace_15[right1]", "White_Palace_15[right1]", "White_Palace_06[left1]", "White_Palace_06[left1]",
"White_Palace_05[right2]", "White_Palace_05[right2]", "White_Palace_04[top1]", "White_Palace_04[top1]",
"White_Palace_19[top1]", "White_Palace_19[top1]", "White_Palace_14[right1]", "White_Palace_14[right1]",
"White_Palace_03_hub[top1]", "White_Palace_03_hub[top1]", "Grubfather_2", "White_Palace_13[left3]",
"White_Palace_13[left3]", "White_Palace_02[left1]", "White_Palace_02[left1]", "White_Palace_12[right1]",
"White_Palace_12[right1]", "White_Palace_07[top1]", "White_Palace_07[top1]", "White_Palace_05[left1]",
"White_Palace_05[left1]", "White_Palace_13[right1]", "White_Palace_13[right1]", "White_Palace_01[top1]",
"White_Palace_01[top1]",
} }
progression_charms = { progression_charms = {
# Baulder Killers # Baldur Killers
"Grubberfly's_Elegy", "Weaversong", "Glowing_Womb", "Grubberfly's_Elegy", "Weaversong", "Glowing_Womb",
# Spore Shroom spots in fungle wastes # Spore Shroom spots in fungal wastes and elsewhere
"Spore_Shroom", "Spore_Shroom",
# Tuk gives egg, # Tuk gives egg,
"Defender's_Crest", "Defender's_Crest",
@ -87,6 +90,14 @@ progression_charms = {
"Grimmchild1", "Grimmchild2" "Grimmchild1", "Grimmchild2"
} }
# Vanilla placements of the following items have no impact on logic, thus we can avoid creating these items and
# locations entirely when the option to randomize them is disabled.
logicless_options = {
"RandomizeVesselFragments", "RandomizeGeoChests", "RandomizeJunkPitChests", "RandomizeRelics",
"RandomizeMaps", "RandomizeJournalEntries", "RandomizeGeoRocks", "RandomizeBossGeo",
"RandomizeLoreTablets", "RandomizeSoulTotems",
}
class HKWeb(WebWorld): class HKWeb(WebWorld):
tutorials = [Tutorial( tutorials = [Tutorial(
@ -125,8 +136,6 @@ class HKWorld(World):
charm_costs: typing.List[int] charm_costs: typing.List[int]
data_version = 2 data_version = 2
allow_white_palace = False
def __init__(self, world, player): def __init__(self, world, player):
super(HKWorld, self).__init__(world, player) super(HKWorld, self).__init__(world, player)
self.created_multi_locations: typing.Dict[str, int] = Counter() self.created_multi_locations: typing.Dict[str, int] = Counter()
@ -136,7 +145,7 @@ class HKWorld(World):
world = self.world world = self.world
charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random) charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random)
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs) self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
world.exclude_locations[self.player].value.update(white_palace_locations) # world.exclude_locations[self.player].value.update(white_palace_locations)
world.local_items[self.player].value.add("Mimic_Grub") world.local_items[self.player].value.add("Mimic_Grub")
for vendor, unit in self.shops.items(): for vendor, unit in self.shops.items():
mini = getattr(world, f"Minimum{unit}Price")[self.player] mini = getattr(world, f"Minimum{unit}Price")[self.player]
@ -149,23 +158,42 @@ class HKWorld(World):
for option_name in disabled: for option_name in disabled:
getattr(world, option_name)[self.player].value = 0 getattr(world, option_name)[self.player].value = 0
def white_palace_exclusions(self):
exclusions = set()
wp = self.world.WhitePalace[self.player]
if wp <= WhitePalace.option_nopathofpain:
exclusions.update(path_of_pain_locations)
if wp <= WhitePalace.option_kingfragment:
exclusions.update(white_palace_checks)
if wp == WhitePalace.option_exclude and self.world.RandomizeCharms[self.player]:
# Ensure KF location is still reachable if charms are non-randomized
exclusions.update(white_palace_transitions)
exclusions.update(white_palace_events)
exclusions.add("King_Fragment")
return exclusions
def create_regions(self): def create_regions(self):
menu_region: Region = create_region(self.world, self.player, 'Menu') menu_region: Region = create_region(self.world, self.player, 'Menu')
self.world.regions.append(menu_region) self.world.regions.append(menu_region)
wp_exclusions = self.white_palace_exclusions()
# Link regions # Link regions
for event_name in event_names: for event_name in event_names:
if event_name in wp_exclusions:
continue
loc = HKLocation(self.player, event_name, None, menu_region) loc = HKLocation(self.player, event_name, None, menu_region)
loc.place_locked_item(HKItem(event_name, loc.place_locked_item(HKItem(event_name,
self.allow_white_palace or event_name not in white_palace_locations, event_name not in wp_exclusions,
None, "Event", self.player)) None, "Event", self.player))
menu_region.locations.append(loc) menu_region.locations.append(loc)
for entry_transition, exit_transition in connectors.items(): for entry_transition, exit_transition in connectors.items():
if entry_transition in wp_exclusions:
continue
if exit_transition: if exit_transition:
# if door logic fulfilled -> award vanilla target as event # if door logic fulfilled -> award vanilla target as event
loc = HKLocation(self.player, entry_transition, None, menu_region) loc = HKLocation(self.player, entry_transition, None, menu_region)
loc.place_locked_item(HKItem(exit_transition, loc.place_locked_item(HKItem(exit_transition,
self.allow_white_palace or exit_transition not in white_palace_locations, exit_transition not in wp_exclusions,
None, "Event", self.player)) None, "Event", self.player))
menu_region.locations.append(loc) menu_region.locations.append(loc)
@ -178,21 +206,26 @@ class HKWorld(World):
geo_replace.add("Shade_Soul") geo_replace.add("Shade_Soul")
geo_replace.add("Descending_Dark") geo_replace.add("Descending_Dark")
wp_exclusions = self.white_palace_exclusions()
for option_key, option in hollow_knight_randomize_options.items(): for option_key, option in hollow_knight_randomize_options.items():
if getattr(self.world, option_key)[self.player]: if getattr(self.world, option_key)[self.player]:
for item_name, location_name in zip(option.items, option.locations): for item_name, location_name in zip(option.items, option.locations):
if location_name in wp_exclusions:
continue
if item_name in geo_replace: if item_name in geo_replace:
item_name = "Geo_Rock-Default" item_name = "Geo_Rock-Default"
item = self.create_item(item_name) item = self.create_item(item_name)
if location_name in white_palace_locations: # self.create_location(location_name).place_locked_item(item)
self.create_location(location_name).place_locked_item(item) if location_name == "Start":
elif location_name == "Start":
self.world.push_precollected(item) self.world.push_precollected(item)
else: else:
self.create_location(location_name) self.create_location(location_name)
pool.append(item) pool.append(item)
# elif option_key not in logicless_options:
else: else:
for item_name, location_name in zip(option.items, option.locations): for item_name, location_name in zip(option.items, option.locations):
if location_name in wp_exclusions and location_name != 'King_Fragment':
continue
item = self.create_item(item_name) item = self.create_item(item_name)
if location_name == "Start": if location_name == "Start":
self.world.push_precollected(item) self.world.push_precollected(item)
@ -201,10 +234,6 @@ class HKWorld(World):
for i in range(self.world.EggShopSlots[self.player].value): for i in range(self.world.EggShopSlots[self.player].value):
self.create_location("Egg_Shop") self.create_location("Egg_Shop")
pool.append(self.create_item("Geo_Rock-Default")) pool.append(self.create_item("Geo_Rock-Default"))
if not self.allow_white_palace:
loc = self.world.get_location("King_Fragment", self.player)
if loc.item and loc.item.name == loc.name:
loc.item.advancement = False
self.world.itempool += pool self.world.itempool += pool
for shopname in self.shops: for shopname in self.shops:
@ -222,7 +251,15 @@ class HKWorld(World):
world = self.world world = self.world
player = self.player player = self.player
if world.logic[player] != 'nologic': if world.logic[player] != 'nologic':
world.completion_condition[player] = lambda state: state.has('DREAMER', player, 3) goal = world.Goal[player]
if goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
else:
# Hollow Knight or Any goal.
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
set_rules(self) set_rules(self)
def fill_slot_data(self): def fill_slot_data(self):
@ -371,3 +408,38 @@ class HKLogicMixin(LogicMixin):
def _hk_start(self, player, start_location: str) -> bool: def _hk_start(self, player, start_location: str) -> bool:
return self.world.StartLocation[player] == start_location return self.world.StartLocation[player] == start_location
def _hk_nail_combat(self, player: int) -> bool:
return self.has_any({'LFFTSLASH', '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')
)
)
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._hk_siblings_ending(player)
and self.has('DREAMNAIL', player, 1)
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
)
)