Skip to content

Commit

Permalink
Merge "Setting to shuffle Ganon's tower into the boss entrance pool" (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
cjohnson57 committed Dec 8, 2024
2 parents 6a62810 + 067f5e2 commit b499369
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 60 deletions.
7 changes: 7 additions & 0 deletions EntranceShuffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def build_one_way_targets(world: World, types_to_include: Iterable[str], exclude
('AdultBoss', ('Spirit Temple Before Boss -> Twinrova Boss Room', { 'index': 0x008D, 'savewarp_addresses': [ 0xB062F2, 0xBC6122 ] }),
('Twinrova Boss Room -> Spirit Temple Before Boss', { 'index': 0x02F5 })),

('SpecialBoss', ('Ganons Castle Main -> Ganons Castle Tower', { 'index': 0x041B }),
('Ganons Castle Tower -> Ganons Castle Main', { 'index': 0x0534 })),

('Interior', ('Kokiri Forest -> KF Midos House', { 'index': 0x0433 }),
('KF Midos House -> Kokiri Forest', { 'index': 0x0443 })),
('Interior', ('Kokiri Forest -> KF Sarias House', { 'index': 0x0437 }),
Expand Down Expand Up @@ -508,13 +511,17 @@ def shuffle_random_entrances(worlds: list[World]) -> None:
if worlds[0].settings.shuffle_bosses == 'full':
entrance_pools['Boss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
entrance_pools['Boss'] += world.get_shufflable_entrances(type='AdultBoss', only_primary=True)
if worlds[0].settings.shuffle_ganon_tower:
entrance_pools['Boss'] += world.get_shufflable_entrances(type='SpecialBoss', only_primary=True)
if worlds[0].settings.open_forest == 'closed':
# Deku is forced vanilla below, so Queen Gohma must be vanilla to ensure she is reachable.
# This is already enforced by the fill algorithm in most cases, but this covers the odd settings combination where it isn't.
entrance_pools['Boss'].remove(world.get_entrance('Deku Tree Before Boss -> Queen Gohma Boss Room'))
elif worlds[0].settings.shuffle_bosses == 'limited':
entrance_pools['ChildBoss'] = world.get_shufflable_entrances(type='ChildBoss', only_primary=True)
entrance_pools['AdultBoss'] = world.get_shufflable_entrances(type='AdultBoss', only_primary=True)
if worlds[0].settings.shuffle_ganon_tower:
entrance_pools['AdultBoss'] += world.get_shufflable_entrances(type='SpecialBoss', only_primary=True)
if worlds[0].settings.open_forest == 'closed':
# Deku is forced vanilla below, so Queen Gohma must be vanilla to ensure she is reachable.
# This is already enforced by the fill algorithm in most cases, but this covers the odd settings combination where it isn't.
Expand Down
34 changes: 24 additions & 10 deletions HintList.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,28 @@ def tokens_required_by_settings(world: World) -> int:
'LH Loach Fishing': lambda world: world.settings.shuffle_loach_reward == 'vanilla',
}

def rainbow_bridge_hint_kind(world: World) -> str:
if world.settings.bridge == 'open':
return 'never'
elif world.settings.bridge == 'vanilla':
return 'always'
elif world.settings.bridge == 'stones':
return 'always' if world.settings.bridge_stones > 1 else 'sometimes'
elif world.settings.bridge == 'medallions':
return 'always' if world.settings.bridge_medallions > 1 else 'sometimes'
elif world.settings.bridge == 'dungeons':
return 'always' if world.settings.bridge_rewards > 2 else 'sometimes' if world.settings.bridge_rewards > 1 else 'never'
elif world.settings.bridge == 'tokens':
return 'always' if world.settings.bridge_tokens > 20 else 'sometimes' if world.settings.bridge_tokens > 10 else 'never'
elif world.settings.bridge == 'hearts':
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'
else:
raise NotImplementedError(f'Unimplemented bridge condition: {world.settings.bridge}')

# Entrance hints required under certain settings
conditional_entrance_always: dict[str, Callable[[World], bool]] = {
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: (world.settings.bridge != 'open'
and (world.settings.bridge != 'stones' or world.settings.bridge_stones > 1)
and (world.settings.bridge != 'medallions' or world.settings.bridge_medallions > 1)
and (world.settings.bridge != 'dungeons' or world.settings.bridge_rewards > 2)
and (world.settings.bridge != 'tokens' or world.settings.bridge_tokens > 20)
and (world.settings.bridge != 'hearts' or world.settings.bridge_hearts > world.settings.starting_hearts + 1)),
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: rainbow_bridge_hint_kind(world) == 'always',
'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),
}

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

# Conditional entrance hints
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: (world.settings.bridge != 'open'
and (world.settings.bridge != 'dungeons' or world.settings.bridge_rewards > 1)
and (world.settings.bridge != 'tokens' or world.settings.bridge_tokens > 10)
and (world.settings.bridge != 'hearts' or world.settings.bridge_hearts > world.settings.starting_hearts)),
'Ganons Castle Grounds -> Ganons Castle Lobby': lambda world: rainbow_bridge_hint_kind(world) != 'never',
'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),
}

# 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:
Expand Down Expand Up @@ -1392,6 +1404,7 @@ def tokens_required_by_settings(world: World) -> int:
'Kakariko Village -> Bottom of the Well': ("a #village well# leads to", None, 'entrance'),

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

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

# Junk hints must satisfy all the following conditions:
# - They aren't inappropriate.
Expand Down
4 changes: 2 additions & 2 deletions Hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def at(spot: Spot, use_alt_hint: bool = False) -> HintArea:
else:
parent_region = current_spot.parent_region

if parent_region.hint and (original_parent.name == 'Root' or parent_region.name != 'Root'):
if (parent_region.hint or (use_alt_hint and parent_region.alt_hint)) and (original_parent.name == 'Root' or parent_region.name != 'Root'):
if use_alt_hint and parent_region.alt_hint:
return parent_region.alt_hint
return parent_region.hint
Expand Down Expand Up @@ -1271,7 +1271,7 @@ def build_gossip_hints(spoiler: Spoiler, worlds: list[World]) -> None:
for world in worlds:
for location in world.hinted_dungeon_reward_locations.values():
if world.settings.enhance_map_compass:
if world.mixed_pools_bosses or world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward'):
if world.entrance_rando_reward_hints:
# In these settings, there is not necessarily one dungeon reward in each dungeon,
# so we instead have each compass hint the area of its dungeon's vanilla reward.
compass_locations = [
Expand Down
16 changes: 15 additions & 1 deletion ItemPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,20 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
dungeon = Dungeon.from_vanilla_reward(ItemFactory(location.vanilla_item, world))
dungeon.reward.append(ItemFactory(item, world))

# Ganon boss key
elif location.vanilla_item == 'Boss Key (Ganons Castle)':
if world.settings.shuffle_ganon_bosskey == 'vanilla':
shuffle_item = False
elif world.settings.shuffle_ganon_bosskey == 'remove':
world.state.collect(ItemFactory(item, world))
item = get_junk_item()[0]
shuffle_item = True
elif world.settings.shuffle_ganon_bosskey in ('any_dungeon', 'overworld', 'keysanity', 'regional'):
shuffle_item = True
else:
dungeon = [dungeon for dungeon in world.dungeons if dungeon.name == 'Ganons Castle'][0]
dungeon.boss_key.append(ItemFactory(item, world))

# Dungeon Items
elif location.dungeon is not None:
dungeon = location.dungeon
Expand All @@ -824,7 +838,7 @@ def get_pool_core(world: World) -> tuple[list[str], dict[str, Item]]:
item = get_junk_item()[0]
shuffle_item = True
else:
shuffle_setting = world.settings.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else world.settings.shuffle_ganon_bosskey
shuffle_setting = world.settings.shuffle_bosskeys
dungeon_collection = dungeon.boss_key
if shuffle_setting == 'vanilla':
shuffle_item = False
Expand Down
6 changes: 3 additions & 3 deletions Patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -2007,7 +2007,7 @@ def update_scrub_text(message: bytearray, text_replacement: list[str], default_p
update_message_by_id(messages, map_id, map_message, allow_duplicates=True)
else:
dungeon_name, compass_id, map_id = dungeon_list[dungeon.name]
if world.mixed_pools_bosses or world.settings.shuffle_dungeon_rewards not in ('vanilla', 'reward'):
if world.entrance_rando_reward_hints:
vanilla_reward = world.get_location(dungeon.vanilla_boss_name).vanilla_item
vanilla_reward_location = world.hinted_dungeon_reward_locations[vanilla_reward]
area = HintArea.at(vanilla_reward_location)
Expand Down Expand Up @@ -2804,9 +2804,9 @@ def configure_dungeon_info(rom: Rom, world: World) -> None:
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_MQ_ENABLE'), int(mq_enable))
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_MQ_NEED_MAP'), int(enhance_map_compass))
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_ENABLE'), int('altar' in world.settings.misc_hints or enhance_map_compass))
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)
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)
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_NEED_ALTAR'), int(not enhance_map_compass and world.settings.shuffle_dungeon_rewards != 'dungeon'))
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')))
rom.write_int32(rom.sym('CFG_DUNGEON_INFO_REWARD_SUMMARY_ENABLE'), int(not world.entrance_rando_reward_hints))
rom.write_bytes(rom.sym('CFG_DUNGEON_REWARDS'), dungeon_rewards)
rom.write_bytes(rom.sym('CFG_DUNGEON_IS_MQ'), dungeon_is_mq)
rom.write_bytes(rom.sym('CFG_DUNGEON_REWARD_AREAS'), dungeon_reward_areas)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ issue. You should always Hard Reset to avoid this issue entirely.
* New setting to speed up the boat ride in the Shadow Temple.
* New `Require Lens of Truth for Treasure Chest Game` setting.
* New option `Market Big Poes` for the `Misc. Hints` setting.
* New setting `Shuffle Ganon's Tower Entrance` to allow shuffling the boss entrance to Ganon himself.

#### Bug fixes
* Ocarina buttons required to play the Song of Time are now part of the `path of time` goal.
Expand Down
24 changes: 23 additions & 1 deletion SettingsList.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ class SettingInfos:
'glitched': {'settings': ['allowed_tricks', 'shuffle_interior_entrances', 'shuffle_hideout_entrances', 'shuffle_grotto_entrances',
'shuffle_dungeon_entrances', 'shuffle_overworld_entrances', 'shuffle_gerudo_valley_river_exit', 'owl_drops',
'warp_songs', 'spawn_positions', 'mq_dungeons_mode', 'mq_dungeons_specific',
'mq_dungeons_count', 'shuffle_bosses', 'dungeon_shortcuts', 'deadly_bonks',
'mq_dungeons_count', 'shuffle_bosses', 'shuffle_ganon_tower', 'dungeon_shortcuts', 'deadly_bonks',
'shuffle_freestanding_items', 'shuffle_pots', 'shuffle_crates', 'shuffle_beehives', 'shuffle_silver_rupees', 'shuffle_wonderitems']},
'none': {'settings': ['allowed_tricks', 'logic_no_night_tokens_without_suns_song', 'reachable_locations']},
},
Expand Down Expand Up @@ -1760,6 +1760,28 @@ class SettingInfos:
'limited': 'Age-Restricted',
'full': 'Full',
},
disable = {
'off' : {'settings': ['shuffle_ganon_tower']},
},
shared = True,
gui_params = {
'randomize_key': 'randomize_settings',
},
)

shuffle_ganon_tower = Checkbutton(
gui_text = "Shuffle Ganon's Tower Entrance",
gui_tooltip = '''\
Shuffle the entrance from Ganon's Castle to
Ganon's Tower, just behind the trials barrier,
into the adult boss entrance pool if "Shuffle
Boss Entrances" is set to "Age-Restricted", or
the boss entrance pool if it's set to "Full".
The entrance from Ganon's Tower to Ganondorf's
boss room is never shuffled.
''',
default = False,
shared = True,
gui_params = {
'randomize_key': 'randomize_settings',
Expand Down
1 change: 1 addition & 0 deletions Spoiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def parse_data(self) -> None:
"Dungeon": 5,
"ChildBoss": 6,
"AdultBoss": 6,
"SpecialBoss": 6,
"Hideout": 7,
"SpecialInterior": 7,
"Interior": 7,
Expand Down
Loading

0 comments on commit b499369

Please sign in to comment.