1060 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			1060 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | import io | ||
|  | import hashlib | ||
|  | import logging | ||
|  | import os | ||
|  | import struct | ||
|  | import random | ||
|  | from collections import OrderedDict | ||
|  | import urllib.request | ||
|  | from urllib.error import URLError, HTTPError | ||
|  | import json | ||
|  | from enum import Enum | ||
|  | 
 | ||
|  | from .HintList import getHint, getHintGroup, Hint, hintExclusions | ||
|  | from .Messages import update_message_by_id | ||
|  | from .TextBox import line_wrap | ||
|  | from .Utils import data_path, read_json | ||
|  | 
 | ||
|  | 
 | ||
|  | bingoBottlesForHints = ( | ||
|  |     "Bottle", "Bottle with Red Potion","Bottle with Green Potion", "Bottle with Blue Potion", | ||
|  |     "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", | ||
|  |     "Bottle with Big Poe", "Bottle with Poe", | ||
|  | ) | ||
|  | 
 | ||
|  | defaultHintDists = [ | ||
|  |     'balanced.json', 'bingo.json', 'ddr.json', 'scrubs.json', 'strong.json', 'tournament.json', 'useless.json', 'very_strong.json' | ||
|  | ] | ||
|  | 
 | ||
|  | class RegionRestriction(Enum): | ||
|  |     NONE = 0, | ||
|  |     DUNGEON = 1, | ||
|  |     OVERWORLD = 2, | ||
|  | 
 | ||
|  | 
 | ||
|  | class GossipStone(): | ||
|  |     def __init__(self, name, location): | ||
|  |         self.name = name | ||
|  |         self.location = location | ||
|  |         self.reachable = True | ||
|  | 
 | ||
|  | 
 | ||
|  | class GossipText(): | ||
|  |     def __init__(self, text, colors=None, prefix="They say that "): | ||
|  |         text = prefix + text | ||
|  |         text = text[:1].upper() + text[1:] | ||
|  |         self.text = text | ||
|  |         self.colors = colors | ||
|  | 
 | ||
|  | 
 | ||
|  |     def to_json(self): | ||
|  |         return {'text': self.text, 'colors': self.colors} | ||
|  | 
 | ||
|  | 
 | ||
|  |     def __str__(self): | ||
|  |         return get_raw_text(line_wrap(colorText(self))) | ||
|  | 
 | ||
|  | #   Abbreviations | ||
|  | #       DMC     Death Mountain Crater | ||
|  | #       DMT     Death Mountain Trail | ||
|  | #       GC      Goron City | ||
|  | #       GV      Gerudo Valley | ||
|  | #       HC      Hyrule Castle | ||
|  | #       HF      Hyrule Field | ||
|  | #       KF      Kokiri Forest | ||
|  | #       LH      Lake Hylia | ||
|  | #       LW      Lost Woods | ||
|  | #       SFM     Sacred Forest Meadow | ||
|  | #       ToT     Temple of Time | ||
|  | #       ZD      Zora's Domain | ||
|  | #       ZF      Zora's Fountain | ||
|  | #       ZR      Zora's River | ||
|  | 
 | ||
|  | gossipLocations = { | ||
|  |     0x0405: GossipStone('DMC (Bombable Wall)',              'DMC Gossip Stone'), | ||
|  |     0x0404: GossipStone('DMT (Biggoron)',                   'DMT Gossip Stone'), | ||
|  |     0x041A: GossipStone('Colossus (Spirit Temple)',         'Colossus Gossip Stone'), | ||
|  |     0x0414: GossipStone('Dodongos Cavern (Bombable Wall)',  'Dodongos Cavern Gossip Stone'), | ||
|  |     0x0411: GossipStone('GV (Waterfall)',                   'GV Gossip Stone'), | ||
|  |     0x0415: GossipStone('GC (Maze)',                        'GC Maze Gossip Stone'), | ||
|  |     0x0419: GossipStone('GC (Medigoron)',                   'GC Medigoron Gossip Stone'), | ||
|  |     0x040A: GossipStone('Graveyard (Shadow Temple)',        'Graveyard Gossip Stone'), | ||
|  |     0x0412: GossipStone('HC (Malon)',                       'HC Malon Gossip Stone'), | ||
|  |     0x040B: GossipStone('HC (Rock Wall)',                   'HC Rock Wall Gossip Stone'), | ||
|  |     0x0413: GossipStone('HC (Storms Grotto)',               'HC Storms Grotto Gossip Stone'), | ||
|  |     0x041F: GossipStone('KF (Deku Tree Left)',              'KF Deku Tree Gossip Stone (Left)'), | ||
|  |     0x0420: GossipStone('KF (Deku Tree Right)',             'KF Deku Tree Gossip Stone (Right)'), | ||
|  |     0x041E: GossipStone('KF (Outside Storms)',              'KF Gossip Stone'), | ||
|  |     0x0403: GossipStone('LH (Lab)',                         'LH Lab Gossip Stone'), | ||
|  |     0x040F: GossipStone('LH (Southeast Corner)',            'LH Gossip Stone (Southeast)'), | ||
|  |     0x0408: GossipStone('LH (Southwest Corner)',            'LH Gossip Stone (Southwest)'), | ||
|  |     0x041D: GossipStone('LW (Bridge)',                      'LW Gossip Stone'), | ||
|  |     0x0416: GossipStone('SFM (Maze Lower)',                 'SFM Maze Gossip Stone (Lower)'), | ||
|  |     0x0417: GossipStone('SFM (Maze Upper)',                 'SFM Maze Gossip Stone (Upper)'), | ||
|  |     0x041C: GossipStone('SFM (Saria)',                      'SFM Saria Gossip Stone'), | ||
|  |     0x0406: GossipStone('ToT (Left)',                       'ToT Gossip Stone (Left)'), | ||
|  |     0x0407: GossipStone('ToT (Left-Center)',                'ToT Gossip Stone (Left-Center)'), | ||
|  |     0x0410: GossipStone('ToT (Right)',                      'ToT Gossip Stone (Right)'), | ||
|  |     0x040E: GossipStone('ToT (Right-Center)',               'ToT Gossip Stone (Right-Center)'), | ||
|  |     0x0409: GossipStone('ZD (Mweep)',                       'ZD Gossip Stone'), | ||
|  |     0x0401: GossipStone('ZF (Fairy)',                       'ZF Fairy Gossip Stone'), | ||
|  |     0x0402: GossipStone('ZF (Jabu)',                        'ZF Jabu Gossip Stone'), | ||
|  |     0x040D: GossipStone('ZR (Near Grottos)',                'ZR Near Grottos Gossip Stone'), | ||
|  |     0x040C: GossipStone('ZR (Near Domain)',                 'ZR Near Domain Gossip Stone'), | ||
|  |     0x041B: GossipStone('HF (Cow Grotto)',                  'HF Cow Grotto Gossip Stone'), | ||
|  | 
 | ||
|  |     0x0430: GossipStone('HF (Near Market Grotto)',          'HF Near Market Grotto Gossip Stone'), | ||
|  |     0x0432: GossipStone('HF (Southeast Grotto)',            'HF Southeast Grotto Gossip Stone'), | ||
|  |     0x0433: GossipStone('HF (Open Grotto)',                 'HF Open Grotto Gossip Stone'), | ||
|  |     0x0438: GossipStone('Kak (Open Grotto)',                'Kak Open Grotto Gossip Stone'), | ||
|  |     0x0439: GossipStone('ZR (Open Grotto)',                 'ZR Open Grotto Gossip Stone'), | ||
|  |     0x043C: GossipStone('KF (Storms Grotto)',               'KF Storms Grotto Gossip Stone'), | ||
|  |     0x0444: GossipStone('LW (Near Shortcuts Grotto)',       'LW Near Shortcuts Grotto Gossip Stone'), | ||
|  |     0x0447: GossipStone('DMT (Storms Grotto)',              'DMT Storms Grotto Gossip Stone'), | ||
|  |     0x044A: GossipStone('DMC (Upper Grotto)',               'DMC Upper Grotto Gossip Stone'), | ||
|  | } | ||
|  | 
 | ||
