diff --git a/DoorShuffle.py b/DoorShuffle.py index 59e412ef..3c614030 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -60,9 +60,12 @@ def link_doors(world, player): if world.mode[player] == 'standard': world.get_portal('Sanctuary', player).destination = True world.get_portal('Desert East', player).destination = True - world.get_portal('Skull 2 West', player).destination = True - world.get_portal('Turtle Rock Lazy Eyes', player).destination = True - world.get_portal('Turtle Rock Eye Bridge', player).destination = True + if world.mode[player] == 'inverted': + world.get_portal('Desert West', player).destination = True + if world.mode[player] == 'open': + world.get_portal('Skull 2 West', player).destination = True + world.get_portal('Turtle Rock Lazy Eyes', player).destination = True + world.get_portal('Turtle Rock Eye Bridge', player).destination = True else: analyze_portals(world, player) for portal in world.dungeon_portals[player]: @@ -638,7 +641,7 @@ def within_dungeon(world, player): dungeon_builders[key].entrance_list = list(entrances_map[key]) recombinant_builders = {} entrances, splits = create_dungeon_entrances(world, player) - builder_info = entrances, splits, world, player + builder_info = entrances, splits, connections_tuple, world, player handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) @@ -662,12 +665,12 @@ def within_dungeon(world, player): def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info): - dungeon_entrances, split_dungeon_entrances, world, player = builder_info + dungeon_entrances, split_dungeon_entrances, c_tuple, world, player = builder_info if dungeon_entrances is None: dungeon_entrances = default_dungeon_entrances if split_dungeon_entrances is None: split_dungeon_entrances = split_region_starts - builder_info = dungeon_entrances, split_dungeon_entrances, world, player + builder_info = dungeon_entrances, split_dungeon_entrances, c_tuple, world, player for name, split_list in split_dungeon_entrances.items(): builder = dungeon_builders.pop(name) @@ -886,7 +889,7 @@ def cross_dungeon(world, player): key_name = dungeon_keys[builder.name] if loc.name != 'Hyrule Castle - Big Key Drop' else dungeon_bigs[builder.name] loc.forced_item = loc.item = ItemFactory(key_name, player) recombinant_builders = {} - builder_info = entrances, splits, world, player + builder_info = entrances, splits, connections_tuple, world, player handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, builder_info) main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player) @@ -1609,6 +1612,7 @@ def change_door_to_small_key(d, world, player): def smooth_door_pairs(world, player): all_doors = [x for x in world.doors if x.player == player] skip = set() + bd_candidates, dashable_counts, bombable_counts = defaultdict(list), defaultdict(int), defaultdict(int) for door in all_doors: if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip and not door.entranceFlag: partner = door.dest @@ -1636,19 +1640,18 @@ def smooth_door_pairs(world, player): remove_pair(door, world, player) elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]: if valid_pair: - if type_a == type_b: - add_pair(door, partner, world, player) - spoiler_type = 'Bomb Door' if type_a == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player) - else: + new_type = type_a + if type_a != type_b: new_type = DoorKind.Dashable if type_a == DoorKind.Dashable or type_b == DoorKind.Dashable else DoorKind.Bombable if type_a != new_type: room_a.change(door.doorListPos, new_type) if type_b != new_type: room_b.change(partner.doorListPos, new_type) - add_pair(door, partner, world, player) - spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player) + add_pair(door, partner, world, player) + spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door' + world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player) + counter = bombable_counts if new_type == DoorKind.Bombable else dashable_counts + counter[door.entrance.parent_region.dungeon] += 1 else: if type_a in [DoorKind.Bombable, DoorKind.Dashable]: room_a.change(door.doorListPos, DoorKind.Normal) @@ -1656,8 +1659,9 @@ def smooth_door_pairs(world, player): elif type_b in [DoorKind.Bombable, DoorKind.Dashable]: room_b.change(partner.doorListPos, DoorKind.Normal) remove_pair(partner, world, player) - elif world.experimental[player] and valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey: - random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b) + elif valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey: + bd_candidates[door.entrance.parent_region.dungeon].append(door) + shuffle_bombable_dashable(bd_candidates, bombable_counts, dashable_counts, world, player) world.paired_doors[player] = [x for x in world.paired_doors[player] if x.pair or x.original] @@ -1694,17 +1698,61 @@ def stateful_door(door, kind): return False -def random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b): - r_kind = random.choices([DoorKind.Normal, DoorKind.Bombable, DoorKind.Dashable], [15, 4, 6], k=1)[0] - if r_kind != DoorKind.Normal: - if door.type == DoorType.Normal: - add_pair(door, partner, world, player) - if type_a != r_kind: - room_a.change(door.doorListPos, r_kind) - if type_b != r_kind: - room_b.change(partner.doorListPos, r_kind) - spoiler_type = 'Bomb Door' if r_kind == DoorKind.Bombable else 'Dash Door' - world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player) +def shuffle_bombable_dashable(bd_candidates, bombable_counts, dashable_counts, world, player): + if world.doorShuffle[player] == 'basic': + for dungeon, candidates in bd_candidates.items(): + diff = bomb_dash_counts[dungeon.name][1] - dashable_counts[dungeon] + if diff > 0: + for chosen in random.sample(candidates, min(diff, len(candidates))): + change_pair_type(chosen, DoorKind.Dashable, world, player) + candidates.remove(chosen) + diff = bomb_dash_counts[dungeon.name][0] - bombable_counts[dungeon] + if diff > 0: + for chosen in random.sample(candidates, min(diff, len(candidates))): + change_pair_type(chosen, DoorKind.Bombable, world, player) + candidates.remove(chosen) + for excluded in candidates: + remove_pair_type_if_present(excluded, world, player) + elif world.doorShuffle[player] == 'crossed': + all_candidates = sum(bd_candidates.values(), []) + all_bomb_counts = sum(bombable_counts.values()) + all_dash_counts = sum(dashable_counts.values()) + if all_dash_counts < 8: + for chosen in random.sample(all_candidates, min(8 - all_dash_counts, len(all_candidates))): + change_pair_type(chosen, DoorKind.Dashable, world, player) + world.spoiler.set_door_type(chosen.name + ' <-> ' + chosen.dest.name, DoorKind.Dashable, player) + all_candidates.remove(chosen) + if all_bomb_counts < 12: + for chosen in random.sample(all_candidates, min(12 - all_bomb_counts, len(all_candidates))): + change_pair_type(chosen, DoorKind.Bombable, world, player) + world.spoiler.set_door_type(chosen.name + ' <-> ' + chosen.dest.name, DoorKind.Bombable, player) + all_candidates.remove(chosen) + for excluded in all_candidates: + remove_pair_type_if_present(excluded, world, player) + + +def change_pair_type(door, new_type, world, player): + room_a = world.get_room(door.roomIndex, player) + room_a.change(door.doorListPos, new_type) + if door.type != DoorType.Interior: + room_b = world.get_room(door.dest.roomIndex, 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) + + +def remove_pair_type_if_present(door, world, player): + room_a = world.get_room(door.roomIndex, player) + if room_a.kind(door) in [DoorKind.Bombable, DoorKind.Dashable]: + room_a.change(door.doorListPos, DoorKind.Normal) + if door.type != DoorType.Interior: + remove_pair(door, world, player) + if door.type != DoorType.Interior: + room_b = world.get_room(door.dest.roomIndex, player) + if room_b.kind(door.dest) in [DoorKind.Bombable, DoorKind.Dashable]: + room_b.change(door.dest.doorListPos, DoorKind.Normal) + remove_pair(door.dest, world, player) def find_inaccessible_regions(world, player): @@ -1727,8 +1775,9 @@ def find_inaccessible_regions(world, player): queue.append(parent) for ext in next_region.exits: connect = ext.connected_region - if connect and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions: - queue.append(connect) + if connect and connect not in queue and connect not in visited_regions: + if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'): + queue.append(connect) world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)]) logger = logging.getLogger('') logger.debug('Inaccessible Regions:') @@ -2798,4 +2847,20 @@ split_portal_defaults = { } } +bomb_dash_counts = { + 'Hyrule Castle': (0, 2), + 'Eastern Palace': (0, 0), + 'Desert Palace': (0, 0), + 'Agahnims Tower': (0, 0), + 'Swamp Palace': (2, 0), + 'Palace of Darkness': (3, 2), + 'Misery Mire': (2, 0), + 'Skull Woods': (2, 0), + 'Ice Palace': (0, 0), + 'Tower of Hera': (0, 0), + 'Thieves Town': (1, 1), + 'Turtle Rock': (0, 2), # 2 bombs kind of for entrances + 'Ganons Tower': (2, 1) +} + diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 1ce3c637..7b2bf229 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -226,7 +226,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro o_state_cache = {} for sector in available_sectors: for door in sector.outstanding_doors: - if not door.stonewall and door not in proposed_map.keys(): + if door not in proposed_map.keys(): hanger_set.add(door) bk_flag = group_flags[door_map[door]] parent = door.entrance.parent_region @@ -1271,7 +1271,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections, dungeon_entrances, split_dungeon_entrances) for name, builder in dungeon_map.items(): - calc_allowance_and_dead_ends(builder, connections_tuple, world.dungeon_portals[player]) + calc_allowance_and_dead_ends(builder, connections_tuple, world, player) if world.mode[player] == 'open' and world.shuffle[player] not in ['crossed', 'insanity']: sanc = find_sector('Sanctuary', candidate_sectors) @@ -1318,7 +1318,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, # restart raise NeutralizingException('Either free location/crystal assignment is already globally invalid') logger.info(world.fish.translate("cli", "cli", "balance.doors")) - builder_info = dungeon_entrances, split_dungeon_entrances, world, player + builder_info = dungeon_entrances, split_dungeon_entrances, connections_tuple, world, player assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builder_info) # the rest assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info) @@ -1376,26 +1376,31 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, break -def calc_allowance_and_dead_ends(builder, connections_tuple, portals): +# todo: split version that adds allowance for potential entrances +def calc_allowance_and_dead_ends(builder, connections_tuple, world, player): + portals = world.dungeon_portals[player] entrances_map, potentials, connections = connections_tuple - needed_connections = [x for x in builder.all_entrances if x not in entrances_map[builder.name]] + name = builder.name if not builder.split_flag else builder.name.rsplit(' ', 1)[0] + needed_connections = [x for x in builder.all_entrances if x not in entrances_map[name]] starting_allowance = 0 used_sectors = set() destination_entrances = [x.door.entrance.parent_region.name for x in portals if x.destination] - for entrance in entrances_map[builder.name]: + dead_ends = [x.door.entrance.parent_region.name for x in portals if x.deadEnd] + for entrance in entrances_map[name]: sector = find_sector(entrance, builder.sectors) - outflow_target = 0 if entrance not in drop_entrances_allowance else 1 - if sector not in used_sectors and sector.adj_outflow() > outflow_target: - if entrance not in destination_entrances: - starting_allowance += 1 - else: - builder.branches -= 1 - used_sectors.add(sector) - elif sector not in used_sectors: - if entrance in destination_entrances and sector.branches() > 0: - builder.branches -= 1 - if entrance not in drop_entrances_allowance: - needed_connections.append(entrance) + if sector: + outflow_target = 0 if entrance not in drop_entrances_allowance else 1 + if sector not in used_sectors and (sector.adj_outflow() > outflow_target or entrance in dead_ends): + if entrance not in destination_entrances: + starting_allowance += 1 + else: + builder.branches -= 1 + used_sectors.add(sector) + elif sector not in used_sectors: + if entrance in destination_entrances and sector.branches() > 0: + builder.branches -= 1 + if entrance not in drop_entrances_allowance: + needed_connections.append(entrance) builder.allowance = starting_allowance for entrance in needed_connections: sector = find_sector(entrance, builder.sectors) @@ -1404,7 +1409,11 @@ def calc_allowance_and_dead_ends(builder, connections_tuple, portals): connect_able = False if entrance in connections.keys(): enabling_region = connections[entrance] - connecting_entrances = [x for x in potentials[enabling_region] if x != entrance and x not in dead_entrances and x not in drop_entrances_allowance] + check_list = list(potentials[enabling_region]) + if enabling_region.name in ['Desert Ledge', 'Desert Palace Entrance (North) Spot']: + alternate = 'Desert Palace Entrance (North) Spot' if enabling_region.name == 'Desert Ledge' else 'Desert Ledge' + check_list.extend(potentials[world.get_region(alternate, player)]) + connecting_entrances = [x for x in check_list if x != entrance and x not in dead_entrances and x not in drop_entrances_allowance] connect_able = len(connecting_entrances) > 0 if is_destination and sector.branches() == 0: # builder.dead_ends += 1 @@ -2633,7 +2642,7 @@ def valid_entrance(builder, sector_list, builder_info): if len(builder.sectors) == 0: is_dead_end = True else: - entrances, splits, world, player = builder_info + entrances, splits, c_tuple, world, player = builder_info if builder.name not in entrances.keys(): name_parts = builder.name.rsplit(' ', 1) entrance_list = splits[name_parts[0]][name_parts[1]] @@ -2765,7 +2774,7 @@ def split_dungeon_builder(builder, split_list, builder_info): continue elif len(split_entrances) <= 0: continue - x, y, world, player = builder_info + ents, splits, c_tuple, world, player = builder_info r_name = split_entrances[0] p = next(x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name) if not p.deadEnd: @@ -2782,6 +2791,7 @@ def split_dungeon_builder(builder, split_list, builder_info): sub_builder.all_entrances.extend(split_entrances) if key not in dungeon_map: dungeon_map[key] = sub_builder = DungeonBuilder(key) + sub_builder.split_flag = True sub_builder.all_entrances = list(split_entrances) for r_name in split_entrances: assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole) @@ -2799,6 +2809,9 @@ def split_dungeon_builder(builder, split_list, builder_info): def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info): + dungeon_entrances, split_dungeon_entrances, connections_tuple, world, player = builder_info + for name, builder in dungeon_map.items(): + calc_allowance_and_dead_ends(builder, connections_tuple, world, player) comb_w_replace = len(dungeon_map) ** len(candidate_sectors) if comb_w_replace <= 10000: combinations = list(itertools.product(dungeon_map.keys(), repeat=len(candidate_sectors))) @@ -3217,7 +3230,7 @@ def identify_branching_issues(dungeon_map, builder_info): def check_for_valid_layout(builder, sector_list, builder_info): - dungeon_entrances, split_dungeon_entrances, world, player = builder_info + dungeon_entrances, split_dungeon_entrances, c_tuple, world, player = builder_info if builder.name in split_dungeon_entrances.keys(): try: temp_builder = DungeonBuilder(builder.name) diff --git a/Main.py b/Main.py index 06c8228b..aaed4a61 100644 --- a/Main.py +++ b/Main.py @@ -25,7 +25,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute from ItemList import generate_itempool, difficulties, fill_prizes, fill_specific_items from Utils import output_path, parse_player_names -__version__ = '0.2.0.17u' +__version__ = '0.2.0.18u' class EnemizerError(RuntimeError): pass diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50f191b4..46f32ff9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -83,15 +83,19 @@ testing to verify logic is all good. ### Experimental features -* Only the random bomb doors and the item counter are currently experimental +* Only the item counter is currently experimental * Item counter is suppressed in Triforce Hunt + #### Temporary debug features * Removed the red square in the upper right corner of the hud if the castle gate is closed # Bug Fixes +* 2.0.18u + * Generation improvements + * Bombs/Dash doors more consistent with the amount in vanilla. * 2.0.17u * Generation improvements * 2.0.16u diff --git a/Rom.py b/Rom.py index ff65fec3..620470e9 100644 --- a/Rom.py +++ b/Rom.py @@ -1375,38 +1375,42 @@ def patch_rom(world, rom, player, team, enemized): rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) - rom.write_byte(0x18004E, 0) # Escape Fill (nothing) - write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0,0,0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0,0,0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0,0,0]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_byte(0x18004E, 0) # Escape Fill (nothing) + write_int16(rom, 0x180183, 300) # Escape fill rupee bow + rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) bow_max, bomb_max, magic_max = 0, 0, 0 + bow_small, magic_small = 0, 0 if world.mode[player] == 'standard': if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']: - rom.write_byte(0x18004E, 1) # Escape Fill (arrows) - write_int16(rom, 0x180183, 300) # Escape fill rupee bow - rom.write_bytes(0x180185, [0,0,70]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0,0,10]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0,0,10]) # Mantle respawn refills (magic, bombs, arrows) - bow_max = 70 + rom.write_byte(0x18004E, 1) # Escape Fill (arrows) + write_int16(rom, 0x180183, 300) # Escape fill rupee bow + rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows) + bow_max, bow_small = 70, 10 elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']: - rom.write_byte(0x18004E, 2) # Escape Fill (bombs) - rom.write_bytes(0x180185, [0,50,0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0,3,0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0,3,0]) # Mantle respawn refills (magic, bombs, arrows) + rom.write_byte(0x18004E, 2) # Escape Fill (bombs) + rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0, 3, 0]) # Mantle respawn refills (magic, bombs, arrows) bomb_max = 50 elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']: - rom.write_byte(0x18004E, 4) # Escape Fill (magic) - rom.write_bytes(0x180185, [0x80,0,0]) # Uncle respawn refills (magic, bombs, arrows) - rom.write_bytes(0x180188, [0x20,0,0]) # Zelda respawn refills (magic, bombs, arrows) - rom.write_bytes(0x18018B, [0x20,0,0]) # Mantle respawn refills (magic, bombs, arrows) - magic_max = 0x80 + rom.write_byte(0x18004E, 4) # Escape Fill (magic) + rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows) + rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows) + magic_max, magic_small = 0x80, 0x20 if world.doorShuffle[player] == 'crossed': # Uncle respawn refills (magic, bombs, arrows) rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)]) rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows) rom.write_bytes(0x18018B, [0x20, 3, 10]) # Mantle respawn refills (magic, bombs, arrows) - + elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest + rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)]) + rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows) + rom.write_bytes(0x18018B, [magic_small, 3, bow_small]) # Mantle respawn refills (magic, bombs, arrows) # patch swamp: Need to enable permanent drain of water as dam or swamp were moved rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)