diff --git a/BaseClasses.py b/BaseClasses.py index fc1dd304..83a14058 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -8,7 +8,7 @@ from Utils import int16_as_bytes class World(object): - def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, place_dungeon_items, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, keysanity, retro, custom, customitemarray, boss_shuffle, hints): + def __init__(self, players, shuffle, logic, mode, swords, difficulty, difficulty_adjustments, timer, progressive, goal, algorithm, accessibility, shuffle_ganon, quickswap, fastmenu, disable_music, retro, custom, customitemarray, boss_shuffle, hints): self.players = players self.shuffle = shuffle self.logic = logic @@ -35,7 +35,6 @@ class World(object): self._entrance_cache = {} self._location_cache = {} self.required_locations = [] - self.place_dungeon_items = place_dungeon_items # configurable in future self.shuffle_bonk_prizes = False self.swamp_patch_required = {player: False for player in range(1, players + 1)} self.powder_patch_required = {player: False for player in range(1, players + 1)} @@ -65,7 +64,10 @@ class World(object): self.quickswap = quickswap self.fastmenu = fastmenu self.disable_music = disable_music - self.keysanity = keysanity + self.mapshuffle = False + self.compassshuffle = False + self.keyshuffle = False + self.bigkeyshuffle = False self.retro = retro self.custom = custom self.customitemarray = customitemarray @@ -175,7 +177,7 @@ class World(object): elif item.name.startswith('Bottle'): if ret.bottle_count(item.player) < self.difficulty_requirements.progressive_bottle_limit: ret.prog_items.add((item.name, item.player)) - elif item.advancement or item.key: + elif item.advancement or item.smallkey or item.bigkey: ret.prog_items.add((item.name, item.player)) for item in self.itempool: @@ -352,12 +354,14 @@ class CollectionState(object): def sweep_for_events(self, key_only=False, locations=None): # this may need improvement + if locations is None: + locations = self.world.get_filled_locations() new_locations = True checked_locations = 0 while new_locations: - if locations is None: - locations = self.world.get_filled_locations() - reachable_events = [location for location in locations if location.event and (not key_only or location.item.key) and location.can_reach(self)] + reachable_events = [location for location in locations if location.event and + (not key_only or (not self.world.keyshuffle and location.item.smallkey) or (not self.world.bigkeyshuffle and location.item.bigkey)) + and location.can_reach(self)] for event in reachable_events: if (event.name, event.player) not in self.events: self.events.append((event.name, event.player)) @@ -677,9 +681,12 @@ class Region(object): return False def can_fill(self, item): - is_dungeon_item = item.key or item.map or item.compass + inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle) + or (item.bigkey and not self.world.bigkeyshuffle) + or (item.map and not self.world.mapshuffle) + or (item.compass and not self.world.compassshuffle)) sewer_hack = self.world.mode == 'standard' and item.name == 'Small Key (Escape)' - if sewer_hack or (is_dungeon_item and not self.world.keysanity): + if sewer_hack or inside_dungeon_item: return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player return True @@ -838,14 +845,18 @@ class Item(object): self.location = None self.player = player - @property - def key(self): - return self.type == 'SmallKey' or self.type == 'BigKey' - @property def crystal(self): return self.type == 'Crystal' + @property + def smallkey(self): + return self.type == 'SmallKey' + + @property + def bigkey(self): + return self.type == 'BigKey' + @property def map(self): return self.type == 'Map' @@ -1036,7 +1047,10 @@ class Spoiler(object): 'item_functionality': self.world.difficulty_adjustments, 'accessibility': self.world.accessibility, 'hints': self.world.hints, - 'keysanity': self.world.keysanity, + 'mapshuffle': self.world.mapshuffle, + 'compassshuffle': self.world.compassshuffle, + 'keyshuffle': self.world.keyshuffle, + 'bigkeyshuffle': self.world.bigkeyshuffle, 'players': self.world.players } @@ -1068,10 +1082,12 @@ class Spoiler(object): outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle']) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Accessibility: %s\n' % self.metadata['accessibility']) - outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.world.place_dungeon_items else 'No')) outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.world.quickswap else 'No')) outfile.write('Menu speed: %s\n' % self.world.fastmenu) - outfile.write('Keysanity enabled: %s\n' % ('Yes' if self.metadata['keysanity'] else 'No')) + outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'] else 'No')) + outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'] else 'No')) + outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'] else 'No')) + outfile.write('Big Key shuffle: %s\n' % ('Yes' if self.metadata['bigkeyshuffle'] else 'No')) outfile.write('Players: %d' % self.world.players) if self.entrances: outfile.write('\n\nEntrances:\n\n') diff --git a/Dungeons.py b/Dungeons.py index 8872e521..65a585a7 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -113,14 +113,13 @@ def fill_dungeons(world): continue # next place dungeon items - if world.place_dungeon_items: - for dungeon_item in dungeon_items: - di_location = dungeon_locations.pop() - world.push_item(di_location, dungeon_item, False) + for dungeon_item in dungeon_items: + di_location = dungeon_locations.pop() + world.push_item(di_location, dungeon_item, False) def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items if item.key or world.place_dungeon_items] + return [item for dungeon in world.dungeons for item in dungeon.all_items] def fill_dungeons_restrictive(world, shuffled_locations): all_state_base = world.get_all_state() @@ -135,16 +134,18 @@ def fill_dungeons_restrictive(world, shuffled_locations): pinball_room.locked = True shuffled_locations.remove(pinball_room) - if world.keysanity: - #in keysanity dungeon items are distributed as part of the normal item pool - for item in world.get_items(): - if item.key: - item.advancement = True - elif item.map or item.compass: - item.priority = True - return + # with shuffled dungeon items they are distributed as part of the normal item pool + for item in world.get_items(): + if (item.smallkey and world.keyshuffle) or (item.bigkey and world.bigkeyshuffle): + all_state_base.collect(item, True) + item.advancement = True + elif (item.map and world.mapshuffle) or (item.compass and world.compassshuffle): + item.priority = True - dungeon_items = get_dungeon_item_pool(world) + dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle) + or (item.bigkey and not world.bigkeyshuffle) + or (item.map and not world.mapshuffle) + or (item.compass and not world.compassshuffle))] # sort in the order Big Key, Small Key, Other before placing dungeon items sort_order = {"BigKey": 3, "SmallKey": 2} diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 1e163126..999fb436 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -85,7 +85,7 @@ In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two loc Graveyard Cave Mimic Cave -Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If keysanity is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses. +Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses. While the exact verbage of location names and item names can be found in the source code, here's a copy for reference: diff --git a/EntranceRandomizer.py b/EntranceRandomizer.py index 13155bd0..547514bb 100755 --- a/EntranceRandomizer.py +++ b/EntranceRandomizer.py @@ -198,20 +198,16 @@ def start(): ''') parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true') parser.add_argument('--disablemusic', help='Disables game music.', action='store_true') - parser.add_argument('--keysanity', help='''\ - Keys (and other dungeon items) are no longer restricted to - their dungeons, but can be anywhere - ''', action='store_true') + parser.add_argument('--mapshuffle', help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true') + parser.add_argument('--compassshuffle', help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true') + parser.add_argument('--keyshuffle', help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') + parser.add_argument('--bigkeyshuffle', help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true') parser.add_argument('--retro', help='''\ Keys are universal, shooting arrows costs rupees, and a few other little things make this more like Zelda-1. ''', action='store_true') parser.add_argument('--custom', default=False, help='Not supported.') parser.add_argument('--customitemarray', default=False, help='Not supported.') - parser.add_argument('--nodungeonitems', help='''\ - Remove Maps and Compasses from Itempool, replacing them by - empty slots. - ''', action='store_true') parser.add_argument('--accessibility', default='items', const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\ Select Item/Location Accessibility. (default: %(default)s) Items: You can reach all unique inventory items. No guarantees about diff --git a/Fill.py b/Fill.py index 70eddca9..de81a69d 100644 --- a/Fill.py +++ b/Fill.py @@ -240,8 +240,8 @@ def distribute_items_restrictive(world, gftower_trash_count=0, fill_locations=No random.shuffle(fill_locations) fill_locations.reverse() - # Make sure the escape small key is placed first in standard keysanity to prevent running out of spots - if world.keysanity and world.mode == 'standard': + # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots + if world.keyshuffle and world.mode == 'standard': progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)' else 0) fill_restrictive(world, world.state, fill_locations, progitempool) @@ -312,7 +312,7 @@ def flood_items(world): location_list = world.get_reachable_locations() random.shuffle(location_list) for location in location_list: - if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.key: + if location.item is not None and not location.item.advancement and not location.item.priority and not location.item.smallkey and not location.item.bigkey: # safe to replace replace_item = location.item replace_item.location = None @@ -332,8 +332,7 @@ def balance_multiworld_progression(world): reachable_locations_count[player] = 0 def get_sphere_locations(sphere_state, locations): - if not world.keysanity: - sphere_state.sweep_for_events(key_only=True, locations=locations) + sphere_state.sweep_for_events(key_only=True, locations=locations) return [loc for loc in locations if sphere_state.can_reach(loc)] while True: @@ -354,7 +353,7 @@ def balance_multiworld_progression(world): candidate_items = [] while True: for location in balancing_sphere: - if location.event: + if location.event and (world.keyshuffle or not location.item.smallkey) and (world.bigkeyshuffle or not location.item.bigkey): balancing_state.collect(location.item, True, location) if location.item.player in balancing_players and not location.locked: candidate_items.append(location) @@ -364,11 +363,14 @@ def balance_multiworld_progression(world): balancing_reachables[location.player] += 1 if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]): break + elif not balancing_sphere: + raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations] items_to_replace = [] for player in balancing_players: locations_to_test = [l for l in unlocked_locations if l.player == player] + # only replace items that end up in another player's world items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player] while items_to_test: testing = items_to_test.pop() @@ -392,7 +394,7 @@ def balance_multiworld_progression(world): new_location = replacement_locations.pop() old_location = items_to_replace.pop() - while not new_location.can_fill(state, old_location.item): + while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)): replacement_locations.insert(0, new_location) new_location = replacement_locations.pop() @@ -407,9 +409,11 @@ def balance_multiworld_progression(world): sphere_locations.append(location) for location in sphere_locations: - if location.event and (world.keysanity or not location.item.key): + if location.event and (world.keyshuffle or not location.item.smallkey) and (world.bigkeyshuffle or not location.item.bigkey): state.collect(location.item, True, location) checked_locations.extend(sphere_locations) if world.has_beaten_game(state): break + elif not sphere_locations: + raise RuntimeError('Not all required items reachable. Something went terribly wrong here.') diff --git a/Gui.py b/Gui.py index f03d8377..8ba32ef0 100755 --- a/Gui.py +++ b/Gui.py @@ -63,12 +63,18 @@ def guiMain(args=None): quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar) openpyramidVar = IntVar() openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar) - keysanityVar = IntVar() - keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar) + mcsbshuffleFrame = Frame(checkBoxFrame) + mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ") + mapshuffleVar = IntVar() + mapshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Maps", variable=mapshuffleVar) + compassshuffleVar = IntVar() + compassshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Compasses", variable=compassshuffleVar) + keyshuffleVar = IntVar() + keyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="Keys", variable=keyshuffleVar) + bigkeyshuffleVar = IntVar() + bigkeyshuffleCheckbutton = Checkbutton(mcsbshuffleFrame, text="BigKeys", variable=bigkeyshuffleVar) retroVar = IntVar() retroCheckbutton = Checkbutton(checkBoxFrame, text="Retro mode (universal keys)", variable=retroVar) - dungeonItemsVar = IntVar() - dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar) disableMusicVar = IntVar() disableMusicCheckbutton = Checkbutton(checkBoxFrame, text="Disable game music", variable=disableMusicVar) shuffleGanonVar = IntVar() @@ -84,9 +90,13 @@ def guiMain(args=None): suppressRomCheckbutton.pack(expand=True, anchor=W) quickSwapCheckbutton.pack(expand=True, anchor=W) openpyramidCheckbutton.pack(expand=True, anchor=W) - keysanityCheckbutton.pack(expand=True, anchor=W) + mcsbshuffleFrame.pack(expand=True, anchor=W) + mcsbLabel.grid(row=0, column=0) + mapshuffleCheckbutton.grid(row=0, column=1) + compassshuffleCheckbutton.grid(row=0, column=2) + keyshuffleCheckbutton.grid(row=0, column=3) + bigkeyshuffleCheckbutton.grid(row=0, column=4) retroCheckbutton.pack(expand=True, anchor=W) - dungeonItemsCheckbutton.pack(expand=True, anchor=W) disableMusicCheckbutton.pack(expand=True, anchor=W) shuffleGanonCheckbutton.pack(expand=True, anchor=W) hintsCheckbutton.pack(expand=True, anchor=W) @@ -385,9 +395,11 @@ def guiMain(args=None): guiargs.create_spoiler = bool(createSpoilerVar.get()) guiargs.suppress_rom = bool(suppressRomVar.get()) guiargs.openpyramid = bool(openpyramidVar.get()) - guiargs.keysanity = bool(keysanityVar.get()) + guiargs.mapshuffle = bool(mapshuffleVar.get()) + guiargs.compassshuffle = bool(compassshuffleVar.get()) + guiargs.keyshuffle = bool(keyshuffleVar.get()) + guiargs.bigkeyshuffle = bool(bigkeyshuffleVar.get()) guiargs.retro = bool(retroVar.get()) - guiargs.nodungeonitems = bool(dungeonItemsVar.get()) guiargs.quickswap = bool(quickSwapVar.get()) guiargs.disablemusic = bool(disableMusicVar.get()) guiargs.shuffleganon = bool(shuffleGanonVar.get()) @@ -1160,10 +1172,11 @@ def guiMain(args=None): # load values from commandline args createSpoilerVar.set(int(args.create_spoiler)) suppressRomVar.set(int(args.suppress_rom)) - keysanityVar.set(args.keysanity) + mapshuffleVar.set(args.mapshuffle) + compassshuffleVar.set(args.compassshuffle) + keyshuffleVar.set(args.keyshuffle) + bigkeyshuffleVar.set(args.bigkeyshuffle) retroVar.set(args.retro) - if args.nodungeonitems: - dungeonItemsVar.set(int(not args.nodungeonitems)) quickSwapVar.set(int(args.quickswap)) disableMusicVar.set(int(args.disablemusic)) if args.count: diff --git a/ItemList.py b/ItemList.py index 69d5a327..55eaf730 100644 --- a/ItemList.py +++ b/ItemList.py @@ -221,8 +221,11 @@ def generate_itempool(world, player): if treasure_hunt_icon is not None: world.treasure_hunt_icon = treasure_hunt_icon - if world.keysanity: - world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player]) + world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player + and ((item.smallkey and world.keyshuffle) + or (item.bigkey and world.bigkeyshuffle) + or (item.map and world.mapshuffle) + or (item.compass and world.compassshuffle))]) # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) diff --git a/Main.py b/Main.py index 07cb0803..b2eb57b3 100644 --- a/Main.py +++ b/Main.py @@ -25,7 +25,7 @@ def main(args, seed=None): start = time.process_time() # initialize the world - world = World(args.multi, args.shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, not args.nodungeonitems, args.accessibility, args.shuffleganon, args.quickswap, args.fastmenu, args.disablemusic, args.keysanity, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints) + world = World(args.multi, args.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.quickswap, args.fastmenu, args.disablemusic, args.retro, args.custom, args.customitemarray, args.shufflebosses, args.hints) logger = logging.getLogger('') if seed is None: random.seed(None) @@ -34,6 +34,19 @@ def main(args, seed=None): world.seed = int(seed) random.seed(world.seed) + world.mapshuffle = args.mapshuffle + world.compassshuffle = args.compassshuffle + world.keyshuffle = args.keyshuffle + world.bigkeyshuffle = args.bigkeyshuffle + + mcsb_name = '' + if all([world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle]): + mcsb_name = '-keysanity' + elif [world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle].count(True) == 1: + mcsb_name = '-mapshuffle' if world.mapshuffle else '-compassshuffle' if world.compassshuffle else '-keyshuffle' if world.keyshuffle else '-bigkeyshuffle' + elif any([world.mapshuffle, world.compassshuffle, world.keyshuffle, world.bigkeyshuffle]): + mcsb_name = '-%s%s%s%sshuffle' % ('M' if world.mapshuffle else '', 'C' if world.compassshuffle else '', 'S' if world.keyshuffle else '', 'B' if world.bigkeyshuffle else '') + world.crystals_needed_for_ganon = random.randint(0, 7) if args.crystals_ganon == 'random' else int(args.crystals_ganon) world.crystals_needed_for_gt = random.randint(0, 7) if args.crystals_gt == 'random' else int(args.crystals_gt) world.open_pyramid = args.openpyramid @@ -83,7 +96,7 @@ def main(args, seed=None): logger.info('Placing Dungeon Items.') shuffled_locations = None - if args.algorithm in ['balanced', 'vt26'] or args.keysanity: + if args.algorithm in ['balanced', 'vt26'] or args.mapshuffle or args.compassshuffle or args.keyshuffle or args.bigkeyshuffle: shuffled_locations = world.get_unfilled_locations() random.shuffle(shuffled_locations) fill_dungeons_restrictive(world, shuffled_locations) @@ -124,7 +137,7 @@ def main(args, seed=None): player_names = parse_names_string(args.names) outfileprefix = 'ER_%s_' % world.seed - outfilesuffix = '%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "") + outfilesuffix = '%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (world.logic, world.difficulty, world.difficulty_adjustments, world.mode, world.goal, "" if world.timer in ['none', 'display'] else "-" + world.timer, world.shuffle, world.algorithm, mcsb_name, "-retro" if world.retro else "", "-prog_" + world.progressive if world.progressive in ['off', 'random'] else "", "-nohints" if not world.hints else "") outfilebase = outfileprefix + outfilesuffix use_enemizer = args.enemizercli and (args.shufflebosses != 'none' or args.shuffleenemies or args.enemy_health != 'default' or args.enemy_health != 'default' or args.enemy_damage or args.shufflepalette or args.shufflepots) @@ -198,7 +211,7 @@ def gt_filler(world): def copy_world(world): # ToDo: Not good yet - ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.place_dungeon_items, world.accessibility, world.shuffle_ganon, world.quickswap, world.fastmenu, world.disable_music, world.keysanity, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints) + ret = World(world.players, world.shuffle, 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.quickswap, world.fastmenu, world.disable_music, world.retro, world.custom, world.customitemarray, world.boss_shuffle, world.hints) ret.required_medallions = world.required_medallions.copy() ret.swamp_patch_required = world.swamp_patch_required.copy() ret.ganon_at_pyramid = world.ganon_at_pyramid.copy() @@ -218,6 +231,10 @@ def copy_world(world): ret.difficulty_requirements = world.difficulty_requirements ret.fix_fake_world = world.fix_fake_world ret.lamps_needed_for_dark_rooms = world.lamps_needed_for_dark_rooms + ret.mapshuffle = world.mapshuffle + ret.compassshuffle = world.compassshuffle + ret.keyshuffle = world.keyshuffle + ret.bigkeyshuffle = world.bigkeyshuffle ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon ret.crystals_needed_for_gt = world.crystals_needed_for_gt @@ -318,8 +335,7 @@ def create_playthrough(world): sphere_candidates = list(prog_locations) logging.getLogger('').debug('Building up collection spheres.') while sphere_candidates: - if not world.keysanity: - state.sweep_for_events(key_only=True) + state.sweep_for_events(key_only=True) sphere = [] # build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres @@ -372,8 +388,7 @@ def create_playthrough(world): state = CollectionState(world) collection_spheres = [] while required_locations: - if not world.keysanity: - state.sweep_for_events(key_only=True) + state.sweep_for_events(key_only=True) sphere = list(filter(state.can_reach, required_locations)) diff --git a/Plando.py b/Plando.py index 527ae799..6e0c8a48 100755 --- a/Plando.py +++ b/Plando.py @@ -174,7 +174,7 @@ def fill_world(world, plando, text_patches): item = ItemFactory(itemstr.strip(), 1) if item is not None: world.push_item(location, item) - if item.key: + if item.smallkey or item.bigkey: location.event = True elif '<=>' in line: entrance, exit = line.split('<=>', 1) diff --git a/README.md b/README.md index b0628d14..4ca56424 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Does not invoke a timer. ### Display Displays a timer on-screen but does not alter the item pool. -This will prevent the dungeon item count feature in Easy and Keysanity from working. +This will prevent the dungeon item count feature in Easy and Compass shuffle from working. ### Timed @@ -264,12 +264,12 @@ generate spoilers for statistical analysis. Use to enable quick item swap with L/R buttons. Press L and R together to switch the state of items like the Mushroom/Powder pair. -## Keysanity +## Map/Compass/Small Key/Big Key shuffle (aka Keysanity) -This setting allows dungeon specific items (Small Key, Big Key, Map, Compass) to be distributed anywhere in the world and not just -in their native dungeon. Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that -is traditionally a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but -the rest of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell +These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon. +Small Keys dropped by enemies or found in pots are not affected. The chest in southeast Skull Woods that is traditionally +a guaranteed Small Key still is. These items will be distributed according to the v26/balanced algorithm, but the rest +of the itempool will respect the algorithm setting. Music for dungeons is randomized so it cannot be used as a tell for which dungeons contain pendants and crystals; finding a Map for a dungeon will allow the overworld map to display its prize. ## Retro @@ -422,10 +422,10 @@ Alters the rate at which the menu opens and closes. (default: normal) Disables game music, resulting in the game sound being just the SFX. (default: False) ``` ---keysanity +--mapshuffle --compassshuffle --keyshuffle --bigkeyshuffle ``` -Enable Keysanity (default: False) +Respectively enable Map/Compass/SmallKey/BigKey shuffle (default: False) ``` --retro @@ -433,13 +433,6 @@ Enable Keysanity (default: False) Enable Retro mode (default: False) -``` ---nodungeonitems -``` - -If set, Compasses and Maps are removed from the dungeon item pools and replaced by empty chests that may end up anywhere in the world. -This may lead to different amount of itempool items being placed in a dungeon than you are used to. (default: False) - ``` --heartbeep [{normal,half,quarter,off}] ``` diff --git a/Rom.py b/Rom.py index 73ce31d7..76cc1cce 100644 --- a/Rom.py +++ b/Rom.py @@ -444,10 +444,10 @@ def patch_rom(world, player, rom): # Keys in their native dungeon should use the orignal item code for keys if location.parent_region.dungeon: dungeon = location.parent_region.dungeon - if location.item is not None and location.item.key and dungeon.is_dungeon_item(location.item): - if location.item.type == "BigKey": + if location.item is not None and dungeon.is_dungeon_item(location.item): + if location.item.bigkey: itemid = 0x32 - if location.item.type == "SmallKey": + if location.item.smallkey: itemid = 0x24 if location.item and location.item.player != player: if location.player_address is not None: @@ -462,15 +462,15 @@ def patch_rom(world, player, rom): # patch music music_addresses = dungeon_music_addresses[location.name] - if world.keysanity: + if world.mapshuffle: music = random.choice([0x11, 0x16]) else: music = 0x11 if 'Pendant' in location.item.name else 0x16 for music_address in music_addresses: rom.write_byte(music_address, music) - if world.keysanity: - rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too in keysanity mode + if world.mapshuffle: + rom.write_byte(0x155C9, random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle # patch entrance/exits/holes for region in world.regions: @@ -811,7 +811,7 @@ def patch_rom(world, player, rom): ERtimeincrease = 10 else: ERtimeincrease = 20 - if world.keysanity: + if world.keyshuffle or world.bigkeyshuffle or world.mapshuffle: ERtimeincrease = ERtimeincrease + 15 if world.clock_mode == 'off': rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode @@ -922,18 +922,24 @@ def patch_rom(world, player, rom): rom.write_byte(0x18005F, world.crystals_needed_for_ganon) rom.write_byte(0x18008A, 0x01 if world.mode == "standard" else 0x00) # block HC upstairs doors in rain state in standard mode - rom.write_byte(0x18016A, 0x01 if world.keysanity else 0x00) # free roaming item text boxes - rom.write_byte(0x18003B, 0x01 if world.keysanity else 0x00) # maps showing crystals on overworld + rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle else 0x00) + | (0x02 if world.compassshuffle else 0x00) + | (0x04 if world.mapshuffle else 0x00) + | (0x08 if world.bigkeyshuffle else 0x00))) # free roaming item text boxes + rom.write_byte(0x18003B, 0x01 if world.mapshuffle else 0x00) # maps showing crystals on overworld # compasses showing dungeon count if world.clock_mode != 'off': rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location - elif world.keysanity: + elif world.compassshuffle: rom.write_byte(0x18003C, 0x01) # show on pickup else: rom.write_byte(0x18003C, 0x00) - rom.write_byte(0x180045, 0xFF if world.keysanity else 0x00) # free roaming items in menu + rom.write_byte(0x180045, ((0x01 if world.keyshuffle else 0x00) + | (0x02 if world.bigkeyshuffle else 0x00) + | (0x04 if world.compassshuffle else 0x00) + | (0x08 if world.mapshuffle else 0x00))) # free roaming items in menu # Map reveals reveal_bytes = { @@ -958,8 +964,8 @@ def patch_rom(world, player, rom): return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000) return 0x0000 - write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.keysanity else 0x0000) # Sahasrahla reveal - write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.keysanity else 0x0000) # Bomb Shop Reveal + write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle else 0x0000) # Sahasrahla reveal + write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle else 0x0000) # Bomb Shop Reveal rom.write_byte(0x180172, 0x01 if world.retro else 0x00) # universal keys rom.write_byte(0x180175, 0x01 if world.retro else 0x00) # rupee bow @@ -1440,8 +1446,10 @@ def write_strings(rom, world, player): # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. items_to_hint = RelevantItems.copy() - if world.keysanity: - items_to_hint.extend(KeysanityItems) + if world.keyshuffle: + items_to_hint.extend(SmallKeys) + if world.bigkeyshuffle: + items_to_hint.extend(BigKeys) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 while hint_count > 0: @@ -2022,28 +2030,30 @@ RelevantItems = ['Bow', 'Magic Upgrade (1/4)' ] -KeysanityItems = ['Small Key (Eastern Palace)', - 'Big Key (Eastern Palace)', - 'Small Key (Escape)', - 'Small Key (Desert Palace)', - 'Big Key (Desert Palace)', - 'Small Key (Tower of Hera)', - 'Big Key (Tower of Hera)', - 'Small Key (Agahnims Tower)', - 'Small Key (Palace of Darkness)', - 'Big Key (Palace of Darkness)', - 'Small Key (Thieves Town)', - 'Big Key (Thieves Town)', - 'Small Key (Swamp Palace)', - 'Big Key (Swamp Palace)', - 'Small Key (Skull Woods)', - 'Big Key (Skull Woods)', - 'Small Key (Ice Palace)', - 'Big Key (Ice Palace)', - 'Small Key (Misery Mire)', - 'Big Key (Misery Mire)', - 'Small Key (Turtle Rock)', - 'Big Key (Turtle Rock)', - 'Small Key (Ganons Tower)', - 'Big Key (Ganons Tower)' - ] +SmallKeys = ['Small Key (Eastern Palace)', + 'Small Key (Escape)', + 'Small Key (Desert Palace)', + 'Small Key (Tower of Hera)', + 'Small Key (Agahnims Tower)', + 'Small Key (Palace of Darkness)', + 'Small Key (Thieves Town)', + 'Small Key (Swamp Palace)', + 'Small Key (Skull Woods)', + 'Small Key (Ice Palace)', + 'Small Key (Misery Mire)', + 'Small Key (Turtle Rock)', + 'Small Key (Ganons Tower)', + ] + +BigKeys = ['Big Key (Eastern Palace)', + 'Big Key (Desert Palace)', + 'Big Key (Tower of Hera)', + 'Big Key (Palace of Darkness)', + 'Big Key (Thieves Town)', + 'Big Key (Swamp Palace)', + 'Big Key (Skull Woods)', + 'Big Key (Ice Palace)', + 'Big Key (Misery Mire)', + 'Big Key (Turtle Rock)', + 'Big Key (Ganons Tower)' + ] diff --git a/Rules.py b/Rules.py index cfd43c63..7d5e381c 100644 --- a/Rules.py +++ b/Rules.py @@ -804,7 +804,7 @@ def set_trock_key_rules(world, player): non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'] - if not world.keysanity: + if not world.keyshuffle: non_big_key_locations += ['Turtle Rock - Big Key Chest'] else: set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2) if item_in_locations(state, 'Big Key (Turtle Rock)', player, [('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)]) else state.has_key('Small Key (Turtle Rock)', player, 4)) @@ -814,7 +814,7 @@ def set_trock_key_rules(world, player): non_big_key_locations += ['Turtle Rock - Crystaroller Room', 'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'] - if not world.keysanity: + if not world.keyshuffle: non_big_key_locations += ['Turtle Rock - Big Key Chest', 'Turtle Rock - Chain Chomps'] # set big key restrictions