|  | gossipLocations_reversemap = { | ||
|  |     stone.name : stone_id for stone_id, stone in gossipLocations.items() | ||
|  | } | ||
|  | 
 | ||
|  | def getItemGenericName(item): | ||
|  |     if item.game != "Ocarina of Time": | ||
|  |         return item.name | ||
|  |     elif item.dungeonitem: | ||
|  |         return item.type | ||
|  |     else: | ||
|  |         return item.name | ||
|  | 
 | ||
|  | 
 | ||
|  | def isRestrictedDungeonItem(dungeon, item): | ||
|  |     if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon': | ||
|  |         return item in dungeon.dungeon_items | ||
|  |     if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon': | ||
|  |         return item in dungeon.small_keys | ||
|  |     if item.type == 'BossKey' and dungeon.world.shuffle_bosskeys == 'dungeon': | ||
|  |         return item in dungeon.boss_key | ||
|  |     if item.type == 'GanonBossKey' and dungeon.world.shuffle_ganon_bosskey == 'dungeon': | ||
|  |         return item in dungeon.boss_key | ||
|  |     return False | ||
|  | 
 | ||
|  | 
 | ||
|  | # Attach a player name to the item or location text. | ||
|  | # If the associated player of the item/location and the world are the same, does nothing. | ||
|  | # Otherwise, attaches the object's player's name to the string. | ||
|  | def attach_name(text, hinted_object, world): | ||
|  |     if hinted_object.player == world.player: | ||
|  |         return text | ||
|  |     return f"{text} for {world.world.get_player_name(hinted_object.player)}" | ||
|  | 
 | ||
|  | 
 | ||
|  | def add_hint(world, groups, gossip_text, count, location=None, force_reachable=False): | ||
|  |     world.hint_rng.shuffle(groups) | ||
|  |     skipped_groups = [] | ||
|  |     duplicates = [] | ||
|  |     first = True | ||
|  |     success = True | ||
|  |     # early failure if not enough | ||
|  |     if len(groups) < int(count): | ||
|  |         return False | ||
|  |     # Randomly round up, if we have enough groups left | ||
|  |     total = int(world.hint_rng.random() + count) if len(groups) > count else int(count) | ||
|  |     while total: | ||
|  |         if groups: | ||
|  |             group = groups.pop(0) | ||
|  | 
 | ||
|  |             if any(map(lambda id: gossipLocations[id].reachable, group)): | ||
|  |                 stone_names = [gossipLocations[id].location for id in group] | ||
|  |                 # stone_locations = [world.get_location(stone_name) for stone_name in stone_names] | ||
|  |                 # Taking out all checks on gossip stone reachability and hint logic | ||
|  |                 if not first or True: # or any(map(lambda stone_location: can_reach_hint(worlds, stone_location, location), stone_locations)): | ||
|  |                     # if first and location: | ||
|  |                     #     # just name the event item after the gossip stone directly | ||
|  |                     #     event_item = None | ||
|  |                     #     for i, stone_name in enumerate(stone_names): | ||
|  |                     #         # place the same event item in each location in the group | ||
|  |                     #         if event_item is None: | ||
|  |                     #             event_item = MakeEventItem(stone_name, stone_locations[i], event_item) | ||
|  |                     #         else: | ||
|  |                     #             MakeEventItem(stone_name, stone_locations[i], event_item) | ||
|  | 
 | ||
|  |                     #     # This mostly guarantees that we don't lock the player out of an item hint | ||
|  |                     #     # by establishing a (hint -> item) -> hint -> item -> (first hint) loop | ||
|  |                     #     location.add_rule(world.parser.parse_rule(repr(event_item.name))) | ||
|  | 
 | ||
|  |                     total -= 1 | ||
|  |                     first = False | ||
|  |                     for id in group: | ||
|  |                         world.gossip_hints[id] = gossip_text | ||
|  |                     # Immediately start choosing duplicates from stones we passed up earlier | ||
|  |                     while duplicates and total: | ||
|  |                         group = duplicates.pop(0) | ||
|  |                         total -= 1 | ||
|  |                         for id in group: | ||
|  |                             world.gossip_hints[id] = gossip_text | ||
|  |                 else: | ||
|  |                     # Temporarily skip this stone but consider it for duplicates | ||
|  |                     duplicates.append(group) | ||
|  |             else: | ||
|  |                 if not force_reachable: | ||
|  |                     # The stones are not readable at all in logic, so we ignore any kind of logic here | ||
|  |                     if not first: | ||
|  |                         total -= 1 | ||
|  |                         for id in group: | ||
|  |                             world.gossip_hints[id] = gossip_text | ||
|  |                     else: | ||
|  |                         # Temporarily skip this stone but consider it for duplicates | ||
|  |                         duplicates.append(group) | ||
|  |                 else: | ||
|  |                     # If flagged to guarantee reachable, then skip | ||
|  |                     # If no stones are reachable, then this will place nothing | ||
|  |                     skipped_groups.append(group) | ||
|  |         else: | ||
|  |             # Out of groups | ||
|  |             if not force_reachable and len(duplicates) >= total: | ||
|  |                 # Didn't find any appropriate stones for this hint, but maybe enough completely unreachable ones. | ||
|  |                 # We'd rather not use reachable stones for this. | ||
|  |                 unr = [group for group in duplicates if all(map(lambda id: not gossipLocations[id].reachable, group))] | ||
|  |                 if len(unr) >= total: | ||
|  |                     duplicates = [group for group in duplicates if group not in unr[:total]] | ||
|  |                     for group in unr[:total]: | ||
|  |                         for id in group: | ||
|  |                             world.gossip_hints[id] = gossip_text | ||
|  |                     # Success | ||
|  |                     break | ||
|  |             # Failure | ||
|  |             success = False | ||
|  |             break | ||
|  |     groups.extend(duplicates) | ||
|  |     groups.extend(skipped_groups) | ||
|  |     return success | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | def writeGossipStoneHints(world, messages): | ||
|  |     for id, gossip_text in world.gossip_hints.items(): | ||
|  |         update_message_by_id(messages, id, str(gossip_text), 0x23) | ||
|  | 
 | ||
