Hollow Knight June 2022 Updates (#720)
This is a combined PR for assorted Hollow Knight updates for June 2022 that have cleared testing. It supersedes any HK-exclusive PRs open by myself or @Alchav unless stated otherwise. Summary of changes below: * Implement Split Claw, Split Cloak, Split Superdash, Randomize Nail, Randomize Focus, Randomize Swim and Elevator * Pass options (@Alchav) * Add support for Deathlink with three different modes (@dewiniaid) * Add customizable additional shop slots per-shop (@Alchav) and overall (@dewiniaid) * Overhaul shop cost output to be more generic and account for all locations with standard costs (such as Stag Stations, Cornifer, and Divine) (@dewiniaid) * Add "CostSanity", allowing random prices using any cost type to be chosen for any location with a cost. (e.g. a Stag station requiring 15 grubs to obtain an item) * Item classification fixes (Map and Journal items are fillter, Mask Shards/Pale Ore/Vessel Fragments are useful) (@Alchav) * Fix Ijii -> Jiji (@Alchav ) * General code quality updates The above changes are only for the HK world.
This commit is contained in:
parent
7d85ab471a
commit
8870b577d0
File diff suppressed because one or more lines are too long
|
@ -74,7 +74,7 @@ class Absorber(ast.NodeTransformer):
|
||||||
self.truth_values = truth_values
|
self.truth_values = truth_values
|
||||||
self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
|
self.truth_values |= {"True", "None", "ANY", "ITEMRANDO"}
|
||||||
self.false_values = false_values
|
self.false_values = false_values
|
||||||
self.false_values |= {"False", "NONE", "RANDOMELEVATORS"}
|
self.false_values |= {"False", "NONE"}
|
||||||
|
|
||||||
super(Absorber, self).__init__()
|
super(Absorber, self).__init__()
|
||||||
|
|
||||||
|
@ -203,7 +203,58 @@ logic_folder = os.path.join(resources_source, "Logic")
|
||||||
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
|
logic_options: typing.Dict[str, str] = hk_loads(os.path.join(data_folder, "logic_settings.json"))
|
||||||
for logic_key, logic_value in logic_options.items():
|
for logic_key, logic_value in logic_options.items():
|
||||||
logic_options[logic_key] = logic_value.split(".", 1)[-1]
|
logic_options[logic_key] = logic_value.split(".", 1)[-1]
|
||||||
del (logic_options["RANDOMELEVATORS"])
|
|
||||||
|
vanilla_cost_data: typing.Dict[str, typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "costs.json"))
|
||||||
|
vanilla_location_costs = {
|
||||||
|
key: {
|
||||||
|
value["term"]: int(value["amount"])
|
||||||
|
}
|
||||||
|
for key, value in vanilla_cost_data.items()
|
||||||
|
if value["amount"] > 0 and value["term"] == "GEO"
|
||||||
|
}
|
||||||
|
|
||||||
|
salubra_geo_costs_by_charm_count = {
|
||||||
|
5: 120,
|
||||||
|
10: 500,
|
||||||
|
18: 900,
|
||||||
|
25: 1400,
|
||||||
|
40: 800
|
||||||
|
}
|
||||||
|
|
||||||
|
# Can't extract this data, so supply it ourselves. Source: the wiki
|
||||||
|
vanilla_shop_costs = {
|
||||||
|
('Sly', 'Simple_Key'): [{'GEO': 950}],
|
||||||
|
('Sly', 'Rancid_Egg'): [{'GEO': 60}],
|
||||||
|
('Sly', 'Lumafly_Lantern'): [{'GEO': 1800}],
|
||||||
|
('Sly', 'Gathering_Swarm'): [{'GEO': 300}],
|
||||||
|
('Sly', 'Stalwart_Shell'): [{'GEO': 200}],
|
||||||
|
('Sly', 'Mask_Shard'): [
|
||||||
|
{'GEO': 150},
|
||||||
|
{'GEO': 500},
|
||||||
|
],
|
||||||
|
('Sly', 'Vessel_Fragment'): [{'GEO': 550}],
|
||||||
|
('Sly_(Key)', 'Heavy_Blow'): [{'GEO': 350}],
|
||||||
|
('Sly_(Key)', 'Elegant_Key'): [{'GEO': 800}],
|
||||||
|
('Sly_(Key)', 'Mask_Shard'): [
|
||||||
|
{'GEO': 800},
|
||||||
|
{'GEO': 1500},
|
||||||
|
],
|
||||||
|
('Sly_(Key)', 'Vessel_Fragment'): [{'GEO': 900}],
|
||||||
|
('Sly_(Key)', 'Sprintmaster'): [{'GEO': 400}],
|
||||||
|
|
||||||
|
('Iselda', 'Wayward_Compass'): [{'GEO': 220}],
|
||||||
|
('Iselda', 'Quill'): [{'GEO': 120}],
|
||||||
|
|
||||||
|
('Salubra', 'Lifeblood_Heart'): [{'GEO': 250}],
|
||||||
|
('Salubra', 'Longnail'): [{'GEO': 300}],
|
||||||
|
('Salubra', 'Steady_Body'): [{'GEO': 120}],
|
||||||
|
('Salubra', 'Shaman_Stone'): [{'GEO': 220}],
|
||||||
|
('Salubra', 'Quick_Focus'): [{'GEO': 800}],
|
||||||
|
|
||||||
|
('Leg_Eater', 'Fragile_Heart'): [{'GEO': 350}],
|
||||||
|
('Leg_Eater', 'Fragile_Greed'): [{'GEO': 250}],
|
||||||
|
('Leg_Eater', 'Fragile_Strength'): [{'GEO': 600}],
|
||||||
|
}
|
||||||
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
|
extra_pool_options: typing.List[typing.Dict[str, typing.Any]] = hk_loads(os.path.join(data_folder, "pools.json"))
|
||||||
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
|
pool_options: typing.Dict[str, typing.Tuple[typing.List[str], typing.List[str]]] = {}
|
||||||
for option in extra_pool_options:
|
for option in extra_pool_options:
|
||||||
|
@ -213,8 +264,23 @@ for option in extra_pool_options:
|
||||||
for pairing in option["Vanilla"]:
|
for pairing in option["Vanilla"]:
|
||||||
items.append(pairing["item"])
|
items.append(pairing["item"])
|
||||||
location_name = pairing["location"]
|
location_name = pairing["location"]
|
||||||
if any(cost_entry["term"] == "CHARMS" for cost_entry in pairing.get("costs", [])):
|
item_costs = pairing.get("costs", [])
|
||||||
location_name += "_(Requires_Charms)"
|
if item_costs:
|
||||||
|
if any(cost_entry["term"] == "CHARMS" for cost_entry in item_costs):
|
||||||
|
location_name += "_(Requires_Charms)"
|
||||||
|
#vanilla_shop_costs[pairing["location"], pairing["item"]] = \
|
||||||
|
cost = {
|
||||||
|
entry["term"]: int(entry["amount"]) for entry in item_costs
|
||||||
|
}
|
||||||
|
# Rando4 doesn't include vanilla geo costs for Salubra charms, so dirty hardcode here.
|
||||||
|
if 'CHARMS' in cost:
|
||||||
|
geo = salubra_geo_costs_by_charm_count.get(cost['CHARMS'])
|
||||||
|
if geo:
|
||||||
|
cost['GEO'] = geo
|
||||||
|
|
||||||
|
key = (pairing["location"], pairing["item"])
|
||||||
|
vanilla_shop_costs.setdefault(key, []).append(cost)
|
||||||
|
|
||||||
locations.append(location_name)
|
locations.append(location_name)
|
||||||
if option["Path"]:
|
if option["Path"]:
|
||||||
# basename carries over from prior entry if no Path given
|
# basename carries over from prior entry if no Path given
|
||||||
|
@ -229,6 +295,12 @@ for option in extra_pool_options:
|
||||||
pool_options[basename] = items, locations
|
pool_options[basename] = items, locations
|
||||||
del extra_pool_options
|
del extra_pool_options
|
||||||
|
|
||||||
|
# reverse all the vanilla shop costs (really, this is just for Salubra).
|
||||||
|
# When we use these later, we pop off the end of the list so this ensures they are still sorted.
|
||||||
|
vanilla_shop_costs = {
|
||||||
|
k: list(reversed(v)) for k, v in vanilla_shop_costs.items()
|
||||||
|
}
|
||||||
|
|
||||||
# items
|
# items
|
||||||
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
|
items: typing.Dict[str, typing.Dict] = hk_loads(os.path.join(data_folder, "items.json"))
|
||||||
logic_items: typing.Set[str] = set()
|
logic_items: typing.Set[str] = set()
|
||||||
|
@ -364,9 +436,15 @@ for event in events:
|
||||||
event_rules.update(connectors_rules)
|
event_rules.update(connectors_rules)
|
||||||
connectors_rules = {}
|
connectors_rules = {}
|
||||||
|
|
||||||
|
|
||||||
|
# Apply some final fixes
|
||||||
|
item_effects.update({
|
||||||
|
'Left_Mothwing_Cloak': {'LEFTDASH': 1},
|
||||||
|
'Right_Mothwing_Cloak': {'RIGHTDASH': 1},
|
||||||
|
})
|
||||||
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
|
names = sorted({"logic_options", "starts", "pool_options", "locations", "multi_locations", "location_to_region_lookup",
|
||||||
"event_names", "item_effects", "items", "logic_items", "region_names",
|
"event_names", "item_effects", "items", "logic_items", "region_names",
|
||||||
"exits", "connectors", "one_ways"})
|
"exits", "connectors", "one_ways", "vanilla_shop_costs", "vanilla_location_costs"})
|
||||||
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
|
warning = "# This module is written by Extractor.py, do not edit manually!.\n\n"
|
||||||
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
|
with open(os.path.join(os.path.dirname(__file__), "ExtractedData.py"), "wt") as py:
|
||||||
py.write(warning)
|
py.write(warning)
|
||||||
|
@ -385,6 +463,6 @@ rules_template = template_env.get_template("RulesTemplate.pyt")
|
||||||
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
|
rules = rules_template.render(location_rules=location_rules, one_ways=one_ways, connectors_rules=connectors_rules,
|
||||||
event_rules=event_rules)
|
event_rules=event_rules)
|
||||||
|
|
||||||
with open("Rules.py", "wt") as py:
|
with open("GeneratedRules.py", "wt") as py:
|
||||||
py.write(warning)
|
py.write(warning)
|
||||||
py.write(rules)
|
py.write(rules)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,6 @@
|
||||||
import typing
|
import typing
|
||||||
from .ExtractedData import logic_options, starts, pool_options
|
from .ExtractedData import logic_options, starts, pool_options
|
||||||
|
from .Rules import cost_terms
|
||||||
|
|
||||||
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
|
from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, SpecialRange
|
||||||
from .Charms import vanilla_costs, names as charm_names
|
from .Charms import vanilla_costs, names as charm_names
|
||||||
|
@ -11,19 +12,6 @@ else:
|
||||||
Random = typing.Any
|
Random = typing.Any
|
||||||
|
|
||||||
|
|
||||||
class Disabled(Toggle):
|
|
||||||
def __init__(self, value: int):
|
|
||||||
super(Disabled, self).__init__(0)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_text(cls, text: str) -> Toggle:
|
|
||||||
return cls(0)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_any(cls, data: typing.Any):
|
|
||||||
return cls(0)
|
|
||||||
|
|
||||||
|
|
||||||
locations = {"option_" + start: i for i, start in enumerate(starts)}
|
locations = {"option_" + start: i for i, start in enumerate(starts)}
|
||||||
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
|
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
|
||||||
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
|
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
|
||||||
|
@ -36,6 +24,8 @@ option_docstrings = {
|
||||||
"randomization.",
|
"randomization.",
|
||||||
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
|
"RandomizeSkills": "Allow for Skills, such as Mantis Claw or Shade Soul, to be randomized into the item pool. "
|
||||||
"Also opens their locations for receiving randomized items.",
|
"Also opens their locations for receiving randomized items.",
|
||||||
|
"RandomizeFocus": "Removes the ability to focus and randomizes it into the item pool.",
|
||||||
|
"RandomizeSwim": "Removes the ability to swim in water and randomizes it into the item pool.",
|
||||||
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
|
"RandomizeCharms": "Allow for Charms to be randomized into the item pool and open their locations for "
|
||||||
"randomization. Includes Charms sold in shops.",
|
"randomization. Includes Charms sold in shops.",
|
||||||
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
|
"RandomizeKeys": "Allow for Keys to be randomized into the item pool. Includes those sold in shops.",
|
||||||
|
@ -59,6 +49,8 @@ option_docstrings = {
|
||||||
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
|
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
|
||||||
"pool and open their locations for randomization.",
|
"pool and open their locations for randomization.",
|
||||||
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
|
||||||
|
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
|
||||||
|
"Mimic Grubs are always placed in your own game.",
|
||||||
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
|
||||||
" and buy an item that is randomized into that location as well.",
|
" and buy an item that is randomized into that location as well.",
|
||||||
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
|
||||||
|
@ -70,6 +62,7 @@ option_docstrings = {
|
||||||
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
|
"RandomizeJournalEntries": "Randomize the Hunter's Journal as well as the findable journal entries into the item "
|
||||||
"pool, and open their locations for randomization. Does not include journal entries "
|
"pool, and open their locations for randomization. Does not include journal entries "
|
||||||
"gained by killing enemies.",
|
"gained by killing enemies.",
|
||||||
|
"RandomizeNail": "Removes the ability to swing the nail left, right and up, and shuffles these into the item pool.",
|
||||||
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
|
"RandomizeGeoRocks": "Randomize Geo Rock rewards into the item pool and open their locations for randomization.",
|
||||||
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
|
"RandomizeBossGeo": "Randomize boss Geo drops into the item pool and open those locations for randomization.",
|
||||||
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
|
"RandomizeSoulTotems": "Randomize Soul Refill items into the item pool and open the Soul Totem locations for"
|
||||||
|
@ -110,12 +103,16 @@ default_on = {
|
||||||
"RandomizeRelics"
|
"RandomizeRelics"
|
||||||
}
|
}
|
||||||
|
|
||||||
# not supported at this time
|
shop_to_option = {
|
||||||
disabled = {
|
"Seer": "SeerRewardSlots",
|
||||||
"RandomizeFocus",
|
"Grubfather": "GrubfatherRewardSlots",
|
||||||
"RandomizeSwim",
|
"Sly": "SlyShopSlots",
|
||||||
"RandomizeMimics",
|
"Sly_(Key)": "SlyKeyShopSlots",
|
||||||
"RandomizeNail",
|
"Iselda": "IseldaShopSlots",
|
||||||
|
"Salubra": "SalubraShopSlots",
|
||||||
|
"Leg_Eater": "LegEaterShopSlots",
|
||||||
|
"Salubra_(Requires_Charms)": "IseldaShopSlots",
|
||||||
|
"Egg_Shop": "EggShopSlots",
|
||||||
}
|
}
|
||||||
|
|
||||||
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
|
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
|
||||||
|
@ -124,9 +121,6 @@ for option_name, option_data in pool_options.items():
|
||||||
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
|
||||||
if option_name in option_docstrings:
|
if option_name in option_docstrings:
|
||||||
extra_data["__doc__"] = option_docstrings[option_name]
|
extra_data["__doc__"] = option_docstrings[option_name]
|
||||||
if option_name in disabled:
|
|
||||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
|
||||||
option = type(option_name, (Disabled,), extra_data)
|
|
||||||
if option_name in default_on:
|
if option_name in default_on:
|
||||||
option = type(option_name, (DefaultOnToggle,), extra_data)
|
option = type(option_name, (DefaultOnToggle,), extra_data)
|
||||||
else:
|
else:
|
||||||
|
@ -142,13 +136,36 @@ for option_name in logic_options.values():
|
||||||
if option_name in option_docstrings:
|
if option_name in option_docstrings:
|
||||||
extra_data["__doc__"] = option_docstrings[option_name]
|
extra_data["__doc__"] = option_docstrings[option_name]
|
||||||
option = type(option_name, (Toggle,), extra_data)
|
option = type(option_name, (Toggle,), extra_data)
|
||||||
if option_name in disabled:
|
|
||||||
extra_data["__doc__"] = "Disabled Option. Not implemented."
|
|
||||||
option = type(option_name, (Disabled,), extra_data)
|
|
||||||
globals()[option.__name__] = option
|
globals()[option.__name__] = option
|
||||||
hollow_knight_logic_options[option.__name__] = option
|
hollow_knight_logic_options[option.__name__] = option
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeElevatorPass(Toggle):
|
||||||
|
"""Adds an Elevator Pass item to the item pool, which is then required to use the large elevators connecting
|
||||||
|
City of Tears to the Forgotten Crossroads and Resting Grounds."""
|
||||||
|
display_name = "Randomize Elevator Pass"
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class SplitMothwingCloak(Toggle):
|
||||||
|
"""Splits the Mothwing Cloak into left- and right-only versions of the item. Randomly adds a second left or
|
||||||
|
right Mothwing cloak item which functions as the upgrade to Shade Cloak."""
|
||||||
|
display_name = "Split Mothwing Cloak"
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class SplitMantisClaw(Toggle):
|
||||||
|
"""Splits the Mantis Claw into left- and right-only versions of the item."""
|
||||||
|
display_name = "Split Mantis Claw"
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class SplitCrystalHeart(Toggle):
|
||||||
|
"""Splits the Crystal Heart into left- and right-only versions of the item."""
|
||||||
|
display_name = "Split Crystal Heart"
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
class MinimumGrubPrice(Range):
|
class MinimumGrubPrice(Range):
|
||||||
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
|
"""The minimum grub price in the range of prices that an item should cost from Grubfather."""
|
||||||
display_name = "Minimum Grub Price"
|
display_name = "Minimum Grub Price"
|
||||||
|
@ -178,7 +195,7 @@ class MaximumEssencePrice(MinimumEssencePrice):
|
||||||
|
|
||||||
|
|
||||||
class MinimumEggPrice(Range):
|
class MinimumEggPrice(Range):
|
||||||
"""The minimum rancid egg price in the range of prices that an item should cost from Ijii.
|
"""The minimum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||||
display_name = "Minimum Egg Price"
|
display_name = "Minimum Egg Price"
|
||||||
range_start = 1
|
range_start = 1
|
||||||
|
@ -187,7 +204,7 @@ class MinimumEggPrice(Range):
|
||||||
|
|
||||||
|
|
||||||
class MaximumEggPrice(MinimumEggPrice):
|
class MaximumEggPrice(MinimumEggPrice):
|
||||||
"""The maximum rancid egg price in the range of prices that an item should cost from Ijii.
|
"""The maximum rancid egg price in the range of prices that an item should cost from Jiji.
|
||||||
Only takes effect if the EggSlotShops option is greater than 0."""
|
Only takes effect if the EggSlotShops option is greater than 0."""
|
||||||
display_name = "Maximum Egg Price"
|
display_name = "Maximum Egg Price"
|
||||||
default = 10
|
default = 10
|
||||||
|
@ -208,6 +225,22 @@ class MaximumCharmPrice(MinimumCharmPrice):
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
|
class MinimumGeoPrice(Range):
|
||||||
|
"""The minimum geo price for items in geo shops."""
|
||||||
|
display_name = "Minimum Geo Price"
|
||||||
|
range_start = 1
|
||||||
|
range_end = 200
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class MaximumGeoPrice(Range):
|
||||||
|
"""The maximum geo price for items in geo shops."""
|
||||||
|
display_name = "Minimum Geo Price"
|
||||||
|
range_start = 1
|
||||||
|
range_end = 2000
|
||||||
|
default = 400
|
||||||
|
|
||||||
|
|
||||||
class RandomCharmCosts(SpecialRange):
|
class RandomCharmCosts(SpecialRange):
|
||||||
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
"""Total Notch Cost of all Charms together. Vanilla sums to 90.
|
||||||
This value is distributed among all charms in a random fashion.
|
This value is distributed among all charms in a random fashion.
|
||||||
|
@ -256,13 +289,91 @@ class PlandoCharmCosts(OptionDict):
|
||||||
return charm_costs
|
return charm_costs
|
||||||
|
|
||||||
|
|
||||||
|
class SlyShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Sly Shop and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Sly Shop Slots"
|
||||||
|
default = 8
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class SlyKeyShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Sly Shop (requiring Shopkeeper's Key) and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Sly Key Shop Slots"
|
||||||
|
default = 6
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class IseldaShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Iselda Shop and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Iselda Shop Slots"
|
||||||
|
default = 2
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class SalubraShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Salubra Shop, and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Salubra Shop Slots"
|
||||||
|
default = 5
|
||||||
|
range_start = 0
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class SalubraCharmShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Salubra Shop (requiring Charms), and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Salubra Charm Shop Slots"
|
||||||
|
default = 5
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class LegEaterShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Leg Eater Shop and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Leg Eater Shop Slots"
|
||||||
|
default = 3
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class GrubfatherRewardSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Grubfather and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Grubfather Reward Slots"
|
||||||
|
default = 7
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class SeerRewardSlots(Range):
|
||||||
|
"""For each extra slot, add a location to the Seer and a filler item to the item pool."""
|
||||||
|
|
||||||
|
display_name = "Seer Reward Reward Slots"
|
||||||
|
default = 8
|
||||||
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
class EggShopSlots(Range):
|
class EggShopSlots(Range):
|
||||||
"""For each slot, add a location to the Egg Shop and a Geo drop to the item pool."""
|
"""For each slot, add a location to the Egg Shop and a filler item to the item pool."""
|
||||||
|
|
||||||
display_name = "Egg Shop Item Slots"
|
display_name = "Egg Shop Item Slots"
|
||||||
range_end = 16
|
range_end = 16
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraShopSlots(Range):
|
||||||
|
"""For each extra slot, add a location to a randomly chosen shop a filler item to the item pool.
|
||||||
|
|
||||||
|
The Egg Shop will be excluded from this list unless it has at least one item.
|
||||||
|
|
||||||
|
Shops are capped at 16 items each.
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Additional Shop Slots"
|
||||||
|
default = 0
|
||||||
|
range_end = 9 * 16 # Number of shops x max slots per shop.
|
||||||
|
|
||||||
|
|
||||||
class Goal(Choice):
|
class Goal(Choice):
|
||||||
"""The goal required of you in order to complete your run in Archipelago."""
|
"""The goal required of you in order to complete your run in Archipelago."""
|
||||||
display_name = "Goal"
|
display_name = "Goal"
|
||||||
|
@ -315,19 +426,70 @@ class StartingGeo(Range):
|
||||||
default = 0
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class CostSanity(Choice):
|
||||||
|
"""If enabled, most locations with costs (like stag stations) will have randomly determined costs.
|
||||||
|
If set to shopsonly, CostSanity will only apply to shops (including Grubfather, Seer and Egg Shop).
|
||||||
|
If set to notshops, CostSanity will only apply to non-shops (e.g. Stag stations and Cornifer locations)
|
||||||
|
|
||||||
|
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||||
|
"""
|
||||||
|
option_off = 0
|
||||||
|
alias_false = 0
|
||||||
|
alias_no = 0
|
||||||
|
option_on = 1
|
||||||
|
alias_true = 1
|
||||||
|
alias_yes = 1
|
||||||
|
option_shopsonly = 2
|
||||||
|
option_notshops = 3
|
||||||
|
display_name = "Cost Sanity"
|
||||||
|
|
||||||
|
|
||||||
|
class CostSanityHybridChance(Range):
|
||||||
|
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
|
||||||
|
range_end = 100
|
||||||
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
|
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
|
||||||
|
for term, cost in cost_terms.items():
|
||||||
|
option_name = f"CostSanity{cost.option}Weight"
|
||||||
|
extra_data = {
|
||||||
|
"__module__": __name__, "range_end": 1000,
|
||||||
|
"__doc__": (
|
||||||
|
f"The likelihood of Costsanity choosing a {cost.option} cost."
|
||||||
|
" Chosen as a sum of all weights from other types."
|
||||||
|
),
|
||||||
|
"default": cost.weight
|
||||||
|
}
|
||||||
|
if cost == 'GEO':
|
||||||
|
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
|
||||||
|
|
||||||
|
option = type(option_name, (Range,), extra_data)
|
||||||
|
globals()[option.__name__] = option
|
||||||
|
cost_sanity_weights[option.__name__] = option
|
||||||
|
|
||||||
|
|
||||||
hollow_knight_options: typing.Dict[str, type(Option)] = {
|
hollow_knight_options: typing.Dict[str, type(Option)] = {
|
||||||
**hollow_knight_randomize_options,
|
**hollow_knight_randomize_options,
|
||||||
|
RandomizeElevatorPass.__name__: RandomizeElevatorPass,
|
||||||
**hollow_knight_logic_options,
|
**hollow_knight_logic_options,
|
||||||
**{
|
**{
|
||||||
option.__name__: option
|
option.__name__: option
|
||||||
for option in (
|
for option in (
|
||||||
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
StartLocation, Goal, WhitePalace, StartingGeo, DeathLink,
|
||||||
|
MinimumGeoPrice, MaximumGeoPrice,
|
||||||
MinimumGrubPrice, MaximumGrubPrice,
|
MinimumGrubPrice, MaximumGrubPrice,
|
||||||
MinimumEssencePrice, MaximumEssencePrice,
|
MinimumEssencePrice, MaximumEssencePrice,
|
||||||
MinimumCharmPrice, MaximumCharmPrice,
|
MinimumCharmPrice, MaximumCharmPrice,
|
||||||
RandomCharmCosts, PlandoCharmCosts,
|
RandomCharmCosts, PlandoCharmCosts,
|
||||||
MinimumEggPrice, MaximumEggPrice, EggShopSlots,
|
MinimumEggPrice, MaximumEggPrice, EggShopSlots,
|
||||||
# Add your new options where it makes sense?
|
SlyShopSlots, SlyKeyShopSlots, IseldaShopSlots,
|
||||||
|
SalubraShopSlots, SalubraCharmShopSlots,
|
||||||
|
LegEaterShopSlots, GrubfatherRewardSlots,
|
||||||
|
SeerRewardSlots, ExtraShopSlots,
|
||||||
|
SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw,
|
||||||
|
CostSanity, CostSanityHybridChance,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
**cost_sanity_weights
|
||||||
}
|
}
|
||||||
|
|
1759
worlds/hk/Rules.py
1759
worlds/hk/Rules.py
File diff suppressed because it is too large
Load Diff
|
@ -2,16 +2,19 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
from collections import Counter
|
from copy import deepcopy
|
||||||
|
import itertools
|
||||||
|
import operator
|
||||||
|
|
||||||
logger = logging.getLogger("Hollow Knight")
|
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, cost_terms
|
||||||
from .Options import hollow_knight_options, hollow_knight_randomize_options, disabled, Goal, WhitePalace
|
from .Options import hollow_knight_options, hollow_knight_randomize_options, Goal, WhitePalace, CostSanity, \
|
||||||
|
shop_to_option
|
||||||
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, vanilla_shop_costs, vanilla_location_costs
|
||||||
from .Charms import names as charm_names
|
from .Charms import names as charm_names
|
||||||
|
|
||||||
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial, ItemClassification
|
from BaseClasses import Region, Entrance, Location, MultiWorld, Item, RegionType, LocationProgressType, Tutorial, ItemClassification
|
||||||
|
@ -98,6 +101,25 @@ logicless_options = {
|
||||||
"RandomizeLoreTablets", "RandomizeSoulTotems",
|
"RandomizeLoreTablets", "RandomizeSoulTotems",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Options that affect vanilla starting items
|
||||||
|
randomizable_starting_items: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||||
|
"RandomizeFocus": ("Focus",),
|
||||||
|
"RandomizeSwim": ("Swim",),
|
||||||
|
"RandomizeNail": ('Upslash', 'Leftslash', 'Rightslash')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shop cost types.
|
||||||
|
shop_cost_types: typing.Dict[str, typing.Tuple[str, ...]] = {
|
||||||
|
"Egg_Shop": ("RANCIDEGGS",),
|
||||||
|
"Grubfather": ("GRUBS",),
|
||||||
|
"Seer": ("ESSENCE",),
|
||||||
|
"Salubra_(Requires_Charms)": ("CHARMS", "GEO"),
|
||||||
|
"Sly": ("GEO",),
|
||||||
|
"Sly_(Key)": ("GEO",),
|
||||||
|
"Iselda": ("GEO",),
|
||||||
|
"Salubra": ("GEO",),
|
||||||
|
"Leg_Eater": ("GEO",),
|
||||||
|
}
|
||||||
|
|
||||||
class HKWeb(WebWorld):
|
class HKWeb(WebWorld):
|
||||||
tutorials = [Tutorial(
|
tutorials = [Tutorial(
|
||||||
|
@ -127,19 +149,18 @@ class HKWorld(World):
|
||||||
item_name_groups = item_name_groups
|
item_name_groups = item_name_groups
|
||||||
|
|
||||||
ranges: typing.Dict[str, typing.Tuple[int, int]]
|
ranges: typing.Dict[str, typing.Tuple[int, int]]
|
||||||
shops: typing.Dict[str, str] = {
|
|
||||||
"Egg_Shop": "Egg",
|
|
||||||
"Grubfather": "Grub",
|
|
||||||
"Seer": "Essence",
|
|
||||||
"Salubra_(Requires_Charms)": "Charm"
|
|
||||||
}
|
|
||||||
charm_costs: typing.List[int]
|
charm_costs: typing.List[int]
|
||||||
|
cached_filler_items = {}
|
||||||
data_version = 2
|
data_version = 2
|
||||||
|
|
||||||
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, typing.List[HKLocation]] = {
|
||||||
|
location: list() for location in multi_locations
|
||||||
|
}
|
||||||
self.ranges = {}
|
self.ranges = {}
|
||||||
|
self.created_shop_items = 0
|
||||||
|
self.vanilla_shop_costs = deepcopy(vanilla_shop_costs)
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
world = self.world
|
world = self.world
|
||||||
|
@ -147,16 +168,14 @@ class HKWorld(World):
|
||||||
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 term, data in cost_terms.items():
|
||||||
mini = getattr(world, f"Minimum{unit}Price")[self.player]
|
mini = getattr(world, f"Minimum{data.option}Price")[self.player]
|
||||||
maxi = getattr(world, f"Maximum{unit}Price")[self.player]
|
maxi = getattr(world, f"Maximum{data.option}Price")[self.player]
|
||||||
# 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[unit] = mini.value, maxi.value
|
self.ranges[term] = mini.value, maxi.value
|
||||||
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
|
world.push_precollected(HKItem(starts[world.StartLocation[self.player].current_key],
|
||||||
True, None, "Event", self.player))
|
True, None, "Event", self.player))
|
||||||
for option_name in disabled:
|
|
||||||
getattr(world, option_name)[self.player].value = 0
|
|
||||||
|
|
||||||
def white_palace_exclusions(self):
|
def white_palace_exclusions(self):
|
||||||
exclusions = set()
|
exclusions = set()
|
||||||
|
@ -199,55 +218,197 @@ class HKWorld(World):
|
||||||
menu_region.locations.append(loc)
|
menu_region.locations.append(loc)
|
||||||
|
|
||||||
def create_items(self):
|
def create_items(self):
|
||||||
|
unfilled_locations = 0
|
||||||
# Generate item pool and associated locations (paired in HK)
|
# Generate item pool and associated locations (paired in HK)
|
||||||
pool: typing.List[HKItem] = []
|
pool: typing.List[HKItem] = []
|
||||||
geo_replace: typing.Set[str] = set()
|
|
||||||
if self.world.RemoveSpellUpgrades[self.player]:
|
|
||||||
geo_replace.add("Abyss_Shriek")
|
|
||||||
geo_replace.add("Shade_Soul")
|
|
||||||
geo_replace.add("Descending_Dark")
|
|
||||||
|
|
||||||
wp_exclusions = self.white_palace_exclusions()
|
wp_exclusions = self.white_palace_exclusions()
|
||||||
|
junk_replace: typing.Set[str] = set()
|
||||||
|
if self.world.RemoveSpellUpgrades[self.player]:
|
||||||
|
junk_replace.update(("Abyss_Shriek", "Shade_Soul", "Descending_Dark"))
|
||||||
|
|
||||||
|
randomized_starting_items = set()
|
||||||
|
for attr, items in randomizable_starting_items.items():
|
||||||
|
if getattr(self.world, attr)[self.player]:
|
||||||
|
randomized_starting_items.update(items)
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
def _add(item_name: str, location_name: str):
|
||||||
|
"""
|
||||||
|
Adds a pairing of an item and location, doing appropriate checks to see if it should be vanilla or not.
|
||||||
|
"""
|
||||||
|
nonlocal unfilled_locations
|
||||||
|
|
||||||
|
vanilla = not randomized
|
||||||
|
excluded = False
|
||||||
|
|
||||||
|
if not vanilla and location_name in wp_exclusions:
|
||||||
|
if location_name == 'King_Fragment':
|
||||||
|
excluded = True
|
||||||
|
else:
|
||||||
|
vanilla = True
|
||||||
|
|
||||||
|
if item_name in junk_replace:
|
||||||
|
item_name = self.get_filler_item_name()
|
||||||
|
|
||||||
|
item = self.create_item(item_name)
|
||||||
|
|
||||||
|
if location_name == "Start":
|
||||||
|
if item_name in randomized_starting_items:
|
||||||
|
pool.append(item)
|
||||||
|
else:
|
||||||
|
self.world.push_precollected(item)
|
||||||
|
return
|
||||||
|
|
||||||
|
if vanilla:
|
||||||
|
location = self.create_vanilla_location(location_name, item)
|
||||||
|
else:
|
||||||
|
pool.append(item)
|
||||||
|
if location_name in multi_locations: # Create shop locations later.
|
||||||
|
return
|
||||||
|
location = self.create_location(location_name)
|
||||||
|
unfilled_locations += 1
|
||||||
|
if 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.world, option_key)[self.player]
|
randomized = 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):
|
||||||
vanilla = not randomized
|
if item_name in junk_replace:
|
||||||
excluded = False
|
item_name = self.get_filler_item_name()
|
||||||
if item_name in geo_replace:
|
|
||||||
item_name = "Geo_Rock-Default"
|
if (item_name == "Crystal_Heart" and self.world.SplitCrystalHeart[self.player]) or \
|
||||||
item = self.create_item(item_name)
|
(item_name == "Mothwing_Cloak" and self.world.SplitMothwingCloak[self.player]):
|
||||||
if location_name == "Start":
|
_add("Left_" + item_name, location_name)
|
||||||
self.world.push_precollected(item)
|
_add("Right_" + item_name, "Split_" + location_name)
|
||||||
continue
|
continue
|
||||||
|
if item_name == "Mantis_Claw" and self.world.SplitMantisClaw[self.player]:
|
||||||
location = self.create_location(location_name)
|
_add("Left_" + item_name, "Left_" + location_name)
|
||||||
if not vanilla and location_name in wp_exclusions:
|
_add("Right_" + item_name, "Right_" + location_name)
|
||||||
if location_name == 'King_Fragment':
|
continue
|
||||||
excluded = True
|
if item_name == "Shade_Cloak" and self.world.SplitMothwingCloak[self.player]:
|
||||||
|
if self.world.random.randint(0, 1):
|
||||||
|
item_name = "Left_Mothwing_Cloak"
|
||||||
else:
|
else:
|
||||||
vanilla = True
|
item_name = "Right_Mothwing_Cloak"
|
||||||
if excluded:
|
|
||||||
location.progress_type = LocationProgressType.EXCLUDED
|
|
||||||
if vanilla:
|
|
||||||
location.place_locked_item(item)
|
|
||||||
else:
|
|
||||||
pool.append(item)
|
|
||||||
|
|
||||||
for i in range(self.world.EggShopSlots[self.player].value):
|
_add(item_name, location_name)
|
||||||
self.create_location("Egg_Shop")
|
|
||||||
pool.append(self.create_item("Geo_Rock-Default"))
|
if self.world.RandomizeElevatorPass[self.player]:
|
||||||
|
randomized = True
|
||||||
|
_add("Elevator_Pass", "Elevator_Pass")
|
||||||
|
|
||||||
|
for shop, locations in self.created_multi_locations.items():
|
||||||
|
for _ in range(len(locations), getattr(self.world, shop_to_option[shop])[self.player].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.world.ExtraShopSlots[self.player].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.world.EggShopSlots[self.player].value: # No eggshop, so don't place items there
|
||||||
|
shops.remove('Egg_Shop')
|
||||||
|
|
||||||
|
for _ in range(additional_shop_items):
|
||||||
|
shop = self.world.random.choice(shops)
|
||||||
|
loc = self.create_location(shop)
|
||||||
|
unfilled_locations += 1
|
||||||
|
if len(self.created_multi_locations[shop]) >= 16:
|
||||||
|
shops.remove(shop)
|
||||||
|
if not shops:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create filler items, if needed
|
||||||
|
if item_count < unfilled_locations:
|
||||||
|
pool.extend(self.create_item(self.get_filler_item_name()) for _ in range(unfilled_locations - item_count))
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
self.apply_costsanity()
|
||||||
|
self.sort_shops_by_cost()
|
||||||
|
|
||||||
for shopname in self.shops:
|
def sort_shops_by_cost(self):
|
||||||
prices: typing.List[int] = []
|
for shop, locations in self.created_multi_locations.items():
|
||||||
locations: typing.List[HKLocation] = []
|
randomized_locations = list(loc for loc in locations if not loc.vanilla)
|
||||||
for x in range(1, self.created_multi_locations[shopname]+1):
|
prices = sorted(
|
||||||
loc = self.world.get_location(self.get_multi_location_name(shopname, x), self.player)
|
(loc.costs for loc in randomized_locations),
|
||||||
locations.append(loc)
|
key=lambda costs: (len(costs),) + tuple(costs.values())
|
||||||
prices.append(loc.cost)
|
)
|
||||||
prices.sort()
|
for loc, costs in zip(randomized_locations, prices):
|
||||||
for loc, price in zip(locations, prices):
|
loc.costs = costs
|
||||||
loc.cost = price
|
|
||||||
|
def apply_costsanity(self):
|
||||||
|
setting = self.world.CostSanity[self.player].value
|
||||||
|
if not setting:
|
||||||
|
return # noop
|
||||||
|
|
||||||
|
def _compute_weights(weights: dict, desc: str) -> typing.Dict[str, int]:
|
||||||
|
if all(x == 0 for x in weights.values()):
|
||||||
|
logger.warning(
|
||||||
|
f"All {desc} weights were zero for {self.world.player_name[self.player]}."
|
||||||
|
f" Setting them to one instead."
|
||||||
|
)
|
||||||
|
weights = {k: 1 for k in weights}
|
||||||
|
|
||||||
|
return {k: v for k, v in weights.items() if v}
|
||||||
|
|
||||||
|
random = self.world.random
|
||||||
|
hybrid_chance = getattr(self.world, f"CostSanityHybridChance")[self.player].value
|
||||||
|
weights = {
|
||||||
|
data.term: getattr(self.world, f"CostSanity{data.option}Weight")[self.player].value
|
||||||
|
for data in cost_terms.values()
|
||||||
|
}
|
||||||
|
weights_geoless = dict(weights)
|
||||||
|
del weights_geoless["GEO"]
|
||||||
|
|
||||||
|
weights = _compute_weights(weights, "CostSanity")
|
||||||
|
weights_geoless = _compute_weights(weights_geoless, "Geoless CostSanity")
|
||||||
|
|
||||||
|
if hybrid_chance > 0:
|
||||||
|
if len(weights) == 1:
|
||||||
|
logger.warning(
|
||||||
|
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||||
|
f" CostSanityHybridChance will not trigger."
|
||||||
|
)
|
||||||
|
if len(weights_geoless) == 1:
|
||||||
|
logger.warning(
|
||||||
|
f"Only one cost type is available for CostSanity in {self.world.player_name[self.player]}'s world."
|
||||||
|
f" CostSanityHybridChance will not trigger in geoless locations."
|
||||||
|
)
|
||||||
|
|
||||||
|
for region in self.world.get_regions(self.player):
|
||||||
|
for location in region.locations:
|
||||||
|
if location.vanilla:
|
||||||
|
continue
|
||||||
|
if not location.costs:
|
||||||
|
continue
|
||||||
|
if location.name == "Vessel_Fragment-Basin":
|
||||||
|
continue
|
||||||
|
if setting == CostSanity.option_notshops and location.basename in multi_locations:
|
||||||
|
continue
|
||||||
|
if setting == CostSanity.option_shopsonly and location.basename not in multi_locations:
|
||||||
|
continue
|
||||||
|
if location.basename in {'Grubfather', 'Seer', 'Eggshop'}:
|
||||||
|
our_weights = dict(weights_geoless)
|
||||||
|
else:
|
||||||
|
our_weights = dict(weights)
|
||||||
|
|
||||||
|
rolls = 1
|
||||||
|
if random.randrange(100) < hybrid_chance:
|
||||||
|
rolls = 2
|
||||||
|
|
||||||
|
if rolls > len(our_weights):
|
||||||
|
terms = list(our_weights.keys()) # Can't randomly choose cost types, using all of them.
|
||||||
|
else:
|
||||||
|
terms = []
|
||||||
|
for _ in range(rolls):
|
||||||
|
term = random.choices(list(our_weights.keys()), list(our_weights.values()))[0]
|
||||||
|
del our_weights[term]
|
||||||
|
terms.append(term)
|
||||||
|
|
||||||
|
location.costs = {term: random.randint(*self.ranges[term]) for term in terms}
|
||||||
|
location.sort_costs()
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
world = self.world
|
world = self.world
|
||||||
|
@ -280,12 +441,24 @@ class HKWorld(World):
|
||||||
# 32 bit int
|
# 32 bit int
|
||||||
slot_data["seed"] = self.world.slot_seeds[self.player].randint(-2147483647, 2147483646)
|
slot_data["seed"] = self.world.slot_seeds[self.player].randint(-2147483647, 2147483646)
|
||||||
|
|
||||||
for shop, unit in self.shops.items():
|
# Backwards compatibility for shop cost data (HKAP < 0.1.0)
|
||||||
slot_data[f"{unit}_costs"] = {
|
if not self.world.CostSanity[self.player]:
|
||||||
f"{shop}_{i}":
|
for shop, terms in shop_cost_types.items():
|
||||||
self.world.get_location(f"{shop}_{i}", self.player).cost
|
unit = cost_terms[next(iter(terms))].option
|
||||||
for i in range(1, 1 + self.created_multi_locations[shop])
|
if unit == "Geo":
|
||||||
}
|
continue
|
||||||
|
slot_data[f"{unit}_costs"] = {
|
||||||
|
loc.name: next(iter(loc.costs.values()))
|
||||||
|
for loc in self.created_multi_locations[shop]
|
||||||
|
}
|
||||||
|
|
||||||
|
# HKAP 0.1.0 and later cost data.
|
||||||
|
location_costs = {}
|
||||||
|
for region in self.world.get_regions(self.player):
|
||||||
|
for location in region.locations:
|
||||||
|
if location.costs:
|
||||||
|
location_costs[location.name] = location.costs
|
||||||
|
slot_data["location_costs"] = location_costs
|
||||||
|
|
||||||
slot_data["notch_costs"] = self.charm_costs
|
slot_data["notch_costs"] = self.charm_costs
|
||||||
|
|
||||||
|
@ -295,30 +468,51 @@ class HKWorld(World):
|
||||||
item_data = item_table[name]
|
item_data = item_table[name]
|
||||||
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
|
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
|
||||||
|
|
||||||
def create_location(self, name: str) -> HKLocation:
|
def create_location(self, name: str, vanilla=False) -> HKLocation:
|
||||||
unit = self.shops.get(name, None)
|
costs = None
|
||||||
if unit:
|
basename = name
|
||||||
cost = self.world.random.randint(*self.ranges[unit])
|
if name in shop_cost_types:
|
||||||
else:
|
costs = {
|
||||||
cost = 0
|
term: self.world.random.randint(*self.ranges[term])
|
||||||
if name in multi_locations:
|
for term in shop_cost_types[name]
|
||||||
self.created_multi_locations[name] += 1
|
}
|
||||||
name = self.get_multi_location_name(name, self.created_multi_locations[name])
|
elif name in vanilla_location_costs:
|
||||||
|
costs = vanilla_location_costs[name]
|
||||||
|
|
||||||
|
multi = self.created_multi_locations.get(name)
|
||||||
|
|
||||||
|
if multi is not None:
|
||||||
|
i = len(multi) + 1
|
||||||
|
name = f"{name}_{i}"
|
||||||
|
|
||||||
region = self.world.get_region("Menu", self.player)
|
region = self.world.get_region("Menu", self.player)
|
||||||
loc = HKLocation(self.player, name, self.location_name_to_id[name], region)
|
loc = HKLocation(self.player, name,
|
||||||
if unit:
|
self.location_name_to_id[name], region, costs=costs, vanilla=vanilla,
|
||||||
loc.unit = unit
|
basename=basename)
|
||||||
loc.cost = cost
|
|
||||||
|
if multi is not None:
|
||||||
|
multi.append(loc)
|
||||||
|
|
||||||
region.locations.append(loc)
|
region.locations.append(loc)
|
||||||
return loc
|
return loc
|
||||||
|
|
||||||
|
def create_vanilla_location(self, location: str, item: Item):
|
||||||
|
costs = self.vanilla_shop_costs.get((location, item.name))
|
||||||
|
location = self.create_location(location, vanilla=True)
|
||||||
|
location.place_locked_item(item)
|
||||||
|
if costs:
|
||||||
|
location.costs = costs.pop()
|
||||||
|
|
||||||
def collect(self, state, item: HKItem) -> bool:
|
def collect(self, state, item: HKItem) -> bool:
|
||||||
change = super(HKWorld, self).collect(state, item)
|
change = super(HKWorld, self).collect(state, item)
|
||||||
if change:
|
if change:
|
||||||
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
for effect_name, effect_value in item_effects.get(item.name, {}).items():
|
||||||
state.prog_items[effect_name, item.player] += effect_value
|
state.prog_items[effect_name, item.player] += effect_value
|
||||||
|
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
|
||||||
|
if state.prog_items.get(('RIGHTDASH', item.player), 0) and \
|
||||||
|
state.prog_items.get(('LEFTDASH', item.player), 0):
|
||||||
|
(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \
|
||||||
|
([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2)
|
||||||
return change
|
return change
|
||||||
|
|
||||||
def remove(self, state, item: HKItem) -> bool:
|
def remove(self, state, item: HKItem) -> bool:
|
||||||
|
@ -348,17 +542,40 @@ class HKWorld(World):
|
||||||
name = world.get_player_name(player)
|
name = world.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 = world.worlds[player]
|
||||||
for shop_name, unit_name in cls.shops.items():
|
|
||||||
for x in range(1, hk_world.created_multi_locations[shop_name]+1):
|
if world.CostSanity[player].value:
|
||||||
loc = world.get_location(hk_world.get_multi_location_name(shop_name, x), player)
|
for loc in sorted(
|
||||||
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost} {unit_name}")
|
(
|
||||||
|
loc for loc in itertools.chain(*(region.locations for region in world.get_regions(player)))
|
||||||
|
if loc.costs
|
||||||
|
), key=operator.attrgetter('name')
|
||||||
|
):
|
||||||
|
spoiler_handle.write(f"\n{loc}: {loc.item} costing {loc.cost_text()}")
|
||||||
|
else:
|
||||||
|
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 get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
|
def get_multi_location_name(self, base: str, i: typing.Optional[int]) -> str:
|
||||||
if i is None:
|
if i is None:
|
||||||
i = self.created_multi_locations[base]
|
i = len(self.created_multi_locations[base]) + 1
|
||||||
assert 0 < i < 18, "limited number of multi location IDs reserved."
|
assert 1 <= 16, "limited number of multi location IDs reserved."
|
||||||
return f"{base}_{i}"
|
return f"{base}_{i}"
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
if self.player not in self.cached_filler_items:
|
||||||
|
fillers = ["One_Geo", "Soul_Refill"]
|
||||||
|
exclusions = self.white_palace_exclusions()
|
||||||
|
for group in (
|
||||||
|
'RandomizeGeoRocks', 'RandomizeSoulTotems', 'RandomizeLoreTablets', 'RandomizeJunkPitChests',
|
||||||
|
'RandomizeRancidEggs'
|
||||||
|
):
|
||||||
|
if getattr(self.world, 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.world.random.choice(self.cached_filler_items[self.player])
|
||||||
|
|
||||||
|
|
||||||
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
|
def create_region(world: MultiWorld, player: int, name: str, location_names=None, exits=None) -> Region:
|
||||||
ret = Region(name, RegionType.Generic, name, player)
|
ret = Region(name, RegionType.Generic, name, player)
|
||||||
|
@ -376,11 +593,34 @@ def create_region(world: MultiWorld, player: int, name: str, location_names=None
|
||||||
|
|
||||||
class HKLocation(Location):
|
class HKLocation(Location):
|
||||||
game: str = "Hollow Knight"
|
game: str = "Hollow Knight"
|
||||||
cost: int = 0
|
costs: typing.Dict[str, int] = None
|
||||||
unit: typing.Optional[str] = None
|
unit: typing.Optional[str] = None
|
||||||
|
vanilla = False
|
||||||
|
basename: str
|
||||||
|
|
||||||
def __init__(self, player: int, name: str, code=None, parent=None):
|
def sort_costs(self):
|
||||||
|
if self.costs is None:
|
||||||
|
return
|
||||||
|
self.costs = {k: self.costs[k] for k in sorted(self.costs.keys(), key=lambda x: cost_terms[x].sort)}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, player: int, name: str, code=None, parent=None,
|
||||||
|
costs: typing.Dict[str, int] = None, vanilla: bool = False, basename: str = None
|
||||||
|
):
|
||||||
|
self.basename = basename or name
|
||||||
super(HKLocation, self).__init__(player, name, code if code else None, parent)
|
super(HKLocation, self).__init__(player, name, code if code else None, parent)
|
||||||
|
self.vanilla = vanilla
|
||||||
|
if costs:
|
||||||
|
self.costs = dict(costs)
|
||||||
|
self.sort_costs()
|
||||||
|
|
||||||
|
def cost_text(self, separator=" and "):
|
||||||
|
if self.costs is None:
|
||||||
|
return None
|
||||||
|
return separator.join(
|
||||||
|
f"{value} {cost_terms[term].singular if value == 1 else cost_terms[term].plural}"
|
||||||
|
for term, value in self.costs.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HKItem(Item):
|
class HKItem(Item):
|
||||||
|
@ -393,6 +633,10 @@ class HKItem(Item):
|
||||||
classification = ItemClassification.progression_skip_balancing
|
classification = ItemClassification.progression_skip_balancing
|
||||||
elif type == "Charm" and name not in progression_charms:
|
elif type == "Charm" and name not in progression_charms:
|
||||||
classification = ItemClassification.progression_skip_balancing
|
classification = ItemClassification.progression_skip_balancing
|
||||||
|
elif type in ("Map", "Journal"):
|
||||||
|
classification = ItemClassification.filler
|
||||||
|
elif type in ("Mask", "Ore", "Vessel"):
|
||||||
|
classification = ItemClassification.useful
|
||||||
elif advancement:
|
elif advancement:
|
||||||
classification = ItemClassification.progression
|
classification = ItemClassification.progression
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,50 +1,20 @@
|
||||||
from ..generic.Rules import set_rule, add_rule
|
# This module is written by Extractor.py, do not edit manually!.
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
units = {
|
def set_generated_rules(hk_world, hk_set_rule):
|
||||||
"Egg": "RANCIDEGGS",
|
|
||||||
"Grub": "GRUBS",
|
|
||||||
"Essence": "ESSENCE",
|
|
||||||
"Charm": "CHARMS",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def hk_set_rule(hk_world, location: str, rule):
|
|
||||||
count = hk_world.created_multi_locations[location]
|
|
||||||
if count:
|
|
||||||
locations = [f"{location}_{x}" for x in range(1, count+1)]
|
|
||||||
elif (location, hk_world.player) in hk_world.world._location_cache:
|
|
||||||
locations = [location]
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
for location in locations:
|
|
||||||
set_rule(hk_world.world.get_location(location, hk_world.player), rule)
|
|
||||||
|
|
||||||
|
|
||||||
def set_shop_prices(hk_world):
|
|
||||||
player = hk_world.player
|
player = hk_world.player
|
||||||
for shop, unit in hk_world.shops.items():
|
fn = partial(hk_set_rule, hk_world)
|
||||||
for i in range(1, 1 + hk_world.created_multi_locations[shop]):
|
|
||||||
loc = hk_world.world.get_location(f"{shop}_{i}", hk_world.player)
|
|
||||||
add_rule(loc, lambda state, unit=units[unit], cost=loc.cost: state.count(unit, player) >= cost)
|
|
||||||
|
|
||||||
|
|
||||||
def set_rules(hk_world):
|
|
||||||
player = hk_world.player
|
|
||||||
world = hk_world.world
|
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
{% for location, rule_text in event_rules.items() %}
|
{% for location, rule_text in event_rules.items() %}
|
||||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
fn("{{location}}", lambda state: {{rule_text}})
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
# Locations
|
# Locations
|
||||||
{% for location, rule_text in location_rules.items() %}
|
{% for location, rule_text in location_rules.items() %}
|
||||||
hk_set_rule(hk_world, "{{location}}", lambda state: {{rule_text}})
|
fn("{{location}}", lambda state: {{rule_text}})
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
# Shop prices
|
|
||||||
set_shop_prices(hk_world)
|
|
||||||
|
|
||||||
# Connectors
|
# Connectors
|
||||||
{% for entrance, rule_text in connectors_rules.items() %}
|
{% for entrance, rule_text in connectors_rules.items() %}
|
||||||
rule = lambda state: {{rule_text}}
|
rule = lambda state: {{rule_text}}
|
||||||
|
@ -54,4 +24,4 @@ def set_rules(hk_world):
|
||||||
world.get_entrance("{{entrance}}_R", player).access_rule = lambda state, entrance= entrance: \
|
world.get_entrance("{{entrance}}_R", player).access_rule = lambda state, entrance= entrance: \
|
||||||
rule(state) and entrance.can_reach(state)
|
rule(state) and entrance.can_reach(state)
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% endfor %}
|
{%- endfor %}
|
Loading…
Reference in New Issue