Merge branch 'main' into logic_bug_fixes

This commit is contained in:
Louis M
2024-07-30 21:17:50 -04:00
committed by GitHub
22 changed files with 191 additions and 153 deletions

View File

@@ -680,13 +680,13 @@ class CollectionState():
def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.advancement and location not in self.events and
not key_only or getattr(location.item, "locked_dungeon_item", False)}
locations = {location for location in locations if location.advancement and location not in self.events}
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
locations -= reachable_events
@@ -1291,8 +1291,6 @@ class Spoiler:
state = CollectionState(multiworld)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)
sphere = set(filter(state.can_reach, required_locations))
for location in sphere:

View File

@@ -646,7 +646,6 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
def get_sphere_locations(sphere_state: CollectionState,
locations: typing.Set[Location]) -> typing.Set[Location]:
sphere_state.sweep_for_events(key_only=True, locations=locations)
return {loc for loc in locations if sphere_state.can_reach(loc)}
def item_percentage(player: int, num: int) -> float:

View File

@@ -231,6 +231,13 @@ def generate_yaml(game: str):
del options[key]
# Detect keys which end with -range, indicating a NamedRange with a possible custom value
elif key_parts[-1].endswith("-range"):
if options[key_parts[-1][:-6]] == "custom":
options[key_parts[-1][:-6]] = val
del options[key]
# Detect random-* keys and set their options accordingly
for key, val in options.copy().items():
if key.startswith("random-"):

View File

@@ -54,7 +54,7 @@
{% macro NamedRange(option_name, option) %}
{{ OptionTitle(option_name, option) }}
<div class="named-range-container">
<select id="{{ option_name }}-select" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
{% for key, val in option.special_range_names.items() %}
{% if option.default == val %}
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
@@ -64,17 +64,17 @@
{% endfor %}
<option value="custom" hidden>Custom</option>
</select>
<div class="named-range-wrapper">
<div class="named-range-wrapper js-required">
<input
type="range"
id="{{ option_name }}"
name="{{ option_name }}"
name="{{ option_name }}-range"
min="{{ option.range_start }}"
max="{{ option.range_end }}"
value="{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}"
{{ "disabled" if option.default == "random" }}
/>
<span id="{{ option_name }}-value" class="range-value js-required">
<span id="{{ option_name }}-value" class="range-value">
{{ option.default | default(option.range_start) if option.default != "random" else option.range_start }}
</span>
{{ RandomizeButton(option_name, option) }}

View File

@@ -11,7 +11,7 @@
<noscript>
<style>
.js-required{
display: none;
display: none !important;
}
</style>
</noscript>

View File

@@ -79,7 +79,7 @@ class TrackerData:
# Normal lookup tables as well.
self.item_name_to_id[game] = game_package["item_name_to_id"]
self.location_name_to_id[game] = game_package["item_name_to_id"]
self.location_name_to_id[game] = game_package["location_name_to_id"]
def get_seed_name(self) -> str:
"""Retrieves the seed name."""

View File

@@ -39,7 +39,7 @@ def create_itempool(world: "HatInTimeWorld") -> List[Item]:
continue
else:
if name == "Scooter Badge":
if world.options.CTRLogic is CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
if world.options.CTRLogic == CTRLogic.option_scooter or get_difficulty(world) >= Difficulty.MODERATE:
item_type = ItemClassification.progression
elif name == "No Bonk Badge" and world.is_dw():
item_type = ItemClassification.progression

View File