|  | 
 | ||
|  | def filterTrailingSpace(text): | ||
|  |     if text.endswith('& '): | ||
|  |         return text[:-1] | ||
|  |     else: | ||
|  |         return text | ||
|  | 
 | ||
|  | 
 | ||
|  | hintPrefixes = [ | ||
|  |     'a few ', | ||
|  |     'some ', | ||
|  |     'plenty of ', | ||
|  |     'a ', | ||
|  |     'an ', | ||
|  |     'the ', | ||
|  |     '', | ||
|  | ] | ||
|  | 
 | ||
|  | def getSimpleHintNoPrefix(item): | ||
|  |     hint = getHint(item.name, True).text | ||
|  | 
 | ||
|  |     for prefix in hintPrefixes: | ||
|  |         if hint.startswith(prefix): | ||
|  |             # return without the prefix | ||
|  |             return hint[len(prefix):] | ||
|  | 
 | ||
|  |     # no prefex | ||
|  |     return hint | ||
|  | 
 | ||
|  | 
 | ||
|  | def colorText(gossip_text): | ||
|  |     colorMap = { | ||
|  |         'White':      '\x40', | ||
|  |         'Red':        '\x41', | ||
|  |         'Green':      '\x42', | ||
|  |         'Blue':       '\x43', | ||
|  |         'Light Blue': '\x44', | ||
|  |         'Pink':       '\x45', | ||
|  |         'Yellow':     '\x46', | ||
|  |         'Black':      '\x47', | ||
|  |     } | ||
|  | 
 | ||
|  |     text = gossip_text.text | ||
|  |     colors = list(gossip_text.colors) if gossip_text.colors is not None else [] | ||
|  |     color = 'White' | ||
|  | 
 | ||
|  |     while '#' in text: | ||
|  |         splitText = text.split('#', 2) | ||
|  |         if len(colors) > 0: | ||
|  |             color = colors.pop() | ||
|  | 
 | ||
|  |         for prefix in hintPrefixes: | ||
|  |             if splitText[1].startswith(prefix): | ||
|  |                 splitText[0] += splitText[1][:len(prefix)] | ||
|  |                 splitText[1] = splitText[1][len(prefix):] | ||
|  |                 break | ||
|  | 
 | ||
|  |         splitText[1] = '\x05' + colorMap[color] + splitText[1] + '\x05\x40' | ||
|  |         text = ''.join(splitText) | ||
|  | 
 | ||
|  |     return text | ||
|  | 
 | ||
|  | 
 | ||
|  | class HintAreaNotFound(RuntimeError): | ||
|  |     pass | ||
|  | 
 | ||
|  | 
 | ||
|  | # Peforms a breadth first search to find the closest hint area from a given spot (location or entrance) | ||
|  | # May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area | ||
|  | # Returns the name of the location if the spot is not in OoT | ||
|  | def get_hint_area(spot): | ||
|  |     if spot.game == 'Ocarina of Time': | ||
|  |         already_checked = [] | ||
|  |         spot_queue = [spot] | ||
|  | 
 | ||
|  |         while spot_queue: | ||
|  |             current_spot = spot_queue.pop(0) | ||
|  |             already_checked.append(current_spot) | ||
|  | 
 | ||
|  |             parent_region = current_spot.parent_region | ||
|  |          | ||
|  |             if parent_region.dungeon: | ||
|  |                 return parent_region.dungeon.hint_text | ||
|  |             elif parent_region.hint_text and (spot.parent_region.name == 'Root' or parent_region.name != 'Root'): | ||
|  |                 return parent_region.hint_text | ||
|  | 
 | ||
|  |             spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances))) | ||
|  | 
 | ||
|  |         raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id)) | ||
|  |     else: | ||
|  |         return spot.name | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_woth_hint(world, checked): | ||
|  |     locations = world.required_locations | ||
|  |     locations = list(filter(lambda location: | ||
|  |         location.name not in checked[location.player] | ||
|  |         and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and location.parent_region.dungeon) | ||
|  |         and location.name not in world.hint_exclusions | ||
|  |         and location.name not in world.hint_type_overrides['woth'] | ||
|  |         and location.item.name not in world.item_hint_type_overrides['woth'], | ||
|  |         locations)) | ||
|  | 
 | ||
|  |     if not locations: | ||
|  |         return None | ||
|  | 
 | ||
|  |     location = world.hint_rng.choice(locations) | ||
|  |     checked[location.player].add(location.name) | ||
|  | 
 | ||
|  |     if location.parent_region.dungeon: | ||
|  |         world.woth_dungeon += 1 | ||
|  |         location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text | ||
|  |     else: | ||
|  |         location_text = get_hint_area(location) | ||
|  | 
 | ||
|  |     if world.triforce_hunt: | ||
|  |         return (GossipText('#%s# is on the path of gold.' % location_text, ['Light Blue']), location) | ||
|  |     else: | ||
|  |         return (GossipText('#%s# is on the way of the hero.' % location_text, ['Light Blue']), location) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_barren_hint(world, checked): | ||
|  |     if not hasattr(world, 'get_barren_hint_prev'): | ||
|  |         world.get_barren_hint_prev = RegionRestriction.NONE | ||
|  | 
 | ||
|  |     areas = list(filter(lambda area: | ||
|  |         area not in checked[world.player] | ||
|  |         and area not in world.hint_type_overrides['barren'] | ||
|  |         and not (world.barren_dungeon >= world.hint_dist_user['dungeons_barren_limit'] and world.empty_areas[area]['dungeon']), | ||
|  |         world.empty_areas.keys())) | ||
|  | 
 | ||
|  |     if not areas: | ||
|  |         return None | ||
|  | 
 | ||
