diff --git a/BaseClasses.py b/BaseClasses.py index df4b8426..6087ef20 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2382,11 +2382,11 @@ class Spoiler(object): listed_locations.update(cave_locations) for dungeon in self.world.dungeons: - dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item] + dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item and not loc.skip] self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations]) listed_locations.update(dungeon_locations) - other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations] + other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and not loc.skip] if other_locations: self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) listed_locations.update(other_locations) diff --git a/DoorShuffle.py b/DoorShuffle.py index 8417a4f3..be164cbf 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2302,6 +2302,8 @@ logical_connections = [ ('TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze Interior'), ('TR Crystal Maze End to Ranged Crystal', 'TR Crystal Maze End - Ranged Crystal'), ('TR Crystal Maze End Ranged Crystal Exit', 'TR Crystal Maze End'), + ('TR Final Abyss Balcony Path', 'TR Final Abyss Ledge'), + ('TR Final Abyss Ledge Path', 'TR Final Abyss Balcony'), ('GT Blocked Stairs Block Path', 'GT Big Chest'), ('GT Speed Torch South Path', 'GT Speed Torch'), diff --git a/Doors.py b/Doors.py index a23f46f0..b6fe5bdd 100644 --- a/Doors.py +++ b/Doors.py @@ -1048,6 +1048,8 @@ def create_doors(world, player): create_door(player, 'TR Crystal Maze End Ranged Crystal Exit', Lgcl), create_door(player, 'TR Crystal Maze North Stairs', StrS).dir(No, 0xc4, Mid, High), create_door(player, 'TR Final Abyss South Stairs', StrS).dir(So, 0xb4, Mid, High), + create_door(player, 'TR Final Abyss Balcony Path', Lgcl), + create_door(player, 'TR Final Abyss Ledge Path', Lgcl), create_door(player, 'TR Final Abyss NW', Nrml).dir(No, 0xb4, Left, High).big_key().pos(0), create_door(player, 'TR Boss SW', Nrml).dir(So, 0xa4, Left, High).no_exit().trap(0x4).pos(0), # .portal(Z, 0x00), -enemizer doesn't work diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5be223c9..f0038f45 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -1326,7 +1326,17 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, polarized_sectors[sector] = None if bow_sectors: assign_bow_sectors(dungeon_map, bow_sectors, global_pole) - assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player) + leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player) + free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole) + for sector in free_location_sectors: + if sector.c_switch: + crystal_switches[sector] = None + elif sector.blue_barrier: + crystal_barriers[sector] = None + elif sector.polarity().is_neutral(): + neutral_sectors[sector] = None + else: + polarized_sectors[sector] = None leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole) ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole) for sector in leftover: @@ -1558,33 +1568,85 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole): assign_sector(sector_list[i], builder, bow_sectors, global_pole) -def assign_location_sectors(dungeon_map, free_location_sectors, global_pole, world, player): +def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole): + population = [n for n in dungeon_map.keys()] + k = round(len(free_location_sectors) * .50) valid = False choices = None + candidates = [] + sector_list = list(free_location_sectors) + while not valid: + candidates = random.sample(sector_list, k=k) + choices = random.choices(population, k=len(candidates)) + sector_dict = defaultdict(list) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + sector_dict[builder].append(candidates[i]) + valid = global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), sector_dict) + for i, choice in enumerate(choices): + builder = dungeon_map[choice] + assign_sector(candidates[i], builder, free_location_sectors, global_pole) + return free_location_sectors + + +def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player): + valid = False + choices = defaultdict(list) sector_list = list(free_location_sectors) random.shuffle(sector_list) orig_location_set = build_orig_location_set(dungeon_map) num_dungeon_items = requested_dungeon_items(world, player) + d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())} + next_sector = sector_list.pop() while not valid: - choices, d_idx, totals = weighted_random_locations(dungeon_map, sector_list) - location_set = {x: set(y) for x, y in orig_location_set.items()} - for i, sector in enumerate(sector_list): - d_name = choices[i].name - choice = d_idx[d_name] - totals[choice] += sector.chest_locations - location_set[d_name].update(sector.chest_location_set) - valid = True - for d_name, idx in d_idx.items(): - free_items = count_reserved_locations(world, player, location_set[d_name]) - target = max(free_items, 2) + num_dungeon_items - if totals[idx] < target: - valid = False - break - for i, choice in enumerate(choices): - builder = dungeon_map[choice.name] - assign_sector(sector_list[i], builder, free_location_sectors, global_pole) + choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, world, player) + if not choice: + break + choices[choice].append(next_sector) + if global_pole.is_valid_multi_choice_2(dungeon_map, dungeon_map.values(), choices): + idx = d_idx[choice.name] + totals[idx] += next_sector.chest_locations + location_set[choice.name].update(next_sector.chest_location_set) + valid = True + for d_name, idx in d_idx.items(): + free_items = count_reserved_locations(world, player, location_set[d_name]) + target = max(free_items, 2) + num_dungeon_items + if totals[idx] < target: + valid = False + break + if not valid: + if len(sector_list) == 0: + choices = defaultdict(list) + sector_list = list(free_location_sectors) + else: + next_sector = sector_list.pop() + else: + choices[choice].remove(next_sector) + for builder, choice_list in choices.items(): + for choice in choice_list: + assign_sector(choice, builder, free_location_sectors, global_pole) + return free_location_sectors +def weighted_random_location(dungeon_map, choices, orig_location_set, world, player): + population = [] + totals = [] + location_set = {x: set(y) for x, y in orig_location_set.items()} + num_dungeon_items = requested_dungeon_items(world, player) + for i, dungeon_builder in enumerate(dungeon_map.values()): + ttl = dungeon_builder.location_cnt + sum(sector.chest_locations for sector in choices[dungeon_builder]) + totals.append(ttl) + builder_set = location_set[dungeon_builder.name] + builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder]))) + free_items = count_reserved_locations(world, player, builder_set) + target = max(free_items, 2) + num_dungeon_items + if ttl < target: + population.append(dungeon_builder) + choice = random.choice(population) if len(population) > 0 else None + return choice, totals, location_set + + +# deprecated def weighted_random_locations(dungeon_map, free_location_sectors): population = [] ttl_assigned = 0 diff --git a/Dungeons.py b/Dungeons.py index 08bb25ba..753ff465 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -170,15 +170,17 @@ mire_regions = [ tr_regions = [ 'TR Main Lobby', 'TR Lobby Ledge', 'TR Compass Room', 'TR Hub', 'TR Torches Ledge', 'TR Torches', 'TR Roller Room', 'TR Tile Room', 'TR Refill', 'TR Pokey 1', 'TR Chain Chomps Top', 'TR Chain Chomps Top - Crystal', - 'TR Chain Chomps Bottom', 'TR Chain Chomps Bottom - Ranged Crystal', 'TR Pipe Pit', 'TR Pipe Ledge', 'TR Lava Dual Pipes', - 'TR Lava Island', 'TR Lava Escape', 'TR Pokey 2 Top', 'TR Pokey 2 Top - Crystal', 'TR Pokey 2 Bottom', 'TR Pokey 2 Bottom - Ranged Crystal', - 'TR Twin Pokeys', 'TR Hallway', 'TR Dodgers', 'TR Big View','TR Big Chest', 'TR Big Chest Entrance', - 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', 'TR Rupees', 'TR Crystaroller Bottom', - 'TR Crystaroller Middle', 'TR Crystaroller Top', 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', - 'TR Crystaroller Middle - Ranged Crystal', 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dash Bridge', 'TR Eye Bridge', + 'TR Chain Chomps Bottom', 'TR Chain Chomps Bottom - Ranged Crystal', 'TR Pipe Pit', 'TR Pipe Ledge', + 'TR Lava Dual Pipes', 'TR Lava Island', 'TR Lava Escape', 'TR Pokey 2 Top', 'TR Pokey 2 Top - Crystal', + 'TR Pokey 2 Bottom', 'TR Pokey 2 Bottom - Ranged Crystal', 'TR Twin Pokeys', 'TR Hallway', 'TR Dodgers', + 'TR Big View','TR Big Chest', 'TR Big Chest Entrance', 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', + 'TR Rupees', 'TR Crystaroller Bottom', 'TR Crystaroller Middle', 'TR Crystaroller Top', + 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Crystaroller Middle - Ranged Crystal', + 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End', - 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss', 'TR Boss', 'Turtle Rock Main Portal', - 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', 'Turtle Rock Eye Bridge Portal' + 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss', + 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', + 'Turtle Rock Eye Bridge Portal' ] gt_regions = [ diff --git a/Fill.py b/Fill.py index 5d214af2..071ec65a 100644 --- a/Fill.py +++ b/Fill.py @@ -7,6 +7,7 @@ from BaseClasses import CollectionState, FillError, LocationType from Items import ItemFactory from Regions import shop_to_location_table, retro_shops from source.item.FillUtil import filter_locations, classify_major_items, replace_trash_item, vanilla_fallback +from source.item.FillUtil import filter_pot_locations def get_dungeon_item_pool(world): @@ -362,6 +363,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None world.itempool.remove(pot_item) pot_locations = [location for location in fill_locations if location.type == LocationType.Pot and location.player == player] + pot_locations = filter_pot_locations(pot_locations, world) fast_fill_helper(world, pot_pool, pot_locations) pots_used = True if pots_used: diff --git a/Main.py b/Main.py index 04d7cf4f..66dde5b7 100644 --- a/Main.py +++ b/Main.py @@ -14,7 +14,7 @@ from Bosses import place_bosses from Items import ItemFactory from KeyDoorShuffle import validate_key_placement from OverworldGlitchRules import create_owg_connections -from PotShuffle import shuffle_pots +from PotShuffle import shuffle_pots, shuffle_pot_switches from Regions import create_regions, create_shops, mark_light_world_regions, create_dungeon_regions, adjust_locations from InvertedRegions import create_inverted_regions, mark_dark_world_regions from EntranceShuffle import link_entrances, link_inverted_entrances @@ -157,8 +157,11 @@ def main(args, seed=None, fish=None): if any(world.potshuffle.values()): logger.info(world.fish.translate("cli", "cli", "shuffling.pots")) for player in range(1, world.players + 1): - if world.potshuffle[player] and world.keydropshuffle[player] != 'potsanity': - shuffle_pots(world, player) + if world.potshuffle[player]: + if world.keydropshuffle[player] != 'potsanity': + shuffle_pots(world, player) + else: + shuffle_pot_switches(world, player) logger.info(world.fish.translate("cli","cli","shuffling.world")) diff --git a/PotShuffle.py b/PotShuffle.py index eb0a481d..7217c0c8 100644 --- a/PotShuffle.py +++ b/PotShuffle.py @@ -1,6 +1,6 @@ from collections import defaultdict -from BaseClasses import PotItem, Pot, PotFlags, CrystalBarrier +from BaseClasses import PotItem, Pot, PotFlags, CrystalBarrier, LocationType from Utils import int16_as_bytes, pc_to_snes movable_switch_rooms = defaultdict(lambda: [], @@ -79,8 +79,10 @@ vanilla_pots = { 61: [Pot(76, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(112, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(24, 22, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(40, 22, PotItem.FiveArrows, 'GT Crystal Inner Circle'), Pot(32, 24, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(20, 26, PotItem.FiveRupees, 'GT Crystal Inner Circle'), Pot(36, 26, PotItem.BigMagic, 'GT Crystal Inner Circle')], 62: [Pot(96, 6, PotItem.Bomb, 'Ice Stalfos Hint'), Pot(100, 6, PotItem.SmallMagic, 'Ice Stalfos Hint'), Pot(88, 10, PotItem.Heart, 'Ice Stalfos Hint'), Pot(92, 10, PotItem.SmallMagic, 'Ice Stalfos Hint')], - 0x3F: [Pot(12, 25, PotItem.OneRupee, 'Ice Hammer Block'), Pot(20, 25, PotItem.OneRupee, 'Ice Hammer Block'), Pot(12, 26, PotItem.Bomb, 'Ice Hammer Block'), Pot(20, 26, PotItem.Bomb, 'Ice Hammer Block'), - Pot(12, 27, PotItem.Switch, 'Ice Hammer Block'), Pot(20, 27, PotItem.Heart, 'Ice Hammer Block'), Pot(28, 23, PotItem.Key, 'Ice Hammer Block')], + 0x3F: [Pot(12, 25, PotItem.OneRupee, 'Ice Hammer Block'), Pot(20, 25, PotItem.OneRupee, 'Ice Hammer Block'), + Pot(12, 26, PotItem.Bomb, 'Ice Hammer Block'), Pot(20, 26, PotItem.Bomb, 'Ice Hammer Block'), + Pot(12, 27, PotItem.Switch, 'Ice Hammer Block'), Pot(20, 27, PotItem.Heart, 'Ice Hammer Block'), + Pot(28, 23, PotItem.Key, 'Ice Hammer Block')], 0x41: [Pot(100, 10, PotItem.Heart, 'Sewers Behind Tapestry'), Pot(52, 15, PotItem.OneRupee, 'Sewers Behind Tapestry'), Pot(52, 16, PotItem.SmallMagic, 'Sewers Behind Tapestry'), Pot(148, 22, PotItem.SmallMagic, 'Sewers Behind Tapestry')], 0x43: [Pot(66, 4, PotItem.FiveArrows, 'Desert Wall Slide'), Pot(78, 4, PotItem.SmallMagic, 'Desert Wall Slide'), Pot(66, 9, PotItem.Heart, 'Desert Wall Slide'), Pot(78, 9, PotItem.Heart, 'Desert Wall Slide'), Pot(112, 28, PotItem.Nothing, 'Desert Tiles 2'), Pot(76, 28, PotItem.Nothing, 'Desert Tiles 2'), Pot(76, 20, PotItem.Nothing, 'Desert Tiles 2'), Pot(112, 20, PotItem.Key, 'Desert Tiles 2')], @@ -191,7 +193,7 @@ vanilla_pots = { 145: [Pot(84, 4, PotItem.Heart, 'Mire Falling Foes'), Pot(104, 4, PotItem.SmallMagic, 'Mire Falling Foes')], 146: [Pot(86, 23, PotItem.Nothing, 'Mire Tall Dark and Roomy'), Pot(92, 23, PotItem.Nothing, 'Mire Tall Dark and Roomy'), Pot(98, 23, PotItem.Nothing, 'Mire Tall Dark and Roomy'), Pot(104, 23, PotItem.Nothing, 'Mire Tall Dark and Roomy')], 0x93: [Pot(28, 7, PotItem.Switch, 'Mire Dark Shooters'), - Pot(0x9C, 0x17, PotItem.Switch, 'Mire Block X', PotFlags.Block), + Pot(0x9C, 0x17, PotItem.Nothing, 'Mire Block X', PotFlags.Block), Pot(96, 7, PotItem.Heart, 'Mire Dark Shooters', PotFlags.NoSwitch)], 0x96: [Pot(14, 18, PotItem.Nothing, 'GT Torch Cross'), Pot(32, 5, PotItem.Nothing, 'GT Torch Cross'), @@ -243,13 +245,13 @@ vanilla_pots = { Pot(48, 0xA, PotItem.Nothing, 'Mire BK Door Room', PotFlags.LowerRegion), Pot(76, 0xA, PotItem.Nothing, 'Mire BK Door Room', PotFlags.LowerRegion)], 0xB3: [Pot(12, 20, PotItem.Key, 'Mire Spikes'), Pot(48, 20, PotItem.SmallMagic, 'Mire Spikes'), Pot(48, 28, PotItem.Switch, 'Mire Spikes')], - 180: [Pot(44, 28, PotItem.BigMagic, 'TR Final Abyss'), Pot(48, 28, PotItem.Heart, 'TR Final Abyss')], - 181: [Pot(112, 4, PotItem.FiveRupees, 'TR Dark Ride'), Pot(112, 15, PotItem.Heart, 'TR Dark Ride'), Pot(76, 16, PotItem.Switch, 'TR Dark Ride'), Pot(112, 16, PotItem.BigMagic, 'TR Dark Ride'), Pot(112, 17, PotItem.Heart, 'TR Dark Ride'), - Pot(112, 28, PotItem.Bomb, 'TR Dark Ride')], - 182: [Pot(94, 9, PotItem.BigMagic, 'TR Refill')], - 183: [Pot(30, 5, PotItem.SmallMagic, 'TR Roller Room')], - 184: [Pot(96, 13, PotItem.Switch, 'Eastern Big Key'), Pot(88, 16, PotItem.Heart, 'Eastern Big Key'), Pot(104, 16, PotItem.Heart, 'Eastern Big Key')], - 185: [Pot(92, 18, PotItem.OneRupee, 'Eastern Cannonball'), Pot(96, 18, PotItem.FiveRupees, 'Eastern Cannonball'), Pot(104, 18, PotItem.FiveRupees, 'Eastern Cannonball'), Pot(108, 18, PotItem.OneRupee, 'Eastern Cannonball')], + 0xB4: [Pot(44, 28, PotItem.BigMagic, 'TR Final Abyss Balcony'), Pot(48, 28, PotItem.Heart, 'TR Final Abyss Balcony')], + 0xB5: [Pot(112, 4, PotItem.FiveRupees, 'TR Dark Ride'), Pot(112, 15, PotItem.Heart, 'TR Dark Ride'), Pot(76, 16, PotItem.Switch, 'TR Dark Ride'), Pot(112, 16, PotItem.BigMagic, 'TR Dark Ride'), Pot(112, 17, PotItem.Heart, 'TR Dark Ride'), + Pot(112, 28, PotItem.Bomb, 'TR Dark Ride')], + 0xB6: [Pot(94, 9, PotItem.BigMagic, 'TR Refill')], + 0xB7: [Pot(30, 5, PotItem.SmallMagic, 'TR Roller Room')], + 0xB8: [Pot(96, 13, PotItem.Switch, 'Eastern Big Key'), Pot(88, 16, PotItem.Heart, 'Eastern Big Key'), Pot(104, 16, PotItem.Heart, 'Eastern Big Key')], + 0xB9: [Pot(92, 18, PotItem.OneRupee, 'Eastern Cannonball'), Pot(96, 18, PotItem.FiveRupees, 'Eastern Cannonball'), Pot(104, 18, PotItem.FiveRupees, 'Eastern Cannonball'), Pot(108, 18, PotItem.OneRupee, 'Eastern Cannonball')], 0xBA: [Pot(100, 8, PotItem.Nothing, 'Eastern Dark Pots'), Pot(88, 8, PotItem.Nothing, 'Eastern Dark Pots'), Pot(94, 4, PotItem.OneRupee, 'Eastern Dark Pots'), Pot(76, 6, PotItem.Heart, 'Eastern Dark Pots'), Pot(112, 6, PotItem.Key, 'Eastern Dark Pots'), Pot(76, 10, PotItem.Heart, 'Eastern Dark Pots'), Pot(112, 10, PotItem.SmallMagic, 'Eastern Dark Pots'), Pot(94, 12, PotItem.OneRupee, 'Eastern Dark Pots')], 0xBC: [Pot(86, 4, PotItem.Heart, 'Thieves Hallway'), Pot(102, 4, PotItem.Key, 'Thieves Hallway'), Pot(138, 3, PotItem.Bomb, 'Thieves Conveyor Maze'), Pot(178, 3, PotItem.Switch, 'Thieves Conveyor Maze'), @@ -271,8 +273,10 @@ vanilla_pots = { 201: [Pot(30, 22, PotItem.OneRupee, 'Eastern Lobby'), Pot(94, 22, PotItem.OneRupee, 'Eastern Lobby'), Pot(60, 22, PotItem.Switch, 'Eastern Lobby')], 203: [Pot(80, 4, PotItem.Nothing, 'Thieves Ambush'), Pot(80, 28, PotItem.Nothing, 'Thieves Ambush'), Pot(88, 16, PotItem.Heart, 'Thieves Ambush'), Pot(88, 28, PotItem.FiveRupees, 'Thieves Ambush')], 204: [Pot(36, 4, PotItem.FiveRupees, 'Thieves Rail Ledge'), Pot(36, 28, PotItem.FiveRupees, 'Thieves Rail Ledge'), Pot(112, 4, PotItem.Heart, 'Thieves BK Corner'), Pot(112, 28, PotItem.Bomb, 'Thieves BK Corner')], - 206: [Pot(76, 8, PotItem.SmallMagic, 'Ice Antechamber'), Pot(80, 8, PotItem.SmallMagic, 'Ice Antechamber'), Pot(108, 12, PotItem.Bomb, 'Ice Antechamber'), Pot(112, 12, PotItem.FiveArrows, 'Ice Antechamber'), - Pot(204, 11, PotItem.Hole, 'Ice Antechamber')], # Pot(0x6c, 8, PotItem.Nothing, 'Ice Antechamber', PotFlags.Block) this one has jellies + 0xCE: [Pot(76, 8, PotItem.SmallMagic, 'Ice Antechamber'), Pot(80, 8, PotItem.SmallMagic, 'Ice Antechamber'), + Pot(108, 12, PotItem.Bomb, 'Ice Antechamber'), Pot(112, 12, PotItem.FiveArrows, 'Ice Antechamber'), + Pot(204, 11, PotItem.Hole, 'Ice Antechamber'), + Pot(0x6c, 8, PotItem.Nothing, 'Ice Antechamber', PotFlags.Block)], 208: [Pot(158, 5, PotItem.SmallMagic, 'Tower Dark Maze'), Pot(140, 11, PotItem.OneRupee, 'Tower Dark Maze'), Pot(42, 13, PotItem.SmallMagic, 'Tower Dark Maze'), Pot(48, 16, PotItem.Heart, 'Tower Dark Maze'), Pot(176, 20, PotItem.OneRupee, 'Tower Dark Maze'), Pot(146, 23, PotItem.FiveRupees, 'Tower Dark Maze'), Pot(12, 28, PotItem.Heart, 'Tower Dark Maze')], 209: [Pot(48, 4, PotItem.BigMagic, 'Mire Conveyor Barrier'), Pot(168, 7, PotItem.OneRupee, 'Mire Conveyor Barrier'), Pot(76, 4, PotItem.OneRupee, 'Mire Neglected Room'), Pot(112, 4, PotItem.FiveArrows, 'Mire Neglected Room'), @@ -350,8 +354,8 @@ def shuffle_pots(world, player): new_pot_contents = PotSecretTable() - for supertile in vanilla_pots: - old_pots = vanilla_pots[supertile] + for super_tile in vanilla_pots: + old_pots = vanilla_pots[super_tile] new_pots = [Pot(pot.x, pot.y, PotItem.Nothing, pot.room, pot.flags) for pot in old_pots] # sort in the order Hole, Switch, Key, Other, Nothing sort_order = {PotItem.Hole: 4, PotItem.Switch: 3, PotItem.Key: 2, PotItem.Nothing: 0} @@ -401,11 +405,54 @@ def shuffle_pots(world, player): else: raise Exception("Switch location in room %s requires logic change" % new_pot.room) - new_pot_contents.room_map[supertile] = new_pots + new_pot_contents.room_map[super_tile] = new_pots world.pot_contents[player] = new_pot_contents +def shuffle_pot_switches(world, player): + import RaceRandom as random + + for super_tile in vanilla_pots: + old_pots = vanilla_pots[super_tile] + new_pots = world.pot_contents[player].room_map[super_tile] + # sort in the order Hole, Switch, Key, Other, Nothing + sort_order = {PotItem.Hole: 4, PotItem.Switch: 3, PotItem.Key: 2, PotItem.Nothing: 0} + old_pots = sorted(old_pots, key=lambda pot: sort_order.get(pot.item, 1), reverse=True) + + for old_pot in old_pots: + if old_pot.item != PotItem.Switch: + break + else: + available_pots = [pot for pot in new_pots if (pot.room == old_pot.room or pot.room in movable_switch_rooms[old_pot.room]) and not (pot.flags & PotFlags.NoSwitch)] + + new_pot = random.choice(available_pots) + new_pot.item, old_pot.item = old_pot.item, new_pot.item + if world.retro[player] and new_pot.item == PotItem.FiveArrows: + new_pot.item = PotItem.FiveRupees + + if new_pot.item == PotItem.Switch and (new_pot.flags & PotFlags.SwitchLogicChange): + if new_pot.room == 'PoD Basement Ledge': + basement = world.get_region(old_pot.room, player) + ledge = world.get_region(new_pot.room, player) + ledge.locations.append(basement.locations.pop()) + elif new_pot.room == 'Swamp Push Statue': + from Rules import set_rule + set_rule(world.get_entrance('Swamp Push Statue NE', player), lambda state: state.has('Cane of Somaria', player)) + world.get_door('Swamp Push Statue NW', player).blocked = True + elif new_pot.room == 'Thieves Attic Hint': + # Rule is created based on barrier + world.get_door('Thieves Attic ES', player).barrier(CrystalBarrier.Orange) + else: + raise Exception("Switch location in room %s requires logic change" % new_pot.room) + for location in world.get_locations(): + if location.player == player and location.type == LocationType.Pot and location.pot.item == PotItem.Switch: + location.real = False + world.dynamic_locations.remove(location) + location.parent.locations.remove(location) + world.clear_location_cache() + + key_drop_data = { 'Hyrule Castle - Map Guard Key Drop': ['Drop', (0x09E20C, 0x72), 'in Hyrule Castle', 'Small Key (Escape)'], 'Hyrule Castle - Boomerang Guard Key Drop': ['Drop', (0x09E204, 0x71), 'in Hyrule Castle', 'Small Key (Escape)'], @@ -459,9 +506,8 @@ class PotSecretTable(object): data_address = pc_to_snes(data_pointer) & 0xFFFF rom.write_bytes(pointer_address + room * 2, int16_as_bytes(data_address)) for pot in self.room_map[room]: - if not pot.empty(): - rom.write_bytes(data_pointer + list_idx * 3, pot.pot_data()) - list_idx += 1 + rom.write_bytes(data_pointer + list_idx * 3, pot.pot_data()) + list_idx += 1 rom.write_bytes(data_pointer + list_idx * 3, [0xFF, 0xFF]) data_pointer += 3 * list_idx + 2 else: @@ -472,7 +518,7 @@ class PotSecretTable(object): size = 0x128 * 2 for room in range(0, 0x128): if room in self.room_map: - pot_list = [p for p in self.room_map[room] if not p.empty()] + pot_list = [p for p in self.room_map[room]] if pot_list: size += len(pot_list) * 3 + 2 return size diff --git a/Regions.py b/Regions.py index ae004bf8..08502d0e 100644 --- a/Regions.py +++ b/Regions.py @@ -726,7 +726,8 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'TR Crystal Maze Interior', 'Turtle Rock', None, ['TR Crystal Maze Interior to End Barrier - Blue', 'TR Crystal Maze Interior to Start Barrier - Blue', 'TR Crystal Maze Interior to Start Bypass', 'TR Crystal Maze Interior to End Bypass']), create_dungeon_region(player, 'TR Crystal Maze End', 'Turtle Rock', None, ['TR Crystal Maze North Stairs', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Crystal Maze End to Ranged Crystal']), create_dungeon_region(player, 'TR Crystal Maze End - Ranged Crystal', 'Turtle Rock', None, ['TR Crystal Maze End Ranged Crystal Exit']), - create_dungeon_region(player, 'TR Final Abyss', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss NW']), + create_dungeon_region(player, 'TR Final Abyss Balcony', 'Turtle Rock', None, ['TR Final Abyss South Stairs', 'TR Final Abyss Balcony Path']), + create_dungeon_region(player, 'TR Final Abyss Ledge', 'Turtle Rock', None, ['TR Final Abyss NW', 'TR Final Abyss Ledge Path']), create_dungeon_region(player, 'TR Boss', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize'], ['TR Boss SW']), # gt @@ -981,49 +982,53 @@ def create_shops(world, player): def adjust_locations(world, player): - if world.keydropshuffle[player] != 'none': - world.pot_contents[player] = PotSecretTable() - for location in key_drop_data.keys(): - loc = world.get_location(location, player) + # handle pots + world.pot_contents[player] = PotSecretTable() + for location, datum in key_drop_data.items(): + loc = world.get_location(location, player) + if 'Drop' == datum[0]: + loc.type = LocationType.Drop + snes_address, room = datum[1] + loc.address = snes_address + else: + loc.type = LocationType.Pot + pot = next(p for p in vanilla_pots[datum[1]] if p.item == PotItem.Key).copy() + loc.pot = pot + world.pot_contents[player].room_map[datum[1]].append(pot) + if world.keydropshuffle[player] == 'none': + loc.skip = True + else: key_item = loc.item key_item.location = None loc.forced_item = None loc.item = None loc.event = False - datum = key_drop_data[location] - # todo: set type of location - if 'Drop' == datum[0]: - loc.type = LocationType.Drop - snes_address, room = datum[1] - loc.address = snes_address - else: - loc.type = LocationType.Pot - pot = next(p for p in vanilla_pots[datum[1]] if p.item == PotItem.Key).copy() - loc.pot = pot - world.pot_contents[player].room_map[datum[1]].append(pot) item_dungeon = key_item.dungeon dungeon = world.get_dungeon(item_dungeon, player) if key_item.smallkey and not world.retro[player]: dungeon.small_keys.append(key_item) elif key_item.bigkey: dungeon.big_key = key_item - if world.keydropshuffle[player] == 'potsanity': - for super_tile, pot_list in vanilla_pots.items(): - for pot_index, pot_orig in enumerate(pot_list): - pot = pot_orig.copy() - if pot.item in [PotItem.Hole, PotItem.Switch]: - world.pot_contents[player].room_map[super_tile].append(pot) - elif pot.item != PotItem.Key: - parent = world.get_region(pot.room, player) - descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}' - pot_location = Location(player, f'{pot.room} {descriptor}', player, hint_text='in a pot', - parent=parent) - world.dynamic_locations.append(pot_location) - pot_location.pot = pot - world.pot_contents[player].room_map[super_tile].append(pot) - pot_location.type = LocationType.Pot - parent.locations.append(pot_location) + for super_tile, pot_list in vanilla_pots.items(): + for pot_index, pot_orig in enumerate(pot_list): + pot = pot_orig.copy() + if pot.item != PotItem.Key: + world.pot_contents[player].room_map[super_tile].append(pot) + if world.keydropshuffle[player] == 'potsanity': + if (pot.item not in [PotItem.Key, PotItem.Hole] + and (pot.item != PotItem.Switch or world.potshuffle[player])): + parent = world.get_region(pot.room, player) + descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}' + # todo: better hints + hint_text = 'under a block' if pot.flags & PotFlags.Block else 'in a pot' + pot_location = Location(player, f'{pot.room} {descriptor}', player, hint_text=hint_text, + parent=parent) + world.dynamic_locations.append(pot_location) + pot_location.pot = pot + + pot_location.type = LocationType.Pot + parent.locations.append(pot_location) if world.shopsanity[player]: index = 0 for shop, location_list in shop_to_location_table.items(): @@ -1041,6 +1046,8 @@ def adjust_locations(world, player): if location: location.type = LocationType.Logical location.real = False + if l not in ['Ganon', 'Agahnim 1', 'Agahnim 2']: + location.skip = True diff --git a/Rom.py b/Rom.py index f5951045..93a33ccc 100644 --- a/Rom.py +++ b/Rom.py @@ -35,7 +35,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '3ee527f67af25e860ffd4364d2369f0e' +RANDOMIZERBASEHASH = 'c1b18f6455af56b738d4fe8f266ef055' class JsonRom(object): @@ -731,10 +731,11 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.Fix_EG my_locations = world.get_filled_locations(player) - valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and l.pot.indicator) + valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item) or (l.type == LocationType.Drop and not l.forced_item) or (l.type == LocationType.Normal and not l.forced_item) or (l.type == LocationType.Shop and world.shopsanity[player]))] + valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) if world.doorShuffle[player] == 'crossed' or world.keydropshuffle[player] != 'none': @@ -742,7 +743,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): rom.write_byte(0x15270, 2) sanctuary = world.get_region('Sanctuary', player) rom.write_byte(0x1597b, sanctuary.dungeon.dungeon_id*2) - update_compasses(rom, valid_locations, world, player) + update_compasses(rom, valid_loc_by_dungeon, world, player) def should_be_bunny(region, mode): if mode != 'inverted': @@ -774,10 +775,14 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): else: rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used rom.write_byte(0x13f030+offset, layout.max_chests) + # todo: fix these for pot shuffle builder = world.dungeon_layouts[player][name] - rom.write_byte(0x13f080+offset, builder.location_cnt % 10) - rom.write_byte(0x13f090+offset, builder.location_cnt // 10) - rom.write_byte(0x13f0a0+offset, builder.location_cnt) + valid_cnt = len(valid_loc_by_dungeon[name]) + if valid_cnt > 99: + logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})') + rom.write_byte(0x13f080+offset, valid_cnt % 10) + rom.write_byte(0x13f090+offset, valid_cnt // 10) + rom.write_byte(0x13f0a0+offset, valid_cnt) bk_status = 1 if builder.bk_required else 0 bk_status = 2 if builder.bk_provided else bk_status rom.write_byte(0x13f040+offset*2, bk_status) @@ -865,19 +870,25 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): if world.keydropshuffle[player] != 'none': rom.write_byte(0x142A50, 1) rom.write_byte(0x142A51, 1) - rom.write_byte(0x142A52, 216-credits_total) # todo: this is now a delta + if world.keydropshuffle[player] == 'potsanity': + rom.write_bytes(0x04DFD8, [0x18, 0x0B, 0x1C]) + rom.write_byte(0x04E002, 0xFF) + rom.write_byte(0x142A52, credits_total - 216) # todo: this is now a delta write_int16(rom, 0x187010, credits_total) # dynamic credits if credits_total != 216: # collection rate address (hi): cr_address = 0x238057 cr_pc = cr_address - 0x120000 # convert to pc + first_top, first_bot = credits_digit((credits_total // 100) % 10) mid_top, mid_bot = credits_digit((credits_total // 10) % 10) last_top, last_bot = credits_digit(credits_total % 10) # top half + rom.write_byte(cr_pc, first_top) rom.write_byte(cr_pc+0x1, mid_top) rom.write_byte(cr_pc+0x2, last_top) # bottom half + rom.write_byte(cr_pc+0x1e, first_bot) rom.write_byte(cr_pc+0x1f, mid_bot) rom.write_byte(cr_pc+0x20, last_bot) @@ -1441,7 +1452,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): elif world.dungeon_counters[player] == 'on': compass_mode = 0x02 # always on elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' - or world.dungeon_counters[player] == 'pickup' or world.keydropshuffle != 'none'): + or world.dungeon_counters[player] == 'pickup' or world.keydropshuffle[player] != 'none'): compass_mode = 0x01 # show on pickup if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': compass_mode |= 0x80 # turn on locating dungeons @@ -2615,11 +2626,15 @@ def patch_shuffled_dark_sanc(world, rom, player): rom.write_bytes(0x180262, [unknown_1, unknown_2, 0x00]) -def update_compasses(rom, valid_locations, world, player): +def valid_dungeon_locations(valid_locations): dungeon_locations = collections.defaultdict(set) for l in valid_locations: if l.parent_region.dungeon: dungeon_locations[l.parent_region.dungeon.name].add(l) + return dungeon_locations + + +def update_compasses(rom, dungeon_locations, world, player): layouts = world.dungeon_layouts[player] provided_dungeon = False for name, builder in layouts.items(): diff --git a/Rules.py b/Rules.py index 581424b7..4cfe0393 100644 --- a/Rules.py +++ b/Rules.py @@ -336,8 +336,8 @@ def global_rules(world, player): set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Final Abyss South Stairs', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Final Abyss NW', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Final Abyss Balcony Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Final Abyss Ledge Path', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) @@ -1912,7 +1912,7 @@ bunny_revivable_entrances = { "Ice Many Pots", "Mire South Fish", "Mire Right Bridge", "Mire Left Bridge", "TR Boss", "Eastern Hint Tile Blocked Path", "Thieves Spike Switch", "Thieves Boss", "Mire Spike Barrier", "Mire Cross", "Mire Hidden Shooters", - "Mire Spikes", "TR Final Abyss", "TR Dark Ride", "TR Pokey 1", "TR Tile Room", + "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride", "TR Pokey 1", "TR Tile Room", "TR Roller Room", "Eastern Cannonball", "Thieves Hallway", "Ice Switch Room", "Mire Tile Room", "Mire Conveyor Crystal", "Mire Hub", "TR Dash Bridge", "TR Hub", "Eastern Boss", "Eastern Lobby", "Thieves Ambush", @@ -1980,7 +1980,7 @@ bunny_impassible_doors = { 'TR Pokey 2 Bottom to Top Barrier - Blue', 'TR Pokey 2 Top to Bottom Barrier - Blue', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap', 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze End to Interior Barrier - Blue', - 'TR Final Abyss South Stairs', 'TR Final Abyss NW', 'GT Hope Room EN', 'GT Blocked Stairs Block Path', + 'TR Final Abyss Balcony Path', 'TR Final Abyss Ledge Path', 'GT Hope Room EN', 'GT Blocked Stairs Block Path', 'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path', 'GT Crystal Conveyor NE', 'GT Crystal Conveyor WN', 'GT Conveyor Cross EN', 'GT Conveyor Cross WN', 'GT Hookshot East-North Path', 'GT Hookshot East-South Path', 'GT Hookshot North-East Path', diff --git a/data/base2current.bps b/data/base2current.bps index 0e6d8e39..13608a21 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 7b40b2c9..57973eec 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -492,6 +492,19 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False): return locations +def filter_pot_locations(locations, world): + if world.algorithm == 'district': + config = world.item_pool_config + restricted = config.location_groups[0].locations + filtered = [l for l in locations if l.name not in restricted or l.player not in restricted[l.name]] + return filtered if len(filtered) > 0 else locations + if world.algorithm == 'vanilla_fill': + # todo: vanilla pot location stuff + pass + return locations + + + vanilla_mapping = { 'Green Pendant': ['Eastern Palace - Prize'], 'Red Pendant': ['Desert Palace - Prize', 'Tower of Hera - Prize'],