@@ -659,6 +659,10 @@ def is_valid_act_combo(world: "HatInTimeWorld", entrance_act: Region,
if exit_act.name not in chapter_finales:
return False
exit_chapter: str = act_chapters.get(exit_act.name)
# make sure that certain time rift combinations never happen
always_block: bool = exit_chapter != "Mafia Town" and exit_chapter != "Subcon Forest"
if not ignore_certain_rules or always_block:
if entrance_act.name in rift_access_regions and exit_act.name in rift_access_regions[entrance_act.name]:
return False
@@ -684,9 +688,12 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
if act.name not in guaranteed_first_acts:
return False
if world.options.ActRandomizer == ActRandomizer.option_light and "Time Rift" in act.name:
return False
# If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels
start_chapter = world.options.StartingChapter
if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON:
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
if "Time Rift" in act.name:
return False
@@ -723,7 +730,8 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings:
return False
if world.options.ShuffleSubconPaintings and act_chapters.get(act.name, "") == "Subcon Forest":
if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name \
and act_chapters.get(act.name, "") == "Subcon Forest":
# Only allow Subcon levels if painting skips are allowed
if diff < Difficulty.MODERATE or world.options.NoPaintingSkips:
return False

View File

@@ -1,7 +1,6 @@
from worlds.AutoWorld import CollectionState
from worlds.generic.Rules import add_rule, set_rule
from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \
shop_locations, event_locs
from .Locations import location_table, zipline_unlocks, is_location_valid, shop_locations, event_locs
from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HitType
from BaseClasses import Location, Entrance, Region
from typing import TYPE_CHECKING, List, Callable, Union, Dict
@@ -148,14 +147,14 @@ def set_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
chapter_list.append(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
chapter_list.append(ChapterIndex.METRO)
chapter_list.remove(starting_chapter)
world.random.shuffle(chapter_list)
# Make sure Alpine is unlocked before any DLC chapters are, as the Alpine door needs to be open to access them
if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
if starting_chapter != ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()):
index1 = 69
index2 = 69
pos: int
@@ -165,7 +164,7 @@ def set_rules(world: "HatInTimeWorld"):
if world.is_dlc1():
index1 = chapter_list.index(ChapterIndex.CRUISE)
if world.is_dlc2() and final_chapter is not ChapterIndex.METRO:
if world.is_dlc2() and final_chapter != ChapterIndex.METRO:
index2 = chapter_list.index(ChapterIndex.METRO)
lowest_index = min(index1, index2)
@@ -242,9 +241,6 @@ def set_rules(world: "HatInTimeWorld"):
if not is_location_valid(world, key):
continue
if key in contract_locations.keys():
continue
loc = world.multiworld.get_location(key, world.player)
for hat in data.required_hats:
@@ -256,7 +252,7 @@ def set_rules(world: "HatInTimeWorld"):
if data.paintings > 0 and world.options.ShuffleSubconPaintings:
add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings))
if data.hit_type is not HitType.none and world.options.UmbrellaLogic:
if data.hit_type != HitType.none and world.options.UmbrellaLogic:
if data.hit_type == HitType.umbrella:
add_rule(loc, lambda state: state.has("Umbrella", world.player))
@@ -518,7 +514,7 @@ def set_hard_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.ICE))
# Hard: clear Rush Hour with Brewing Hat only
if world.options.NoTicketSkips is not NoTicketSkips.option_true:
if world.options.NoTicketSkips != NoTicketSkips.option_true:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
lambda state: can_use_hat(state, world, HatType.BREWING))
else:

View File