|  |     # Randomly choose between overworld or dungeon | ||
|  |     dungeon_areas = list(filter(lambda area: world.empty_areas[area]['dungeon'], areas)) | ||
|  |     overworld_areas = list(filter(lambda area: not world.empty_areas[area]['dungeon'], areas)) | ||
|  |     if not dungeon_areas: | ||
|  |         # no dungeons left, default to overworld | ||
|  |         world.get_barren_hint_prev = RegionRestriction.OVERWORLD | ||
|  |     elif not overworld_areas: | ||
|  |         # no overworld left, default to dungeons | ||
|  |         world.get_barren_hint_prev = RegionRestriction.DUNGEON | ||
|  |     else: | ||
|  |         if world.get_barren_hint_prev == RegionRestriction.NONE: | ||
|  |             # 50/50 draw on the first hint | ||
|  |             world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.5, 0.5])[0] | ||
|  |         elif world.get_barren_hint_prev == RegionRestriction.DUNGEON: | ||
|  |             # weights 75% against drawing dungeon again | ||
|  |             world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.25, 0.75])[0] | ||
|  |         elif world.get_barren_hint_prev == RegionRestriction.OVERWORLD: | ||
|  |             # weights 75% against drawing overworld again | ||
|  |             world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.75, 0.25])[0] | ||
|  | 
 | ||
|  |     if world.get_barren_hint_prev == RegionRestriction.DUNGEON: | ||
|  |         areas = dungeon_areas | ||
|  |     else: | ||
|  |         areas = overworld_areas | ||
|  |     if not areas: | ||
|  |         return None | ||
|  | 
 | ||
|  |     area_weights = [world.empty_areas[area]['weight'] for area in areas] | ||
|  | 
 | ||
|  |     area = world.hint_rng.choices(areas, weights=area_weights)[0] | ||
|  |     if world.empty_areas[area]['dungeon']: | ||
|  |         world.barren_dungeon += 1 | ||
|  | 
 | ||
|  |     checked[world.player].add(area) | ||
|  | 
 | ||
|  |     return (GossipText("plundering #%s# is a foolish choice." % area, ['Pink']), None) | ||
|  | 
 | ||
|  | 
 | ||
|  | def is_not_checked(location, checked): | ||
|  |     return not (location.name in checked[location.player] or get_hint_area(location) in checked) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_good_item_hint(world, checked): | ||
|  |     locations = list(filter(lambda location: | ||
|  |         is_not_checked(location, checked) | ||
|  |         and not location.locked | ||
|  |         and location.name not in world.hint_exclusions | ||
|  |         and location.name not in world.hint_type_overrides['item'] | ||
|  |         and location.item.name not in world.item_hint_type_overrides['item'], | ||
|  |         world.major_item_locations)) | ||
|  |     if not locations: | ||
|  |         return None | ||
|  | 
 | ||
|  |     location = world.hint_rng.choice(locations) | ||
|  |     checked[location.player].add(location.name) | ||
|  | 
 | ||
|  |     item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text | ||
|  |     if location.parent_region.dungeon: | ||
|  |         location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text | ||
|  |         return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),  | ||
|  |             ['Green', 'Red']), location) | ||
|  |     else: | ||
|  |         location_text = get_hint_area(location) | ||
|  |         return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),  | ||
|  |             ['Red', 'Green']), location) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_specific_item_hint(world, checked): | ||
|  |     if len(world.named_item_pool) == 0: | ||
|  |         logger = logging.getLogger('') | ||
|  |         logger.info("Named item hint requested, but pool is empty.") | ||
|  |         return None   | ||
|  |     while True: | ||
|  |         itemname = world.named_item_pool.pop(0) | ||
|  |         if itemname == "Bottle" and world.hint_dist == "bingo": | ||
|  |             locations = [ | ||
|  |                 location for location in world.world.get_filled_locations() | ||
|  |                 if (is_not_checked(location, checked) | ||
|  |                     and location.name not in world.hint_exclusions | ||
|  |                     and location.item.name in bingoBottlesForHints | ||
|  |                     and not location.locked | ||
|  |                     and location.name not in world.hint_type_overrides['named-item']) | ||
|  |             ] | ||
|  |         else: | ||
|  |             locations = [ | ||
|  |                 location for location in world.world.get_filled_locations() | ||
|  |                 if (is_not_checked(location, checked) | ||
|  |                     and location.name not in world.hint_exclusions | ||
|  |                     and location.item.name == itemname | ||
|  |                     and not location.locked | ||
|  |                     and location.name not in world.hint_type_overrides['named-item']) | ||
|  |             ] | ||
|  |         if len(locations) > 0: | ||
|  |             break | ||
|  |         if len(world.named_item_pool) == 0: | ||
|  |             return None | ||
|  | 
 | ||
|  |     location = world.hint_rng.choice(locations) | ||
|  |     checked[location.player].add(location.name) | ||
|  |     item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text | ||
|  |      | ||
|  |     if location.parent_region.dungeon: | ||
|  |         location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text | ||
|  |         if world.hint_dist_user.get('vague_named_items', False): | ||
|  |             return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) | ||
|  |         else: | ||
|  |             return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),  | ||
|  |                 ['Green', 'Red']), location) | ||
|  |     else: | ||
|  |         location_text = get_hint_area(location) | ||
|  |         if world.hint_dist_user.get('vague_named_items', False): | ||
|  |             return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location) | ||
|  |         else: | ||
|  |             return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),  | ||
|  |                 ['Red', 'Green']), location) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_random_location_hint(world, checked): | ||
|  |     locations = list(filter(lambda location: | ||
|  |         is_not_checked(location, checked) | ||
|  |         and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward') | ||
|  |         # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items | ||
|  |         and not location.locked | ||
|  |         and location.name not in world.hint_exclusions | ||
|  |         and location.name not in world.hint_type_overrides['item'] | ||
|  |         and location.item.name not in world.item_hint_type_overrides['item'], | ||
|  |         world.world.get_filled_locations(world.player))) | ||
|  |     if not locations: | ||
|  |         return None | ||
|  | 
 | ||
|  |     location = world.hint_rng.choice(locations) | ||
|  |     checked[location.player].add(location.name) | ||
|  |     dungeon = location.parent_region.dungeon | ||
|  | 
 | ||
|  |     item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text | ||
|  |     if dungeon: | ||
|  |         location_text = getHint(dungeon.name, world.clearer_hints).text | ||
|  |         return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),  | ||
|  |             ['Green', 'Red']), location) | ||
|  |     else: | ||
|  |         location_text = get_hint_area(location) | ||
|  |         return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),  | ||
|  |             ['Red', 'Green']), location) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_specific_hint(world, checked, type): | ||
|  |     hintGroup = getHintGroup(type, world) | ||
|  |     hintGroup = list(filter(lambda hint: is_not_checked(world.get_location(hint.name), checked), hintGroup)) | ||
|  |     if not hintGroup: | ||
|  |         return None | ||
|  | 
 | ||
