diff --git a/BaseClasses.py b/BaseClasses.py index cf0432a5..c85156d3 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -20,7 +20,7 @@ from source.dungeon.RoomObject import RoomObject class World(object): def __init__(self, players, owShuffle, owCrossed, owMixed, 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.owShuffle = owShuffle.copy() @@ -78,6 +78,7 @@ class World(object): self.prizes = {} self.dynamic_regions = [] self.dynamic_locations = [] + self.spoiler_mode = spoiler_mode self.spoiler = Spoiler(self) self.lamps_needed_for_dark_rooms = 1 self.owedges = [] @@ -106,6 +107,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 @@ -2869,6 +2871,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 = {'all'} + elif world.spoiler_mode == 'debug': + self.settings = {'all', 'debug'} + else: + self.settings = {} + self.suppress_spoiler_locations = ['Big Bomb', 'Frog', 'Dark Blacksmith Ruins', 'Middle Aged Man', 'Lost Old Man', 'Old Man Drop Off'] @@ -3012,27 +3025,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 'all' in self.settings or ('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 and not loc.skip] + 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 not loc.skip 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 and not loc.skip] + 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 not loc.skip 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) @@ -3132,85 +3147,88 @@ class Spoiler(object): outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) if self.metadata['user_notes']: outfile.write('User Notes:'.ljust(line_width) + '%s\n' % self.metadata['user_notes']) - outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm) outfile.write('Players:'.ljust(line_width) + '%d\n' % self.world.players) outfile.write('Teams:'.ljust(line_width) + '%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('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player]) outfile.write('\n') - outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) - outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) - outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player]) - if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: - outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) - outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) - outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) - outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) - outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) - outfile.write('\n') - outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) - outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player]) - outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) - outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) - outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player]) - outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player]) - outfile.write('Beemizer:'.ljust(line_width) + '%s\n' % self.metadata['beemizer'][player]) - outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player])) - outfile.write('\n') - outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player])) - outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) - outfile.write('Pottery Mode:'.ljust(line_width) + '%s\n' % self.metadata['pottery'][player]) - outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) - outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) - outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) - outfile.write('\n') - outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla': - outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) - outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) - if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': - outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) - outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) - outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) - outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) - outfile.write('\n') - outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) - if self.metadata['shuffle'][player] != 'vanilla': - outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) - outfile.write('Shuffle Link\'s House:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) - outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) - outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) - if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]: - outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) - outfile.write('\n') - outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) - outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) - outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) - outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) - outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) - outfile.write('\n') - outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) - if self.metadata['door_shuffle'][player] != 'vanilla': - outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) - outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) - outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) - outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) - outfile.write('Spiral Stairs Self-Loop:'.ljust(line_width) + '%s\n' % yn(self.metadata['door_self_loops'][player])) - outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) - outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) - outfile.write('\n') - outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) - outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) - if self.metadata['enemy_shuffle'][player] != 'none': - outfile.write('Enemy Logic:'.ljust(line_width) + '%s\n' % self.metadata['any_enemy_logic'][player]) - outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) - outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) - outfile.write('\n') - outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) - outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) - outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) + if 'all' in self.settings or 'settings' in self.settings: + outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player]) + outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player]) + outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player]) + if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']: + outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player]) + outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) + outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) + outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) + outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) + outfile.write('\n') + outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) + outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player]) + outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player]) + outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player]) + outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player]) + outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player]) + outfile.write('Beemizer:'.ljust(line_width) + '%s\n' % self.metadata['beemizer'][player]) + outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player])) + outfile.write('\n') + outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player])) + outfile.write('Bonk Drops:'.ljust(line_width) + '%s\n' % yn(self.metadata['bonk_drops'][player])) + outfile.write('Pottery Mode:'.ljust(line_width) + '%s\n' % self.metadata['pottery'][player]) + outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player])) + outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['dropshuffle'][player]) + outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player]) + outfile.write('\n') + outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player]) + if self.metadata['ow_shuffle'][player] != 'vanilla': + outfile.write('Free Terrain:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_terrain'][player])) + outfile.write('Crossed OW:'.ljust(line_width) + '%s\n' % self.metadata['ow_crossed'][player]) + if self.metadata['ow_shuffle'][player] != 'vanilla' or self.metadata['ow_crossed'][player] != 'none': + outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_keepsimilar'][player])) + outfile.write('OW Tile Flip (Mixed):'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_mixed'][player])) + outfile.write('Whirlpool Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['ow_whirlpool'][player])) + outfile.write('Flute Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_fluteshuffle'][player]) + outfile.write('\n') + outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player]) + if self.metadata['shuffle'][player] != 'vanilla': + outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player])) + outfile.write('Shuffle Link\'s House:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player])) + outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player])) + outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player]) + if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]: + outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player]) + outfile.write('\n') + outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player])) + outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) + outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) + outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) + outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) + outfile.write('\n') + outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) + if self.metadata['door_shuffle'][player] != 'vanilla': + outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player]) + outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player]) + outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player]) + outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player])) + outfile.write('Spiral Stairs Self-Loop:'.ljust(line_width) + '%s\n' % yn(self.metadata['door_self_loops'][player])) + outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player])) + outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player]) + outfile.write('\n') + outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player]) + outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player]) + if self.metadata['enemy_shuffle'][player] != 'none': + outfile.write('Enemy Logic:'.ljust(line_width) + '%s\n' % self.metadata['any_enemy_logic'][player]) + outfile.write('Enemy Health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player]) + outfile.write('Enemy Damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player]) + outfile.write('\n') + outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player])) + outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player])) + outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race'])) if self.startinventory: outfile.write('Starting Inventory:'.ljust(line_width)) @@ -3247,70 +3265,74 @@ class Spoiler(object): self.parse_data() with open(filename, 'a') as outfile: line_width = 35 - outfile.write('\n\nRequirements:\n\n') - for dungeon, medallion in self.medallions.items(): - outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) - 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 'all' in self.settings or 'requirements' in self.settings: + outfile.write('\n\nRequirements:\n\n') + for dungeon, medallion in self.medallions.items(): + outfile.write(f'{dungeon}:'.ljust(line_width) + '%s Medallion\n' % medallion) + 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 'all' in self.settings or '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.maps: - # flute shuffle - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - outfile.write('\n\nFlute Spots:\n\n') - break - for player in range(1, self.world.players + 1): - if ('flute', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('flute', player)]['text']) + if 'all' in self.settings or 'flute' in self.settings: + # flute shuffle + for player in range(1, self.world.players + 1): + if ('flute', player) in self.maps: + outfile.write('\n\nFlute Spots:\n\n') + break + for player in range(1, self.world.players + 1): + if ('flute', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('flute', player)]['text']) - # overworld tile flips - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - outfile.write('\n\nOW Tile Flips:\n\n') - break - for player in range(1, self.world.players + 1): - if ('swaps', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('swaps', player)]['text']) - - # crossed groups - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - outfile.write('\n\nOW Crossed Groups:\n\n') - break - for player in range(1, self.world.players + 1): - if ('groups', player) in self.maps: - if self.world.players > 1: - outfile.write(str('(Player ' + str(player) + ')\n')) # player name - outfile.write(self.maps[('groups', player)]['text']) + if 'all' in self.settings or 'overworld' in self.settings: + # overworld tile flips + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: + outfile.write('\n\nOW Tile Flips:\n\n') + break + for player in range(1, self.world.players + 1): + if ('swaps', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('swaps', player)]['text']) + + # crossed groups + for player in range(1, self.world.players + 1): + if ('groups', player) in self.maps: + outfile.write('\n\nOW Crossed Groups:\n\n') + break + for player in range(1, self.world.players + 1): + if ('groups', player) in self.maps: + if self.world.players > 1: + outfile.write(str('(Player ' + str(player) + ')\n')) # player name + outfile.write(self.maps[('groups', player)]['text']) - if self.overworlds: + if self.overworlds and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nOverworld Edges:\n\n') # overworld transitions 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","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) - if self.whirlpools: + if self.whirlpools and ('all' in self.settings or 'overworld' in self.settings): outfile.write('\n\nWhirlpools:\n\n') # whirlpools 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","whirlpools",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","whirlpools",entry['exit'])) for entry in self.whirlpools.values()])) - if self.entrances: + if self.entrances and ('all' in self.settings or 'entrances' in self.settings): # entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly outfile.write('\n\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 ('all' in self.settings or '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 '', @@ -3319,33 +3341,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 ('all' in self.settings or '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 ('all' in self.settings or '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 'all' in self.settings or '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 'all' in self.settings or '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 91c6a209..c9603654 100644 --- a/CLI.py +++ b/CLI.py @@ -236,7 +236,7 @@ def parse_settings(): "mixed_travel": "prevent", "standardize_palettes": "standardize", 'aga_randomness': True, - + "triforce_pool": 0, "triforce_goal": 0, "triforce_pool_min": 0, @@ -267,10 +267,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/Fill.py b/Fill.py index b7e4b7cf..cc6141e8 100644 --- a/Fill.py +++ b/Fill.py @@ -1115,7 +1115,7 @@ def set_prize_drops(world, player): for player, drop_config in drops.items(): for pack_num in range(1, 8): if f'Pack {pack_num}' in drop_config: - for prize, idx in enumerate(drop_config[f'Pack {pack_num}']): + for idx, prize in enumerate(drop_config[f'Pack {pack_num}']): chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize] prizes[(pack_num-1)*8 + idx] = chosen for tree_pull_tier in range(1, 4): diff --git a/Gui.py b/Gui.py index 5155091d..bbb3eada 100755 --- a/Gui.py +++ b/Gui.py @@ -44,7 +44,7 @@ def save_settings(gui, args, filename): if not os.path.exists(settings_path): os.makedirs(settings_path) output_args = {} - settings = ["create_rom", "suppress_rom", "bps", "create_spoiler", "suppress_spoiler", + settings = ["create_rom", "suppress_rom", "bps", "spoiler", "calc_playthrough", "skip_playthrough", "print_custom_yaml", "settingsonload", "rom", "outputpath"] if filename == "settings.json": diff --git a/Main.py b/Main.py index c36bad50..8f67ff4c 100644 --- a/Main.py +++ b/Main.py @@ -41,7 +41,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.1.10' +version_number = '1.4.1.11' version_branch = '-u' __version__ = f'{version_number}{version_branch}' @@ -87,7 +87,6 @@ def main(args, seed=None, fish=None): for i in zip(args.logic.values(), args.door_shuffle.values()): if i[0] == 'hybridglitches' and i[1] != 'vanilla': raise RuntimeError(BabelFish().translate("cli","cli","hybridglitches.door.shuffle")) - if seed is None: random.seed(None) world.seed = random.randint(0, 999999999) @@ -160,7 +159,7 @@ def main(args, seed=None, fish=None): world.settings = CustomSettings() world.settings.create_from_world(world, args) - 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 or args.create_spoiler): @@ -365,12 +364,12 @@ def main(args, seed=None, fish=None): if args.mystery and not (args.suppress_meta or args.create_spoiler): 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: @@ -379,7 +378,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: @@ -394,7 +393,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) @@ -452,7 +451,7 @@ def init_world(args, fish): world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, 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 world.boots_hint = args.boots_hint.copy() @@ -530,7 +529,7 @@ def copy_world(world): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, 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() @@ -725,7 +724,7 @@ def copy_world_premature(world, player): # ToDo: Not good yet ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, 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 e937ce1b..cec045e9 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('--no_race', action='store_true') parser.add_argument('--suppress_rom', action='store_true') parser.add_argument('--suppress_meta', action='store_true') @@ -64,7 +64,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/RELEASENOTES.md b/RELEASENOTES.md index 8d57d657..d3c2892c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -141,6 +141,16 @@ These are now independent of retro mode and have three options: None, Random, an # Patch Notes +* 1.4.1.11u + * New Feature: Several spoiler levels added: None, Settings-only, Semi, Full, Debug + * Semi includes only entrances, prizes, and medallions (potential new spoiler mode being worked on, definition may change) + * Entrance: Lite/Lean support enemy drop shuffle + * Standard: Re-added tutorial guard near large rock + * Enemizer + * Fixed the overwriting of bonk fairies + * Fixed broken graphics on hyrule castle + * Enemy bans + * Customizer: Fixed bug with customizing prize packs * 1.4.1.10u * Vanilla key logic: Fix for vanilla layout Misery Mire which allows more complex key logic. Locations blocked by crystal switch access are only locked by 2 keys thanks to that being the minimum in Mire to reach one of two crystal switches. * Autotracking: Fix for chest turn counter with chest containing multiworld items (Thanks Hiimcody) diff --git a/Utils.py b/Utils.py index 06a83a19..b0f64517 100644 --- a/Utils.py +++ b/Utils.py @@ -347,11 +347,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 dbbc8c8b..fc102f1b 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 15dae0aa..e98c9228 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -61,7 +61,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 53ea2c74..27bafd29 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -236,7 +236,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 1f62560c..41d54fd8 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -146,7 +146,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/enemizer/Enemizer.py b/source/enemizer/Enemizer.py index 65009fdd..021b7f03 100644 --- a/source/enemizer/Enemizer.py +++ b/source/enemizer/Enemizer.py @@ -260,7 +260,7 @@ def get_randomize_able_sprites_ow(area_id, data_tables): req = data_tables.sprite_requirements[key] if isinstance(req, dict): continue - if not req.static and req.can_randomize: + if not req.static and req.can_randomize and not sprite.static: sprite_table[idx] = sprite return sprite_table diff --git a/source/enemizer/OwEnemyList.py b/source/enemizer/OwEnemyList.py index 58c15b43..f89b16c6 100644 --- a/source/enemizer/OwEnemyList.py +++ b/source/enemizer/OwEnemyList.py @@ -29,6 +29,7 @@ def init_vanilla_sprites_ow(): create_sprite(0x21b, EnemySprite.GreenKnifeGuard, 0x20, 0x1A, '', 0x09CB51) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x2D, 0x25, '', 0x09CB54) create_sprite(0x21b, EnemySprite.TutorialGuard, 0x20, 0x29, '', 0x09CB57) + create_sprite(0x21b, EnemySprite.TutorialGuard, 0x3C, 0x2A, '', 0x09CB5B) create_sprite(0x21d, EnemySprite.Apple, 0x0B, 0x06, '', 0x09CB5B, bonk=True) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x09, 0x1E, '', 0x09CB5F) create_sprite(0x22b, EnemySprite.TutorialGuard, 0x0B, 0x1E, '', 0x09CB62) diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 2a935048..4496fb97 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -687,7 +687,11 @@ def setup_required_overworld_groups(sheets): sheets[4].add_sprite_to_sheet([None, None, None, None], {0xF, 0x9F}) # Waterfall of wishing (pre/post-Aga) sheets[3].add_sprite_to_sheet([None, None, None, 14], {0x14, 0xA4}) # Graveyard (pre/post-Aga) sheets[1].add_sprite_to_sheet([None, None, 76, 0x3F], {0x1B, 0xAB}) # Hyrule Castle (pre/post-Aga) - sheets[2].add_sprite_to_sheet([None, None, None, 0x3F], {}) # Hyrule Castle - rain state + ## group 0 set to 0x48 for tutortial guards + ## group 1 & 2 set for green knife guards (and probably normal green guard) + ## group 3 set for lightning gate + sheets[2].add_sprite_to_sheet([0x48, 0x49, 0x13, 0x3F], {}) # Hyrule Castle - rain state + # Smithy/Race/Kak (pre/post-Aga) sheets[6].add_sprite_to_sheet([0x4F, 0x49, 0x4A, 0x50], {0x18, 0x22, 0x28, 0xA8, 0xB2, 0xB8}) sheets[8].add_sprite_to_sheet([None, None, 18, None], {0x30, 0xC0}) # Desert (pre/post-Aga) diff --git a/source/enemizer/enemy_deny.yaml b/source/enemizer/enemy_deny.yaml index d6dc6564..46b1176c 100644 --- a/source/enemizer/enemy_deny.yaml +++ b/source/enemizer/enemy_deny.yaml @@ -325,6 +325,7 @@ UwGeneralDeny: - [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur" - [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4" + - [0x00c6, 5, ["Bumper"]] - [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots - [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1" - [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2" @@ -419,10 +420,12 @@ OwGeneralDeny: - [0x03, 10, ["Gibo"]] # OldMan eating Gibo - [0x05, 11, ["Bumper", "AntiFairyCircle"]] # Blocks path to portal - [0x1e, 3, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle"]] # forbid a beamos here + - [0x35, 8, ["RollerVerticalUp", "RollerVerticalDown"]] # blocks the dock - [0x40, 0, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 7, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 13, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] - [0x40, 14, ["Beamos", "Bumper", "BigSpike", "AntiFairyCircle", "Thief"]] + - [0x40, 16, ["RollerVerticalUp", "RollerVerticalDown"]] # Ropa near back hole is really large as a roller - [0x55, 6, ["BigSpike"]] - [0x57, 5, ["RollerVerticalUp", "RollerVerticalDown"]] - [0x5e, 0, ["Gibo"]] # kiki eating Gibo diff --git a/source/gui/bottom.py b/source/gui/bottom.py index 1c8f838a..1cd99e0a 100644 --- a/source/gui/bottom.py +++ b/source/gui/bottom.py @@ -133,7 +133,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))) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index 54c58417..cc000c82 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -1340,18 +1340,24 @@ def do_limited_shuffle_exclude_drops(pool_def, avail, lw=True): def do_vanilla_connect(pool_def, avail): - if pool_def['condition'] == 'shopsanity': + if 'shopsanity' in pool_def['condition']: if avail.world.shopsanity[avail.player]: return - elif pool_def['condition'] == 'pottery': # this condition involves whether caves with pots are shuffled or not + if 'pottery' in pool_def['condition']: # this condition involves whether caves with pots are shuffled or not if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']: return - elif pool_def['condition'] == 'takeany': + if 'takeany' in pool_def['condition']: if avail.world.take_any[avail.player] == 'fixed': return - elif pool_def['condition'] == 'bonk': + if 'bonk' in pool_def['condition']: if avail.world.shuffle_bonk_drops[avail.player]: return + if 'dropshuffle' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys']: + return + if 'enemy_drop' in pool_def['condition']: + if avail.world.dropshuffle[avail.player] not in ['none', 'keys'] or avail.world.enemy_shuffle[avail.player] != 'none': + return defaults = {**default_connections, **(inverted_default_connections if avail.inverted != avail.world.is_tile_swapped(0x1b, avail.player) else open_default_connections)} for entrance in pool_def['entrances']: if entrance in avail.entrances: @@ -1871,35 +1877,56 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Mire Fairy', 'Archery Game', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', - 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', - 'East Dark World Hint', 'Kakariko Gamble Game', 'Long Fairy Cave', - 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'East Dark World Hint', + 'Kakariko Gamble Game', 'Bush Covered House', 'Fortune Teller (Light)', + 'Lost Woods Gamble', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', - 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_takeanys': { 'special': 'vanilla', 'condition': 'takeany', - 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)'], + 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy'], + }, + 'fixed_takeanys_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': ['takeany', 'enemy_drop'], + 'entrances': ['Bonk Fairy (Dark)'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'fixed_bonk': { 'special': 'vanilla', - 'condition': 'bonk', + 'condition': ['enemy_drop', 'bonk'], 'entrances': ['Good Bee Cave'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps @@ -1908,15 +1935,15 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Good Bee Cave', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', 'Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)', - 'Links House', 'Tavern North'] + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Desert Fairy', + 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy', + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] }, 'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything 'special': 'old_man_cave_east', @@ -1962,35 +1989,56 @@ modes = { 'special': 'vanilla', 'condition': '', 'entrances': ['Mire Fairy', 'Archery Game', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', - 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'Dark Lake Hylia Shop', - 'East Dark World Hint', 'Kakariko Gamble Game', 'Long Fairy Cave', - 'Bush Covered House', 'Fortune Teller (Light)', 'Lost Woods Gamble', - 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy', 'Bonk Fairy (Light)'], + 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Fairy', 'East Dark World Hint', + 'Kakariko Gamble Game', 'Bush Covered House', 'Fortune Teller (Light)', + 'Lost Woods Gamble', 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy'], }, 'fixed_shops': { 'special': 'vanilla', 'condition': 'shopsanity', - 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', - 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', - 'Lake Hylia Shop'], + 'entrances': ['Dark Death Mountain Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', + 'Red Shield Shop', 'Kakariko Shop', 'Lake Hylia Shop', 'Dark Lake Hylia Shop'], }, 'fixed_takeanys': { 'special': 'vanilla', 'condition': 'takeany', - 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)'], + 'entrances': ['Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy'], + }, + 'fixed_takeanys_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': ['takeany', 'enemy_drop'], + 'entrances': ['Bonk Fairy (Dark)'], }, 'fixed_pottery': { 'special': 'vanilla', 'condition': 'pottery', 'entrances': ['Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', - 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint'] + '20 Rupee Cave', '50 Rupee Cave', 'Palace of Darkness Hint', + 'Dark Lake Hylia Ledge Spike Cave', 'Mire Hint'] + }, + 'fixed_enemy_drops_fairies': { + 'special': 'vanilla', + 'condition': 'enemy_drop', + 'entrances': ['Long Fairy Cave', 'Bonk Fairy (Light)'] + }, + 'fixed_pots_n_bones_fairies': { + 'special': 'vanilla', + 'condition': ['pottery', 'enemy_drop'], + 'entrances': ['Hookshot Fairy'] + }, + 'fixed_pots_n_bones': { + 'special': 'vanilla', + 'condition': ['pottery', 'dropshuffle'], + 'entrances': ['Light World Bomb Hut'] + }, + 'fixed_shop_n_bones': { + 'special': 'vanilla', + 'condition': ['shopsanity', 'enemy_drop'], + 'entrances': ['Capacity Upgrade'] }, 'fixed_bonk': { 'special': 'vanilla', - 'condition': 'bonk', + 'condition': ['enemy_drop', 'bonk'], 'entrances': ['Good Bee Cave'] }, 'item_caves': { # shuffles shops/pottery if they weren't fixed in the last steps @@ -1999,15 +2047,15 @@ modes = { 'Ice Rod Cave', 'Dam', 'Bonk Rock Cave', 'Library', 'Potion Shop', 'Mini Moldorm Cave', 'Checkerboard Cave', 'Graveyard Cave', 'Cave 45', 'Sick Kids House', 'Blacksmiths Hut', 'Sahasrahlas Hut', 'Aginahs Cave', 'Chicken House', 'Kings Grave', 'Blinds Hideout', - 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Good Bee Cave', + 'Waterfall of Wishing', 'Dark Death Mountain Shop', 'Dark Lake Hylia Shop', 'Dark Potion Shop', 'Dark Lumberjack Shop', 'Dark World Shop', 'Red Shield Shop', 'Kakariko Shop', 'Capacity Upgrade', 'Lake Hylia Shop', 'Lumberjack House', 'Snitch Lady (West)', 'Snitch Lady (East)', 'Tavern (Front)', 'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy', - 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', - 'Mire Hint', 'Desert Fairy', 'Light Hype Fairy', 'Dark Death Mountain Fairy', - 'Dark Lake Hylia Ledge Fairy', 'Bonk Fairy (Dark)', - 'Links House', 'Tavern North'] # inverted links house gets substituted + 'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave', 'Desert Fairy', + 'Light Hype Fairy', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Ledge Fairy', + 'Bonk Fairy (Dark)', 'Good Bee Cave', 'Long Fairy Cave', 'Bonk Fairy (Light)', + 'Mire Hint', 'Links House', 'Tavern North'] } } }, diff --git a/source/rom/DataTables.py b/source/rom/DataTables.py index 69b570a1..f082a4f5 100644 --- a/source/rom/DataTables.py +++ b/source/rom/DataTables.py @@ -94,6 +94,7 @@ class DataTables: # _00FA81 is LW normal # _00FAC1 is LW post-aga # _00FB01 is DW + # _00FA41 is rain state self.write_ow_sprite_data_to_rom(rom) for sprite, stats in self.enemy_stats.items(): # write health to rom @@ -134,9 +135,11 @@ class DataTables: # calculate how big this table is going to be? # bytes = sum(1+len(x)*3 for x in self.ow_enemy_table.values() if len(x) > 0)+1 # ending_byte = 0x09CB3B + bytes - max_per_state = {0: 0x40, 1: 0x90, 2: 0x8D} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla) + max_per_state = {0: 0x40, 1: 0x90, 2: 0x8B} # dropped max on state 2 to steal space for a couple extra sprites (Murahdahla, extra tutorial guard) + pointer_address = snes_to_pc(0x09C881) - data_pointer = snes_to_pc(0x09CB3B) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla) + # currently borrowed 10 bytes, used 9 (2xMurah + TutorialGuard) + data_pointer = snes_to_pc(0x09CB38) # was originally 0x09CB41 - stealing space for a couple extra sprites (Murahdahla, extra tutorial guard) empty_pointer = pc_to_snes(data_pointer) & 0xFFFF rom.write_byte(data_pointer, 0xff) cached_dark_world = {}