Skip to content

Commit b499369

Browse files
committed
Merge "Setting to shuffle Ganon's tower into the boss entrance pool" (#2063)
2 parents 6a62810 + 067f5e2 commit b499369

16 files changed

Lines changed: 150 additions & 60 deletions

EntranceShuffle.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def build_one_way_targets(world: World, types_to_include: Iterable[str], exclude
130130
('AdultBoss', ('Spirit Temple Before Boss -> Twinrova Boss Room', { 'index': 0x008D, 'savewarp_addresses': [ 0xB062F2, 0xBC6122 ] }),
131131
('Twinrova Boss Room -> Spirit Temple Before Boss', { 'index': 0x02F5 })),
132132

133+
('SpecialBoss', ('Ganons Castle Main -> Ganons Castle Tower', { 'index': 0x041B }),
134+
('Ganons Castle Tower -> Ganons Castle Main', { 'index': 0x0534 })),
135+
133136
('Interior', ('Kokiri Forest -> KF Midos House', { 'index': 0x0433 }),
134137
('KF Midos House -> Kokiri Forest', { 'index': 0x0443 })),
135138
('Interior', ('Kokiri Forest -> KF Sarias House', { 'index': 0x0437 }),
@@ -508,13 +511,17 @@ def shuffle_random_entrances(worlds: list[World]) -> None:
508511
if worlds[0].settings.shuffle_bosses == 'full':
509512
entrance_pools['Boss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
510513
entrance_pools['Boss'] += world.get_shufflable_entrances(type='AdultBoss', only_primary=True)
514+
if worlds[0].settings.shuffle_ganon_tower:
515+
entrance_pools['Boss'] += world.get_shufflable_entrances(type='SpecialBoss', only_primary=True)
511516
if worlds[0].settings.open_forest == 'closed':
512517
# Deku is forced vanilla below, so Queen Gohma must be vanilla to ensure she is reachable.
513518
# This is already enforced by the fill algorithm in most cases, but this covers the odd settings combination where it isn't.
514519
entrance_pools['Boss'].remove(world.get_entrance('Deku Tree Before Boss -> Queen Gohma Boss Room'))
515520
elif worlds[0].settings.shuffle_bosses == 'limited':
516521
entrance_pools['ChildBoss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
517522
entrance_pools['AdultBoss'] = world.get_shufflable_entrances(type='AdultBoss', only_primary=True)
523+
if worlds[0].settings.shuffle_ganon_tower:
524+
entrance_pools['AdultBoss'] += world.get_shufflable_entrances(type='SpecialBoss', only_primary=True)
518525
if worlds[0].settings.open_forest == 'closed':
519526
# Deku is forced vanilla below, so Queen Gohma must be vanilla to ensure she is reachable.
520527
# This is already enforced by the fill algorithm in most cases, but this covers the odd settings combination where it isn't.

HintList.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,28 @@ def tokens_required_by_settings(world: World) -> int:
227227
'LH Loach Fishing': lambda world: world.settings.shuffle_loach_reward == 'vanilla',
228228
}
229229

230+
def rainbow_bridge_hint_kind(world: World) -> str:
231+
if world.settings.bridge == 'open':
232+
return 'never'
233+
elif world.settings.bridge == 'vanilla':
234+
return 'always'
235+
elif world.settings.bridge == 'stones':
236+
return 'always' if world.settings.bridge_stones > 1 else 'sometimes'
237+
elif world.settings.bridge == 'medallions':
238+
return 'always' if world.settings.bridge_medallions > 1 else 'sometimes'
239+
elif world.settings.bridge == 'dungeons':
240+
return 'always' if world.settings.bridge_rewards > 2 else 'sometimes' if world.settings.bridge_rewards > 1 else 'never'
241+
elif world.settings.bridge == 'tokens':
242+
return 'always' if world.settings.bridge_tokens > 20 else 'sometimes' if world.settings.bridge_tokens > 10 else 'never'
243+
elif world.settings.bridge == 'hearts':
244+
return 'always' if world.settings.bridge_hearts > world.settings.starting_hearts + 1 else 'sometimes' if world.settings.bridge_hearts > world.settings.starting_hearts else 'never'
245+
else:
246+
raise NotImplementedError(f'Unimplemented bridge condition: {world.settings.bridge}')
247+
230248
# Entrance hints required under certain settings
231249
conditional_entrance_always: dict[str, Callable[[World], bool]] = {
232-
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: (world.settings.bridge != 'open'
233-
and (world.settings.bridge != 'stones' or world.settings.bridge_stones > 1)
234-
and (world.settings.bridge != 'medallions' or world.settings.bridge_medallions > 1)
235-
and (world.settings.bridge != 'dungeons' or world.settings.bridge_rewards > 2)
236-
and (world.settings.bridge != 'tokens' or world.settings.bridge_tokens > 20)
237-
and (world.settings.bridge != 'hearts' or world.settings.bridge_hearts > world.settings.starting_hearts + 1)),
250+
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: rainbow_bridge_hint_kind(world) == 'always',
251+
'Ganons Castle Main -> Ganons Castle Tower': lambda world: world.settings.trials > 3 or (rainbow_bridge_hint_kind(world) == 'always' and not world.shuffle_special_dungeon_entrances),
238252
}
239253

240254
# Dual hints required under certain settings
@@ -276,10 +290,8 @@ def tokens_required_by_settings(world: World) -> int:
276290
'Twinrova Rewards': lambda world: world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward'),
277291

278292
# Conditional entrance hints
279-
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: (world.settings.bridge != 'open'
280-
and (world.settings.bridge != 'dungeons' or world.settings.bridge_rewards > 1)
281-
and (world.settings.bridge != 'tokens' or world.settings.bridge_tokens > 10)
282-
and (world.settings.bridge != 'hearts' or world.settings.bridge_hearts > world.settings.starting_hearts)),
293+
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: rainbow_bridge_hint_kind(world) != 'never',
294+
'Ganons Castle Main -> Ganons Castle Tower': lambda world: world.settings.trials > 0 or (rainbow_bridge_hint_kind(world) != 'never' and not world.shuffle_special_dungeon_entrances),
283295
}
284296

285297
# Table of hints, format is (name, hint text, clear hint text, type of hint) there are special characters that are read for certain in game commands:
@@ -1392,6 +1404,7 @@ def tokens_required_by_settings(world: World) -> int:
13921404
'Kakariko Village -> Bottom of the Well': ("a #village well# leads to", None, 'entrance'),
13931405

13941406
'Ganons Castle Grounds -> Ganons Castle Lobby': ("the #rainbow bridge# leads to", None, 'entrance'),
1407+
'Ganons Castle Main -> Ganons Castle Tower': ("a #castle barrier# protects the way to", "#Ganon's trials# protect the way to", 'entrance'),
13951408

13961409
'KF Links House': ("Link's House", None, 'region'),
13971410
'Temple of Time': ("the #Temple of Time#", None, 'region'),
@@ -1479,6 +1492,7 @@ def tokens_required_by_settings(world: World) -> int:
14791492
'Morpha Boss Room': ("the #Giant Aquatic Amoeba#", "#Morpha#", 'region'),
14801493
'Bongo Bongo Boss Room': ("the #Phantom Shadow Beast#", "#Bongo Bongo#", 'region'),
14811494
'Twinrova Boss Room': ("the #Sorceress Sisters#", "#Twinrova#", 'region'),
1495+
'Ganons Castle Tower': ("#Ganon's Tower#", None, 'region'),
14821496

14831497
# Junk hints must satisfy all the following conditions:
14841498
# - They aren't inappropriate.

Hints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def at(spot: Spot, use_alt_hint: bool = False) -> HintArea:
441441
else:
442442
parent_region = current_spot.parent_region
443443

444-
if parent_region.hint and (original_parent.name == 'Root' or parent_region.name != 'Root'):
444+
if (parent_region.hint or (use_alt_hint and parent_region.alt_hint)) and (original_parent.name == 'Root' or parent_region.name != 'Root'):
445445
if use_alt_hint and parent_region.alt_hint:
446446
return parent_region.alt_hint
447447
return parent_region.hint
@@ -1271,7 +1271,7 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
12711271
for world in worlds:
12721272
for location in world.hinted_dungeon_reward_locations.values():
12731273
if world.settings.enhance_map_compass:
1274-
if world.mixed_pools_bosses or world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward'):
1274+
if world.entrance_rando_reward_hints:
12751275
# In these settings, there is not necessarily one dungeon reward in each dungeon,
12761276
# so we instead have each compass hint the area of its dungeon's vanilla reward.
12771277
compass_locations = [

ItemPool.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,20 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
812812
dungeon = Dungeon.from_vanilla_reward(ItemFactory(location.vanilla_item, world))
813813
dungeon.reward.append(ItemFactory(item, world))
814814

815+
# Ganon boss key
816+
elif location.vanilla_item == 'Boss Key (Ganons Castle)':
817+
if world.settings.shuffle_ganon_bosskey == 'vanilla':
818+
shuffle_item = False
819+
elif world.settings.shuffle_ganon_bosskey == 'remove':
820+
world.state.collect(ItemFactory(item, world))
821+
item = get_junk_item()[0]
822+
shuffle_item = True
823+
elif world.settings.shuffle_ganon_bosskey in ('any_dungeon', 'overworld', 'keysanity', 'regional'):
824+
shuffle_item = True
825+
else:
826+
dungeon = [dungeon for dungeon in world.dungeons if dungeon.name == 'Ganons Castle'][0]
827+
dungeon.boss_key.append(ItemFactory(item, world))
828+
815829
# Dungeon Items
816830
elif location.dungeon is not None:
817831
dungeon = location.dungeon
@@ -824,7 +838,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
824838
item = get_junk_item()[0]
825839
shuffle_item = True
826840
else:
827-
shuffle_setting = world.settings.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else world.settings.shuffle_ganon_bosskey
841+
shuffle_setting = world.settings.shuffle_bosskeys
828842
dungeon_collection = dungeon.boss_key
829843
if shuffle_setting == 'vanilla':
830844
shuffle_item = False

Patches.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
20072007
update_message_by_id(messages, map_id, map_message, allow_duplicates=True)
20082008
else:
20092009
dungeon_name, compass_id, map_id = dungeon_list[dungeon.name]
2010-
if world.mixed_pools_bosses or world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward'):
2010+
if world.entrance_rando_reward_hints:
20112011
vanilla_reward = world.get_location(dungeon.vanilla_boss_name).vanilla_item
20122012
vanilla_reward_location = world.hinted_dungeon_reward_locations[vanilla_reward]
20132013
area = HintArea.at(vanilla_reward_location)
@@ -2804,9 +2804,9 @@ def configure_dungeon_info(rom: Rom, world: World) -> None:
28042804
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_MQ_ENABLE'), int(mq_enable))
28052805
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_MQ_NEED_MAP'), int(enhance_map_compass))
28062806
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_ENABLE'), int('altar' in world.settings.misc_hints or enhance_map_compass))
2807-
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_NEED_COMPASS'), (2 if world.mixed_pools_bosses or world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward') else 1) if enhance_map_compass and world.settings.shuffle_dungeon_rewards != 'dungeon' else 0)
2807+
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_NEED_COMPASS'), (2 if world.entrance_rando_reward_hints else 1) if enhance_map_compass and world.settings.shuffle_dungeon_rewards != 'dungeon' else 0)
28082808
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_NEED_ALTAR'), int(not enhance_map_compass and world.settings.shuffle_dungeon_rewards != 'dungeon'))
2809-
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_SUMMARY_ENABLE'), int(not world.mixed_pools_bosses and world.settings.shuffle_dungeon_rewards in ('vanilla', 'reward')))
2809+
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_SUMMARY_ENABLE'), int(not world.entrance_rando_reward_hints))
28102810
rom.write_bytes(rom.sym('CFG_DUNGEON_REWARDS'), dungeon_rewards)
28112811
rom.write_bytes(rom.sym('CFG_DUNGEON_IS_MQ'), dungeon_is_mq)
28122812
rom.write_bytes(rom.sym('CFG_DUNGEON_REWARD_AREAS'), dungeon_reward_areas)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ issue. You should always Hard Reset to avoid this issue entirely.
118118
* New setting to speed up the boat ride in the Shadow Temple.
119119
* New `Require Lens of Truth for Treasure Chest Game` setting.
120120
* New option `Market Big Poes` for the `Misc. Hints` setting.
121+
* New setting `Shuffle Ganon's Tower Entrance` to allow shuffling the boss entrance to Ganon himself.
121122

122123
#### Bug fixes
123124
* Ocarina buttons required to play the Song of Time are now part of the `path of time` goal.

SettingsList.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ class SettingInfos:
667667
'glitched': {'settings': ['allowed_tricks', 'shuffle_interior_entrances', 'shuffle_hideout_entrances', 'shuffle_grotto_entrances',
668668
'shuffle_dungeon_entrances', 'shuffle_overworld_entrances', 'shuffle_gerudo_valley_river_exit', 'owl_drops',
669669
'warp_songs', 'spawn_positions', 'mq_dungeons_mode', 'mq_dungeons_specific',
670-
'mq_dungeons_count', 'shuffle_bosses', 'dungeon_shortcuts', 'deadly_bonks',
670+
'mq_dungeons_count', 'shuffle_bosses', 'shuffle_ganon_tower', 'dungeon_shortcuts', 'deadly_bonks',
671671
'shuffle_freestanding_items', 'shuffle_pots', 'shuffle_crates', 'shuffle_beehives', 'shuffle_silver_rupees', 'shuffle_wonderitems']},
672672
'none': {'settings': ['allowed_tricks', 'logic_no_night_tokens_without_suns_song', 'reachable_locations']},
673673
},
@@ -1760,6 +1760,28 @@ class SettingInfos:
17601760
'limited': 'Age-Restricted',
17611761
'full': 'Full',
17621762
},
1763+
disable = {
1764+
'off' : {'settings': ['shuffle_ganon_tower']},
1765+
},
1766+
shared = True,
1767+
gui_params = {
1768+
'randomize_key': 'randomize_settings',
1769+
},
1770+
)
1771+
1772+
shuffle_ganon_tower = Checkbutton(
1773+
gui_text = "Shuffle Ganon's Tower Entrance",
1774+
gui_tooltip = '''\
1775+
Shuffle the entrance from Ganon's Castle to
1776+
Ganon's Tower, just behind the trials barrier,
1777+
into the adult boss entrance pool if "Shuffle
1778+
Boss Entrances" is set to "Age-Restricted", or
1779+
the boss entrance pool if it's set to "Full".
1780+
1781+
The entrance from Ganon's Tower to Ganondorf's
1782+
boss room is never shuffled.
1783+
''',
1784+
default = False,
17631785
shared = True,
17641786
gui_params = {
17651787
'randomize_key': 'randomize_settings',

Spoiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def parse_data(self) -> None:
118118
"Dungeon": 5,
119119
"ChildBoss": 6,
120120
"AdultBoss": 6,
121+
"SpecialBoss": 6,
121122
"Hideout": 7,
122123
"SpecialInterior": 7,
123124
"Interior": 7,

0 commit comments

Comments
 (0)