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
95 changes: 81 additions & 14 deletions worlds/hk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")]
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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]]:
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.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])
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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}")
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"
}
11 changes: 8 additions & 3 deletions worlds/hk/classes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar, NamedTuple
from typing import ClassVar, NamedTuple, TYPE_CHECKING

from BaseClasses import (
CollectionState,
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions worlds/hk/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import IntEnum

# Shop cost types.
shop_cost_types: dict[str, tuple[str, ...]] = {
"Egg_Shop": ("RANCIDEGGS",),
Expand All @@ -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
Loading
Loading