@@ -1,15 +1,16 @@
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
calculate_yarn_costs
calculate_yarn_costs, alps_hooks
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
get_total_locations
from .Rules import set_rules
from .Rules import set_rules, has_paintings
from .Options import AHITOptions, slot_data_options, adjust_options, RandomizeHatOrder, EndGoal, create_option_groups
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item
from .Types import HatType, ChapterIndex, HatInTimeItem, hat_type_to_item, Difficulty
from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes
from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from Utils import local_path
@@ -86,19 +87,27 @@ class HatInTimeWorld(World):
if self.is_dw_only():
return
# If our starting chapter is 4 and act rando isn't on, force hookshot into inventory
# If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock
start_chapter: ChapterIndex = ChapterIndex(self.options.StartingChapter)
# Take care of some extremely restrictive starts in other chapters with act shuffle off
if not self.options.ActRandomizer:
start_chapter = self.options.StartingChapter
if start_chapter == ChapterIndex.ALPINE:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if self.options.UmbrellaLogic:
self.multiworld.push_precollected(self.create_item("Umbrella"))
if start_chapter == ChapterIndex.ALPINE or start_chapter == ChapterIndex.SUBCON:
if not self.options.ActRandomizer:
if start_chapter == ChapterIndex.ALPINE:
self.multiworld.push_precollected(self.create_item("Hookshot Badge"))
if self.options.UmbrellaLogic:
self.multiworld.push_precollected(self.create_item("Umbrella"))
if start_chapter == ChapterIndex.SUBCON and self.options.ShuffleSubconPaintings:
if self.options.ShuffleAlpineZiplines:
ziplines = list(alps_hooks.keys())
ziplines.remove("Zipline Unlock - The Twilight Bell Path") # not enough checks from this one
self.multiworld.push_precollected(self.create_item(self.random.choice(ziplines)))
elif start_chapter == ChapterIndex.SUBCON:
if self.options.ShuffleSubconPaintings:
self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock"))
elif start_chapter == ChapterIndex.BIRDS:
if self.options.UmbrellaLogic:
if self.options.LogicDifficulty < Difficulty.EXPERT:
self.multiworld.push_precollected(self.create_item("Umbrella"))
elif self.options.LogicDifficulty < Difficulty.MODERATE:
self.multiworld.push_precollected(self.create_item("Umbrella"))
def create_regions(self):
# noinspection PyClassVar
@@ -119,7 +128,10 @@ class HatInTimeWorld(World):
# place vanilla contract locations if contract shuffle is off
if not self.options.ShuffleActContracts:
for name in contract_locations.keys():
self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name))
loc = self.get_location(name)
loc.place_locked_item(create_item(self, name))
if self.options.ShuffleSubconPaintings and loc.name != "Snatcher's Contract - The Subcon Well":
add_rule(loc, lambda state: has_paintings(state, self, 1))
def create_items(self):
if self.has_yarn():
@@ -317,7 +329,7 @@ class HatInTimeWorld(World):
def remove(self, state: "CollectionState", item: "Item") -> bool:
old_count: int = state.count(item.name, self.player)
change = super().collect(state, item)
change = super().remove(state, item)
if change and old_count == 1:
if "Stamp" in item.name:
if "2 Stamp" in item.name:

View File

@@ -76,10 +76,6 @@ class ALttPItem(Item):
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def locked_dungeon_item(self):
return self.location.locked and self.dungeon_item
class LTTPRegionType(IntEnum):
LightWorld = 1

View File

@@ -660,11 +660,18 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
end
local tech
local force = game.forces["player"]
if call.parameter == nil then
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
return
end
chunks = split(call.parameter, "\t")
local item_name = chunks[1]
local index = chunks[2]
local source = chunks[3] or "Archipelago"
if index == -1 then -- for coop sync and restoring from an older savegame
if index == nil then
game.print("ap-get-technology is only to be used by the Archipelago Factorio Client")
return
elseif index == -1 then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})

View File

@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
check_2 = await snes_read(ctx, 0xF53749, 1)
if check_1 != b'01' or check_2 != b'01':
if check_1 != b'\x01' or check_2 != b'\x01':
return
def get_range(data_range):

View File