|  |     hint = world.hint_rng.choice(hintGroup) | ||
|  |     location = world.get_location(hint.name) | ||
|  |     checked[location.player].add(location.name) | ||
|  | 
 | ||
|  |     if location.name in world.hint_text_overrides: | ||
|  |         location_text = world.hint_text_overrides[location.name] | ||
|  |     else: | ||
|  |         location_text = hint.text | ||
|  |     if '#' not in location_text: | ||
|  |         location_text = '#%s#' % location_text | ||
|  |     item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text | ||
|  | 
 | ||
|  |     return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),  | ||
|  |         ['Green', 'Red']), location) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_sometimes_hint(world, checked): | ||
|  |     return get_specific_hint(world, checked, 'sometimes') | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_song_hint(world, checked): | ||
|  |     return get_specific_hint(world, checked, 'song') | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_overworld_hint(world, checked): | ||
|  |     return get_specific_hint(world, checked, 'overworld') | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_dungeon_hint(world, checked): | ||
|  |     return get_specific_hint(world, checked, 'dungeon') | ||
|  | 
 | ||
|  | 
 | ||
|  | # probably broken | ||
|  | def get_entrance_hint(world, checked): | ||
|  |     if not world.entrance_shuffle: | ||
|  |         return None | ||
|  | 
 | ||
|  |     entrance_hints = list(filter(lambda hint: hint.name not in checked[world.player], getHintGroup('entrance', world))) | ||
|  |     shuffled_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).shuffled, entrance_hints)) | ||
|  | 
 | ||
|  |     regions_with_hint = [hint.name for hint in getHintGroup('region', world)] | ||
|  |     valid_entrance_hints = list(filter(lambda entrance_hint: | ||
|  |                                        (world.get_entrance(entrance_hint.name).connected_region.name in regions_with_hint or | ||
|  |                                         world.get_entrance(entrance_hint.name).connected_region.dungeon), shuffled_entrance_hints)) | ||
|  | 
 | ||
|  |     if not valid_entrance_hints: | ||
|  |         return None | ||
|  | 
 | ||
|  |     entrance_hint = world.hint_rng.choice(valid_entrance_hints) | ||
|  |     entrance = world.get_entrance(entrance_hint.name) | ||
|  |     checked[world.player].add(entrance.name) | ||
|  | 
 | ||
|  |     entrance_text = entrance_hint.text | ||
|  | 
 | ||
|  |     if '#' not in entrance_text: | ||
|  |         entrance_text = '#%s#' % entrance_text | ||
|  | 
 | ||
|  |     connected_region = entrance.connected_region | ||
|  |     if connected_region.dungeon: | ||
|  |         region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text | ||
|  |     else: | ||
|  |         region_text = getHint(connected_region.name, world.clearer_hints).text | ||
|  | 
 | ||
|  |     if '#' not in region_text: | ||
|  |         region_text = '#%s#' % region_text | ||
|  | 
 | ||
|  |     return (GossipText('%s %s.' % (entrance_text, region_text), ['Light Blue', 'Green']), None) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_junk_hint(world, checked): | ||
|  |     hints = getHintGroup('junk', world) | ||
|  |     hints = list(filter(lambda hint: hint.name not in checked[world.player], hints)) | ||
|  |     if not hints: | ||
|  |         return None | ||
|  | 
 | ||
|  |     hint = world.hint_rng.choice(hints) | ||
|  |     checked[world.player].add(hint.name) | ||
|  | 
 | ||
|  |     return (GossipText(hint.text, prefix=''), None) | ||
|  | 
 | ||
|  | 
 | ||
|  | hint_func = { | ||
|  |     'trial':      lambda world, checked: None, | ||
|  |     'always':     lambda world, checked: None, | ||
|  |     'woth':             get_woth_hint, | ||
|  |     'barren':           get_barren_hint, | ||
|  |     'item':             get_good_item_hint, | ||
|  |     'sometimes':        get_sometimes_hint, | ||
|  |     'song':             get_song_hint, | ||
|  |     'overworld':        get_overworld_hint, | ||
|  |     'dungeon':          get_dungeon_hint, | ||
|  |     'entrance':         get_entrance_hint, | ||
|  |     'random':           get_random_location_hint, | ||
|  |     'junk':             get_junk_hint, | ||
|  |     'named-item':       get_specific_item_hint | ||
|  | } | ||
|  | 
 | ||
