diff --git a/BaseClasses.py b/BaseClasses.py index ca44b0a3..dd39cfc9 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -146,7 +146,7 @@ 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('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'default') set_player_attr('shopsanity', False) @@ -1905,6 +1905,9 @@ class Door(object): return world.get_room(self.roomIndex, self.player).kind(self) return None + def dungeon_name(self): + return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave' + def __eq__(self, other): return isinstance(other, self.__class__) and self.name == other.name @@ -2962,7 +2965,7 @@ bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions # 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} +trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2} # sfx_shuffle and other adjust items does not affect settings code diff --git a/CLI.py b/CLI.py index 0df0d03a..536543f2 100644 --- a/CLI.py +++ b/CLI.py @@ -215,7 +215,7 @@ def parse_settings(): 'door_shuffle': 'vanilla', 'intensity': 2, 'door_type_mode': 'original', - 'trap_door_mode': 'vanilla', + 'trap_door_mode': 'optional', 'key_logic_algorithm': 'default', 'decoupledoors': False, 'experimental': False, diff --git a/DoorShuffle.py b/DoorShuffle.py index f1457b36..308a0731 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1760,12 +1760,12 @@ class DoorTypePool: self.tricky += counts[6] def chaos_shuffle(self, counts): - weights = [1, 2, 4, 3, 2, 1] + weights = [1, 2, 4, 3, 2] return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)] @staticmethod def get_choices(number): - return [max(number+i, 0) for i in range(-1, 5)] + return [max(number+i, 0) for i in range(-1, 4)] class BuilderDoorCandidates: @@ -1801,14 +1801,17 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) ttl = 0 suggestion_map, trap_map, flex_map = {}, {}, {} remaining = door_type_pool.traps - if player in world.custom_door_types: + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: 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) # todo: + if 'Mire Warping Pool' in builder.master_sector.region_set(): + custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player)) + world.custom_door_types[player]['Trap Door'] = custom_trap_doors + 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]) @@ -1852,6 +1855,10 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player) # time to re-assign else: trap_map = {dungeon: [] for dungeon in pool} + for dungeon in pool: + builder = world.dungeon_layouts[player][dungeon] + if 'Mire Warping Pool' in builder.master_sector.region_set(): + trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player)) reassign_trap_doors(trap_map, world, player) for name, traps in trap_map.items(): used_doors.update(traps) @@ -1863,7 +1870,7 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, ttl = 0 suggestion_map, bk_map, flex_map = {}, {}, {} remaining = door_type_pool.bigs - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'] else: custom_bk_doors = defaultdict(list) @@ -1925,7 +1932,7 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl suggestion_map, small_map, flex_map = {}, {}, {} remaining = door_type_pool.smalls total_keys = remaining - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'] else: custom_key_doors = defaultdict(list) @@ -2025,7 +2032,7 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl remaining_bomb = door_type_pool.bombable remaining_dash = door_type_pool.dashable - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'] custom_dash_doors = world.custom_door_types[player]['Dash Door'] else: @@ -2164,7 +2171,7 @@ def find_trappable_candidates(builder, world, player): def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True): trap_door_pool = builder.candidates.trap trap_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]: custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name] else: custom_trap_doors = [] @@ -2319,20 +2326,16 @@ def reassign_trap_doors(trap_map, world, player): d.blocked = False for d in traps: change_door_to_trap(d, world, player) - world.spoiler.set_door_type(d.name, 'Trap Door', player) - logger.debug('Trap Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player) + logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})') 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' + return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW'] def find_current_trap_doors(builder, world, player): - checker = exclude_boss_traps if world.trap_door_mode[player] == 'vanilla' else exclude_logic_traps + checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True) current_doors = [] for region in builder.master_sector.regions: for ext in region.exits: @@ -2452,7 +2455,7 @@ def find_big_key_door_candidates(region, checked, used, world, player): def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True): bk_door_pool = builder.candidates.big bk_doors_needed = suggested - if player in world.custom_door_types: + if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]: custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name] else: custom_bk_doors = [] @@ -2527,8 +2530,8 @@ def reassign_big_key_doors(bk_map, world, player): world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_big_key(d1, world, player) change_door_to_big_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}') + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -2545,8 +2548,8 @@ def reassign_big_key_doors(bk_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_big_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Big Key Door', player) - logger.debug(f'Big Key Door: {d.name}') + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player) + logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_big_key(d, world, player): @@ -2596,7 +2599,7 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k logger = logging.getLogger('') key_door_pool = list(builder.candidates.small) key_doors_needed = target - if player in world.custom_door_types: + if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]: custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name] else: custom_key_doors = [] @@ -2724,7 +2727,7 @@ def find_valid_bd_combination(builder, suggested, world, player): bd_door_pool = builder.candidates.bomb_dash bomb_doors_needed, dash_doors_needed = suggested ttl_needed = bomb_doors_needed + dash_doors_needed - if player in world.custom_door_types: + if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]: custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name] custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name] else: @@ -2800,7 +2803,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d1, kind, world, player) change_door_to_kind(d2, kind, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player) else: d = obj if d.type is DoorType.Interior: @@ -2814,7 +2817,7 @@ def do_bombable_dashable(proposal, kind, world, player): change_door_to_kind(d.dest, kind, world, player) add_pair(d, d.dest, world, player) spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(d.name, spoiler_type, player) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player) def find_current_bd_doors(builder, world): @@ -3017,8 +3020,8 @@ def reassign_key_doors(small_map, world, player): world.paired_doors[player].append(PairedDoor(d1.name, d2.name)) change_door_to_small_key(d1, world, player) change_door_to_small_key(d2, world, player) - world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player) - logger.debug('Key Door: %s', d1.name+' <-> '+d2.name) + world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})') else: d = obj if d.type is DoorType.Interior: @@ -3034,8 +3037,8 @@ def reassign_key_doors(small_map, world, player): if stateful_door(d.dest, dest_room.kind(d.dest)): change_door_to_small_key(d.dest, world, player) add_pair(d, d.dest, world, player) - world.spoiler.set_door_type(d.name, 'Key Door', player) - logger.debug('Key Door: %s', d.name) + world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player) + logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})') def change_door_to_small_key(d, world, player): @@ -3225,7 +3228,7 @@ def change_pair_type(door, new_type, world, player): room_b.change(door.dest.doorListPos, new_type) add_pair(door, door.dest, world, player) spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player) + world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player) def remove_pair_type_if_present(door, world, player): @@ -4578,7 +4581,7 @@ 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, 4, 2, 0, 0, 0), + 'Misery Mire': (6, 3, 5, 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), diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 7881df0a..9be244f8 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -3565,7 +3565,7 @@ def check_for_valid_layout(builder, sector_list, builder_info): builder.exception_list = list(sector_list) return True, {}, package except (GenerationException, NeutralizingException, OtherGenException) as e: - logging.getLogger('').info(f'Bailing on this layout for', e) + logging.getLogger('').info(f'Bailing on this layout for {builder.name}', exc_info=1) builder.split_dungeon_map = None builder.valid_proposal = None if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door: diff --git a/Main.py b/Main.py index 83e14f47..a39fbe57 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.7-u' +__version__ = '1.2.0.8-u' from source.classes.BabelFish import BabelFish diff --git a/README.md b/README.md index c7fd99bd..6af52f44 100644 --- a/README.md +++ b/README.md @@ -139,20 +139,25 @@ Four options here, and all of them only take effect if Dungeon Door Shuffle is n * Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously * Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change -* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled. +* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled, excluding those by bosses. * Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more. 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. +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. +* No Removal: This does not remove any trap doors. +* Removed If Blocking Path: Dungeon generation is relaxed to allow annoying trap doors to be removed if necessary. 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. -CLI: `--trap_door_mode [vanilla|boss|oneway]` +If trap doors are shuffled the first two option behave the same. The last option overrides the shuffle because there is nothing left to shuffle. Boss traps are never shuffled. + +In all cases, that the trap door near the mire cutscene chest (Mire Warping Pool ES) is left alone because it enforces the use of fire to get to the chest. + +CLI: `--trap_door_mode [vanilla|optional|boss|oneway]` ### Key Logic Algorithm diff --git a/RELEASENOTES.md b/RELEASENOTES.md index efa12d9f..0328487c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,8 +109,14 @@ These are now independent of retro mode and have three options: None, Random, an # Bug Fixes and Notes * 1.2.0.8-u - * Removed a Triforce text - * Fix for Desert Tiles 1 key door + * New Features: trap_door_mode and key_logic_algorithm + * Change S&Q in door shuffle + standard during escape to spawn as Uncle + * Fix for vanilla doors + certain ER modes + * Fix for unintentional decoupled door in standard + * Fix a problem with BK doors being one-sided + * Change to how wilds keys are placed in standard, better randomization + * Removed a Triforce text + * Fix for Desert Tiles 1 key door * 1.2.0.7-u * Fix for some misery mire key logic * Minor standard generation fix diff --git a/mystery_testsuite.yml b/mystery_testsuite.yml index aca535c3..f919b7cc 100644 --- a/mystery_testsuite.yml +++ b/mystery_testsuite.yml @@ -7,12 +7,30 @@ algorithm: district: 1 door_shuffle: vanilla: 1 - basic: 2 + basic: 1 + partitioned: 1 crossed: 3 # crossed yield more errors so is preferred intensity: 1: 1 2: 1 3: 2 # intensity 3 usually yield more errors +door_type_mode: + original: 2 + big: 2 + all: 1 + chaos: 1 +trap_door_mode: + vanilla: 3 # more errors + optional: 1 + boss: 1 + oneway: 1 +key_logic_algorithm: + default: 1 + partial: 0 + strict: 0 +decoupledoors: + off: 9 # more strict + on: 1 dropshuffle: on: 1 off: 1 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 53eb2f45..f85e51cd 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -196,6 +196,7 @@ "trap_door_mode": { "choices": [ "vanilla", + "optional", "boss", "oneway" ] diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index d27e69ab..4d5fb151 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -242,8 +242,9 @@ "trap_door_mode" : [ "Trap Door Removal (default: %(default)s)", "vanilla: No trap door removal", - "boss: Remove boss traps", - "oneway: Remove annoying trap doors" + "optional: Trap doors removed if blocking", + "boss: Also remove boss traps", + "oneway: Remove all annoying trap doors" ], "key_logic_algorithm": [ "Key Logic Algorithm (default: %(default)s)", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 54cd4965..b8166baa 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -93,8 +93,9 @@ "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.trap_door_mode.optional": "Removed If Blocking Path", + "randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps", + "randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps", "randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm", "randomizer.dungeon.key_logic_algorithm.default": "Default", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index d442ae21..9749486e 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -46,12 +46,16 @@ }, "trap_door_mode": { "type": "selectbox", - "default": "vanilla", + "default": "optional", "options": [ "vanilla", + "optional", "boss", "oneway" - ] + ], + "config": { + "width": 30 + } }, "key_logic_algorithm": { "type": "selectbox", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 8052b728..1eac3fb1 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -631,6 +631,7 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors, flag) + # same as above but traps are ignored, and flag is not used def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player): for door in get_doors(world, region, player): if door in proposed_map and door.name in valid_doors: @@ -651,6 +652,27 @@ class ExplorationState(object): elif not self.in_door_list(door, self.avail_doors): self.append_door_to_list(door, self.avail_doors) + # same as above but traps are checked for + def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player): + for door in get_doors(world, region, player): + if door in proposed_map and door.name in valid_doors: + self.visited_doors.add(door) + if self.can_traverse(door): + if door.controller is not None: + door = door.controller + if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors: + if not self.in_door_list_ic(door, self.unattached_doors): + self.append_door_to_list(door, self.unattached_doors) + else: + other = self.find_door_in_list(door, self.unattached_doors) + if self.crystal != other.crystal: + other.crystal = CrystalBarrier.Either + elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door, + self.event_doors): + self.append_door_to_list(door, self.event_doors) + elif not self.in_door_list(door, self.avail_doors): + self.append_door_to_list(door, self.avail_doors) + def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player): for door in get_doors(world, region, player): if self.can_traverse_ignore_traps(door) and door not in proposed_traps: @@ -837,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi local_state = state.copy() for region in search_regions: local_state.visit_region(region) - local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player) while len(local_state.avail_doors) > 0: explorable_door = local_state.next_avail_door() if explorable_door.door in proposed_map: @@ -848,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(connect_region)): local_state.visit_region(connect_region) - local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) + if world.trap_door_mode[player] == 'vanilla': + local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player) + else: + local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player) return local_state