Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 85 additions & 11 deletions worlds/hk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
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, Region
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
Expand All @@ -30,6 +31,7 @@
Goal,
GrubHuntGoal,
HKOptions,
ShuffleEntrancesMode,
StartLocation,
WhitePalace,
hollow_knight_options,
Expand All @@ -43,7 +45,6 @@

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")]
Expand Down Expand Up @@ -87,11 +88,13 @@ class HKWorld(RandomizerCoreWorld, World):
rc_locations: list[dict[str, Any]] = hk_locations
item_class = HKItem
location_class = HKLocation
explicit_indirect_conditions = True
region_class = HKRegion

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: list[tuple[str, str]]

cached_filler_items: ClassVar[dict[int, list[str]]] = {} # per player cache
grub_count: int
Expand All @@ -113,6 +116,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 = list()

def white_palace_exclusions(self) -> set[str]:
exclusions = set()
Expand Down Expand Up @@ -380,6 +384,8 @@ def validate_start(self, start_location_key: str) -> list[list[str]]:
valid_items.append("PRECISEMOVEMENT")
if self.options.DangerousSkips:
valid_items.append("DANGEROUSSKIPS")
if self.options.RandomizeEntrances:
valid_items.append("ROOMRANDO")
start_location_logic = starts[start_location_key]["logic"]

if not start_location_logic: # empty logic means always good
Expand All @@ -398,14 +404,14 @@ def create_regions(self):

for loc in self.white_palace_exclusions():
self.get_location(loc).progress_type = LocationProgressType.EXCLUDED

location_to_option = {
location: option for option, data in pool_options.items() for location in data["randomized"]["locations"]
}
location_to_option["Elevator_Pass"] = "RandomizeElevatorPass"
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]]:
Expand Down Expand Up @@ -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.RandomizeEntrances:
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 == self.origin_region_name
and ex.connected_region
and ex.connected_region.name == self.start_location_region
)
]

if not exits:
return

for ex in exits:
trans_data = transitions.get(ex.name, {})
sides = trans_data.get("sides", "Both")
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 = 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])
Expand Down Expand Up @@ -997,6 +1042,8 @@ def fill_slot_data(self):

slot_data["is_race"] = self.settings.disable_spoilers or self.multiworld.is_race

slot_data["entrance_pairs"] = {src: tgt for src, tgt in self.entrance_pairs} if self.options.RandomizeEntrances else {}

return slot_data

# write_spoiler
Expand Down Expand Up @@ -1029,3 +1076,30 @@ 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()}")

# Entrance randomization spoiler
spoiler_handle.write("\n\nEntrance Randomization:")
for hk_world in hk_worlds:
if hasattr(hk_world, "entrance_pairs"):
spoiler_handle.write(f"\n{hk_world.player_name}\n\nONE WAYS:")
duals = set()
for src, tgt in hk_world.entrance_pairs:
if (src, tgt) in duals:
continue # already recorded as a dual
dual = False
for oldsrc, oldtgt in hk_world.entrance_pairs:
if oldsrc == tgt and oldtgt == src:
duals.add((src, tgt))
duals.add((tgt, src))
dual = True
break
if not dual:
spoiler_handle.write(f"\n{src} -> {tgt}")

spoiler_handle.write(f"\n\nTWO WAYS:")
done = set()
for dual in duals:
if (dual[1], dual[0]) in done:
continue
done.add(dual)
spoiler_handle.write(f"\n{dual[0]} <-> {dual[1]}")
5 changes: 4 additions & 1 deletion worlds/hk/archipelago.json
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{"game": "Hollow Knight", "world_version": "1.0.3"}
{
"game": "Hollow Knight",
"world_version": "1.0.4"
}
20 changes: 20 additions & 0 deletions worlds/hk/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,25 @@ class SplitCrystalHeart(Toggle):
default = False


class RandomizeEntrances(Toggle):
"""Enable entrance/transition randomization."""
display_name = "Randomize Entrances"
default = False


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"
Expand Down Expand Up @@ -596,6 +615,7 @@ class CostSanityHybridChance(Range):
for option in (
StartLocation, Goal, GrubHuntGoal, WhitePalace, ExtraPlatforms, AddUnshuffledLocations, StartingGeo,
DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms,
RandomizeEntrances, ShuffleEntrancesMode,
MinimumGeoPrice, MaximumGeoPrice,
MinimumGrubPrice, MaximumGrubPrice,
MinimumEssencePrice, MaximumEssencePrice,
Expand Down