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
This commit is contained in:
espeon65536 2021-09-07 19:01:12 -05:00 committed by Fabian Dill
parent f39defbe06
commit 9dc3f3f38b
3 changed files with 72 additions and 26 deletions

View File

@ -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'])

View File

@ -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.

View File

@ -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