|  | hint_dist_keys = { | ||
|  |     'trial', | ||
|  |     'always', | ||
|  |     'woth', | ||
|  |     'barren', | ||
|  |     'item', | ||
|  |     'song', | ||
|  |     'overworld', | ||
|  |     'dungeon', | ||
|  |     'entrance', | ||
|  |     'sometimes', | ||
|  |     'random', | ||
|  |     'junk', | ||
|  |     'named-item' | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  | # builds out general hints based on location and whether an item is required or not | ||
|  | def buildWorldGossipHints(world, checkedLocations=None): | ||
|  |     # Seed the RNG | ||
|  |     world.hint_rng = world.world.slot_seeds[world.player] | ||
|  | 
 | ||
|  |     # rebuild hint exclusion list | ||
|  |     hintExclusions(world, clear_cache=True) | ||
|  | 
 | ||
|  |     world.barren_dungeon = 0 | ||
|  |     world.woth_dungeon = 0 | ||
|  | 
 | ||
|  |     if checkedLocations is None: | ||
|  |         checkedLocations = {player: set() for player in world.world.player_ids} | ||
|  | 
 | ||
|  |     # If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting | ||
|  |     # Can only be forced with vanilla bridge | ||
|  |     if world.bridge != 'vanilla': | ||
|  |         try: | ||
|  |             light_arrow_location = world.world.find_item("Light Arrows", world.player) | ||
|  |             checkedLocations[light_arrow_location.player].add(light_arrow_location.name) | ||
|  |         except StopIteration: # start with them | ||
|  |             pass | ||
|  | 
 | ||
|  |     stoneIDs = list(gossipLocations.keys()) | ||
|  | 
 | ||
|  |     if 'disabled' in world.hint_dist_user: | ||
|  |         for stone_name in world.hint_dist_user['disabled']: | ||
|  |             try: | ||
|  |                 stone_id = gossipLocations_reversemap[stone_name] | ||
|  |             except KeyError: | ||
|  |                 raise ValueError(f'Gossip stone location "{stone_name}" is not valid') | ||
|  |             stoneIDs.remove(stone_id) | ||
|  |             (gossip_text, _) = get_junk_hint(world, checkedLocations) | ||
|  |             world.gossip_hints[stone_id] = gossip_text | ||
|  | 
 | ||
|  |     stoneGroups = [] | ||
|  |     if 'groups' in world.hint_dist_user: | ||
|  |         for group_names in world.hint_dist_user['groups']: | ||
|  |             group = [] | ||
|  |             for stone_name in group_names: | ||
|  |                 try: | ||
|  |                     stone_id = gossipLocations_reversemap[stone_name] | ||
|  |                 except KeyError: | ||
|  |                     raise ValueError(f'Gossip stone location "{stone_name}" is not valid') | ||
|  | 
 | ||
|  |                 stoneIDs.remove(stone_id) | ||
|  |                 group.append(stone_id) | ||
|  |             stoneGroups.append(group) | ||
|  |     # put the remaining locations into singleton groups | ||
|  |     stoneGroups.extend([[id] for id in stoneIDs]) | ||
|  | 
 | ||
|  |     world.hint_rng.shuffle(stoneGroups) | ||
|  | 
 | ||
|  | 
 | ||
|  |     # Load hint distro from distribution file or pre-defined settings | ||
|  |     # | ||
|  |     # 'fixed' key is used to mimic the tournament distribution, creating a list of fixed hint types to fill | ||
|  |     # Once the fixed hint type list is exhausted, weighted random choices are taken like all non-tournament sets | ||
|  |     # This diverges from the tournament distribution where leftover stones are filled with sometimes hints (or random if no sometimes locations remain to be hinted) | ||
|  |     sorted_dist = {} | ||
|  |     type_count = 1 | ||
|  |     hint_dist = OrderedDict({}) | ||
|  |     fixed_hint_types = [] | ||
|  |     max_order = 0 | ||
|  |     for hint_type in world.hint_dist_user['distribution']: | ||
|  |         if world.hint_dist_user['distribution'][hint_type]['order'] > 0: | ||
|  |             hint_order = int(world.hint_dist_user['distribution'][hint_type]['order']) | ||
|  |             sorted_dist[hint_order] = hint_type | ||
|  |             if max_order < hint_order: | ||
|  |                 max_order = hint_order | ||
|  |             type_count = type_count + 1 | ||
|  |     if (type_count - 1) < max_order: | ||
|  |         raise Exception("There are gaps in the custom hint orders. Please revise your plando file to remove them.") | ||
|  |     for i in range(1, type_count): | ||
|  |         hint_type = sorted_dist[i] | ||
|  |         if world.hint_dist_user['distribution'][hint_type]['copies'] > 0: | ||
|  |             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']) | ||
|  |         hint_dist.move_to_end(hint_type) | ||
|  |         fixed_hint_types.extend([hint_type] * int(fixed_num)) | ||
|  | 
 | ||
|  |     hint_types, hint_prob = zip(*hint_dist.items()) | ||
|  |     hint_prob, _ = zip(*hint_prob) | ||
|  | 
 | ||
|  |     # Add required location hints, only if hint copies > 0 | ||
|  |     if hint_dist['always'][1] > 0: | ||
|  |         alwaysLocations = getHintGroup('always', world) | ||
|  |         for hint in alwaysLocations: | ||
|  |             location = world.get_location(hint.name) | ||
|  |             checkedLocations[location.player].add(hint.name) | ||
|  |             if location.item.name in bingoBottlesForHints and world.hint_dist == 'bingo': | ||
|  |                 always_item = 'Bottle' | ||
|  |             else: | ||
|  |                 always_item = location.item.name | ||
|  |             if always_item in world.named_item_pool: | ||
|  |                 world.named_item_pool.remove(always_item) | ||
|  | 
 | ||
|  |             if location.name in world.hint_text_overrides: | ||
|  |                 location_text = world.hint_text_overrides[location.name] | ||
|  |             else: | ||
|  |                 location_text = getHint(location.name, world.clearer_hints).text | ||
|  |             if '#' not in location_text: | ||
|  |                 location_text = '#%s#' % location_text | ||
|  |             item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text | ||
|  |             add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),  | ||
|  |                 ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True) | ||
|  |             logging.getLogger('').debug('Placed always hint for %s.', location.name) | ||
|  | 
 | ||
|  |     # Add trial hints, only if hint copies > 0 | ||
|  |     if hint_dist['trial'][1] > 0: | ||
|  |         if world.trials == 6: | ||
|  |             add_hint(world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True) | ||
|  |         elif world.trials == 0: | ||
|  |             add_hint(world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True) | ||
|  |         elif world.trials < 6 and world.trials > 3: | ||
|  |             for trial,skipped in world.skipped_trials.items(): | ||
|  |                 if skipped: | ||
|  |                     add_hint(world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True) | ||
|  |         elif world.trials <= 3 and world.trials > 0: | ||
|  |             for trial,skipped in world.skipped_trials.items(): | ||
|  |                 if not skipped: | ||
|  |                     add_hint(world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True) | ||
|  | 
 | ||
|  |     # Add user-specified hinted item locations if using a built-in hint distribution | ||
|  |     # Raise error if hint copies is zero | ||
|  |     if len(world.named_item_pool) > 0 and world.hint_dist_user['named_items_required']: | ||
|  |         if hint_dist['named-item'][1] == 0: | ||
|  |             raise Exception('User-provided item hints were requested, but copies per named-item hint is zero') | ||
|  |         else: | ||
|  |             for i in range(0, len(world.named_item_pool)): | ||
|  |                 hint = get_specific_item_hint(world, checkedLocations) | ||
|  |                 if hint == None: | ||
|  |                     raise Exception('No valid hints for user-provided item') | ||
|  |                 else: | ||
|  |                     gossip_text, location = hint | ||
|  |                     place_ok = add_hint(world, stoneGroups, gossip_text, hint_dist['named-item'][1], location) | ||
|  |                     if not place_ok: | ||
|  |                         raise Exception('Not enough gossip stones for user-provided item hints') | ||
|  |      | ||
|  |     # Shuffle named items hints | ||
|  |     # When all items are not required to be hinted, this allows for | ||
|  |     # opportunity-style hints to be drawn at random from the defined list. | ||
|  |     world.hint_rng.shuffle(world.named_item_pool) | ||
|  | 
 | ||
|  |     hint_types = list(hint_types) | ||
|  |     hint_prob  = list(hint_prob) | ||
|  |     hint_counts = {} | ||
|  | 
 | ||
