From 9dc3f3f38bce6ce612c5c18806adbd3ccf8609af Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Tue, 7 Sep 2021 19:01:12 -0500 Subject: [PATCH] Hint generation improvements Only generate the required hint data for a world based on its hint distribution Set various major items as nonprogression never_exclude based on settings --- worlds/oot/Hints.py | 1 - worlds/oot/Items.py | 7 +++- worlds/oot/__init__.py | 90 +++++++++++++++++++++++++++++++----------- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 6f22478c..9b79f135 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -714,7 +714,6 @@ def buildWorldGossipHints(world, checkedLocations=None): fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed'] hint_weight = world.hint_dist_user['distribution'][hint_type]['weight'] else: - logging.getLogger('').warning("Hint copies is zero for type %s. Assuming this hint type should be disabled.", hint_type) fixed_num = 0 hint_weight = 0 hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies']) diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index fcdb2237..b9121c72 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -22,9 +22,10 @@ def ap_id_to_oot_data(ap_id): class OOTItem(Item): game: str = "Ocarina of Time" - def __init__(self, name, player, data, event): + def __init__(self, name, player, data, event, force_not_advancement): (type, advancement, index, special) = data - adv = True if advancement else False # this looks silly but the table uses True, False, and None + # "advancement" is True, False or None; some items are not advancement based on settings + adv = bool(advancement) and not force_not_advancement super(OOTItem, self).__init__(name, adv, oot_data_to_ap_id(data, event), player) self.type = type self.index = index @@ -32,6 +33,8 @@ class OOTItem(Item): self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False + if force_not_advancement: + self.never_exclude = True # The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)" # This checks if the item it's looking for is a small key, using the small key property. diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 690bb364..a1b56849 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -244,6 +244,25 @@ class OOTWorld(World): self.always_hints = [hint.name for hint in getRequiredHints(self)] + # Determine items which are not considered advancement based on settings. They will never be excluded. + self.nonadvancement_items = {'Double Defense', 'Ice Arrows'} + if (self.damage_multiplier != 'ohko' and self.damage_multiplier != 'quadruple' and + self.shuffle_scrubs == 'off' and not self.shuffle_grotto_entrances): + # nayru's love may be required to prevent forced damage + self.nonadvancement_items.add('Nayrus Love') + if getattr(self, 'logic_grottos_without_agony', False) and self.hints != 'agony': + # Stone of Agony skippable if not used for hints or grottos + self.nonadvancement_items.add('Stone of Agony') + if (not self.shuffle_special_interior_entrances and not self.shuffle_overworld_entrances and + not self.warp_songs and not self.spawn_positions): + # Serenade and Prelude are never required unless one of those settings is enabled + self.nonadvancement_items.add('Serenade of Water') + self.nonadvancement_items.add('Prelude of Light') + if self.logic_rules == 'glitchless': + # Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless + self.nonadvancement_items.add('Biggoron Sword') + self.nonadvancement_items.add('Giants Knife') + def load_regions_from_json(self, file_path): region_json = read_json(file_path) @@ -390,8 +409,8 @@ class OOTWorld(World): def create_item(self, name: str): if name in item_table: - return OOTItem(name, self.player, item_table[name], False) - return OOTItem(name, self.player, ('Event', True, None, None), True) + return OOTItem(name, self.player, item_table[name], False, (name in self.nonadvancement_items)) + return OOTItem(name, self.player, ('Event', True, None, None), True, False) def make_event_item(self, name, location, item=None): if item is None: @@ -665,39 +684,61 @@ class OOTWorld(World): # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations. def stage_generate_output(world: MultiWorld, output_directory: str): - try: - items_by_region = {player: {} for player in world.get_game_players("Ocarina of Time") if world.worlds[player].hints != 'none'} - if items_by_region: - for player in items_by_region: - for r in world.worlds[player].regions: - items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0} - for d in world.worlds[player].dungeons: - items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0} - del (items_by_region[player]["Link's Pocket"]) - del (items_by_region[player][None]) + def hint_type_players(hint_type: str) -> set: + return {autoworld.player for autoworld in world.get_game_worlds("Ocarina of Time") + if autoworld.hints != 'none' and autoworld.hint_dist_user['distribution'][hint_type]['copies'] > 0} + try: + item_hint_players = hint_type_players('item') + barren_hint_players = hint_type_players('barren') + woth_hint_players = hint_type_players('woth') + + items_by_region = {} + for player in barren_hint_players: + items_by_region[player] = {} + for r in world.worlds[player].regions: + items_by_region[player][r.hint_text] = {'dungeon': False, 'weight': 0, 'is_barren': True} + for d in world.worlds[player].dungeons: + items_by_region[player][d.hint_text] = {'dungeon': True, 'weight': 0, 'is_barren': True} + del (items_by_region[player]["Link's Pocket"]) + del (items_by_region[player][None]) + + if item_hint_players: # loop once over all locations to gather major items. Check oot locations for barren/woth if needed for loc in world.get_locations(): player = loc.item.player autoworld = world.worlds[player] - if ((player in items_by_region and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item'])) - or (loc.player in items_by_region and loc.name in world.worlds[loc.player].added_hint_types['item'])): + if ((player in item_hint_players and (autoworld.is_major_item(loc.item) or loc.item.name in autoworld.item_added_hint_types['item'])) + or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])): autoworld.major_item_locations.append(loc) - if loc.game == "Ocarina of Time": - if loc.item.code and (not loc.locked or loc.item.type == 'Song'): # shuffled item + if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or loc.item.type == 'Song'): + if loc.player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[loc.player][hint_area]['weight'] += 1 if loc.item.advancement: - # Non-locked progression. Increment counter - items_by_region[loc.player][hint_area]['prog_items'] += 1 - # Skip item at location and see if game is still beatable + items_by_region[loc.player][hint_area]['is_barren'] = False + if loc.player in woth_hint_players and loc.item.advancement: + # Skip item at location and see if game is still beatable + state = CollectionState(world) + state.locations_checked.add(loc) + if not world.can_beat_game(state): + world.worlds[loc.player].required_locations.append(loc) + elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth + for player in (barren_hint_players | woth_hint_players): + for loc in world.worlds[player].get_locations(): + if loc.item.code and (not loc.locked or loc.item.type == 'Song'): + if player in barren_hint_players: + hint_area = get_hint_area(loc) + items_by_region[player][hint_area]['weight'] += 1 + if loc.item.advancement: + items_by_region[player][hint_area]['is_barren'] = False + if player in woth_hint_players and loc.item.advancement: state = CollectionState(world) state.locations_checked.add(loc) if not world.can_beat_game(state): - world.worlds[loc.player].required_locations.append(loc) - - for autoworld in world.get_game_worlds("Ocarina of Time"): - autoworld.empty_areas = {region: info for (region, info) in items_by_region[autoworld.player].items() if not info['prog_items']} + world.worlds[player].required_locations.append(loc) + for player in barren_hint_players: + world.worlds[player].empty_areas = {region: info for (region, info) in items_by_region[player].items() if info['is_barren']} except Exception as e: raise e finally: @@ -723,6 +764,9 @@ class OOTWorld(World): if item.type == 'Token': return self.bridge == 'tokens' or self.lacs_condition == 'tokens' + if item.name in self.nonadvancement_items: + return True + if item.type in ('Drop', 'Event', 'Shop', 'DungeonReward') or not item.advancement: return False