From 9f0a8e6d4874adf90603a6e911e4e038f86b3dd2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Nov 2021 22:57:54 +0100 Subject: [PATCH] LttP: add hint options "Vendors" and "Full" LttP: fix hint grammar if a Location isn't an ALttPLocation --- BaseClasses.py | 8 +- worlds/alttp/Options.py | 13 +- worlds/alttp/Rom.py | 326 ++++++++++++++++++++-------------------- 3 files changed, 183 insertions(+), 164 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d9cf9c61..01124129 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -255,14 +255,18 @@ class MultiWorld(): def get_items(self) -> list: return [loc.item for loc in self.get_filled_locations()] + self.itempool - def find_items(self, item, player: int) -> List[Location]: + def find_item_locations(self, item, player: int) -> List[Location]: return [location for location in self.get_locations() if - location.item is not None and location.item.name == item and location.item.player == player] + location.item and location.item.name == item and location.item.player == player] def find_item(self, item, player: int) -> Location: return next(location for location in self.get_locations() if location.item and location.item.name == item and location.item.player == player) + def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]: + return [location for location in self.get_locations() if + location.item and location.item.name in items and location.item.player == player] + def create_item(self, item_name: str, player: int) -> Item: return self.worlds[player].create_item(item_name) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 95705130..01eda055 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -145,10 +145,17 @@ class RestrictBossItem(Toggle): displayname = "Prevent Dungeon Item on Boss" -class Hints(DefaultOnToggle): - """Put item and entrance placement hints on telepathic tiles and some NPCs. - Additionally King Zora and Bottle Merchant say what they're selling.""" +class Hints(Choice): + """Vendors: King Zora and Bottle Merchant say what they're selling. + On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.""" displayname = "Hints" + option_off = 0 + option_vendors = 1 + option_on = 2 + option_full = 3 + default = 2 + alias_false = 0 + alias_true = 2 class EnemyShuffle(Toggle): diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 7235844a..c5d7680b 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -21,8 +21,7 @@ import concurrent.futures import bsdiff4 from typing import Optional -from BaseClasses import CollectionState, Region -from worlds.alttp.SubClasses import ALttPLocation +from BaseClasses import CollectionState, Region, Location from worlds.alttp.Shops import ShopType, ShopPriceType from worlds.alttp.Dungeons import dungeon_music_addresses from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address @@ -1535,7 +1534,7 @@ def patch_rom(world, rom, player, enemized): } def get_reveal_bytes(itemName): - locations = world.find_items(itemName, player) + locations = world.find_item_locations(itemName, player) if len(locations) < 1: return 0x0000 location = locations[0] @@ -2114,7 +2113,7 @@ def write_strings(rom, world, player): if dest.player != player: if ped_hint: hint += f" for {world.player_name[dest.player]}!" - elif type(dest) in [Region, ALttPLocation]: + elif isinstance(dest, (Region, Location)): hint += f" in {world.player_name[dest.player]}'s world" else: hint += f" for {world.player_name[dest.player]}" @@ -2130,171 +2129,180 @@ def write_strings(rom, world, player): vendor_location = world.get_location("Bottle Merchant", player) tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \ f"\n ≥ I want\n no way!\n{{CHOICE}}" - - tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' - hint_locations = HintLocations.copy() - local_random.shuffle(hint_locations) - all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] - local_random.shuffle(all_entrances) - - # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. - entrances_to_hint = {} - entrances_to_hint.update(InconvenientDungeonEntrances) - if world.shuffle_ganon: - if world.mode[player] == 'inverted': - entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) + if world.hints[player].value >= 2: + if world.hints[player] == "full": + tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!' else: - entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) - if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: - for entrance in all_entrances: - if entrance.name in entrances_to_hint: - this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( - entrance.connected_region) + '.' - tt[hint_locations.pop(0)] = this_hint - entrances_to_hint = {} - break - # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. - entrances_to_hint.update(InconvenientOtherEntrances) - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: - hint_count = 0 - elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: - hint_count = 2 - else: - hint_count = 4 - for entrance in all_entrances: - if entrance.name in entrances_to_hint: - if hint_count: - this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( - entrance.connected_region) + '.' - tt[hint_locations.pop(0)] = this_hint - entrances_to_hint.pop(entrance.name) - hint_count -= 1 - else: - break + tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' + hint_locations = HintLocations.copy() + local_random.shuffle(hint_locations) + all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] + local_random.shuffle(all_entrances) - # Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. - if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: - entrances_to_hint.update(ConnectorEntrances) - entrances_to_hint.update(DungeonEntrances) - if world.mode[player] == 'inverted': - entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'}) - else: - entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) - elif world.shuffle[player] == 'restricted': - entrances_to_hint.update(ConnectorEntrances) - entrances_to_hint.update(OtherEntrances) - if world.mode[player] == 'inverted': - entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'}) - entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'}) - entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'}) - else: - entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) - entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) - if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: - entrances_to_hint.update(InsanityEntrances) + # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. + entrances_to_hint = {} + entrances_to_hint.update(InconvenientDungeonEntrances) if world.shuffle_ganon: if world.mode[player] == 'inverted': - entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) + entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'}) else: - entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) - hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 0 - for entrance in all_entrances: - if entrance.name in entrances_to_hint: - if hint_count: - this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( - entrance.connected_region) + '.' - tt[hint_locations.pop(0)] = this_hint - entrances_to_hint.pop(entrance.name) - hint_count -= 1 - else: - break - - # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. - locations_to_hint = InconvenientLocations.copy() - if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: - locations_to_hint.extend(InconvenientVanillaLocations) - local_random.shuffle(locations_to_hint) - hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 5 - for location in locations_to_hint[:hint_count]: - if location == 'Swamp Left': - if local_random.randint(0, 1): - first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) - second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) - else: - second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) - first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) - this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.') - tt[hint_locations.pop(0)] = this_hint - elif location == 'Mire Left': - if local_random.randint(0, 1): - first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) - second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) - else: - second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) - first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) - this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.') - tt[hint_locations.pop(0)] = this_hint - elif location == 'Tower of Hera - Big Key Chest': - this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Ganons Tower - Big Chest': - this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Thieves\' Town - Big Chest': - this_hint = 'The big chest in Thieves\' Town contains ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Ice Palace - Big Chest': - this_hint = 'The big chest in Ice Palace contains ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Eastern Palace - Big Key Chest': - this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Sahasrahla': - this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint - elif location == 'Graveyard Cave': - this_hint = 'The cave north of the graveyard contains ' + hint_text( - world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint + entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'}) + if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + for entrance in all_entrances: + if entrance.name in entrances_to_hint: + this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( + entrance.connected_region) + '.' + tt[hint_locations.pop(0)] = this_hint + entrances_to_hint = {} + break + # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. + entrances_to_hint.update(InconvenientOtherEntrances) + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + hint_count = 0 + elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']: + hint_count = 2 else: - this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' - tt[hint_locations.pop(0)] = this_hint + hint_count = 4 + for entrance in all_entrances: + if entrance.name in entrances_to_hint: + if hint_count: + this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( + entrance.connected_region) + '.' + tt[hint_locations.pop(0)] = this_hint + entrances_to_hint.pop(entrance.name) + hint_count -= 1 + else: + break - # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. - items_to_hint = RelevantItems.copy() - if world.smallkey_shuffle[player]: - items_to_hint.extend(SmallKeys) - if world.bigkey_shuffle[player]: - items_to_hint.extend(BigKeys) - local_random.shuffle(items_to_hint) - hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'] else 8 - while hint_count > 0 and items_to_hint: - this_item = items_to_hint.pop(0) - this_location = world.find_items(this_item, player) - if this_location: - local_random.shuffle(this_location) - this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' - tt[hint_locations.pop(0)] = this_hint - hint_count -= 1 + # Next we handle hints for randomly selected other entrances, + # curating the selection intelligently based on shuffle. + if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: + entrances_to_hint.update(ConnectorEntrances) + entrances_to_hint.update(DungeonEntrances) + if world.mode[player] == 'inverted': + entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'}) + else: + entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) + elif world.shuffle[player] == 'restricted': + entrances_to_hint.update(ConnectorEntrances) + entrances_to_hint.update(OtherEntrances) + if world.mode[player] == 'inverted': + entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'}) + entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'}) + entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'}) + else: + entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) + entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) + if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: + entrances_to_hint.update(InsanityEntrances) + if world.shuffle_ganon: + if world.mode[player] == 'inverted': + entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) + else: + entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'}) + hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', + 'dungeonscrossed'] else 0 + for entrance in all_entrances: + if entrance.name in entrances_to_hint: + if hint_count: + this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text( + entrance.connected_region) + '.' + tt[hint_locations.pop(0)] = this_hint + entrances_to_hint.pop(entrance.name) + hint_count -= 1 + else: + break - # All remaining hint slots are filled with junk hints. It is done this way to ensure the same junk hint isn't selected twice. - junk_hints = junk_texts.copy() - local_random.shuffle(junk_hints) - for location, text in zip(hint_locations, junk_hints): - tt[location] = text + # Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable. + locations_to_hint = InconvenientLocations.copy() + if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed']: + locations_to_hint.extend(InconvenientVanillaLocations) + local_random.shuffle(locations_to_hint) + hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', + 'dungeonscrossed'] else 5 + for location in locations_to_hint[:hint_count]: + if location == 'Swamp Left': + if local_random.randint(0, 1): + first_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) + second_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) + else: + second_item = hint_text(world.get_location('Swamp Palace - West Chest', player).item) + first_item = hint_text(world.get_location('Swamp Palace - Big Key Chest', player).item) + this_hint = ('The westmost chests in Swamp Palace contain ' + first_item + ' and ' + second_item + '.') + tt[hint_locations.pop(0)] = this_hint + elif location == 'Mire Left': + if local_random.randint(0, 1): + first_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) + second_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) + else: + second_item = hint_text(world.get_location('Misery Mire - Compass Chest', player).item) + first_item = hint_text(world.get_location('Misery Mire - Big Key Chest', player).item) + this_hint = ('The westmost chests in Misery Mire contain ' + first_item + ' and ' + second_item + '.') + tt[hint_locations.pop(0)] = this_hint + elif location == 'Tower of Hera - Big Key Chest': + this_hint = 'Waiting in the Tower of Hera basement leads to ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Ganons Tower - Big Chest': + this_hint = 'The big chest in Ganon\'s Tower contains ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Thieves\' Town - Big Chest': + this_hint = 'The big chest in Thieves\' Town contains ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Ice Palace - Big Chest': + this_hint = 'The big chest in Ice Palace contains ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Eastern Palace - Big Key Chest': + this_hint = 'The antifairy guarded chest in Eastern Palace contains ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Sahasrahla': + this_hint = 'Sahasrahla seeks a green pendant for ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + elif location == 'Graveyard Cave': + this_hint = 'The cave north of the graveyard contains ' + hint_text( + world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + else: + this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' + tt[hint_locations.pop(0)] = this_hint + + # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. + items_to_hint = RelevantItems.copy() + if world.smallkey_shuffle[player]: + items_to_hint.extend(SmallKeys) + if world.bigkey_shuffle[player]: + items_to_hint.extend(BigKeys) + local_random.shuffle(items_to_hint) + if world.hints[player] == "full": + hint_count = len(hint_locations) # fill all remaining hint locations with Item hints. + else: + hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', + 'dungeonscrossed'] else 8 + hint_count = min(hint_count, len(items_to_hint), len(hint_locations)) + if hint_count: + locations = world.find_items_in_locations(set(items_to_hint), player) + local_random.shuffle(locations) + for x in range(hint_count): + this_location = locations.pop() + this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.' + tt[hint_locations.pop(0)] = this_hint + + if hint_locations: + # All remaining hint slots are filled with junk hints. + # It is done this way to ensure the same junk hint isn't selected twice. + junk_hints = junk_texts.copy() + local_random.shuffle(junk_hints) + for location, text in zip(hint_locations, junk_hints): + tt[location] = text # We still need the older hints of course. Those are done here. - silverarrows = world.find_items('Silver Bow', player) + silverarrows = world.find_item_locations('Silver Bow', player) local_random.shuffle(silverarrows) silverarrow_hint = ( ' %s?' % hint_text(silverarrows[0]).replace('Ganon\'s', 'my')) if silverarrows else '?\nI think not!' @@ -2302,7 +2310,7 @@ def write_strings(rom, world, player): tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint if world.worlds[player].has_progressive_bows and (world.difficulty_requirements[player].progressive_bow_limit >= 2 or ( world.swordless[player] or world.logic[player] == 'noglitches')): - prog_bow_locs = world.find_items('Progressive Bow', player) + prog_bow_locs = world.find_item_locations('Progressive Bow', player) world.slot_seeds[player].shuffle(prog_bow_locs) found_bow = False found_bow_alt = False