diff --git a/BaseClasses.py b/BaseClasses.py index 9c5f9e9c..157dab9a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1618,7 +1618,7 @@ class Entrance(object): def can_reach_thru(self, state, start_region, ignore_underworld=False, ignore_ledges=False, allow_save_quit=False): def explore_region(region, path = []): nonlocal found - if region not in explored_regions or len(explored_regions[region]) > len(path): + if region not in explored_regions: explored_regions[region] = path for exit in region.exits: if exit.connected_region and (not ignore_ledges or exit.spot_type != 'Ledge') \ @@ -1628,20 +1628,28 @@ class Entrance(object): found = True explored_regions[self.parent_region] = path + [exit] elif not ignore_underworld or region.type == exit.connected_region.type or exit.connected_region.type not in [RegionType.Cave, RegionType.Dungeon]: - explore_region(exit.connected_region, path + [exit]) + exits_to_traverse.append(tuple((exit, path))) - found = False - explored_regions = {} - explore_region(start_region.entrances[0].parent_region) - if found: - self.temp_path = explored_regions[self.parent_region] - elif allow_save_quit: - world = self.parent_region.world if self.parent_region else None - exit = world.get_entrance('Links House S&Q', self.player) - explore_region(exit.connected_region, [exit]) + def traverse_paths(region, start_path=[]): + explore_region(region, start_path) + while not found and len(exits_to_traverse): + exit, path = exits_to_traverse.pop(0) + explore_region(exit.connected_region, path + [exit]) if found: self.temp_path = explored_regions[self.parent_region] + found = False + explored_regions = {} + exits_to_traverse = list() + traverse_paths(start_region.entrances[0].parent_region) + + if not found and allow_save_quit: + explored_regions = {} + exits_to_traverse = list() + world = self.parent_region.world if self.parent_region else None + exit = world.get_entrance('Links House S&Q', self.player) + traverse_paths(exit.connected_region, [exit]) + #TODO: Implement residual mirror portal placing for the previous leg, to be used for the final destination return found @@ -2910,8 +2918,33 @@ class Spoiler(object): outfile.write(f'{fairy}: {bottle}\n') if self.overworlds: - # overworlds: overworld transitions; outfile.write('\n\nOverworld:\n\n') + # overworld tile swaps + swap_output = False + for player in range(1, self.world.players + 1): + if self.world.owMixed[player]: + from OverworldShuffle import tile_swap_spoiler_table + if not swap_output: + swap_output = True + outfile.write('OW Tile Swaps:\n') + outfile.write(('' if self.world.players == 1 else str('(Player ' + str(player) + ')')).ljust(11)) # player name + s = list(map(lambda x: ' ' if x not in self.world.owswaps[player][0] else 'S', [i for i in range(0x40)])) + outfile.write((tile_swap_spoiler_table + '\n\n') % ( s[0x02], s[0x07], \ + s[0x00], s[0x03], s[0x05], \ + s[0x00], s[0x02],s[0x03], s[0x05], s[0x07], s[0x0a], s[0x0f], \ + s[0x0a], s[0x0f], + s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], s[0x10],s[0x11],s[0x12],s[0x13],s[0x14],s[0x15],s[0x16],s[0x17], + s[0x18], s[0x1a],s[0x1b], s[0x1d],s[0x1e], \ + s[0x22], s[0x25], s[0x1a], s[0x1d], \ + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], s[0x18], s[0x1b], s[0x1e], \ + s[0x30], s[0x32],s[0x33],s[0x34],s[0x35], s[0x37], s[0x22], s[0x25], \ + s[0x3a],s[0x3b],s[0x3c], s[0x3f], \ + s[0x28],s[0x29],s[0x2a],s[0x2b],s[0x2c],s[0x2d],s[0x2e],s[0x2f], \ + s[0x32],s[0x33],s[0x34], s[0x37], \ + s[0x30], s[0x35], + s[0x3a],s[0x3b],s[0x3c], s[0x3f])) + + # overworld transitions outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()])) if self.entrances: diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d13649..866c1d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 0.2.2.1 +- Allow normal Link speed with Old Man following if not in his cave or WDM +- Fixed issue with Flute exits not getting placed on the correct tiles +- Hints in Lite/Lean ER no longer refer to entrances that are guaranteed vanilla +- Added Links House entrance to hint candidate list in ER when it is shuffled +- Added Tile Swaps ASCII map to Spoiler Log when Tile Swap is enabled +- Fixed issue with Whirlpool Shuffle not abiding by Polar rules + ### 0.2.2.0 - Delivering Big Red Bomb is now in logic - Smith/Purple Chest have proper dynamic pathing to fix logical issues diff --git a/EntranceShuffle.py b/EntranceShuffle.py index 34733fc2..5b3b821d 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -428,13 +428,21 @@ def link_entrances(world, player): caves = Cave_Base + (dw_dungeons if not invFlag else lw_dungeons) connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (dw_entrances if not invFlag else lw_entrances)] connect_inaccessible_regions(world, [], connector_entrances, caves, player) + if invFlag: + lw_dungeons = list(set(lw_dungeons) & set(caves)) + else: + dw_dungeons = list(set(dw_dungeons) & set(caves)) caves = list(set(Cave_Base) & set(caves)) + (lw_dungeons if not invFlag else dw_dungeons) connector_entrances = [e for e in list(zip(*default_connector_connections + default_dungeon_connections + open_default_dungeon_connections))[0] if e in (lw_entrances if not invFlag else dw_entrances)] connect_inaccessible_regions(world, connector_entrances, [], caves, player) + if not invFlag: + lw_dungeons = list(set(lw_dungeons) & set(caves)) + else: + dw_dungeons = list(set(dw_dungeons) & set(caves)) - lw_dungeons = list(set(lw_dungeons) & set(caves)) + (Old_Man_House if not invFlag else []) - dw_dungeons = list(set(dw_dungeons) & set(caves)) + ([] if not invFlag else Old_Man_House) + lw_dungeons = lw_dungeons + (Old_Man_House if not invFlag else []) + dw_dungeons = dw_dungeons + ([] if not invFlag else Old_Man_House) caves = list(set(Cave_Base) & set(caves)) + DW_Mid_Dungeon_Exits # place old man, has limited options diff --git a/OWEdges.py b/OWEdges.py index 14849137..f13e3b32 100644 --- a/OWEdges.py +++ b/OWEdges.py @@ -1478,7 +1478,6 @@ OWExitTypes = { 'Dark Bonk Rocks Cliff Ledge Drop', 'Bomb Shop Cliff Ledge Drop', 'Hammer Bridge South Cliff Ledge Drop', - 'Ice Lake Northeast Pier Hop', 'Ice Lake Moat Bomb Jump', 'Ice Lake Area Cliff Ledge Drop', 'Ice Palace Island FAWT Ledge Drop', @@ -1623,6 +1622,7 @@ OWExitTypes = { 'Hype Cave Landing', 'Ice Lake Northeast Water Drop', 'Ice Lake Northeast Pier', + 'Ice Lake Northeast Pier Hop', 'Ice Lake Moat Water Entry', 'Ice Palace Approach', 'Ice Palace Leave', diff --git a/OverworldShuffle.py b/OverworldShuffle.py index cad95825..dfce3ccb 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -3,7 +3,7 @@ from BaseClasses import OWEdge, WorldType, RegionType, Direction, Terrain, PolSl from Regions import mark_dark_world_regions, mark_light_world_regions from OWEdges import OWTileRegions, OWTileGroups, OWEdgeGroups, OWExitTypes, OpenStd, parallel_links, IsParallel -__version__ = '0.2.2.0-u' +__version__ = '0.2.2.1-u' def link_overworld(world, player): # setup mandatory connections @@ -197,16 +197,17 @@ def link_overworld(world, player): else: whirlpool_candidates = [[],[]] for (from_owid, from_whirlpool, from_region), (to_owid, to_whirlpool, to_region) in default_whirlpool_connections: - if world.owCrossed[player] != 'none': - whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) - whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) + if world.owCrossed[player] == 'polar' and world.owMixed[player] and from_owid == 0x55: + # connect the 2 DW whirlpools in Polar Mixed + connect_simple(world, from_whirlpool, to_region, player) + connect_simple(world, to_whirlpool, from_region, player) else: - if world.get_region(from_region, player).type == RegionType.LightWorld: + if world.owCrossed[player] != 'none' or world.get_region(from_region, player).type == RegionType.LightWorld: whirlpool_candidates[0].append(tuple((from_owid, from_whirlpool, from_region))) else: whirlpool_candidates[1].append(tuple((from_owid, from_whirlpool, from_region))) - if world.get_region(to_region, player).type == RegionType.LightWorld: + if world.owCrossed[player] != 'none' or world.get_region(to_region, player).type == RegionType.LightWorld: whirlpool_candidates[0].append(tuple((to_owid, to_whirlpool, to_region))) else: whirlpool_candidates[1].append(tuple((to_owid, to_whirlpool, to_region))) @@ -723,9 +724,7 @@ def reorganize_groups(world, groups, player): def create_flute_exits(world, player): for region in (r for r in world.regions if r.player == player and r.terrain == Terrain.Land and r.name not in ['Zoras Domain', 'Master Sword Meadow', 'Hobo Bridge']): - if (not world.owMixed[player] and region.type == RegionType.LightWorld) \ - or (world.owMixed[player] and region.type in [RegionType.LightWorld, RegionType.DarkWorld] \ - and (region.name not in world.owswaps[player][1] or region.name in world.owswaps[player][2])): + if region.type == (RegionType.LightWorld if world.mode != 'inverted' else RegionType.DarkWorld): exitname = 'Flute From ' + region.name exit = Entrance(region.player, exitname, region) exit.spot_type = 'Flute' @@ -1589,3 +1588,23 @@ flute_data = { 0x3c: (['South Pass Area', 'Dark South Pass Area'], 0x3c, 0x0584, 0x0ed0, 0x081e, 0x0f38, 0x0898, 0x0f45, 0x08a3, 0xfffe, 0x0002, 0x0f38, 0x0898), 0x3f: (['Octoballoon Area', 'Bomber Corner Area'], 0x3f, 0x0810, 0x0f05, 0x0e75, 0x0f67, 0x0ef3, 0x0f72, 0x0efa, 0xfffb, 0x000b, 0x0f80, 0x0ef0) } + +tile_swap_spoiler_table = \ +""" 0 1 2 3 4 5 6 7 + +---+-+---+---+-+ + 01234567 A(00)| |%s| | |%s| + +--------+ | %s +-+ %s | %s +-+ +A(00)|%s %s%s %s %s| B(08)| |%s| | |%s| +B(08)| %s %s| +-+-+-+-+-+-+-+-+ +C(10)|%s%s%s%s%s%s%s%s| C(10)|%s|%s|%s|%s|%s|%s|%s|%s| +D(18)|%s %s%s %s%s | +-+-+-+-+-+-+-+-+ +E(20)| %s %s | D(18)| |%s| |%s| | +F(28)|%s%s%s%s%s%s%s%s| | %s +-+ %s +-+ %s | +G(30)|%s %s%s%s%s %s| E(20)| |%s| |%s| | +H(38)| %s%s%s %s| +-+-+-+-+-+-+-+-+ + +--------+ F(28)|%s|%s|%s|%s|%s|%s|%s|%s| + +-+-+-+-+-+-+-+-+ + G(30)| |%s|%s|%s| |%s| + | %s +-+-+-+ %s +-+ + H(38)| |%s|%s|%s| |%s| + +---+-+-+-+---+-+""" diff --git a/Rom.py b/Rom.py index f4dd88af..42d990ed 100644 --- a/Rom.py +++ b/Rom.py @@ -33,7 +33,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'e3373be98af9d6de1cb1ab12176ecb0e' +RANDOMIZERBASEHASH = '9df10796c8a8fe07d81fc0012700934a' class JsonRom(object): @@ -2161,14 +2161,14 @@ def write_strings(rom, world, player, team): else: hint_count = 4 for entrance in all_entrances: - if entrance.name in entrances_to_hint: - if hint_count > 0: + if hint_count > 0: + if entrance.name in entrances_to_hint: this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.' tt[hint_locations.pop(0)] = this_hint entrances_to_hint.pop(entrance.name) hint_count -= 1 - else: - break + else: + break #Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle. if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']: @@ -2180,9 +2180,15 @@ def write_strings(rom, world, player, team): entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'}) elif world.shuffle[player] == 'restricted': entrances_to_hint.update(ConnectorEntrances) - entrances_to_hint.update(OtherEntrances) + entrances_to_hint.update(ItemEntrances) + if world.shuffle[player] not in ['lite', 'lean']: + entrances_to_hint.update(ShopEntrances) + entrances_to_hint.update(OtherEntrances) + elif world.shopsanity[player]: + entrances_to_hint.update(ShopEntrances) + if world.shufflelinks[player] and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: + entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) - entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']: entrances_to_hint.update(InsanityEntrances) if world.shuffle_ganon: @@ -2254,17 +2260,15 @@ def write_strings(rom, world, player, team): else: this_hint = location + ' contains ' + hint_text(world.get_location(location, player).item) + '.' tt[hint_locations.pop(0)] = this_hint - - # Adding a guaranteed hint for the Flute in overworld shuffle. + + # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. + items_to_hint = RelevantItems.copy() if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: + # Adding a guaranteed hint for the Flute in overworld shuffle. this_location = world.find_items_not_key_only('Ocarina', player) if this_location: this_hint = this_location[0].item.hint_text + ' can be found ' + hint_text(this_location[0]) + '.' tt[hint_locations.pop(0)] = this_hint - - # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. - items_to_hint = RelevantItems.copy() - if world.owShuffle[player] != 'vanilla' or world.owMixed[player]: items_to_hint.remove('Ocarina') if world.keyshuffle[player]: items_to_hint.extend(SmallKeys) @@ -2273,7 +2277,7 @@ def write_strings(rom, world, player, team): random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8 hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0 - hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owMixed[player] else 0 + hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0 while hint_count > 0: this_item = items_to_hint.pop(0) this_location = world.find_items_not_key_only(this_item, player) @@ -2734,16 +2738,46 @@ DungeonEntrances = {'Eastern Palace': 'Eastern Palace', 'Desert Palace Entrance (North)': 'The northmost cave in the desert' } -OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', - 'Lake Hylia Fairy': 'A cave NE of Lake Hylia', +ItemEntrances = {'Blinds Hideout': 'Blind\'s old house', + 'Chicken House': 'The chicken lady\'s house', + 'Aginahs Cave': 'The open desert cave', + 'Sahasrahlas Hut': 'The house near armos', + 'Blacksmiths Hut': 'The old smithery', + 'Sick Kids House': 'The central house in Kakariko', + 'Mini Moldorm Cave': 'The cave south of Lake Hylia', + 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', + 'Library': 'The old library', + 'Potion Shop': 'The witch\'s building', + 'Dam': 'The old dam', + 'Waterfall of Wishing': 'Going behind the waterfall', + 'Bonk Rock Cave': 'The rock pile near Sanctuary', + 'Graveyard Cave': 'The graveyard ledge', + 'Checkerboard Cave': 'The NE desert ledge', + 'Cave 45': 'The ledge south of haunted grove', + 'Kings Grave': 'The northeastmost grave', + 'C-Shaped House': 'The NE house in Village of Outcasts', + 'Mire Shed': 'The western hut in the mire', + 'Spike Cave': 'The ledge cave on west dark DM', + 'Hype Cave': 'The cave south of the old bomb shop', + 'Brewery': 'The Village of Outcasts building with no door', + 'Chest Game': 'The westmost building in the Village of Outcasts', + 'Big Bomb Shop': 'The old bomb shop' + } + +ShopEntrances = {'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', + 'Kakariko Shop': 'The old Kakariko shop', + 'Capacity Upgrade': 'The cave on the island', + 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', + 'Dark World Shop': 'The hammer sealed building', + 'Red Shield Shop': 'The fenced in building', + 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', + 'Dark World Potion Shop': 'The building near the catfish', + 'Dark World Lumberjack Shop': 'The northmost Dark World building' + } + +OtherEntrances = {'Lake Hylia Fairy': 'A cave NE of Lake Hylia', 'Light Hype Fairy': 'The cave south of your house', 'Desert Fairy': 'The cave near the desert', - 'Chicken House': 'The chicken lady\'s house', - 'Aginahs Cave': 'The open desert cave', - 'Sahasrahlas Hut': 'The house near armos', - 'Cave Shop (Lake Hylia)': 'The cave NW Lake Hylia', - 'Blacksmiths Hut': 'The old smithery', - 'Sick Kids House': 'The central house in Kakariko', 'Lost Woods Gamble': 'A tree trunk door', 'Fortune Teller (Light)': 'A building NE of Kakariko', 'Snitch Lady (East)': 'A house guarded by a snitch', @@ -2751,49 +2785,24 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', 'Bush Covered House': 'A house with an uncut lawn', 'Tavern (Front)': 'A building with a backdoor', 'Light World Bomb Hut': 'A Kakariko building with no door', - 'Kakariko Shop': 'The old Kakariko shop', - 'Mini Moldorm Cave': 'The cave south of Lake Hylia', 'Long Fairy Cave': 'The eastmost portal cave', 'Good Bee Cave': 'The open cave SE Lake Hylia', '20 Rupee Cave': 'The rock SE Lake Hylia', '50 Rupee Cave': 'The rock near the desert', - 'Ice Rod Cave': 'The sealed cave SE Lake Hylia', - 'Library': 'The old library', - 'Potion Shop': 'The witch\'s building', - 'Dam': 'The old dam', 'Lumberjack House': 'The lumberjack house', 'Lake Hylia Fortune Teller': 'The building NW Lake Hylia', 'Kakariko Gamble Game': 'The old Kakariko gambling den', - 'Waterfall of Wishing': 'Going behind the waterfall', - 'Capacity Upgrade': 'The cave on the island', - 'Bonk Rock Cave': 'The rock pile near Sanctuary', - 'Graveyard Cave': 'The graveyard ledge', - 'Checkerboard Cave': 'The NE desert ledge', - 'Cave 45': 'The ledge south of haunted grove', - 'Kings Grave': 'The northeastmost grave', 'Bonk Fairy (Light)': 'The rock pile near your home', 'Hookshot Fairy': 'The left paired cave on east DM', 'Bonk Fairy (Dark)': 'The rock pile near the old bomb shop', 'Dark Lake Hylia Fairy': 'The cave NE dark Lake Hylia', - 'C-Shaped House': 'The NE house in Village of Outcasts', 'Dark Death Mountain Fairy': 'The SW cave on dark DM', - 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', - 'Dark World Shop': 'The hammer sealed building', - 'Red Shield Shop': 'The fenced in building', - 'Mire Shed': 'The western hut in the mire', 'East Dark World Hint': 'The dark cave near the eastmost portal', 'Dark Desert Hint': 'The cave east of the mire', - 'Spike Cave': 'The ledge cave on west dark DM', 'Palace of Darkness Hint': 'The building south of Kiki', 'Dark Lake Hylia Ledge Spike Cave': 'The rock SE dark Lake Hylia', - 'Cave Shop (Dark Death Mountain)': 'The base of east dark DM', - 'Dark World Potion Shop': 'The building near the catfish', 'Archery Game': 'The old archery game', - 'Dark World Lumberjack Shop': 'The northmost Dark World building', - 'Hype Cave': 'The cave south of the old bomb shop', - 'Brewery': 'The Village of Outcasts building with no door', 'Dark Lake Hylia Ledge Hint': 'The open cave SE dark Lake Hylia', - 'Chest Game': 'The westmost building in the Village of Outcasts', 'Dark Desert Fairy': 'The eastern hut in the mire', 'Dark Lake Hylia Ledge Fairy': 'The sealed cave SE dark Lake Hylia', 'Fortune Teller (Dark)': 'The building NE the Village of Outcasts' diff --git a/asm/owrando.asm b/asm/owrando.asm index 283c358f..f244cb79 100644 --- a/asm/owrando.asm +++ b/asm/owrando.asm @@ -37,6 +37,10 @@ db #$b0 ; BCS to replace BEQ org $06907f ; < 3107f - sprite_prep.asm:2170 (LDA $7EF3CA) lda $8a : and.b #$40 +; override Link speed with Old Man following +org $09a32e ; < bank_09.asm:7457 (LDA.b #$0C : STA.b $5E) +jsl OWOldManSpeed + ; Dark Bonk Rocks Rain Sequence Guards (allowing Tile Swap on Dark Bonk Rocks) ;org $09c957 ; <- 4c957 ;dw #$cb5f ; matches value on Central Bonk Rocks screen @@ -162,6 +166,22 @@ OWSmithAccept: clc : rtl + sec : rtl } +OWOldManSpeed: +{ + lda $1b : beq .outdoors + lda $a0 : and #$fe : cmp #$f0 : beq .vanilla ; if in cave where you find Old Man + bra .normalspeed + .outdoors + lda $8a : cmp #$03 : beq .vanilla ; if on WDM screen + + .normalspeed + lda $5e : cmp #$0c : rtl + stz $5e : rtl + + .vanilla + lda #$0c : sta $5e ; what we wrote over + rtl +} org $aa9000 OWEdgeTransition: diff --git a/data/base2current.bps b/data/base2current.bps index be48a7a2..a73808cb 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