diff --git a/BaseClasses.py b/BaseClasses.py index c2da8e91..d9952095 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -29,6 +29,8 @@ class World(object): self.doorShuffle = doorShuffle.copy() self.intensity = {} self.door_type_mode = {} + self.trap_door_mode = {} + self.key_logic_algorithm = {} self.logic = logic.copy() self.mode = mode.copy() self.swords = swords.copy() @@ -144,6 +146,8 @@ class World(object): set_player_attr('pot_pool', {}) set_player_attr('decoupledoors', False) set_player_attr('door_type_mode', 'original') + set_player_attr('trap_door_mode', 'vanilla') + set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') @@ -2434,6 +2438,8 @@ class Spoiler(object): 'door_shuffle': self.world.doorShuffle, 'intensity': self.world.intensity, 'door_type_mode': self.world.door_type_mode, + 'trap_door_mode': self.world.trap_door_mode, + 'key_logic': self.world.key_logic_algorithm, 'decoupledoors': self.world.decoupledoors, 'dungeon_counters': self.world.dungeon_counters, 'item_pool': self.world.difficulty, @@ -2640,6 +2646,8 @@ class Spoiler(object): if self.metadata['door_shuffle'][player] != 'vanilla': outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n") outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n") + outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n") + outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n") outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n") outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n") outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n") @@ -2931,15 +2939,19 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique # byte 10: settings_version -# byte 11: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode) +# byte 11: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions -# psuedoboots does not effect code -# sfx_shuffle and other adjust items does not effect settings code +# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) +overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} +trap_door_mode = {'vanilla': 0, 'boss': 1, 'oneway': 2} +key_logic_algo = {'loose': 0, 'default': 1, 'partial': 2, 'strict': 4} + +# sfx_shuffle and other adjust items does not affect settings code # Bump this when making changes that are not backwards compatible (nearly all of them) settings_version = 1 @@ -2983,7 +2995,10 @@ class Settings(object): settings_version, (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 - | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]) + | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), + + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6 + | trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3051,6 +3066,11 @@ class Settings(object): args.bow_mode[p] = r(bow_mode)[(settings[11] & 0x70) >> 4] args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2] args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] + if len(settings) > 12: + args.pseudoboots[p] = True if settings[12] & 0x80 else False + args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6] + args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4] + args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07] class KeyRuleType(FastEnum): diff --git a/CLI.py b/CLI.py index 0bbf1181..0df0d03a 100644 --- a/CLI.py +++ b/CLI.py @@ -140,7 +140,8 @@ def parse_cli(argv, no_defaults=False): 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', - 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode']: + 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', + 'trap_door_mode', 'key_logic_algorithm']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -214,6 +215,8 @@ def parse_settings(): 'door_shuffle': 'vanilla', 'intensity': 2, 'door_type_mode': 'original', + 'trap_door_mode': 'vanilla', + 'key_logic_algorithm': 'default', 'decoupledoors': False, 'experimental': False, 'dungeon_counters': 'default', diff --git a/DoorShuffle.py b/DoorShuffle.py index a4cf7397..f53fe867 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1781,58 +1781,61 @@ def shuffle_door_types(door_type_pools, paths, world, player): def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player): used_doors = set() for pool, door_type_pool in door_type_pools: - ttl = 0 - suggestion_map, trap_map, flex_map = {}, {}, {} - remaining = door_type_pool.traps - if player in world.custom_door_types: - custom_trap_doors = world.custom_door_types[player]['Trap Door'] - else: - custom_trap_doors = defaultdict(list) + if world.trap_door_mode[player] != 'oneway': + ttl = 0 + suggestion_map, trap_map, flex_map = {}, {}, {} + remaining = door_type_pool.traps + if player in world.custom_door_types: + custom_trap_doors = world.custom_door_types[player]['Trap Door'] + else: + custom_trap_doors = defaultdict(list) - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - find_trappable_candidates(builder, world, player) - if custom_trap_doors[dungeon]: - builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) - remaining -= len(custom_trap_doors[dungeon]) - ttl += len(builder.candidates.trap) - if ttl == 0: - continue - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - proportion = len(builder.candidates.trap) - calc = int(round(proportion * door_type_pool.traps/ttl)) - suggested = min(proportion, calc) - remaining -= suggested - suggestion_map[dungeon] = suggested - flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 - for dungeon in pool: - builder = world.dungeon_layouts[player][dungeon] - valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], - start_regions_map[dungeon], paths, world, player, - drop=True) - trap_map[dungeon] = valid_traps - if trap_number < suggestion_map[dungeon]: - flex_map[dungeon] = 0 - remaining += suggestion_map[dungeon] - trap_number - suggestion_map[dungeon] = trap_number - builder_order = [x for x in pool if flex_map[x] > 0] - random.shuffle(builder_order) - queue = deque(builder_order) - while len(queue) > 0 and remaining > 0: - dungeon = queue.popleft() - builder = world.dungeon_layouts[player][dungeon] - increased = suggestion_map[dungeon] + 1 - valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], - paths, world, player) - if valid_traps: + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + find_trappable_candidates(builder, world, player) # todo: + if custom_trap_doors[dungeon]: + builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon]) + remaining -= len(custom_trap_doors[dungeon]) + ttl += len(builder.candidates.trap) + if ttl == 0: + continue + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + proportion = len(builder.candidates.trap) + calc = int(round(proportion * door_type_pool.traps/ttl)) + suggested = min(proportion, calc) + remaining -= suggested + suggestion_map[dungeon] = suggested + flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0 + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon], + start_regions_map[dungeon], paths, world, player, + drop=True) trap_map[dungeon] = valid_traps - remaining -= 1 - suggestion_map[dungeon] = increased - flex_map[dungeon] -= 1 - if flex_map[dungeon] > 0: - queue.append(dungeon) - # time to re-assign + if trap_number < suggestion_map[dungeon]: + flex_map[dungeon] = 0 + remaining += suggestion_map[dungeon] - trap_number + suggestion_map[dungeon] = trap_number + builder_order = [x for x in pool if flex_map[x] > 0] + random.shuffle(builder_order) + queue = deque(builder_order) + while len(queue) > 0 and remaining > 0: + dungeon = queue.popleft() + builder = world.dungeon_layouts[player][dungeon] + increased = suggestion_map[dungeon] + 1 + valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon], + paths, world, player) + if valid_traps: + trap_map[dungeon] = valid_traps + remaining -= 1 + suggestion_map[dungeon] = increased + flex_map[dungeon] -= 1 + if flex_map[dungeon] > 0: + queue.append(dungeon) + # time to re-assign + else: + trap_map = {dungeon: [] for dungeon in pool} reassign_trap_doors(trap_map, world, player) for name, traps in trap_map.items(): used_doors.update(traps) @@ -2138,7 +2141,7 @@ def find_trappable_candidates(builder, world, player): for ext in world.get_region(r, player).exits: if ext.door: d = ext.door - if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name: + if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d): builder.candidates.trap.append(d) @@ -2281,7 +2284,7 @@ def reassign_trap_doors(trap_map, world, player): logger = logging.getLogger('') for name, traps in trap_map.items(): builder = world.dungeon_layouts[player][name] - queue = deque(find_current_trap_doors(builder)) + queue = deque(find_current_trap_doors(builder, world, player)) while len(queue) > 0: d = queue.pop() if d.type is DoorType.Interior and d not in traps: @@ -2304,12 +2307,21 @@ def reassign_trap_doors(trap_map, world, player): logger.debug('Trap Door: %s', d.name) -def find_current_trap_doors(builder): +def exclude_boss_traps(d): + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW', + 'Mire Warping Pool ES'] + +def exclude_logic_traps(d): + return d.name != 'Mire Warping Pool ES' + + +def find_current_trap_doors(builder, world, player): + checker = exclude_boss_traps if world.trap_door_mode[player] == 'vanilla' else exclude_logic_traps current_doors = [] for region in builder.master_sector.regions: for ext in region.exits: d = ext.door - if d and d.blocked and d.trapFlag != 0: # could exclude removing boss doors here + if d and d.blocked and d.trapFlag != 0 and checker(d): current_doors.append(d) return current_doors @@ -4550,8 +4562,8 @@ door_type_counts = { 'Agahnims Tower': (4, 0, 1, 0, 0, 1, 0), 'Swamp Palace': (6, 0, 0, 2, 0, 0, 0), 'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0), - 'Misery Mire': (6, 3, 5, 2, 0, 0, 0), - 'Skull Woods': (5, 0, 2, 2, 0, 1, 0), + 'Misery Mire': (6, 3, 4, 2, 0, 0, 0), + 'Skull Woods': (5, 0, 1, 2, 0, 1, 0), 'Ice Palace': (6, 1, 3, 0, 0, 0, 0), 'Tower of Hera': (1, 1, 0, 0, 0, 0, 0), 'Thieves Town': (3, 1, 2, 1, 1, 0, 0), diff --git a/Main.py b/Main.py index 02b7d032..83e14f47 100644 --- a/Main.py +++ b/Main.py @@ -110,6 +110,8 @@ def main(args, seed=None, fish=None): world.beemizer = args.beemizer.copy() world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.door_type_mode = args.door_type_mode.copy() + world.trap_door_mode = args.trap_door_mode.copy() + world.key_logic_algorithm = args.key_logic_algorithm.copy() world.decoupledoors = args.decoupledoors.copy() world.experimental = args.experimental.copy() world.dungeon_counters = args.dungeon_counters.copy() diff --git a/README.md b/README.md index bbb9b9bf..2cd2b881 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,27 @@ Four options here, and all of them only take effect if Dungeon Door Shuffle is n CLI: `--door_type_mode [original|big|all|chaos]` +### Trap Door Removal + +Three options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla. + +* Normal: This does not remove any trap doors. Note that boss trap doors are never shuffled in this mode. +* Remove Boss Traps: Boss traps are removed this includes the one near Mothula. +* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps. Note, that the trap door near the mire cutscene chest is left alone because it enforces the use of fire to get to the chest. + +CLI: `--trap_door_mode [vanilla|boss|oneway]` + +### Key Logic Algorithm + +Determines how small key door logic works. + +* Loose: Skips placement rules checks. Currently, experimental to see what kinds of problems can arise. +* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable) +* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches. +* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic. + +CLI: `--key_logic [loose|default|partial|strict]` + ### Decouple Doors This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions. diff --git a/mystery_example.yml b/mystery_example.yml index 275359fc..dc1a21a7 100644 --- a/mystery_example.yml +++ b/mystery_example.yml @@ -22,6 +22,15 @@ big: 2 all: 1 chaos: 1 + trap_door_mode: + vanilla: 1 + boss: 0 + oneway: 0 + key_logic_algorithm: + loose: 0 + default: 1 + partial: 0 + strict: 0 decoupledoors: off dropshuffle: on: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index a09b1722..0926a8c9 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -193,6 +193,21 @@ "chaos" ] }, + "trap_door_mode": { + "choices": [ + "vanilla", + "boss", + "oneway" + ] + }, + "key_logic_algorithm": { + "choices": [ + "loose", + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "action": "store_true", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 0d3d4cdd..8e574ee0 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -239,6 +239,19 @@ "all: Adds traps doors (and any future supported door types)", "chaos: Increases the number of door types in all dungeon pools" ], + "trap_door_mode" : [ + "Trap Door Removal (default: %(default)s)", + "vanilla: No trap door removal", + "boss: Remove boss traps", + "oneway: Remove annoying trap doors" + ], + "key_logic_algorithm": [ + "Key Logic Algorithm (default: %(default)s)", + "loose: Allow more randomization", + "default: Balance between safety and randomization", + "partial: Partial protection when using certain minor glitches", + "strict: Ensure small keys are available" + ], "decoupledoors" : [ "Door entrances and exits are decoupled" ], "experimental": [ "Enable experimental features. (default: %(default)s)" ], "dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ], diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 418d623b..3135572e 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -91,6 +91,17 @@ "randomizer.dungeon.door_type_mode.all": "Adds Trap Doors", "randomizer.dungeon.door_type_mode.chaos": "Increases all door types", + "randomizer.dungeon.trap_door_mode": "Trap Door Removal", + "randomizer.dungeon.trap_door_mode.vanilla": "No Removal", + "randomizer.dungeon.trap_door_mode.boss": "Remove Boss Traps", + "randomizer.dungeon.trap_door_mode.oneway": "Remove Annoying Traps", + + "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", + "randomizer.dungeon.key_logic_algorithm.loose": "Loose", + "randomizer.dungeon.key_logic_algorithm.default": "Default", + "randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection", + "randomizer.dungeon.key_logic_algorithm.strict": "Strict", + "randomizer.dungeon.experimental": "Enable Experimental Features", "randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index bdedbfba..cecfeeff 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -33,7 +33,7 @@ }, "door_type_mode": { "type": "selectbox", - "default": "basic", + "default": "original", "options": [ "original", "big", @@ -44,6 +44,25 @@ "width": 45 } }, + "trap_door_mode": { + "type": "selectbox", + "default": "vanilla", + "options": [ + "vanilla", + "boss", + "oneway" + ] + }, + "key_logic_algorithm": { + "type": "selectbox", + "default": "default", + "options": [ + "loose", + "default", + "partial", + "strict" + ] + }, "decoupledoors": { "type": "checkbox" }, "keydropshuffle": { "type": "checkbox" }, "pottery": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index af6e47ff..fd15253f 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -109,6 +109,8 @@ class CustomSettings(object): args.standardize_palettes[p]) args.intensity[p] = get_setting(settings['intensity'], args.intensity[p]) args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p]) + args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p]) + args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p]) args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p]) args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p]) args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p]) @@ -219,6 +221,8 @@ class CustomSettings(object): settings_dict[p]['door_shuffle'] = world.doorShuffle[p] settings_dict[p]['intensity'] = world.intensity[p] settings_dict[p]['door_type_mode'] = world.door_type_mode[p] + settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p] + settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p] settings_dict[p]['decoupledoors'] = world.decoupledoors[p] settings_dict[p]['logic'] = world.logic[p] settings_dict[p]['mode'] = world.mode[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index e5ecd68c..d6624d8f 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -98,6 +98,8 @@ SETTINGSTOPROCESS = { "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", "door_type_mode": "door_type_mode", + "trap_door_mode": "trap_door_mode", + "key_logic_algorithm": "key_logic_algorithm", "decoupledoors": "decoupledoors", "keydropshuffle": "keydropshuffle", "dropshuffle": "dropshuffle", diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index b5fef26d..322ed447 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -84,6 +84,8 @@ def roll_settings(weights): ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla' ret.intensity = get_choice('intensity') ret.door_type_mode = get_choice('door_type_mode') + ret.trap_door_mode = get_choice('trap_door_mode') + ret.key_logic_algorithm = get_choice('key_logic_algorithm') ret.decoupledoors = get_choice('decoupledoors') == 'on' ret.experimental = get_choice('experimental') == 'on' ret.collection_rate = get_choice('collection_rate') == 'on'