|  |     custom_fixed = True | ||
|  |     while stoneGroups: | ||
|  |         if fixed_hint_types: | ||
|  |             hint_type = fixed_hint_types.pop(0) | ||
|  |             copies = hint_dist[hint_type][1] | ||
|  |             if copies > len(stoneGroups): | ||
|  |                 # Quiet to avoid leaking information. | ||
|  |                 logging.getLogger('').debug(f'Not enough gossip stone locations ({len(stoneGroups)} groups) for fixed hint type {hint_type} with {copies} copies, proceeding with available stones.') | ||
|  |                 copies = len(stoneGroups) | ||
|  |         else: | ||
|  |             custom_fixed = False | ||
|  |             # Make sure there are enough stones left for each hint type | ||
|  |             num_types = len(hint_types) | ||
|  |             hint_types = list(filter(lambda htype: hint_dist[htype][1] <= len(stoneGroups), hint_types)) | ||
|  |             new_num_types = len(hint_types) | ||
|  |             if new_num_types == 0: | ||
|  |                 raise Exception('Not enough gossip stone locations for remaining weighted hint types.') | ||
|  |             elif new_num_types < num_types: | ||
|  |                 hint_prob = [] | ||
|  |                 for htype in hint_types: | ||
|  |                     hint_prob.append(hint_dist[htype][0]) | ||
|  |             try: | ||
|  |                 # Weight the probabilities such that hints that are over the expected proportion | ||
|  |                 # will be drawn less, and hints that are under will be drawn more. | ||
|  |                 # This tightens the variance quite a bit. The variance can be adjusted via the power | ||
|  |                 weighted_hint_prob = [] | ||
|  |                 for w1_type, w1_prob in zip(hint_types, hint_prob): | ||
|  |                     p = w1_prob | ||
|  |                     if p != 0: # If the base prob is 0, then it's 0 | ||
|  |                         for w2_type, w2_prob in zip(hint_types, hint_prob): | ||
|  |                             if w2_prob != 0: # If the other prob is 0, then it has no effect | ||
|  |                                 # Raising this term to a power greater than 1 will decrease variance | ||
|  |                                 # Conversely, a power less than 1 will increase variance | ||
|  |                                 p = p * (((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1)) | ||
|  |                     weighted_hint_prob.append(p) | ||
|  | 
 | ||
|  |                 hint_type = world.hint_rng.choices(hint_types, weights=weighted_hint_prob)[0] | ||
|  |                 copies = hint_dist[hint_type][1] | ||
|  |             except IndexError: | ||
|  |                 raise Exception('Not enough valid hints to fill gossip stone locations.') | ||
|  | 
 | ||
|  |         hint = hint_func[hint_type](world, checkedLocations) | ||
|  | 
 | ||
|  |         if hint == None: | ||
|  |             index = hint_types.index(hint_type) | ||
|  |             hint_prob[index] = 0 | ||
|  |             # Zero out the probability in the base distribution in case the probability list is modified | ||
|  |             # to fit hint types in remaining gossip stones | ||
|  |             hint_dist[hint_type] = (0.0, copies) | ||
|  |         else: | ||
|  |             gossip_text, location = hint | ||
|  |             place_ok = add_hint(world, stoneGroups, gossip_text, copies, location) | ||
|  |             if place_ok: | ||
|  |                 hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1 | ||
|  |                 if location is None: | ||
|  |                     logging.getLogger('').debug('Placed %s hint.', hint_type) | ||
|  |                 else: | ||
|  |                     logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name) | ||
|  |             if not place_ok and custom_fixed: | ||
|  |                 logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name) | ||
|  |                 fixed_hint_types.insert(0, hint_type) | ||
|  | 
 | ||
|  | 
 | ||
|  | # builds text that is displayed at the temple of time altar for child and adult, rewards pulled based off of item in a fixed order. | ||
|  | def buildAltarHints(world, messages, include_rewards=True, include_wincons=True): | ||
|  |     # text that appears at altar as a child. | ||
|  |     child_text = '\x08' | ||
|  |     if include_rewards: | ||
|  |         bossRewardsSpiritualStones = [ | ||
|  |             ('Kokiri Emerald',   'Green'),  | ||
|  |             ('Goron Ruby',       'Red'),  | ||
|  |             ('Zora Sapphire',    'Blue'), | ||
|  |         ] | ||
|  |         child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04' | ||
|  |         for (reward, color) in bossRewardsSpiritualStones: | ||
|  |             child_text += buildBossString(reward, color, world) | ||
|  |     child_text += getHint('Child Altar Text End', world.clearer_hints).text | ||
|  |     child_text += '\x0B' | ||
|  |     update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20) | ||
|  | 
 | ||
|  |     # text that appears at altar as an adult. | ||
|  |     adult_text = '\x08' | ||
|  |     adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04' | ||
|  |     if include_rewards: | ||
|  |         bossRewardsMedallions = [ | ||
|  |             ('Light Medallion',  'Light Blue'), | ||
|  |             ('Forest Medallion', 'Green'), | ||
|  |             ('Fire Medallion',   'Red'), | ||
|  |             ('Water Medallion',  'Blue'), | ||
|  |             ('Shadow Medallion', 'Pink'), | ||
|  |             ('Spirit Medallion', 'Yellow'), | ||
|  |         ] | ||
|  |         for (reward, color) in bossRewardsMedallions: | ||
|  |             adult_text += buildBossString(reward, color, world) | ||
|  |     if include_wincons: | ||
|  |         adult_text += buildBridgeReqsString(world) | ||
|  |         adult_text += '\x04' | ||
|  |         adult_text += buildGanonBossKeyString(world) | ||
|  |     else: | ||
|  |         adult_text += getHint('Adult Altar Text End', world.clearer_hints).text | ||
|  |     adult_text += '\x0B' | ||
|  |     update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20) | ||
|  | 
 | ||
|  | 
 | ||
|  | # pulls text string from hintlist for reward after sending the location to hintlist. | ||
|  | def buildBossString(reward, color, world): | ||
|  |     for location in world.world.get_filled_locations(world.player): | ||
|  |         if location.item.name == reward: | ||
|  |             item_icon = chr(location.item.special['item_id']) | ||
|  |             location_text = getHint(location.name, world.clearer_hints).text | ||
|  |             return str(GossipText("\x08\x13%s%s" % (item_icon, location_text), [color], prefix='')) + '\x04' | ||
|  |     return '' | ||
|  | 
 | ||
|  | 
 | ||
|  | def buildBridgeReqsString(world): | ||
|  |     string = "\x13\x12" # Light Arrow Icon | ||
|  |     if world.bridge == 'open': | ||
|  |         string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells." | ||
|  |     else: | ||
|  |         item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text | ||
|  |         if world.bridge == 'medallions': | ||
|  |             item_req_string = str(world.bridge_medallions) + ' ' + item_req_string | ||
|  |         elif world.bridge == 'stones': | ||
|  |             item_req_string = str(world.bridge_stones) + ' ' + item_req_string | ||
|  |         elif world.bridge == 'dungeons': | ||
|  |             item_req_string = str(world.bridge_rewards) + ' ' + item_req_string | ||
|  |         elif world.bridge == 'tokens': | ||
|  |             item_req_string = str(world.bridge_tokens) + ' ' + item_req_string | ||
|  |         if '#' not in item_req_string: | ||
|  |             item_req_string = '#%s#' % item_req_string | ||
|  |         string += "The awakened ones will await for the Hero to collect %s." % item_req_string | ||
|  |     return str(GossipText(string, ['Green'], prefix='')) | ||
|  | 
 | ||
