diff --git a/BaseClasses.py b/BaseClasses.py index 75cd317a..f91b81bd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1260,13 +1260,9 @@ class Spoiler(): self.multiworld = world self.hashes = {} self.entrances = OrderedDict() - self.medallions = {} self.playthrough = {} self.unreachables = set() - self.locations = {} self.paths = {} - self.shops = [] - self.bosses = OrderedDict() def set_entrance(self, entrance: str, exit_: str, direction: str, player: int): if self.multiworld.players == 1: @@ -1276,126 +1272,6 @@ class Spoiler(): self.entrances[(entrance, direction, player)] = OrderedDict( [('player', player), ('entrance', entrance), ('exit', exit_), ('direction', direction)]) - def parse_data(self): - from worlds.alttp.SubClasses import LTTPRegionType - self.medallions = OrderedDict() - for player in self.multiworld.get_game_players("A Link to the Past"): - self.medallions[f'Misery Mire ({self.multiworld.get_player_name(player)})'] = \ - self.multiworld.required_medallions[player][0] - self.medallions[f'Turtle Rock ({self.multiworld.get_player_name(player)})'] = \ - self.multiworld.required_medallions[player][1] - - self.locations = OrderedDict() - listed_locations = set() - lw_locations = [] - dw_locations = [] - cave_locations = [] - for loc in self.multiworld.get_locations(): - if loc.game == "A Link to the Past": - if loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.LightWorld and loc.show_in_spoiler: - lw_locations.append(loc) - elif loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.DarkWorld and loc.show_in_spoiler: - dw_locations.append(loc) - elif loc not in listed_locations and loc.parent_region and \ - loc.parent_region.type == LTTPRegionType.Cave and loc.show_in_spoiler: - cave_locations.append(loc) - - self.locations['Light World'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - lw_locations]) - listed_locations.update(lw_locations) - - self.locations['Dark World'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - dw_locations]) - listed_locations.update(dw_locations) - - self.locations['Caves'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - cave_locations]) - listed_locations.update(cave_locations) - - for dungeon in self.multiworld.dungeons.values(): - dungeon_locations = [loc for loc in self.multiworld.get_locations() if - loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler] - self.locations[str(dungeon)] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - dungeon_locations]) - listed_locations.update(dungeon_locations) - - other_locations = [loc for loc in self.multiworld.get_locations() if - loc not in listed_locations and loc.show_in_spoiler] - if other_locations: - self.locations['Other Locations'] = OrderedDict( - [(str(location), str(location.item) if location.item is not None else 'Nothing') for location in - other_locations]) - listed_locations.update(other_locations) - - self.shops = [] - from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display - for shop in self.multiworld.shops: - if not shop.custom: - continue - shopdata = { - 'location': str(shop.region), - 'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop' - } - for index, item in enumerate(shop.inventory): - if item is None: - continue - my_price = item['price'] // price_rate_display.get(item['price_type'], 1) - shopdata['item_{}'.format( - index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}" - - if item['player'] > 0: - shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', - '(Player {}) — '.format( - item['player'])) - - if item['max'] == 0: - continue - shopdata['item_{}'.format(index)] += " x {}".format(item['max']) - - if item['replacement'] is None: - continue - shopdata['item_{}'.format( - index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}" - self.shops.append(shopdata) - - for player in self.multiworld.get_game_players("A Link to the Past"): - self.bosses[str(player)] = OrderedDict() - self.bosses[str(player)]["Eastern Palace"] = self.multiworld.get_dungeon("Eastern Palace", player).boss.name - self.bosses[str(player)]["Desert Palace"] = self.multiworld.get_dungeon("Desert Palace", player).boss.name - self.bosses[str(player)]["Tower Of Hera"] = self.multiworld.get_dungeon("Tower of Hera", player).boss.name - self.bosses[str(player)]["Hyrule Castle"] = "Agahnim" - self.bosses[str(player)]["Palace Of Darkness"] = self.multiworld.get_dungeon("Palace of Darkness", - player).boss.name - self.bosses[str(player)]["Swamp Palace"] = self.multiworld.get_dungeon("Swamp Palace", player).boss.name - self.bosses[str(player)]["Skull Woods"] = self.multiworld.get_dungeon("Skull Woods", player).boss.name - self.bosses[str(player)]["Thieves Town"] = self.multiworld.get_dungeon("Thieves Town", player).boss.name - self.bosses[str(player)]["Ice Palace"] = self.multiworld.get_dungeon("Ice Palace", player).boss.name - self.bosses[str(player)]["Misery Mire"] = self.multiworld.get_dungeon("Misery Mire", player).boss.name - self.bosses[str(player)]["Turtle Rock"] = self.multiworld.get_dungeon("Turtle Rock", player).boss.name - if self.multiworld.mode[player] != 'inverted': - self.bosses[str(player)]["Ganons Tower Basement"] = \ - self.multiworld.get_dungeon('Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[ - 'middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = self.multiworld.get_dungeon('Ganons Tower', player).bosses[ - 'top'].name - else: - self.bosses[str(player)]["Ganons Tower Basement"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name - self.bosses[str(player)]["Ganons Tower Middle"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name - self.bosses[str(player)]["Ganons Tower Top"] = \ - self.multiworld.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name - - self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2" - self.bosses[str(player)]["Ganon"] = "Ganon" - def create_playthrough(self, create_paths: bool = True): """Destructive to the world while it is run, damage gets repaired afterwards.""" from itertools import chain @@ -1547,30 +1423,7 @@ class Spoiler(): self.paths[str(multiworld.get_region('Inverted Big Bomb Shop', player))] = \ get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player)) - def to_json(self): - self.parse_data() - out = OrderedDict() - out['Entrances'] = list(self.entrances.values()) - out.update(self.locations) - out['Special'] = self.medallions - if self.hashes: - out['Hashes'] = self.hashes - if self.shops: - out['Shops'] = self.shops - out['playthrough'] = self.playthrough - out['paths'] = self.paths - out['Bosses'] = self.bosses - - return json.dumps(out) - def to_file(self, filename: str): - self.parse_data() - - def bool_to_text(variable: Union[bool, str]) -> str: - if type(variable) == str: - return variable - return 'Yes' if variable else 'No' - def write_option(option_key: str, option_obj: type(Options.Option)): res = getattr(self.multiworld, option_key)[player] display_name = getattr(option_obj, "display_name", option_key) @@ -1600,38 +1453,6 @@ class Spoiler(): write_option(f_option, option) AutoWorld.call_single(self.multiworld, "write_spoiler_header", player, outfile) - if player in self.multiworld.get_game_players("A Link to the Past"): - outfile.write('%s%s\n' % ('Hash: ', self.hashes[player])) - - outfile.write('Logic: %s\n' % self.multiworld.logic[player]) - outfile.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[player]) - outfile.write('Mode: %s\n' % self.multiworld.mode[player]) - outfile.write('Goal: %s\n' % self.multiworld.goal[player]) - if "triforce" in self.multiworld.goal[player]: # triforce hunt - outfile.write("Pieces available for Triforce: %s\n" % - self.multiworld.triforce_pieces_available[player]) - outfile.write("Pieces required for Triforce: %s\n" % - self.multiworld.triforce_pieces_required[player]) - outfile.write('Difficulty: %s\n' % self.multiworld.difficulty[player]) - outfile.write('Item Functionality: %s\n' % self.multiworld.item_functionality[player]) - outfile.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[player]) - if self.multiworld.shuffle[player] != "vanilla": - outfile.write('Entrance Shuffle Seed %s\n' % self.multiworld.worlds[player].er_seed) - outfile.write('Shop inventory shuffle: %s\n' % - bool_to_text("i" in self.multiworld.shop_shuffle[player])) - outfile.write('Shop price shuffle: %s\n' % - bool_to_text("p" in self.multiworld.shop_shuffle[player])) - outfile.write('Shop upgrade shuffle: %s\n' % - bool_to_text("u" in self.multiworld.shop_shuffle[player])) - outfile.write('New Shop inventory: %s\n' % - bool_to_text("g" in self.multiworld.shop_shuffle[player] or - "f" in self.multiworld.shop_shuffle[player])) - outfile.write('Custom Potion Shop: %s\n' % - bool_to_text("w" in self.multiworld.shop_shuffle[player])) - outfile.write('Enemy health: %s\n' % self.multiworld.enemy_health[player]) - outfile.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[player]) - outfile.write('Prize shuffle %s\n' % - self.multiworld.shuffle_prizes[player]) if self.entrances: outfile.write('\n\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % (f'{self.multiworld.get_player_name(entry["player"])}: ' @@ -1640,30 +1461,14 @@ class Spoiler(): '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances.values()])) - if self.medallions: - outfile.write('\n\nMedallions:\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'\n{dungeon}: {medallion}') - AutoWorld.call_all(self.multiworld, "write_spoiler", outfile) + locations = [(str(location), str(location.item) if location.item is not None else "Nothing") + for location in self.multiworld.get_locations()] outfile.write('\n\nLocations:\n\n') outfile.write('\n'.join( - ['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in - grouping.items()])) + ['%s: %s' % (location, item) for location, item in locations])) - if self.shops: - outfile.write('\n\nShops:\n\n') - outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join( - item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if - item)) for shop in self.shops)) - - for player in self.multiworld.get_game_players("A Link to the Past"): - if self.multiworld.boss_shuffle[player] != 'none': - bossmap = self.bosses[str(player)] if self.multiworld.players > 1 else self.bosses - outfile.write( - f'\n\nBosses{(f" ({self.multiworld.get_player_name(player)})" if self.multiworld.players > 1 else "")}:\n') - outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join( [' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [ diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d9250484..5f33b152 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -3,6 +3,7 @@ import os import random import threading import typing +from collections import OrderedDict import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld @@ -19,8 +20,8 @@ from .Client import ALTTPSNIClient from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules -from .Shops import create_shops, ShopSlotFill -from .SubClasses import ALttPItem +from .Shops import create_shops, ShopSlotFill, ShopType, price_rate_display, price_type_display_name +from .SubClasses import ALttPItem, LTTPRegionType from worlds.AutoWorld import World, WebWorld, LogicMixin lttp_logger = logging.getLogger("A Link to the Past") @@ -520,6 +521,126 @@ class ALTTPWorld(World): else: logging.warning(f"Could not trash fill Ganon's Tower for player {player}.") + def write_spoiler_header(self, spoiler_handle: typing.TextIO) -> None: + def bool_to_text(variable: typing.Union[bool, str]) -> str: + if type(variable) == str: + return variable + return "Yes" if variable else "No" + + spoiler_handle.write('Logic: %s\n' % self.multiworld.logic[self.player]) + spoiler_handle.write('Dark Room Logic: %s\n' % self.multiworld.dark_room_logic[self.player]) + spoiler_handle.write('Mode: %s\n' % self.multiworld.mode[self.player]) + spoiler_handle.write('Goal: %s\n' % self.multiworld.goal[self.player]) + if "triforce" in self.multiworld.goal[self.player]: # triforce hunt + spoiler_handle.write("Pieces available for Triforce: %s\n" % + self.multiworld.triforce_pieces_available[self.player]) + spoiler_handle.write("Pieces required for Triforce: %s\n" % + self.multiworld.triforce_pieces_required[self.player]) + spoiler_handle.write('Difficulty: %s\n' % self.multiworld.difficulty[self.player]) + spoiler_handle.write('Item Functionality: %s\n' % self.multiworld.item_functionality[self.player]) + spoiler_handle.write('Entrance Shuffle: %s\n' % self.multiworld.shuffle[self.player]) + if self.multiworld.shuffle[self.player] != "vanilla": + spoiler_handle.write('Entrance Shuffle Seed %s\n' % self.er_seed) + spoiler_handle.write('Shop inventory shuffle: %s\n' % + bool_to_text("i" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Shop price shuffle: %s\n' % + bool_to_text("p" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Shop upgrade shuffle: %s\n' % + bool_to_text("u" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('New Shop inventory: %s\n' % + bool_to_text("g" in self.multiworld.shop_shuffle[self.player] or + "f" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Custom Potion Shop: %s\n' % + bool_to_text("w" in self.multiworld.shop_shuffle[self.player])) + spoiler_handle.write('Enemy health: %s\n' % self.multiworld.enemy_health[self.player]) + spoiler_handle.write('Enemy damage: %s\n' % self.multiworld.enemy_damage[self.player]) + spoiler_handle.write('Prize shuffle %s\n' % self.multiworld.shuffle_prizes[self.player]) + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + spoiler_handle.write("\n\nMedallions:\n") + spoiler_handle.write(f"\nMisery Mire ({self.multiworld.get_player_name(self.player)}):" + f" {self.multiworld.required_medallions[self.player][0]}") + spoiler_handle.write( + f"\nTurtle Rock ({self.multiworld.get_player_name(self.player)}):" + f" {self.multiworld.required_medallions[self.player][1]}") + + if self.multiworld.boss_shuffle[self.player] != "none": + def create_boss_map() -> typing.Dict: + boss_map = { + "Eastern Palace": self.multiworld.get_dungeon("Eastern Palace", self.player).boss.name, + "Desert Palace": self.multiworld.get_dungeon("Desert Palace", self.player).boss.name, + "Tower Of Hera": self.multiworld.get_dungeon("Tower of Hera", self.player).boss.name, + "Hyrule Castle": "Agahnim", + "Palace Of Darkness": self.multiworld.get_dungeon("Palace of Darkness", + self.player).boss.name, + "Swamp Palace": self.multiworld.get_dungeon("Swamp Palace", self.player).boss.name, + "Skull Woods": self.multiworld.get_dungeon("Skull Woods", self.player).boss.name, + "Thieves Town": self.multiworld.get_dungeon("Thieves Town", self.player).boss.name, + "Ice Palace": self.multiworld.get_dungeon("Ice Palace", self.player).boss.name, + "Misery Mire": self.multiworld.get_dungeon("Misery Mire", self.player).boss.name, + "Turtle Rock": self.multiworld.get_dungeon("Turtle Rock", self.player).boss.name, + "Ganons Tower": "Agahnim 2", + "Ganon": "Ganon" + } + if self.multiworld.mode[self.player] != 'inverted': + boss_map.update({ + "Ganons Tower Basement": + self.multiworld.get_dungeon("Ganons Tower", self.player).bosses["bottom"].name, + "Ganons Tower Middle": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + "middle"].name, + "Ganons Tower Top": self.multiworld.get_dungeon("Ganons Tower", self.player).bosses[ + "top"].name + }) + else: + boss_map.update({ + "Ganons Tower Basement": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["bottom"].name, + "Ganons Tower Middle": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["middle"].name, + "Ganons Tower Top": self.multiworld.get_dungeon("Inverted Ganons Tower", self.player).bosses["top"].name + }) + return boss_map + + bossmap = create_boss_map() + spoiler_handle.write( + f'\n\nBosses{(f" ({self.multiworld.get_player_name(self.player)})" if self.multiworld.players > 1 else "")}:\n') + spoiler_handle.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) + + def build_shop_info() -> typing.Dict: + shop = self.multiworld.shops[self.player] + if not shop.custom: + return None + + shop_data = { + "location": str(shop.region), + "type": "Take Any" if shop.type == ShopType.TakeAny else "Shop" + } + + for index, item in enumerate(shop.inventory): + if item is None: + continue + price = item["price"] // price_rate_display.get(item["price_type"], 1) + shop_data["item_{}".format(index)] = f"{item['item']} - {price} {price_type_display_name[item['price_type']]}" + if item["player"]: + shop_data["item_{}".format(index)] =\ + shop_data["item_{}".format(index)].replace("—", "(Player {}) — ".format(item["player"])) + + if item["max"] == 0: + continue + shop_data["item_{}".format(index)] += " x {}".format(item["max"]) + if item["replacement"] is None: + continue + shop_data["item_{}".format(index)] +=\ + f", {item['replacement']} - {item['replacement_price']}" \ + f" {price_type_display_name[item['replacement_price_type']]}" + + return shop_data + + shop_data = build_shop_info() + if shop_data is not None: + spoiler_handle.write('\n\nShops:\n\n') + spoiler_handle.write(''.join("{} [{}]\n {}".format(shop_data['location'], shop_data['type'], "\n ".join( + item for item in [shop_data.get('item_0', None), shop_data.get('item_1', None), shop_data.get('item_2', None)] if + item)))) + def get_filler_item_name(self) -> str: if self.multiworld.goal[self.player] == "icerodhunt": item = "Nothing"