From f2a1858b592c8d030c755f69f974fe35e733d6df Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Mar 2021 00:47:17 +0100 Subject: [PATCH] Implement (most) Hollow Knight Options --- BaseClasses.py | 55 ++++++++++++++++++++++-------- Fill.py | 2 -- Main.py | 6 ++++ Mystery.py | 5 +-- Options.py | 72 ++++++++++++++++++++++++++-------------- worlds/alttp/Dungeons.py | 5 ++- worlds/hk/__init__.py | 53 +++++++++++++++++++++++++++-- 7 files changed, 150 insertions(+), 48 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9b52a2bc..8afc5f73 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -131,16 +131,18 @@ class MultiWorld(): set_player_attr('plando_connections', []) set_player_attr('game', "A Link to the Past") set_player_attr('completion_condition', lambda state: True) - - for hk_logic in {"MILDSKIPS", "SPICYSKIPS", "FIREBALLSKIPS", "ACIDSKIPS", "SPIKETUNNELS", - "DARKROOMS", "CURSED", "SHADESKIPS"}: - set_player_attr(hk_logic, False) - set_player_attr("NOTCURSED", True) + import Options + for hk_option in Options.hollow_knight_options: + set_player_attr(hk_option, False) self.worlds = [] #for i in range(players): # self.worlds.append(worlds.alttp.ALTTPWorld({}, i)) + @property + def NOTCURSED(self): # not here to stay + return {player: not cursed for player, cursed in self.CURSED.items()} + def secure(self): self.random = secrets.SystemRandom() @@ -264,7 +266,7 @@ class MultiWorld(): soft_collect(item) if keys: - for p in range(1, self.players + 1): + for p in self.alttp_player_ids: from worlds.alttp.Items import ItemFactory for item in ItemFactory( ['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)', @@ -310,7 +312,7 @@ class MultiWorld(): if location.can_fill(self.state, item, False): location.item = item item.location = location - item.world = self + item.world = self # try to not have this here anymore if collect: self.state.collect(item, location.event, location) @@ -954,10 +956,7 @@ class Region(object): return False def can_fill(self, item: Item): - inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player]) - or (item.bigkey and not self.world.bigkeyshuffle[item.player]) - or (item.map and not self.world.mapshuffle[item.player]) - or (item.compass and not self.world.compassshuffle[item.player])) + inside_dungeon_item = item.locked_dungeon_item sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)' if sewer_hack or inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player @@ -1037,7 +1036,7 @@ class Dungeon(object): return self.dungeon_items + self.keys def is_dungeon_item(self, item: Item) -> bool: - return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] + return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items) def __eq__(self, other: Dungeon) -> bool: if not other: @@ -1072,7 +1071,7 @@ class Location(): def __init__(self, player: int, name: str = '', address:int = None, parent=None): self.name = name self.address = address - self.parent_region = parent + self.parent_region: Region = parent self.recursion_count = 0 self.player = player self.item = None @@ -1162,6 +1161,31 @@ class Item(): def compass(self) -> bool: return self.type == 'Compass' + @property + def dungeon_item(self) -> Optional[str]: + if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}: + return self.type + + @property + def shuffled_dungeon_item(self) -> bool: + dungeon_item_type = self.dungeon_item + if dungeon_item_type: + return {"SmallKey" : self.world.keyshuffle, + "BigKey": self.world.bigkeyshuffle, + "Map": self.world.mapshuffle, + "Compass": self.world.compassshuffle}[dungeon_item_type][self.player] + return False + + @property + def locked_dungeon_item(self) -> bool: + dungeon_item_type = self.dungeon_item + if dungeon_item_type: + return not {"SmallKey" : self.world.keyshuffle, + "BigKey": self.world.bigkeyshuffle, + "Map": self.world.mapshuffle, + "Compass": self.world.compassshuffle}[dungeon_item_type][self.player] + return False + def __repr__(self): return self.__str__() @@ -1367,6 +1391,11 @@ class Spoiler(object): outfile.write('Progression Balanced: %s\n' % ( 'Yes' if self.metadata['progression_balancing'][player] else 'No')) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + if player in self.world.hk_player_ids: + import Options + for hk_option in Options.hollow_knight_options: + res = getattr(self.world, hk_option)[player] + outfile.write(f'{hk_option+":":33}{res}\n') if player in self.world.alttp_player_ids: for team in range(self.world.teams): outfile.write('%s%s\n' % ( diff --git a/Fill.py b/Fill.py index 15707bbf..7be4a105 100644 --- a/Fill.py +++ b/Fill.py @@ -50,8 +50,6 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si break else: - # fill in name of world for item - item_to_place.world = world # we filled all reachable spots. Maybe the game can be beaten anyway? unplaced_items.append(item_to_place) if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game(): diff --git a/Main.py b/Main.py index 6ef8da6c..c68d96cf 100644 --- a/Main.py +++ b/Main.py @@ -129,6 +129,9 @@ def main(args, seed=None): world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy() world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() + import Options + for hk_option in Options.hollow_knight_options: + setattr(world, hk_option, getattr(args, hk_option)) world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)} @@ -258,6 +261,9 @@ def main(args, seed=None): logger.info("Running Item Plando") + for item in world.itempool: + item.world = world + distribute_planned(world) logger.info('Placing Dungeon Prizes.') diff --git a/Mystery.py b/Mystery.py index 8c1ed6d6..9c0c2a86 100644 --- a/Mystery.py +++ b/Mystery.py @@ -447,6 +447,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b ret = argparse.Namespace() ret.name = get_choice('name', weights) ret.accessibility = get_choice('accessibility', weights) + ret.progression_balancing = get_choice('progression_balancing', weights, True) ret.game = get_choice("game", weights, "A Link to the Past") ret.local_items = set() @@ -495,10 +496,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False) - ret.progression_balancing = get_choice('progression_balancing', weights, True) - # item_placement = get_choice('item_placement') - # not supported in ER - dungeon_items = get_choice('dungeon_items', weights) if dungeon_items == 'full' or dungeon_items == True: dungeon_items = 'mcsb' diff --git a/Options.py b/Options.py index 04c21e3a..7ddc2611 100644 --- a/Options.py +++ b/Options.py @@ -10,11 +10,11 @@ class AssembleOptions(type): options.update(base.options) name_lookup.update(name_lookup) new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if - name.startswith("option_")} + name.startswith("option_")} attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) options.update(new_options) - #apply aliases, without name_lookup + # apply aliases, without name_lookup options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if name.startswith("alias_")}) return super(AssembleOptions, cls).__new__(cls, name, bases, attrs) @@ -77,6 +77,12 @@ class Toggle(Option): else: return self.value > other + def __bool__(self): + return bool(self.value) + + def __int__(self): + return int(self.value) + def get_option_name(self): return bool(self.value) @@ -108,18 +114,21 @@ class Logic(Choice): class Objective(Choice): option_crystals = 0 - #option_pendants = 1 + # option_pendants = 1 option_triforce_pieces = 2 option_pedestal = 3 option_bingo = 4 -local_objective = Toggle # local triforce pieces, local dungeon prizes etc. + +local_objective = Toggle # local triforce pieces, local dungeon prizes etc. + class Goal(Choice): option_kill_ganon = 0 option_kill_ganon_and_gt_agahnim = 1 option_hand_in = 2 + class Accessibility(Choice): option_locations = 0 option_items = 1 @@ -185,31 +194,44 @@ RandomizeSoulTotems = Toggle RandomizePalaceTotems = Toggle RandomizeLoreTablets = Toggle RandomizeLifebloodCocoons = Toggle +RandomizeFlames = Toggle hollow_knight_randomize_options: typing.Dict[str, Option] = { - "RandomizeDreamers" : RandomizeDreamers, - "RandomizeSkills" : RandomizeSkills, - "RandomizeCharms" : RandomizeCharms, - "RandomizeKeys" : RandomizeKeys, - "RandomizeGeoChests" : RandomizeGeoChests, - "RandomizeMaskShards" : RandomizeMaskShards, - "RandomizeVesselFragments" : RandomizeVesselFragments, - "RandomizeCharmNotches" : RandomizeCharmNotches, - "RandomizePaleOre" : RandomizePaleOre, - "RandomizeRancidEggs" : RandomizeRancidEggs, - "RandomizeRelics" : RandomizeRelics, - "RandomizeMaps" : RandomizeMaps, - "RandomizeStags" : RandomizeStags, - "RandomizeGrubs" : RandomizeGrubs, - "RandomizeWhisperingRoots" : RandomizeWhisperingRoots, - "RandomizeRocks" : RandomizeRocks, - "RandomizeSoulTotems" : RandomizeSoulTotems, - "RandomizePalaceTotems" : RandomizePalaceTotems, - "RandomizeLoreTablets" : RandomizeLoreTablets, - "RandomizeLifebloodCocoons" : RandomizeLifebloodCocoons, + "RandomizeDreamers": RandomizeDreamers, + "RandomizeSkills": RandomizeSkills, + "RandomizeCharms": RandomizeCharms, + "RandomizeKeys": RandomizeKeys, + "RandomizeGeoChests": RandomizeGeoChests, + "RandomizeMaskShards": RandomizeMaskShards, + "RandomizeVesselFragments": RandomizeVesselFragments, + "RandomizeCharmNotches": RandomizeCharmNotches, + "RandomizePaleOre": RandomizePaleOre, + "RandomizeRancidEggs": RandomizeRancidEggs, + "RandomizeRelics": RandomizeRelics, + "RandomizeMaps": RandomizeMaps, + "RandomizeStags": RandomizeStags, + "RandomizeGrubs": RandomizeGrubs, + "RandomizeWhisperingRoots": RandomizeWhisperingRoots, + "RandomizeRocks": RandomizeRocks, + "RandomizeSoulTotems": RandomizeSoulTotems, + "RandomizePalaceTotems": RandomizePalaceTotems, + "RandomizeLoreTablets": RandomizeLoreTablets, + "RandomizeLifebloodCocoons": RandomizeLifebloodCocoons, + "RandomizeFlames": RandomizeFlames } -hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options} +hollow_knight_skip_options: typing.Dict[str, type(Option)] = { + "MILDSKIPS": Toggle, + "SPICYSKIPS": Toggle, + "FIREBALLSKIPS": Toggle, + "ACIDSKIPS": Toggle, + "SPIKETUNNELS": Toggle, + "DARKROOMS": Toggle, + "CURSED": Toggle, + "SHADESKIPS": Toggle, +} + +hollow_knight_options: typing.Dict[str, Option] = {**hollow_knight_randomize_options, **hollow_knight_skip_options} if __name__ == "__main__": import argparse diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 93eed1e1..10706df5 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -115,7 +115,10 @@ def fill_dungeons(world): def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items] + items = [item for dungeon in world.dungeons for item in dungeon.all_items] + for item in items: + item.world = world + return items def fill_dungeons_restrictive(world): """Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside.""" diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 183f224e..c06ff2a4 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -33,8 +33,9 @@ class HKLocation(Location): class HKItem(Item): game = "Hollow Knight" - def __init__(self, name, advancement, code, player: int = None): + def __init__(self, name, advancement, code, type, player: int = None): super(HKItem, self).__init__(name, advancement, code, player) + self.type = type def gen_hollow(world: MultiWorld, player: int): @@ -48,12 +49,33 @@ def gen_hollow(world: MultiWorld, player: int): def link_regions(world: MultiWorld, player: int): world.get_entrance('Hollow Nest S&Q', player).connect(world.get_region('Hollow Nest', player)) +not_shufflable_types = {"Essence_Boss"} + +option_to_type_lookup = { + "Root": "RandomizeWhisperingRoots", + "Dreamer": "RandomizeDreamers", + "Geo": "RandomizeGeoChests", + "Skill": "RandomizeSkills", + "Map": "RandomizeMaps", + "Relic": "RandomizeRelics", + "Charm": "RandomizeCharms", + "Notch": "RandomizeCharmNotches", + "Key": "RandomizeKeys", + "Stag": "RandomizeStags", + "Flame": "RandomizeFlames", + "Grub": "RandomizeGrubs", + "Cocoon": "RandomizeLifebloodCocoons", + "Mask": "RandomizeMaskShards", + "Ore": "RandomizePaleOre", + "Egg": "RandomizeRancidEggs", + "Vessel": "RandomizeVesselFragments", +} def gen_items(world: MultiWorld, player: int): pool = [] for item_name, item_data in item_table.items(): - item = HKItem(item_name, item_data.advancement, item_data.id, player=player) + item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=player) if item_data.type == "Event": event_location = world.get_location(item_name, player) @@ -62,10 +84,35 @@ def gen_items(world: MultiWorld, player: int): event_location.locked = True if item.name == "King's_Pass": world.push_precollected(item) + elif item_data.type == "Cursed": + if world.CURSED[player]: + raise Exception("Cursed is not implemented yet.") + # implement toss_junk for HK first + else: + event_location = world.get_location(item_name, player) + world.push_item(event_location, item) + event_location.event = True + event_location.locked = True + world.push_precollected(item) + elif item_data.type == "Fake": pass + elif item_data.type in not_shufflable_types: + location = world.get_location(item_name, player) + world.push_item(location, item) + location.event = item.advancement + location.locked = True else: - pool.append(item) + target = option_to_type_lookup[item.type] + shuffle_it = getattr(world, target) + if shuffle_it[player]: + pool.append(item) + else: + location = world.get_location(item_name, player) + world.push_item(location, item) + location.event = item.advancement + location.locked = True + logger.debug(f"Placed {item_name} to vanilla for player {player}") world.itempool += pool