@@ -1,10 +1,12 @@
import typing
import re
from dataclasses import dataclass, make_dataclass
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
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
if typing.TYPE_CHECKING:
@@ -538,3 +540,5 @@ hollow_knight_options: typing.Dict[str, type(Option)] = {
},
**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!
continue
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 .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, \
shop_to_option
shop_to_option, HKOptions
from .ExtractedData import locations, starts, multi_locations, location_to_region_lookup, \
event_names, item_effects, connectors, one_ways, vanilla_shop_costs, vanilla_location_costs
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.
""" # from https://www.hollowknight.com
game: str = "Hollow Knight"
option_definitions = hollow_knight_options
options_dataclass = HKOptions
options: HKOptions
web = HKWeb()
@@ -155,8 +156,8 @@ class HKWorld(World):
charm_costs: typing.List[int]
cached_filler_items = {}
def __init__(self, world, player):
super(HKWorld, self).__init__(world, player)
def __init__(self, multiworld, player):
super(HKWorld, self).__init__(multiworld, player)
self.created_multi_locations: typing.Dict[str, typing.List[HKLocation]] = {
location: list() for location in multi_locations
}
@@ -165,29 +166,29 @@ class HKWorld(World):
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
def generate_early(self):
world = self.multiworld
charm_costs = world.RandomCharmCosts[self.player].get_costs(world.random)
self.charm_costs = world.PlandoCharmCosts[self.player].get_costs(charm_costs)
# world.exclude_locations[self.player].value.update(white_palace_locations)
options = self.options
charm_costs = options.RandomCharmCosts.get_costs(self.random)
self.charm_costs = options.PlandoCharmCosts.get_costs(charm_costs)
# options.exclude_locations.value.update(white_palace_locations)
for term, data in cost_terms.items():
mini = getattr(world, f"Minimum{data.option}Price")[self.player]
maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
mini = getattr(options, f"Minimum{data.option}Price")
maxi = getattr(options, f"Maximum{data.option}Price")
# if minimum > maximum, set minimum to maximum
mini.value = min(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))
def white_palace_exclusions(self):
exclusions = set()
wp = self.multiworld.WhitePalace[self.player]
wp = self.options.WhitePalace
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:
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
exclusions.update(white_palace_transitions)
exclusions.update(white_palace_events)
@@ -200,7 +201,7 @@ class HKWorld(World):
# check for any goal that godhome events are relevant to
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
all_event_names.update(set(godhome_event_names))
@@ -230,12 +231,12 @@ class HKWorld(World):
pool: typing.List[HKItem] = []
wp_exclusions = self.white_palace_exclusions()
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"))
randomized_starting_items = set()
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)
# noinspection PyShadowingNames
@@ -257,7 +258,7 @@ class HKWorld(World):
if item_name in junk_replace:
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 item_name in randomized_starting_items:
@@ -281,55 +282,55 @@ class HKWorld(World):
location.progress_type = LocationProgressType.EXCLUDED
for option_key, option in hollow_knight_randomize_options.items():
randomized = getattr(self.multiworld, option_key)[self.player]
if all([not randomized, option_key in logicless_options, not self.multiworld.AddUnshuffledLocations[self.player]]):
randomized = getattr(self.options, option_key)
if all([not randomized, option_key in logicless_options, not self.options.AddUnshuffledLocations]):
continue
for item_name, location_name in zip(option.items, option.locations):
if item_name in junk_replace:
item_name = self.get_filler_item_name()
if (item_name == "Crystal_Heart" and self.multiworld.SplitCrystalHeart[self.player]) or \
(item_name == "Mothwing_Cloak" and self.multiworld.SplitMothwingCloak[self.player]):
if (item_name == "Crystal_Heart" and self.options.SplitCrystalHeart) or \
(item_name == "Mothwing_Cloak" and self.options.SplitMothwingCloak):
_add("Left_" + item_name, location_name, randomized)
_add("Right_" + item_name, "Split_" + location_name, randomized)
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("Right_" + item_name, "Right_" + location_name, randomized)
continue
if item_name == "Shade_Cloak" and self.multiworld.SplitMothwingCloak[self.player]:
if self.multiworld.random.randint(0, 1):
if item_name == "Shade_Cloak" and self.options.SplitMothwingCloak:
if self.random.randint(0, 1):
item_name = "Left_Mothwing_Cloak"
else:
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)
continue
_add(item_name, location_name, randomized)
if self.multiworld.RandomizeElevatorPass[self.player]:
if self.options.RandomizeElevatorPass:
randomized = True
_add("Elevator_Pass", "Elevator_Pass", randomized)
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)
unfilled_locations += 1
# Balance the 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.
if additional_shop_items > 0:
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')
if shops:
for _ in range(additional_shop_items):
shop = self.multiworld.random.choice(shops)
shop = self.random.choice(shops)
loc = self.create_location(shop)
unfilled_locations += 1
if len(self.created_multi_locations[shop]) >= 16:
@@ -355,7 +356,7 @@ class HKWorld(World):
loc.costs = costs
def apply_costsanity(self):
setting = self.multiworld.CostSanity[self.player].value
setting = self.options.CostSanity.value
if not setting:
return # noop
@@ -369,10 +370,10 @@ class HKWorld(World):
return {k: v for k, v in weights.items() if v}
random = self.multiworld.random
hybrid_chance = getattr(self.multiworld, f"CostSanityHybridChance")[self.player].value
random = self.random
hybrid_chance = getattr(self.options, f"CostSanityHybridChance").value
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()
}
weights_geoless = dict(weights)
@@ -427,22 +428,22 @@ class HKWorld(World):
location.sort_costs()
def set_rules(self):
world = self.multiworld
multiworld = self.multiworld
player = self.player
goal = world.Goal[player]
goal = self.options.Goal
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:
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:
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:
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:
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:
# 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)
@@ -450,8 +451,8 @@ class HKWorld(World):
slot_data = {}
options = slot_data["options"] = {}
for option_name in self.option_definitions:
option = getattr(self.multiworld, option_name)[self.player]
for option_name in hollow_knight_options:
option = getattr(self.options, option_name)
try:
optionvalue = int(option.value)
except TypeError:
@@ -460,10 +461,10 @@ class HKWorld(World):
options[option_name] = optionvalue
# 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)
if not self.multiworld.CostSanity[self.player]:
if not self.options.CostSanity:
for shop, terms in shop_cost_types.items():
unit = cost_terms[next(iter(terms))].option
if unit == "Geo":
@@ -498,7 +499,7 @@ class HKWorld(World):
basename = name
if name in shop_cost_types:
costs = {
term: self.multiworld.random.randint(*self.ranges[term])
term: self.random.randint(*self.ranges[term])
for term in shop_cost_types[name]
}
elif name in vanilla_location_costs:
@@ -512,7 +513,7 @@ class HKWorld(World):
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,
None, region, costs=costs, vanilla=vanilla,
basename=basename)
@@ -560,26 +561,26 @@ class HKWorld(World):
return change
@classmethod
def stage_write_spoiler(cls, world: MultiWorld, spoiler_handle):
hk_players = world.get_game_players(cls.game)
def stage_write_spoiler(cls, multiworld: MultiWorld, spoiler_handle):
hk_players = multiworld.get_game_players(cls.game)
spoiler_handle.write('\n\nCharm Notches:')
for player in hk_players:
name = world.get_player_name(player)
name = multiworld.get_player_name(player)
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):
spoiler_handle.write(f"\n{charm_names[charm_number]}: {cost}")
spoiler_handle.write('\n\nShop Prices:')
for player in hk_players:
name = world.get_player_name(player)
name = multiworld.get_player_name(player)
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(
(
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
), key=operator.attrgetter('name')
):
@@ -603,15 +604,15 @@ class HKWorld(World):
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
'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
exclusions)
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:
ret = Region(name, player, world)
def create_region(multiworld: MultiWorld, player: int, name: str, location_names=None) -> Region:
ret = Region(name, player, multiworld)
if location_names:
for location in location_names:
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)
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:
return self.multiworld.StartLocation[player] == 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
)
)
return self.multiworld.worlds[player].options.StartLocation == start_location

View File

@@ -1556,6 +1556,8 @@
room: Owl Hallway
door: Shortcut to Hedge Maze
Roof: True
The Incomparable:
door: Observant Entrance
panels:
DOWN:
id: Maze Room/Panel_down_up
@@ -1967,6 +1969,9 @@
door: Eight Door
Orange Tower Sixth Floor:
painting: True
Hedge Maze:
room: Hedge Maze
door: Observant Entrance
panels:
Achievement:
id: Countdown Panels/Panel_incomparable_incomparable
@@ -7649,6 +7654,8 @@
LEAP:
id: Double Room/Panel_leap_leap
tag: midwhite
required_door:
door: Door to Cross
doors:
Door to Cross:
id: Double Room Area Doors/Door_room_4a

Binary file not shown.

View File

@@ -1,2 +1 @@
nest-asyncio >= 1.5.5
six >= 1.16.0

View File

@@ -215,7 +215,6 @@ class SMZ3World(World):
niceItems = TotalSMZ3Item.Item.CreateNicePool(self.smz3World)
junkItems = TotalSMZ3Item.Item.CreateJunkPool(self.smz3World)
allJunkItems = niceItems + junkItems
self.junkItemsNames = [item.Type.name for item in junkItems]
if (self.smz3World.Config.Keysanity):
@@ -228,7 +227,8 @@ class SMZ3World(World):
self.multiworld.push_precollected(SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item))
itemPool = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in progressionItems] + \
[SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in allJunkItems]
[SMZ3Item(item.Type.name, ItemClassification.useful, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in niceItems] + \
[SMZ3Item(item.Type.name, ItemClassification.filler, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in junkItems]
self.smz3DungeonItems = [SMZ3Item(item.Type.name, ItemClassification.progression, item.Type, self.item_name_to_id[item.Type.name], self.player, item) for item in self.dungeon]
self.multiworld.itempool += itemPool

View File

@@ -2212,7 +2212,7 @@ id,region,name,tags,mod_name
3808,Shipping,Shipsanity: Mystery Box,"SHIPSANITY",
3809,Shipping,Shipsanity: Golden Tag,"SHIPSANITY",
3810,Shipping,Shipsanity: Deluxe Bait,"SHIPSANITY",
3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT",
3811,Shipping,Shipsanity: Moss,"SHIPSANITY,SHIPSANITY_FULL_SHIPMENT",
3812,Shipping,Shipsanity: Mossy Seed,"SHIPSANITY",
3813,Shipping,Shipsanity: Sonar Bobber,"SHIPSANITY",
3814,Shipping,Shipsanity: Tent Kit,"SHIPSANITY",
1 id region name tags mod_name
2212 3808 Shipping Shipsanity: Mystery Box SHIPSANITY
2213 3809 Shipping Shipsanity: Golden Tag SHIPSANITY
2214 3810 Shipping Shipsanity: Deluxe Bait SHIPSANITY
2215 3811 Shipping Shipsanity: Moss SHIPSANITY,SHIPSANITY_CROP,SHIPSANITY_FULL_SHIPMENT SHIPSANITY,SHIPSANITY_FULL_SHIPMENT
2216 3812 Shipping Shipsanity: Mossy Seed SHIPSANITY
2217 3813 Shipping Shipsanity: Sonar Bobber SHIPSANITY
2218 3814 Shipping Shipsanity: Tent Kit SHIPSANITY

View File

@@ -121,7 +121,7 @@ class TunicWorld(World):
cls.seed_groups[group] = SeedGroup(logic_rules=tunic.options.logic_rules.value,
laurels_at_10_fairies=tunic.options.laurels_location == 3,
fixed_shop=bool(tunic.options.fixed_shop),
plando=multiworld.plando_connections[tunic.player])
plando=tunic.options.plando_connections)
continue
# lower value is more restrictive
@@ -134,9 +134,9 @@ class TunicWorld(World):
if tunic.options.fixed_shop:
cls.seed_groups[group]["fixed_shop"] = True
if multiworld.plando_connections[tunic.player]:
if tunic.options.plando_connections:
# loop through the connections in the player's yaml
for cxn in multiworld.plando_connections[tunic.player]:
for cxn in tunic.options.plando_connections:
new_cxn = True
for group_cxn in cls.seed_groups[group]["plando"]:
# if neither entrance nor exit match anything in the group, add to group