From 20e60c8a1dcb2a2d62361a6056db9d4464ab6e9c Mon Sep 17 00:00:00 2001 From: aerinon Date: Mon, 22 Apr 2024 08:35:17 -0600 Subject: [PATCH] feat(spoiler): introducing custom spoiler levels --- BaseClasses.py | 191 ++++++++++-------- CLI.py | 5 +- Main.py | 16 +- Mystery.py | 4 +- Utils.py | 4 +- resources/app/cli/args.json | 16 +- resources/app/cli/lang/en.json | 10 +- resources/app/gui/lang/en.json | 7 +- .../gui/randomize/generation/checkboxes.json | 11 +- source/classes/constants.py | 2 +- source/gui/bottom.py | 1 - 11 files changed, 156 insertions(+), 111 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 64b33852..d2ddee2d 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -21,7 +21,7 @@ from source.dungeon.RoomObject import RoomObject class World(object): def __init__(self, players, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments, - timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints): + timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints, spoiler_mode): self.players = players self.teams = 1 self.shuffle = shuffle.copy() @@ -68,6 +68,7 @@ class World(object): self.hints = hints.copy() self.dynamic_regions = [] self.dynamic_locations = [] + self.spoiler_mode = spoiler_mode self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.doors = [] @@ -89,6 +90,7 @@ class World(object): self.data_tables = {} self.damage_table = {} + for player in range(1, players + 1): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val @@ -2497,6 +2499,17 @@ class Spoiler(object): self.metadata = {} self.shops = [] self.bosses = OrderedDict() + if world.spoiler_mode == 'settings': + self.settings = {'settings'} + elif world.spoiler_mode == 'semi': + self.settings = {'settings', 'entrances', 'requirements', 'prizes'} + elif world.spoiler_mode == 'full': + self.settings = {'settings', 'entrances', 'requirements', 'prizes', 'shops', 'doors', 'items', 'bosses', 'misc'} + elif world.spoiler_mode == 'debug': + self.settings = {'settings', 'entrances', 'requirements', 'prizes', 'shops', 'doors', 'items', 'misc', 'bosses', 'debug'} + else: + self.settings = {} + def set_entrance(self, entrance, exit, direction, player): if self.world.players == 1: @@ -2609,27 +2622,29 @@ class Spoiler(object): self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0] self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] + def include_item(item): + return ('items' in self.settings and not item.crystal) or ('prizes' in self.settings and item.crystal) self.locations = OrderedDict() listed_locations = set() - lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld] + lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and include_item(loc.item)] self.locations['Light World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations]) listed_locations.update(lw_locations) - dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld] + dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and include_item(loc.item)] self.locations['Dark World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations]) listed_locations.update(dw_locations) - cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip] + cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip and include_item(loc.item)] self.locations['Caves'] = OrderedDict([(location.gen_name(), 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.world.dungeons: - dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip] + dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip and include_item(loc.item)] self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), 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.world.get_locations() if loc not in listed_locations and not loc.skip] + other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip and include_item(loc.item)] if other_locations: self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) listed_locations.update(other_locations) @@ -2721,65 +2736,68 @@ class Spoiler(object): outfile.write('ALttP Dungeon Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed)) if self.metadata['user_notes']: outfile.write('User Notes: %s\n' % self.metadata['user_notes']) - outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) + if 'settings' in self.settings: + outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Players: %d\n' % self.world.players) outfile.write('Teams: %d\n' % self.world.teams) for player in range(1, self.world.players + 1): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) - outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') + if 'settings' in self.settings: + outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) - outfile.write('Mode: %s\n' % self.metadata['mode'][player]) - outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) - outfile.write('Goal: %s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: - outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) - outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) - outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) - outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) - outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) - outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") - outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) - outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) - outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") - outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") - outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") - outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") - outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") - outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) - if self.metadata['shuffle'][player] != 'vanilla': - outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") - outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") - outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") - outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") - outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") - if self.metadata['goal'][player] != 'trinity': - outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) - outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) - if self.metadata['door_shuffle'][player] != 'vanilla': - outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") - outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") - outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") - outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") - outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") - outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") - outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") - outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") - outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") - outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") - outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") - outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) - outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) - outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") - outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) - outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) - outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) - if self.metadata['enemy_shuffle'][player] != 'none': - outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") - outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") - outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) + if 'settings' in self.settings: + outfile.write('Mode: %s\n' % self.metadata['mode'][player]) + outfile.write('Swords: %s\n' % self.metadata['weapons'][player]) + outfile.write('Goal: %s\n' % self.metadata['goal'][player]) + if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: + outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player]) + outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player]))) + outfile.write('Crystals required for Ganon: %s\n' % (str(self.world.crystals_ganon_orig[player]))) + outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) + outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n") + outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) + outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) + outfile.write(f"Flute Mode: {self.metadata['flute_mode'][player]}\n") + outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") + outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n") + outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") + outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'][player])}\n") + outfile.write(f"Back of Tavern Shuffled: {yn(self.metadata['shuffletavern'][player])}\n") + outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n") + outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n") + outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n") + if self.metadata['goal'][player] != 'trinity': + outfile.write('Pyramid hole pre-opened: %s\n' % (self.metadata['open_pyramid'][player])) + outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") + outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") + outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") + outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") + outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") + outfile.write(f"Spiral Stairs can self-loop: {yn(self.metadata['door_self_loops'][player])}\n") + outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") + outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") + outfile.write(f"Drop Shuffle: {self.metadata['dropshuffle'][player]}\n") + outfile.write(f"Pottery Mode: {self.metadata['pottery'][player]}\n") + outfile.write(f"Pot Shuffle (Legacy): {yn(self.metadata['potshuffle'][player])}\n") + outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No')) + outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No')) + outfile.write(f"Small Key shuffle: {self.metadata['keyshuffle'][player]}\n") + outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No')) + outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player]) + outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player]) + if self.metadata['enemy_shuffle'][player] != 'none': + outfile.write(f"Enemy logic: {self.metadata['any_enemy_logic'][player]}\n") + outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n") + outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No')) if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) @@ -2817,26 +2835,29 @@ class Spoiler(object): with open(filename, 'a') as outfile: line_width = 35 - outfile.write('\nRequirements:\n\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'{dungeon}: {medallion} Medallion\n') - for player in range(1, self.world.players + 1): - player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') - if self.world.crystals_gt_orig[player] == 'random': - outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) - if self.world.crystals_ganon_orig[player] == 'random': - outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - outfile.write('\n\nBottle Refills:\n\n') - for fairy, bottle in self.bottles.items(): - outfile.write(f'{fairy}: {bottle}\n') + if 'requirements' in self.settings: + outfile.write('\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}: {medallion} Medallion\n') + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + if self.world.crystals_gt_orig[player] == 'random': + outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) + if self.world.crystals_ganon_orig[player] == 'random': + outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - if self.entrances: + if 'misc' in self.settings: + outfile.write('\n\nBottle Refills:\n\n') + for fairy, bottle in self.bottles.items(): + outfile.write(f'{fairy}: {bottle}\n') + + if self.entrances and 'entrances' in self.settings: # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly outfile.write('\nEntrances:\n\n') outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta", "entrances", entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta", "entrances", entry['exit'])) for entry in self.entrances.values()])) - if self.doors: + if self.doors and 'doors' in self.settings: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( ['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', @@ -2845,33 +2866,37 @@ class Spoiler(object): self.world.fish.translate("meta", "doors", entry['exit']), '({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] != 'basic' else '') for entry in self.doors.values()])) - if self.lobbies: + if self.lobbies and 'doors' in self.settings: outfile.write('\n\nDungeon Lobbies:\n\n') outfile.write('\n'.join( [f"{'Player {0}: '.format(entry['player']) if self.world.players > 1 else ''}{entry['lobby_name']}: {entry['door_name']}" for entry in self.lobbies.values()])) - if self.doorTypes: + if self.doorTypes and 'doors' in self.settings: # doorNames: For some reason these come in combined, somehow need to split on the thing to translate # doorTypes: Small Key, Bombable, Bonkable outfile.write('\n\nDoor Types:\n\n') outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta", "doors", entry['doorNames']), self.world.fish.translate("meta", "doorTypes", entry['type'])) for entry in self.doorTypes.values()])) + # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names outfile.write('\n\nLocations:\n\n') - outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) for grouping in self.locations.values() for (location, item) in grouping.items()])) + outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) + for grouping in self.locations.values() for (location, item) in grouping.items()])) # locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name # items: Item names - outfile.write('\n\nShops:\n\n') - outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", 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)) + if 'shops' in self.settings: + outfile.write('\n\nShops:\n\n') + outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta", "locations", shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta", "items", 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 range(1, self.world.players + 1): - if self.world.boss_shuffle[player] != 'none': - bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses - outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') - outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) + if 'bosses' in self.settings: + for player in range(1, self.world.players + 1): + if self.world.boss_shuffle[player] != 'none': + bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses + outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n') + outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']])) def extras(self, filename): # todo: conditional on enemy shuffle mode diff --git a/CLI.py b/CLI.py index 1066b845..1c1e0a30 100644 --- a/CLI.py +++ b/CLI.py @@ -224,7 +224,7 @@ def parse_settings(): 'mixed_travel': 'prevent', 'standardize_palettes': 'standardize', 'aga_randomness': True, - + "triforce_pool": 0, "triforce_goal": 0, "triforce_pool_min": 0, @@ -253,10 +253,9 @@ def parse_settings(): 'msu_resume': False, 'collection_rate': False, - # Spoiler defaults to TRUE + 'spoiler': 'full', # Playthrough defaults to TRUE # ROM defaults to TRUE - "create_spoiler": True, "calc_playthrough": True, "create_rom": True, "bps": False, diff --git a/Main.py b/Main.py index 8e49e3f0..4bdeae9b 100644 --- a/Main.py +++ b/Main.py @@ -85,7 +85,7 @@ def main(args, seed=None, fish=None): raise RuntimeError(BabelFish().translate("cli","cli","hybridglitches.door.shuffle")) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, - args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints) + args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints, args.spoiler) world.customizer = customized if customized else None logger = logging.getLogger('') if seed is None: @@ -202,7 +202,7 @@ def main(args, seed=None, fish=None): if item: world.push_precollected(item) - if args.create_spoiler and not args.jsonout: + if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "create.meta")) world.spoiler.meta_to_file(output_path(f'{outfilebase}_Spoiler.txt')) if args.mystery and not args.suppress_meta: @@ -406,12 +406,12 @@ def main(args, seed=None, fish=None): if args.mystery and not args.suppress_meta: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_meta.txt')) - elif args.create_spoiler and not args.jsonout: + elif world.spoiler_mode != 'none' and not args.jsonout: world.spoiler.hashes_to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.create_spoiler and not args.jsonout: + if world.spoiler_mode != 'none' and not args.jsonout: logger.info(world.fish.translate("cli", "cli", "patching.spoiler")) world.spoiler.to_file(output_path(f'{outfilebase}_Spoiler.txt')) - if args.loglevel == 'debug': + if 'debug' in world.spoiler.settings: world.spoiler.extras(output_path(f'{outfilebase}_Spoiler.txt')) if not args.skip_playthrough: @@ -420,7 +420,7 @@ def main(args, seed=None, fish=None): if args.jsonout: print(json.dumps({**jsonout, 'spoiler': world.spoiler.to_json()})) - elif args.create_spoiler: + elif world.spoiler_mode != 'none': logger.info(world.fish.translate("cli","cli","patching.spoiler")) if args.jsonout: with open(output_path('%s_Spoiler.json' % outfilebase), 'w') as outfile: @@ -435,7 +435,7 @@ def main(args, seed=None, fish=None): logger.info("") logger.info(world.fish.translate("cli","cli","made.rom") % (YES if (args.create_rom) else NO)) logger.info(world.fish.translate("cli","cli","made.playthrough") % (YES if (args.calc_playthrough) else NO)) - logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and args.create_spoiler) else NO)) + logger.info(world.fish.translate("cli","cli","made.spoiler") % (YES if (not args.jsonout and world.spoiler_mode != 'none') else NO)) logger.info(world.fish.translate("cli","cli","seed") + ": %s", world.seed) logger.info(world.fish.translate("cli","cli","total.time"), time.perf_counter() - start) @@ -449,7 +449,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, - world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints) + world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints, world.spoiler_mode) ret.teams = world.teams ret.player_names = copy.deepcopy(world.player_names) ret.remote_items = world.remote_items.copy() diff --git a/Mystery.py b/Mystery.py index 170dd8bc..9d81b344 100644 --- a/Mystery.py +++ b/Mystery.py @@ -28,7 +28,7 @@ def main(): parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--names', default='') parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1)) - parser.add_argument('--create_spoiler', action='store_true') + parser.add_argument('--spoiler', default='none', choices=['none', 'settings', 'semi', 'full', 'debug']) parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--suppress_meta', action='store_true') parser.add_argument('--bps', action='store_true') @@ -63,7 +63,7 @@ def main(): erargs = parse_cli(['--multi', str(args.multi)]) erargs.seed = seed erargs.names = args.names - erargs.create_spoiler = args.create_spoiler + erargs.spoiler = args.spoiler erargs.suppress_rom = args.suppress_rom erargs.suppress_meta = args.suppress_meta erargs.bps = args.bps diff --git a/Utils.py b/Utils.py index 50ab1b84..6d52d5f0 100644 --- a/Utils.py +++ b/Utils.py @@ -342,11 +342,11 @@ def update_deprecated_args(args): # Don't do: Yes # Do: No if "suppress_spoiler" in argVars: - args.create_spoiler = not args.suppress_spoiler in truthy + args.spoiler = 'none' # Don't do: No # Do: Yes if "create_spoiler" in argVars: - args.suppress_spoiler = not args.create_spoiler in truthy + args.spoiler = 'full' # ROM defaults to TRUE # Don't do: Yes diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 6aedcad8..c2d31606 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -4,14 +4,14 @@ "action": "store_true", "type": "bool" }, - "create_spoiler": { - "action": "store_false", - "dest": "suppress_spoiler", - "type": "bool", - "help": "suppress" - }, - "suppress_spoiler": { - "action": "store_true" + "spoiler": { + "choices": [ + "none", + "settings", + "semi", + "full", + "debug" + ] }, "mystery": { "action": "store_true", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 1f0defb9..b57508fe 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -60,7 +60,15 @@ }, "help": { "lang": [ "App Language, if available, defaults to English" ], - "create_spoiler": [ "Output a Spoiler File" ], + "spoiler": [ + "Spoiler File Options. (default: %(default)s)", + "None: No Spoiler", + "Meta: Meta information only about game. Intended for mystery settings", + "Settings: Only settings information", + "Semi: ", + "Full: Full spoiler generated", + "Debug: Includes debug information" + ], "bps": [ "Output BPS patches instead of ROMs"], "logic": [ "Select Enforcement of Item Requirements. (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index c14dee25..aa8be106 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -206,7 +206,12 @@ "randomizer.generation.bps": "Create BPS Patches", - "randomizer.generation.createspoiler": "Create Spoiler Log", + "randomizer.generation.spoiler": "Create Spoiler Log", + "randomizer.generation.spoiler.none": "None", + "randomizer.generation.spoiler.settings": "Settings Only", + "randomizer.generation.spoiler.semi": "Semi (Entrances and Prizes)", + "randomizer.generation.spoiler.full": "Full", + "randomizer.generation.spoiler.debug": "Debug", "randomizer.generation.createrom": "Create Patched ROM", "randomizer.generation.calcplaythrough": "Calculate Playthrough", "randomizer.generation.print_custom_yaml": "Print Customizer File", diff --git a/resources/app/gui/randomize/generation/checkboxes.json b/resources/app/gui/randomize/generation/checkboxes.json index 6e377027..3eeab78b 100644 --- a/resources/app/gui/randomize/generation/checkboxes.json +++ b/resources/app/gui/randomize/generation/checkboxes.json @@ -2,7 +2,16 @@ "checkboxes": { "createrom": { "type": "checkbox" }, "bps": { "type": "checkbox" }, - "createspoiler": { "type": "checkbox" }, + "spoiler": { + "type": "selectbox", + "options": [ + "none", + "settings", + "semi", + "full", + "debug" + ] + }, "calcplaythrough": { "type": "checkbox" }, "print_custom_yaml": { "type": "checkbox" } } diff --git a/source/classes/constants.py b/source/classes/constants.py index 15797e03..0950211e 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -134,7 +134,7 @@ SETTINGSTOPROCESS = { }, "generation": { "bps": "bps", - "createspoiler": "create_spoiler", + "spoiler": "spoiler", "createrom": "create_rom", "calcplaythrough": "calc_playthrough", "print_custom_yaml": "print_custom_yaml", diff --git a/source/gui/bottom.py b/source/gui/bottom.py index b9fdd9b2..5abb3de6 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -126,7 +126,6 @@ def bottom_frame(self, parent, args=None): made[k] = m.group(1) + m.group(2) + ' ' + m.group(4) successMsg += (made["rom"] % (YES if (guiargs.create_rom) else NO)) + "\n" successMsg += (made["playthrough"] % (YES if (guiargs.calc_playthrough) else NO)) + "\n" - successMsg += (made["spoiler"] % (YES if (not guiargs.jsonout and guiargs.create_spoiler) else NO)) + "\n" successMsg += (made["enemizer"] % (YES if needEnemizer else NO)) + "\n" # FIXME: English successMsg += ("Seed%s: %s" % ('s' if len(seeds) > 1 else "", ','.join(str(x) for x in seeds)))