|  | 
 | ||
|  | def buildGanonBossKeyString(world): | ||
|  |     string = "\x13\x74" # Boss Key Icon | ||
|  |     if world.shuffle_ganon_bosskey == 'remove': | ||
|  |         string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#." | ||
|  |     else: | ||
|  |         if world.shuffle_ganon_bosskey == 'on_lacs': | ||
|  |             item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text | ||
|  |             if world.lacs_condition == 'medallions': | ||
|  |                 item_req_string = str(world.lacs_medallions) + ' ' + item_req_string | ||
|  |             elif world.lacs_condition == 'stones': | ||
|  |                 item_req_string = str(world.lacs_stones) + ' ' + item_req_string | ||
|  |             elif world.lacs_condition == 'dungeons': | ||
|  |                 item_req_string = str(world.lacs_rewards) + ' ' + item_req_string | ||
|  |             elif world.lacs_condition == 'tokens': | ||
|  |                 item_req_string = str(world.lacs_tokens) + ' ' + item_req_string | ||
|  |             if '#' not in item_req_string: | ||
|  |                 item_req_string = '#%s#' % item_req_string | ||
|  |             bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string | ||
|  |         else: | ||
|  |             bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text | ||
|  |         string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string | ||
|  |     return str(GossipText(string, ['Yellow'], prefix='')) | ||
|  | 
 | ||
|  | 
 | ||
|  | # fun new lines for Ganon during the final battle | ||
|  | def buildGanonText(world, messages): | ||
|  |     # empty now unused messages to make space for ganon lines | ||
|  |     update_message_by_id(messages, 0x70C8, " ") | ||
|  |     update_message_by_id(messages, 0x70C9, " ") | ||
|  |     update_message_by_id(messages, 0x70CA, " ") | ||
|  | 
 | ||
|  |     # lines before battle | ||
|  |     ganonLines = getHintGroup('ganonLine', world) | ||
|  |     world.hint_rng.shuffle(ganonLines) | ||
|  |     text = get_raw_text(ganonLines.pop().text) | ||
|  |     update_message_by_id(messages, 0x70CB, text) | ||
|  | 
 | ||
|  |     # light arrow hint or validation chest item | ||
|  |     if world.starting_items['Light Arrows'] > 0: | ||
|  |         text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text) | ||
|  |         text += "\x05\x42your pocket\x05\x40" | ||
|  |     else: | ||
|  |         try: | ||
|  |             find_light_arrows = world.world.find_item('Light Arrows', world.player) | ||
|  |             text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text) | ||
|  |             location = find_light_arrows | ||
|  |             location_hint = get_hint_area(location) | ||
|  |             if world.player != location.player: | ||
|  |                 text += "\x05\x42%s's\x05\x40 %s" % (world.world.get_player_name(location.player), get_raw_text(location_hint)) | ||
|  |             else: | ||
|  |                 location_hint = location_hint.replace('Ganon\'s Castle', 'my castle') | ||
|  |                 text += get_raw_text(location_hint) | ||
|  |         except StopIteration: | ||
|  |             text = get_raw_text(getHint('Validation Line', world.clearer_hints).text) | ||
|  |             for location in world.world.get_filled_locations(world.player): | ||
|  |                 if location.name == 'Ganons Tower Boss Key Chest': | ||
|  |                     text += get_raw_text(getHint(getItemGenericName(location.item), world.clearer_hints).text) | ||
|  |                     break | ||
|  |     text += '!' | ||
|  | 
 | ||
|  |     update_message_by_id(messages, 0x70CC, text) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_raw_text(string): | ||
|  |     text = '' | ||
|  |     for char in string: | ||
|  |         if char == '^': | ||
|  |             text += '\x04' # box break | ||
|  |         elif char == '&': | ||
|  |             text += '\x01' # new line | ||
|  |         elif char == '@': | ||
|  |             text += '\x0F' # print player name | ||
|  |         elif char == '#': | ||
|  |             text += '\x05\x40' # sets color to white | ||
|  |         else: | ||
|  |             text += char | ||
|  |     return text | ||
|  | 
 | ||
|  | 
 | ||
|  | def HintDistFiles(): | ||
|  |     return [os.path.join(data_path('Hints/'), d) for d in defaultHintDists] + [ | ||
|  |             os.path.join(data_path('Hints/'), d) | ||
|  |             for d in sorted(os.listdir(data_path('Hints/'))) | ||
|  |             if d.endswith('.json') and d not in defaultHintDists] | ||
|  | 
 | ||
|  | 
 | ||
|  | def HintDistList(): | ||
|  |     dists = {} | ||
|  |     for d in HintDistFiles(): | ||
|  |         dist = read_json(d) | ||
|  |         dist_name = dist['name'] | ||
|  |         gui_name = dist['gui_name'] | ||
|  |         dists.update({ dist_name: gui_name }) | ||
|  |     return dists | ||
|  | 
 | ||
|  | 
 | ||
|  | def HintDistTips(): | ||
|  |     tips = "" | ||
|  |     first_dist = True | ||
|  |     line_char_limit = 33 | ||
|  |     for d in HintDistFiles(): | ||
|  |         if not first_dist: | ||
|  |             tips = tips + "\n" | ||
|  |         else: | ||
|  |             first_dist = False | ||
|  |         dist = read_json(d) | ||
|  |         gui_name = dist['gui_name'] | ||
|  |         desc = dist['description'] | ||
|  |         i = 0 | ||
|  |         end_of_line = False | ||
|  |         tips = tips + "<b>" | ||
|  |         for c in gui_name: | ||
|  |             if c == " " and end_of_line: | ||
|  |                 tips = tips + "\n" | ||
|  |                 end_of_line = False | ||
|  |             else: | ||
|  |                 tips = tips + c | ||
|  |                 i = i + 1 | ||
|  |                 if i > line_char_limit: | ||
|  |                     end_of_line = True | ||
|  |                     i = 0 | ||
|  |         tips = tips + "</b>: " | ||
|  |         i = i + 2 | ||
|  |         for c in desc: | ||
|  |             if c == " " and end_of_line: | ||
|  |                 tips = tips + "\n" | ||
|  |                 end_of_line = False | ||
|  |             else: | ||
|  |                 tips = tips + c | ||
|  |                 i = i + 1 | ||
|  |                 if i > line_char_limit: | ||
|  |                     end_of_line = True | ||
|  |                     i = 0 | ||
|  |         tips = tips + "\n" | ||
|  |     return tips |