Merge branch 'OverworldShuffleDev' into OverworldShuffle

This commit is contained in:
codemann8
2024-01-10 06:09:28 -06:00
18 changed files with 431 additions and 131 deletions

View File

@@ -310,7 +310,7 @@ class World(object):
return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player) return self.is_tile_swapped(0x03, player) and self.is_tile_swapped(0x1b, player)
def is_bombshop_start(self, player): def is_bombshop_start(self, player):
return self.is_tile_swapped(0x2c, player) and (self.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] or not self.shufflelinks[player]) return self.is_tile_swapped(0x2c, player)
def is_pyramid_open(self, player): def is_pyramid_open(self, player):
if self.open_pyramid[player] == 'yes': if self.open_pyramid[player] == 'yes':
@@ -318,7 +318,7 @@ class World(object):
elif self.open_pyramid[player] == 'no': elif self.open_pyramid[player] == 'no':
return False return False
else: else:
if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']:
return False return False
elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']: elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']:
return True return True
@@ -3464,7 +3464,7 @@ class Pot(object):
# byte 0: DDDE EEEE (DR, ER) # byte 0: DDDE EEEE (DR, ER)
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4} dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4}
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10} 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10, "district": 11}
# byte 1: LLLW WSS? (logic, mode, sword) # byte 1: LLLW WSS? (logic, mode, sword)
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5} logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5}

111
Bosses.py
View File

@@ -4,11 +4,12 @@ import RaceRandom as random
from BaseClasses import Boss, FillError from BaseClasses import Boss, FillError
def BossFactory(boss, player): def BossFactory(boss, player, on_ice=False):
if boss is None: if boss is None:
return None return None
if boss in boss_table: if boss in boss_table:
enemizer_name, defeat_rule = boss_table[boss] enemizer_name, normal_defeat_rule, ice_defeat_rule = boss_table[boss]
defeat_rule = ice_defeat_rule if on_ice else normal_defeat_rule
return Boss(boss, enemizer_name, defeat_rule, player) return Boss(boss, enemizer_name, defeat_rule, player)
logging.getLogger('').error('Unknown Boss: %s', boss) logging.getLogger('').error('Unknown Boss: %s', boss)
@@ -41,16 +42,21 @@ def MoldormDefeatRule(state, player):
def HelmasaurKingDefeatRule(state, player): def HelmasaurKingDefeatRule(state, player):
return (state.has('Hammer', player) or state.can_use_bombs(player)) and (state.has_sword(player) or state.can_shoot_arrows(player)) return (state.has('Hammer', player) or state.can_use_bombs(player)) and (state.has_sword(player) or state.can_shoot_arrows(player))
def IceHelmasaurKingDefeatRule(state, player):
return state.can_use_bombs(player) and (state.has_sword(player) or state.can_shoot_arrows(player))
def ArrghusDefeatRule(state, player): def ArrghusDefeatRule(state, player):
if not state.has('Hookshot', player): if not state.has('Hookshot', player):
return False return False
# TODO: ideally we would have a check for bow and silvers, which combined with the
# hookshot is enough. This is not coded yet because the silvers that only work in pyramid feature
# makes this complicated
if state.has_blunt_weapon(player): if state.has_blunt_weapon(player):
return True return True
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or #assuming mostly gitting two puff with one shot if state.can_shoot_arrows(player) and state.has('Silver Arrows', player) and state.world.difficulty_adjustments[player] not in ['hard', 'expert']:
return True
return ((state.has('Fire Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 12))) or # assuming mostly getting two puffs with one shot
(state.has('Ice Rod', player) and state.can_use_bombs(player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) (state.has('Ice Rod', player) and state.can_use_bombs(player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
@@ -64,9 +70,27 @@ def MothulaDefeatRule(state, player):
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16)) (state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16))
) )
def BlindDefeatRule(state, player): def BlindDefeatRule(state, player):
return state.has_blunt_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) return state.has_blunt_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
def IceBlindDefeatRule(state, player):
return (
(
# weapon
state.has_beam_sword(player) or
state.has('Cane of Somaria', player) or
(state.has('Cane of Byrna', player) and state.can_extend_magic(player, 16))
) and
(
# protection
state.has('Red Shield', player) or
(state.has('Cane of Byrna', player) and state.world.difficulty_adjustments[player] not in ['hard', 'expert'])
)
)
def KholdstareDefeatRule(state, player): def KholdstareDefeatRule(state, player):
return ( return (
( (
@@ -90,9 +114,39 @@ def KholdstareDefeatRule(state, player):
) )
) )
def IceKholdstareDefeatRule(state, player):
return (
(
state.has('Fire Rod', player) or
(
state.has('Bombos', player) and
# FIXME: the following only actually works for the vanilla location for swordless
(state.has_sword(player) or state.world.swords[player] == 'swordless')
)
) and
(
state.has_beam_sword(player) or
(state.has('Fire Rod', player) and state.can_extend_magic(player, 20)) or
# FIXME: this actually only works for the vanilla location for swordless
(
state.has('Fire Rod', player) and
state.has('Bombos', player) and
(state.has_sword(player) or state.world.swords[player] == 'swordless') and
state.can_extend_magic(player, 16)
)
)
)
def VitreousDefeatRule(state, player): def VitreousDefeatRule(state, player):
return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_blunt_weapon(player) return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_blunt_weapon(player)
def IceVitreousDefeatRule(state, player):
return (state.can_shoot_arrows(player) and state.can_use_bombs(player)) or state.has_beam_sword(player)
def TrinexxDefeatRule(state, player): def TrinexxDefeatRule(state, player):
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
return False return False
@@ -102,24 +156,36 @@ def TrinexxDefeatRule(state, player):
(state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or (state.has('Master Sword', player) and state.can_extend_magic(player, 16)) or
(state.has_sword(player) and state.can_extend_magic(player, 32))) (state.has_sword(player) and state.can_extend_magic(player, 32)))
def IceTrinexxDefeatRule(state, player):
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player) and state.has_Boots(player)):
return False
return (state.has('Golden Sword', player) or
(state.has('Tempered Sword', player) and state.can_extend_magic(player, 16)) or
((state.has('Hammer', player) or
state.has('Master Sword', player)) and state.can_extend_magic(player, 32))) # rod spam rule
def AgahnimDefeatRule(state, player): def AgahnimDefeatRule(state, player):
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
boss_table = { boss_table = {
'Armos Knights': ('Armos', ArmosKnightsDefeatRule), 'Armos Knights': ('Armos', ArmosKnightsDefeatRule, ArmosKnightsDefeatRule),
'Lanmolas': ('Lanmola', LanmolasDefeatRule), 'Lanmolas': ('Lanmola', LanmolasDefeatRule, LanmolasDefeatRule),
'Moldorm': ('Moldorm', MoldormDefeatRule), 'Moldorm': ('Moldorm', MoldormDefeatRule, MoldormDefeatRule),
'Helmasaur King': ('Helmasaur', HelmasaurKingDefeatRule), 'Helmasaur King': ('Helmasaur', HelmasaurKingDefeatRule, IceHelmasaurKingDefeatRule),
'Arrghus': ('Arrghus', ArrghusDefeatRule), 'Arrghus': ('Arrghus', ArrghusDefeatRule, ArrghusDefeatRule),
'Mothula': ('Mothula', MothulaDefeatRule), 'Mothula': ('Mothula', MothulaDefeatRule, MothulaDefeatRule),
'Blind': ('Blind', BlindDefeatRule), 'Blind': ('Blind', BlindDefeatRule, IceBlindDefeatRule),
'Kholdstare': ('Kholdstare', KholdstareDefeatRule), 'Kholdstare': ('Kholdstare', KholdstareDefeatRule, IceKholdstareDefeatRule),
'Vitreous': ('Vitreous', VitreousDefeatRule), 'Vitreous': ('Vitreous', VitreousDefeatRule, IceVitreousDefeatRule),
'Trinexx': ('Trinexx', TrinexxDefeatRule), 'Trinexx': ('Trinexx', TrinexxDefeatRule, IceTrinexxDefeatRule),
'Agahnim': ('Agahnim', AgahnimDefeatRule), 'Agahnim': ('Agahnim', AgahnimDefeatRule, AgahnimDefeatRule),
'Agahnim2': ('Agahnim2', AgahnimDefeatRule) 'Agahnim2': ('Agahnim2', AgahnimDefeatRule, AgahnimDefeatRule)
} }
def can_place_boss(world, player, boss, dungeon_name, level=None): def can_place_boss(world, player, boss, dungeon_name, level=None):
if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace': if world.swords[player] in ['swordless'] and boss == 'Kholdstare' and dungeon_name != 'Ice Palace':
return False return False
@@ -132,6 +198,11 @@ def can_place_boss(world, player, boss, dungeon_name, level=None):
if boss in ["Blind"]: if boss in ["Blind"]:
return False return False
# no Trinexx on Ice in doors without doing some health modelling
if world.doorShuffle[player] != 'vanilla' and boss == 'Trinexx':
if dungeon_name == 'Ganons Tower' and level == 'bottom':
return False
if dungeon_name == 'Tower of Hera' and boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]: if dungeon_name == 'Tower of Hera' and boss in ["Armos Knights", "Arrghus", "Blind", "Trinexx", "Lanmolas"]:
return False return False
@@ -151,6 +222,7 @@ def place_bosses(world, player):
['Tower of Hera', None], ['Tower of Hera', None],
['Skull Woods', None], ['Skull Woods', None],
['Ganons Tower', 'middle'], ['Ganons Tower', 'middle'],
['Ganons Tower', 'bottom'],
['Eastern Palace', None], ['Eastern Palace', None],
['Desert Palace', None], ['Desert Palace', None],
['Palace of Darkness', None], ['Palace of Darkness', None],
@@ -159,7 +231,6 @@ def place_bosses(world, player):
['Ice Palace', None], ['Ice Palace', None],
['Misery Mire', None], ['Misery Mire', None],
['Turtle Rock', None], ['Turtle Rock', None],
['Ganons Tower', 'bottom'],
] ]
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
@@ -246,4 +317,4 @@ def place_boss(boss, level, loc, loc_text, world, player):
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0] loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')' loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text) logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player) world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player, level == 'bottom')

View File

@@ -1,10 +1,18 @@
# Changelog # Changelog
## 0.3.4.1
- Implemented new District ER mode option
- Added alternate boss logic when in GT Ice Basement
- Updated Inverted 2.0 to start in Bomb Shop regardless of ER mode
- Fixed broken customizer features with OWR Tile Flip
- Allowing Insanity ER + Standard to decouple standard entrances
## 0.3.4.0 ## 0.3.4.0
- \~Merged in some things from DR v1.4.0.0-v~ - \~Merged in DR v1.2.0.23~
- Improved bunny-walking algorithm - Improved bunny-walking algorithm
- Improved multiworld balancing - Improved multiworld balancing
- Implemented Hyrid Major Glitches logic (thanks Muffins/Espeon) - \~Merged in some things from DR v1.4.0.0-v~
- Implemented Hybrid Major Glitches logic (thanks Muffins/Espeon)
- Added sparkles to Bonk Drop locations for better visibility - Added sparkles to Bonk Drop locations for better visibility
- Some tweaks/improvements to Shuffle Song Instruments - Some tweaks/improvements to Shuffle Song Instruments
- Replaced Save Settings on Exit with Settings on Load - Replaced Save Settings on Exit with Settings on Load

View File

@@ -262,23 +262,24 @@ def vanilla_key_logic(world, player):
world.key_layout[player][builder.name] = key_layout world.key_layout[player][builder.name] = key_layout
log_key_logic(builder.name, key_layout.key_logic) log_key_logic(builder.name, key_layout.key_logic)
# special adjustments for vanilla # special adjustments for vanilla
if world.mode[player] != 'standard' and world.dropshuffle[player] == 'none': if world.keyshuffle[player] != 'universal':
# adjust hc doors if world.mode[player] != 'standard' and not world.dropshuffle[player]:
def adjust_hc_door(door_rule): # adjust hc doors
if door_rule.new_rules[KeyRuleType.WorstCase] == 3: def adjust_hc_door(door_rule):
door_rule.new_rules[KeyRuleType.WorstCase] = 2 if door_rule.new_rules[KeyRuleType.WorstCase] == 3:
door_rule.small_key_num = 2 door_rule.new_rules[KeyRuleType.WorstCase] = 2
door_rule.small_key_num = 2
rules = world.key_logic[player]['Hyrule Castle'].door_rules rules = world.key_logic[player]['Hyrule Castle'].door_rules
adjust_hc_door(rules['Sewers Secret Room Key Door S']) adjust_hc_door(rules['Sewers Secret Room Key Door S'])
adjust_hc_door(rules['Hyrule Dungeon Map Room Key Door S']) adjust_hc_door(rules['Hyrule Dungeon Map Room Key Door S'])
adjust_hc_door(rules['Sewers Dark Cross Key Door N']) adjust_hc_door(rules['Sewers Dark Cross Key Door N'])
# adjust pod front door # adjust pod front door
pod_front = world.key_logic[player]['Palace of Darkness'].door_rules['PoD Middle Cage N'] pod_front = world.key_logic[player]['Palace of Darkness'].door_rules['PoD Middle Cage N']
if pod_front.new_rules[KeyRuleType.WorstCase] == 6: if pod_front.new_rules[KeyRuleType.WorstCase] == 6:
pod_front.new_rules[KeyRuleType.WorstCase] = 1 pod_front.new_rules[KeyRuleType.WorstCase] = 1
pod_front.small_key_num = 1 pod_front.small_key_num = 1
# gt logic? I'm unsure it needs adjusting # gt logic? I'm unsure it needs adjusting
def validate_vanilla_reservation(dungeon, world, player): def validate_vanilla_reservation(dungeon, world, player):

View File

@@ -986,7 +986,8 @@ def balance_prices(world, player):
def check_hints(world, player): def check_hints(world, player):
if world.shuffle[player] in ['simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity']: if (world.shuffle[player] in ['simple', 'restricted', 'full', 'district', 'swapped', 'crossed', 'insanity']
or (world.shuffle[player] in ['lite', 'lean'] and world.shopsanity[player])):
for shop, location_list in shop_to_location_table.items(): for shop, location_list in shop_to_location_table.items():
if shop in ['Capacity Upgrade', 'Paradox Shop', 'Potion Shop']: if shop in ['Capacity Upgrade', 'Paradox Shop', 'Potion Shop']:
continue # near the queen, near potions, and near 7 chests are fine continue # near the queen, near potions, and near 7 chests are fine

View File

@@ -37,7 +37,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new
from source.tools.BPS import create_bps_from_data from source.tools.BPS import create_bps_from_data
from source.classes.CustomSettings import CustomSettings from source.classes.CustomSettings import CustomSettings
version_number = '1.2.0.22' version_number = '1.2.0.23'
version_branch = '-u' version_branch = '-u'
__version__ = f'{version_number}{version_branch}' __version__ = f'{version_number}{version_branch}'

View File

@@ -8,7 +8,7 @@ from OWEdges import OWTileRegions, OWEdgeGroups, OWEdgeGroupsTerrain, OWExitType
from OverworldGlitchRules import create_owg_connections from OverworldGlitchRules import create_owg_connections
from Utils import bidict from Utils import bidict
version_number = '0.3.4.0' version_number = '0.3.4.1'
# branch indicator is intentionally different across branches # branch indicator is intentionally different across branches
version_branch = '' version_branch = ''
@@ -1044,10 +1044,10 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
raise GenerationException('Could not find valid tile flips') raise GenerationException('Could not find valid tile flips')
# tile shuffle happens here # tile shuffle happens here
removed = copy.deepcopy(nonflipped_groups) removed = []
if 0 < undefined_chance < 100: if 0 < undefined_chance < 100:
for group in [g for g in groups if g not in nonflipped_groups]: for group in groups:
if group not in flipped_groups and random.randint(1, 100) > undefined_chance: if group[0] in nonflipped_groups or (group[0] not in flipped_groups and random.randint(1, 100) > undefined_chance):
removed.append(group) removed.append(group)
# save shuffled tiles to list # save shuffled tiles to list
@@ -1072,7 +1072,7 @@ def shuffle_tiles(world, groups, result_list, do_grouped, forced_flips, player):
attempts -= 1 attempts -= 1
continue continue
# ensure sanc can be placed in LW in certain modes # ensure sanc can be placed in LW in certain modes
if not do_grouped and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lean', 'swapped', 'crossed', 'insanity'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'): if not do_grouped and world.shuffle[player] in ['simple', 'restricted', 'full', 'district'] and world.mode[player] != 'inverted' and (world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3 or world.mode[player] == 'standard'):
free_dw_drops = parity[5] + (1 if world.shuffle_ganon[player] else 0) free_dw_drops = parity[5] + (1 if world.shuffle_ganon[player] else 0)
free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon[player] else 0) free_drops = 6 + (1 if world.mode[player] != 'standard' else 0) + (1 if world.shuffle_ganon[player] else 0)
if free_dw_drops == free_drops: if free_dw_drops == free_drops:
@@ -1124,7 +1124,7 @@ def define_tile_groups(world, do_grouped, player):
return False return False
# sanctuary/chapel should not be flipped if S+Q guaranteed to output on that screen # sanctuary/chapel should not be flipped if S+Q guaranteed to output on that screen
if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull'] \ if 0x13 in group and ((world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district'] \
and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \ and (world.mode[player] in ['standard', 'inverted'] or world.doorShuffle[player] != 'crossed' or world.intensity[player] < 3)) \
or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')): or (world.shuffle[player] in ['lite', 'lean'] and world.mode[player] == 'inverted')):
return False return False
@@ -1138,24 +1138,31 @@ def define_tile_groups(world, do_grouped, player):
groups.append([0x80]) groups.append([0x80])
groups.append([0x81]) groups.append([0x81])
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple']: # hyrule castle and sanctuary connector
merge_groups([[0x03, 0x0a], [0x28, 0x29]]) if world.shuffle[player] in ['vanilla', 'district'] or (world.mode[player] == 'standard' and world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']):
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']:
merge_groups([[0x13, 0x14]])
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted']:
merge_groups([[0x05, 0x07]])
if world.shuffle[player] == 'vanilla' or (world.mode[player] == 'standard' and world.shuffle[player] in ['dungeonssimple', 'dungeonsfull']):
merge_groups([[0x13, 0x14, 0x1b]]) merge_groups([[0x13, 0x14, 0x1b]])
# sanctuary and grave connector
if world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite']:
merge_groups([[0x13, 0x14]])
# cross-screen connector
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'simple', 'district']:
merge_groups([[0x03, 0x0a], [0x28, 0x29]])
# turtle rock connector
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'simple', 'restricted', 'district']:
merge_groups([[0x05, 0x07]])
# all non-parallel screens
if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped): if world.owShuffle[player] == 'vanilla' and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]]) merge_groups([[0x00, 0x2d, 0x80], [0x0f, 0x81], [0x1a, 0x1b], [0x28, 0x29], [0x30, 0x3a]])
# special case: non-parallel keep similar
if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped): if world.owShuffle[player] == 'parallel' and world.owKeepSimilar[player] and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x28, 0x29]]) merge_groups([[0x28, 0x29]])
# whirlpool screens
if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped): if not world.owWhirlpoolShuffle[player] and (world.owCrossed[player] == 'none' or do_grouped):
merge_groups([[0x0f, 0x35], [0x12, 0x15, 0x33, 0x3f]]) merge_groups([[0x0f, 0x35], [0x12, 0x15, 0x33, 0x3f]])
@@ -1562,7 +1569,7 @@ def validate_layout(world, player):
for dest_region in entrance_connectors[region_name]: for dest_region in entrance_connectors[region_name]:
if dest_region not in explored_regions: if dest_region not in explored_regions:
explore_region(dest_region) explore_region(dest_region)
if world.shuffle[player] not in ['insanity'] and region_name in sane_connectors: if world.shuffle[player] not in ['district', 'insanity'] and region_name in sane_connectors:
for dest_region in sane_connectors[region_name]: for dest_region in sane_connectors[region_name]:
if dest_region not in explored_regions: if dest_region not in explored_regions:
explore_region(dest_region) explore_region(dest_region)
@@ -1619,11 +1626,13 @@ def validate_layout(world, player):
break break
# check if entrances in region could be used to access region # check if entrances in region could be used to access region
if world.shuffle[player] != 'vanilla': if world.shuffle[player] != 'vanilla':
# TODO: For District ER, we need to check if there is a dropdown or connector that is able to connect
for entrance in [e for e in unreachable_regions[region_name].exits if e.spot_type == 'Entrance']: for entrance in [e for e in unreachable_regions[region_name].exits if e.spot_type == 'Entrance']:
if (entrance.name == 'Links House' and (world.mode[player] == 'inverted' or not world.shufflelinks[player] or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ if (entrance.name == 'Links House' and ((not world.is_bombshop_start(player) and not world.shufflelinks[player]) or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \
or (entrance.name == 'Big Bomb Shop' and (world.mode[player] != 'inverted' or not world.shufflelinks[player] or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \ or (entrance.name == 'Big Bomb Shop' and ((world.is_bombshop_start(player) and not world.shufflelinks[player]) or world.shuffle[player] in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean'])) \
or (entrance.name == 'Ganons Tower' and (world.mode[player] != 'inverted' and not world.shuffle_ganon[player])) \ or (entrance.name == 'Ganons Tower' and (not world.is_atgt_swapped(player) and not world.shuffle_ganon[player])) \
or (entrance.name in ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] and world.shuffle[player] not in ['insanity']) \ or (entrance.name == 'Agahnims Tower' and (world.is_atgt_swapped(player) and not world.shuffle_ganon[player])) \
or (entrance.name in ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)'] and world.shuffle[player] not in ['district', 'insanity']) \
or (entrance.name == 'Tavern North' and not world.shuffletavern[player]): or (entrance.name == 'Tavern North' and not world.shuffletavern[player]):
continue # these are fixed entrances and cannot be used for gaining access to region continue # these are fixed entrances and cannot be used for gaining access to region
if entrance.name not in drop_entrances \ if entrance.name not in drop_entrances \

View File

@@ -111,9 +111,6 @@ These are now independent of retro mode and have three options: None, Random, an
* 1.4.0.1v * 1.4.0.1v
* Key logic: Vanilla key logic fixes. Statically set some HC logic and PoD front door * Key logic: Vanilla key logic fixes. Statically set some HC logic and PoD front door
* 1.4.0.0v
* Generation: fix for bunny walk logic taking up too much memory
* Key Logic: Partial is now the new default
* 1.3.0.9v * 1.3.0.9v
* Ganonhunt: playthrough no longer collects crystals * Ganonhunt: playthrough no longer collects crystals
* Vanilla Fill: Uncle weapon is always a sword, medallions for Mire/TR will be vanilla * Vanilla Fill: Uncle weapon is always a sword, medallions for Mire/TR will be vanilla
@@ -123,7 +120,10 @@ These are now independent of retro mode and have three options: None, Random, an
* MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold) * MW Progression Balancing: Change to be percentage based instead of raw count. (80% threshold)
* Take anys: Good Bee cave chosen as take any should no longer prevent generation * Take anys: Good Bee cave chosen as take any should no longer prevent generation
* Money balancing: Fixed generation issue * Money balancing: Fixed generation issue
1.2.0.22u 1.2.0.23u
* Generation: fix for bunny walk logic taking up too much memory
* Key Logic: Partial is now the new default
* 1.2.0.22u
* Flute can't be activated in rain state (except glitched modes) (Thanks codemann!) * Flute can't be activated in rain state (except glitched modes) (Thanks codemann!)
* ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used) * ER: Minor fix for Link's House on DM in Insanity (escape cave should not be re-used)
* Logic issues: * Logic issues:

26
Rom.py
View File

@@ -799,7 +799,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
owFlags |= 0x0200 owFlags |= 0x0200
# setting spriteID to D8, a placeholder sprite we use to inform ROM to spawn a dynamic item # setting spriteID to D9, a placeholder sprite we use to inform ROM to spawn a dynamic item
#for address in bonk_addresses: #for address in bonk_addresses:
for address in [b for b in bonk_addresses if b != 0x4D0AE]: # temp fix for screen 1A murahdahla sprite replacement for address in [b for b in bonk_addresses if b != 0x4D0AE]: # temp fix for screen 1A murahdahla sprite replacement
rom.write_byte(address, 0xD9) rom.write_byte(address, 0xD9)
@@ -1549,7 +1549,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# b - Big Key # b - Big Key
# a - Small Key # a - Small Key
# #
enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none' enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla'
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00)
| (0x02 if world.bigkeyshuffle[player] else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00)
@@ -1667,7 +1667,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
# rom.write_byte(snes_to_pc(0x0DB730), 0x08) # allows chickens to travel across water # rom.write_byte(snes_to_pc(0x0DB730), 0x08) # allows chickens to travel across water
# allow smith into multi-entrance caves in appropriate shuffles # allow smith into multi-entrance caves in appropriate shuffles
if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'): if world.shuffle[player] in ['restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity'] or (world.shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
rom.write_byte(0x18004C, 0x01) rom.write_byte(0x18004C, 0x01)
# set correct flag for hera basement item # set correct flag for hera basement item
@@ -2142,7 +2142,7 @@ def write_strings(rom, world, player, team):
tt.removeUnwantedText() tt.removeUnwantedText()
# Let's keep this guy's text accurate to the shuffle setting. # Let's keep this guy's text accurate to the shuffle setting.
if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple']: if world.shuffle[player] in ['vanilla', 'dungeonsfull', 'dungeonssimple', 'lite', 'lean']:
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.' tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
@@ -2193,7 +2193,7 @@ def write_strings(rom, world, player, team):
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones. # Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
if world.shuffle[player] not in ['lite', 'lean']: if world.shuffle[player] not in ['lite', 'lean']:
entrances_to_hint.update(InconvenientOtherEntrances) entrances_to_hint.update(InconvenientOtherEntrances)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'swapped']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'district']:
hint_count = 0 hint_count = 0
elif world.shuffle[player] in ['simple', 'restricted']: elif world.shuffle[player] in ['simple', 'restricted']:
hint_count = 2 hint_count = 2
@@ -2227,7 +2227,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update(OtherEntrances) entrances_to_hint.update(OtherEntrances)
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'}) entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean']: if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean', 'district']:
if world.shufflelinks[player]: if world.shufflelinks[player]:
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'}) entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
entrances_to_hint.update({'Links House': 'The hero\'s old residence'}) entrances_to_hint.update({'Links House': 'The hero\'s old residence'})
@@ -2244,7 +2244,7 @@ def write_strings(rom, world, player, team):
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'}) entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
else: else:
entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'}) entrances_to_hint.update({'Pyramid Entrance': 'The pyramid ledge'})
hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'swapped'] else 0 hint_count = 4 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 0
hint_count -= 2 if world.shuffle[player] not in ['simple', 'restricted'] else 0 hint_count -= 2 if world.shuffle[player] not in ['simple', 'restricted'] else 0
for entrance in all_entrances: for entrance in all_entrances:
if entrance.name in entrances_to_hint: if entrance.name in entrances_to_hint:
@@ -2263,7 +2263,7 @@ def write_strings(rom, world, player, team):
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']: if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
locations_to_hint.extend(InconvenientVanillaLocations) locations_to_hint.extend(InconvenientVanillaLocations)
random.shuffle(locations_to_hint) random.shuffle(locations_to_hint)
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'swapped'] else 5 hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 5
hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
del locations_to_hint[hint_count:] del locations_to_hint[hint_count:]
for location in locations_to_hint: for location in locations_to_hint:
@@ -2338,7 +2338,7 @@ def write_strings(rom, world, player, team):
if world.bigkeyshuffle[player]: if world.bigkeyshuffle[player]:
items_to_hint.extend(BigKeys) items_to_hint.extend(BigKeys)
random.shuffle(items_to_hint) random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'swapped'] else 8 hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' 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 and len(items_to_hint) > 0: while hint_count > 0 and len(items_to_hint) > 0:
@@ -2382,11 +2382,11 @@ def write_strings(rom, world, player, team):
choices.clear() choices.clear()
choices.append(location_item) choices.append(location_item)
if hint_type == 'foolish': if hint_type == 'foolish':
if district.dungeons and world.shuffle[player] != 'vanilla': if district.dungeons and world.shuffle[player] not in ['vanilla', 'district']:
choices.extend(district.dungeons) choices.extend(district.dungeons)
hint_type = 'dungeon_path' hint_type = 'dungeon_path'
elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple', elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple',
'dungeonsfull']: 'dungeonsfull', 'district']:
choices.extend([x.hint_text for x in district.access_points]) choices.extend([x.hint_text for x in district.access_points])
hint_type = 'connector' hint_type = 'connector'
if hint_type == 'foolish': if hint_type == 'foolish':
@@ -2732,10 +2732,6 @@ def set_inverted_mode(world, player, rom, inverted_buffer):
write_int16s(rom, snes_to_pc(0x1BB810), [0x00BE, 0x00C0, 0x013E]) # update pyramid hole entrance write_int16s(rom, snes_to_pc(0x1BB810), [0x00BE, 0x00C0, 0x013E]) # update pyramid hole entrance
write_int16s(rom, snes_to_pc(0x1BB836), [0x001B, 0x001B, 0x001B]) write_int16s(rom, snes_to_pc(0x1BB836), [0x001B, 0x001B, 0x001B])
write_int16(rom, snes_to_pc(0x308300), 0x0140) # add extra pyramid hole
write_int16(rom, snes_to_pc(0x308320), 0x001B)
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
rom.write_byte(snes_to_pc(0x308340), 0x7B)
rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # make retreat bat gfx available in HC area rom.write_byte(snes_to_pc(0x00DB9D), 0x1A) # make retreat bat gfx available in HC area
rom.write_byte(snes_to_pc(0x00DC09), 0x1A) rom.write_byte(snes_to_pc(0x00DC09), 0x1A)

View File

@@ -1067,7 +1067,7 @@ def ow_inverted_rules(world, player):
else: else:
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player)) # barrier gets removed after killing agahnim, rule for that added later set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has_beam_sword(player)) # barrier gets removed after killing agahnim, rule for that added later
set_rule(world.get_entrance('GT Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player)) set_rule(world.get_entrance('GT Approach', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player))
set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity')) set_rule(world.get_entrance('GT Leave', player), lambda state: state.has_crystals(world.crystals_needed_for_gt[player], player) or state.world.shuffle[player] in ('restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity'))
if world.is_tile_swapped(0x03, player): if world.is_tile_swapped(0x03, player):
set_rule(world.get_entrance('Spectacle Rock Approach', player), lambda state: world.logic[player] in ['noglitches', 'minorglitches'] and state.has_Pearl(player)) set_rule(world.get_entrance('Spectacle Rock Approach', player), lambda state: world.logic[player] in ['noglitches', 'minorglitches'] and state.has_Pearl(player))
@@ -1742,16 +1742,19 @@ def set_bunny_rules(world, player, inverted):
# for each such entrance a new option is added that consist of: # for each such entrance a new option is added that consist of:
# a) being able to reach it, and # a) being able to reach it, and
# b) being able to access all entrances from there to `region` # b) being able to access all entrances from there to `region`
queue = deque([(region, [], {region})]) queue = deque([(region, [], {region}, [region])])
seen_sets = set([frozenset({region})]) seen_sets = set([frozenset({region})])
while queue: while queue:
(current, path, seen) = queue.popleft() (current, path, seen, region_path) = queue.popleft()
for entrance in current.entrances: for entrance in current.entrances:
if entrance.door and entrance.door.blocked:
continue
new_region = entrance.parent_region new_region = entrance.parent_region
new_seen = seen.union({new_region}) new_seen = seen.union({new_region})
if new_region.type in (RegionType.Cave, RegionType.Dungeon) and new_seen in seen_sets: if new_region.type in (RegionType.Cave, RegionType.Dungeon) and new_seen in seen_sets:
continue continue
new_path = path + [entrance.access_rule] new_path = path + [entrance.access_rule]
new_region_path = region_path + [new_region]
seen_sets.add(frozenset(new_seen)) seen_sets.add(frozenset(new_seen))
if not is_link(new_region): if not is_link(new_region):
if world.logic[player] in ['owglitches', 'hybridglitches']: if world.logic[player] in ['owglitches', 'hybridglitches']:
@@ -1796,7 +1799,7 @@ def set_bunny_rules(world, player, inverted):
continue continue
if is_bunny(new_region): if is_bunny(new_region):
# todo: if not owg or hmg and entrance is in bunny_impassible_doors, then skip this nonsense? # todo: if not owg or hmg and entrance is in bunny_impassible_doors, then skip this nonsense?
queue.append((new_region, new_path, new_seen)) queue.append((new_region, new_path, new_seen, new_region_path))
else: else:
# we have reached pure light world, so we have a new possible option # we have reached pure light world, so we have a new possible option
possible_options.append(path_to_access_rule(new_path, entrance)) possible_options.append(path_to_access_rule(new_path, entrance))
@@ -1838,10 +1841,11 @@ def set_bunny_rules(world, player, inverted):
continue continue
add_rule(location, get_rule_to_add(region, location)) add_rule(location, get_rule_to_add(region, location))
for ent_name in bunny_pocket_entrances: if world.logic[player] in ['owglitches', 'hybridglitches']:
bunny_exit = world.get_entrance(ent_name, player) for ent_name in bunny_pocket_entrances:
if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region) and not can_bunny_pocket_to(world, ent_name, player): bunny_exit = world.get_entrance(ent_name, player)
add_rule(bunny_exit, lambda state: state.has_Pearl(player)) if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region) and not can_bunny_pocket_to(world, ent_name, player):
add_rule(bunny_exit, lambda state: state.has_Pearl(player))
drop_dungeon_entrances = { drop_dungeon_entrances = {

View File

@@ -51,6 +51,7 @@ def main(args=None):
test("Full ", "--shuffle full") test("Full ", "--shuffle full")
test("Lite ", "--shuffle lite") test("Lite ", "--shuffle lite")
test("Lean ", "--shuffle lean") test("Lean ", "--shuffle lean")
test("District ", "--shuffle district")
test("Swapped ", "--shuffle swapped") test("Swapped ", "--shuffle swapped")
test("Crossed ", "--shuffle crossed") test("Crossed ", "--shuffle crossed")
test("Insanity ", "--shuffle insanity") test("Insanity ", "--shuffle insanity")

View File

@@ -14,7 +14,7 @@ ALL_SETTINGS = {
'mode': ['open', 'standard', 'inverted'], 'mode': ['open', 'standard', 'inverted'],
'goal': ['ganon', 'pedestal', 'triforcehunt', 'trinity', 'crystals', 'dungeons'], 'goal': ['ganon', 'pedestal', 'triforcehunt', 'trinity', 'crystals', 'dungeons'],
'swords': ['random', 'swordless', 'assured'], 'swords': ['random', 'swordless', 'assured'],
'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','swapped','crossed','insanity'], 'shuffle': ['vanilla','simple','restricted','full','dungeonssimple','dungeonsfull','lite','lean','district','swapped','crossed','insanity'],
'shufflelinks': [True, False], 'shufflelinks': [True, False],
'shuffleganon': [True, False], 'shuffleganon': [True, False],
'door_shuffle': ['vanilla', 'basic', 'crossed'], 'door_shuffle': ['vanilla', 'basic', 'crossed'],
@@ -39,7 +39,7 @@ SETTINGS = {
'goal': ['ganon'], 'goal': ['ganon'],
'swords': ['random'], 'swords': ['random'],
'shuffle': ['vanilla', 'shuffle': ['vanilla',
'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'swapped', 'crossed', 'insanity' 'dungeonssimple', 'dungeonsfull', 'simple', 'restricted', 'full', 'lite', 'lean', 'district', 'swapped', 'crossed', 'insanity'
], ],
'shufflelinks': [True, False], 'shufflelinks': [True, False],
'shuffleganon': [True, False], 'shuffleganon': [True, False],

View File

@@ -209,6 +209,7 @@
"full", "full",
"lite", "lite",
"lean", "lean",
"district",
"swapped", "swapped",
"crossed", "crossed",
"insanity", "insanity",

View File

@@ -212,6 +212,7 @@
" item locations are shuffled in separate pools. Non-item", " item locations are shuffled in separate pools. Non-item",
" locations remain vanilla. Connectors are same-world.", " locations remain vanilla. Connectors are same-world.",
"Lean: Same as Lite, except connectors can travel cross worlds.", "Lean: Same as Lite, except connectors can travel cross worlds.",
"District: Entrances are shuffled within their overworld districts.",
"Crossed: Mix cave and dungeon entrances freely while allowing", "Crossed: Mix cave and dungeon entrances freely while allowing",
" caves to cross between worlds.", " caves to cross between worlds.",
"Swapped: Same as Crossed, but entrances switch places in pairs.", "Swapped: Same as Crossed, but entrances switch places in pairs.",

View File

@@ -181,6 +181,7 @@
"randomizer.entrance.entranceshuffle.restricted": "Restricted", "randomizer.entrance.entranceshuffle.restricted": "Restricted",
"randomizer.entrance.entranceshuffle.full": "Full", "randomizer.entrance.entranceshuffle.full": "Full",
"randomizer.entrance.entranceshuffle.lean": "Lean", "randomizer.entrance.entranceshuffle.lean": "Lean",
"randomizer.entrance.entranceshuffle.district": "District",
"randomizer.entrance.entranceshuffle.swapped": "Swapped", "randomizer.entrance.entranceshuffle.swapped": "Swapped",
"randomizer.entrance.entranceshuffle.crossed": "Crossed", "randomizer.entrance.entranceshuffle.crossed": "Crossed",
"randomizer.entrance.entranceshuffle.insanity": "Insanity", "randomizer.entrance.entranceshuffle.insanity": "Insanity",

View File

@@ -9,6 +9,7 @@
"full", "full",
"lite", "lite",
"lean", "lean",
"district",
"swapped", "swapped",
"crossed", "crossed",
"insanity", "insanity",

View File

@@ -129,11 +129,11 @@ def create_item_pool_config(world):
groups = LocationGroup('Major').locs(init_set) groups = LocationGroup('Major').locs(init_set)
if world.bigkeyshuffle[player]: if world.bigkeyshuffle[player]:
groups.locations.extend(mode_grouping['Big Keys']) groups.locations.extend(mode_grouping['Big Keys'])
if world.dropshuffle[player] != 'none': if world.dropshuffle[player]:
groups.locations.extend(mode_grouping['Big Key Drops']) groups.locations.extend(mode_grouping['Big Key Drops'])
if world.keyshuffle[player] != 'none': if world.keyshuffle[player] != 'none':
groups.locations.extend(mode_grouping['Small Keys']) groups.locations.extend(mode_grouping['Small Keys'])
if world.dropshuffle[player] != 'none': if world.dropshuffle[player]:
groups.locations.extend(mode_grouping['Key Drops']) groups.locations.extend(mode_grouping['Key Drops'])
if world.pottery[player] not in ['none', 'cave']: if world.pottery[player] not in ['none', 'cave']:
groups.locations.extend(mode_grouping['Pot Keys']) groups.locations.extend(mode_grouping['Pot Keys'])

View File

@@ -13,6 +13,8 @@ class EntrancePool(object):
self.inverted = False self.inverted = False
self.coupled = True self.coupled = True
self.swapped = False self.swapped = False
self.assumed_loose_caves = False
self.keep_drops_together = True
self.default_map = {} self.default_map = {}
self.one_way_map = {} self.one_way_map = {}
self.skull_handled = False self.skull_handled = False
@@ -54,6 +56,7 @@ def link_entrances_new(world, player):
avail_pool.entrances = set(i_drop_map.keys()).union(i_entrance_map.keys()).union(i_single_ent_map.keys()) avail_pool.entrances = set(i_drop_map.keys()).union(i_entrance_map.keys()).union(i_single_ent_map.keys())
avail_pool.exits = set(i_entrance_map.values()).union(i_drop_map.values()).union(i_single_ent_map.values()) avail_pool.exits = set(i_entrance_map.values()).union(i_drop_map.values()).union(i_single_ent_map.values())
avail_pool.inverted = world.mode[player] == 'inverted' avail_pool.inverted = world.mode[player] == 'inverted'
avail_pool.assumed_loose_caves = world.shuffle[player] == 'district'
inverted_substitution(avail_pool, avail_pool.entrances, True, True) inverted_substitution(avail_pool, avail_pool.entrances, True, True)
inverted_substitution(avail_pool, avail_pool.exits, False, True) inverted_substitution(avail_pool, avail_pool.exits, False, True)
avail_pool.original_entrances.update(avail_pool.entrances) avail_pool.original_entrances.update(avail_pool.entrances)
@@ -93,6 +96,8 @@ def link_entrances_new(world, player):
raise RuntimeError(f'Shuffle mode {mode} is not yet supported') raise RuntimeError(f'Shuffle mode {mode} is not yet supported')
mode_cfg = copy.deepcopy(modes[mode]) mode_cfg = copy.deepcopy(modes[mode])
avail_pool.swapped = mode_cfg['undefined'] == 'swap' avail_pool.swapped = mode_cfg['undefined'] == 'swap'
avail_pool.keep_drops_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True
avail_pool.coupled = mode_cfg['decoupled'] != 'on' if 'decoupled' in mode_cfg else True
if avail_pool.is_standard(): if avail_pool.is_standard():
do_standard_connections(avail_pool) do_standard_connections(avail_pool)
pool_list = mode_cfg['pools'] if 'pools' in mode_cfg else {} pool_list = mode_cfg['pools'] if 'pools' in mode_cfg else {}
@@ -106,8 +111,7 @@ def link_entrances_new(world, player):
connect_random(holes, targets, avail_pool) connect_random(holes, targets, avail_pool)
elif special_shuffle == 'normal_drops': elif special_shuffle == 'normal_drops':
cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False cross_world = mode_cfg['cross_world'] == 'on' if 'cross_world' in mode_cfg else False
keep_together = mode_cfg['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_cfg else True do_holes_and_linked_drops(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, cross_world)
do_holes_and_linked_drops(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, cross_world, keep_together)
elif special_shuffle == 'fixed_shuffle': elif special_shuffle == 'fixed_shuffle':
do_fixed_shuffle(avail_pool, pool['entrances']) do_fixed_shuffle(avail_pool, pool['entrances'])
elif special_shuffle == 'same_world': elif special_shuffle == 'same_world':
@@ -126,10 +130,18 @@ def link_entrances_new(world, player):
do_limited_shuffle_exclude_drops(pool, avail_pool, False) do_limited_shuffle_exclude_drops(pool, avail_pool, False)
elif special_shuffle == 'vanilla': elif special_shuffle == 'vanilla':
do_vanilla_connect(pool, avail_pool) do_vanilla_connect(pool, avail_pool)
elif special_shuffle == 'district':
drops = []
world_limiter = LW_Entrances if pool['condition'] == 'lightworld' else DW_Entrances
entrances = [e for e in pool['entrances'] if e in world_limiter]
if 'drops' in pool:
drops = [e for e in pool['drops'] if combine_linked_drop_map[e] in world_limiter]
entrances, exits = find_entrances_and_exits(avail_pool, entrances+drops)
do_main_shuffle(entrances, exits, avail_pool, mode_cfg)
elif special_shuffle == 'skull': elif special_shuffle == 'skull':
entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances']) entrances, exits = find_entrances_and_exits(avail_pool, pool['entrances'])
rem_ent = None rem_ent = None
if avail_pool.world.shuffle[avail_pool.player] in ['dungeons-simple', 'simple', 'restricted'] \ if avail_pool.world.shuffle[avail_pool.player] in ['dungeonssimple', 'simple', 'restricted'] \
and not avail_pool.world.is_tile_swapped(0x00, avail_pool.player): and not avail_pool.world.is_tile_swapped(0x00, avail_pool.player):
rem_ent = random.choice(['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']) rem_ent = random.choice(['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)'])
entrances.remove(rem_ent) entrances.remove(rem_ent)
@@ -149,6 +161,8 @@ def link_entrances_new(world, player):
do_vanilla_connections(avail_pool) do_vanilla_connections(avail_pool)
elif undefined_behavior in ['shuffle', 'swap']: elif undefined_behavior in ['shuffle', 'swap']:
do_main_shuffle(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, mode_cfg) do_main_shuffle(set(avail_pool.entrances), set(avail_pool.exits), avail_pool, mode_cfg)
elif undefined_behavior == 'error':
assert len(avail_pool.entrances)+len(avail_pool.exits) == 0, 'Not all entrances were placed in their districts'
# afterward # afterward
@@ -191,10 +205,8 @@ def do_vanilla_connections(avail_pool):
def do_main_shuffle(entrances, exits, avail, mode_def): def do_main_shuffle(entrances, exits, avail, mode_def):
cross_world = mode_def['cross_world'] == 'on' if 'cross_world' in mode_def else False cross_world = mode_def['cross_world'] == 'on' if 'cross_world' in mode_def else False
avail.coupled = mode_def['decoupled'] != 'on' if 'decoupled' in mode_def else True
# drops and holes # drops and holes
keep_together = mode_def['keep_drops_together'] == 'on' if 'keep_drops_together' in mode_def else True do_holes_and_linked_drops(entrances, exits, avail, cross_world)
do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together)
if not avail.coupled: if not avail.coupled:
avail.decoupled_entrances.extend(entrances) avail.decoupled_entrances.extend(entrances)
@@ -310,7 +322,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
return not avail.is_standard() or x != 'Bonk Fairy (Light)' return not avail.is_standard() or x != 'Bonk Fairy (Light)'
# old man S&Q cave # old man S&Q cave
if not cross_world: if not cross_world and not avail.assumed_loose_caves:
#TODO: Add Swapped ER support for this #TODO: Add Swapped ER support for this
# OM Cave entrance in lw/dw if cross_world off # OM Cave entrance in lw/dw if cross_world off
if 'Old Man Cave Exit (West)' in rem_exits: if 'Old Man Cave Exit (West)' in rem_exits:
@@ -382,7 +394,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
def do_old_man_cave_exit(entrances, exits, avail, cross_world): def do_old_man_cave_exit(entrances, exits, avail, cross_world):
if 'Old Man Cave Exit (East)' in exits: if 'Old Man Cave Exit (East)' in exits:
from EntranceShuffle import build_accessible_region_list from EntranceShuffle import build_accessible_region_list
if not avail.world.is_tile_swapped(0x03, avail.player): if not avail.world.is_tile_swapped(0x03, avail.player) or avail.world.shuffle[avail.player] == 'district':
region_name = 'West Death Mountain (Top)' region_name = 'West Death Mountain (Top)'
else: else:
region_name = 'West Dark Death Mountain (Top)' region_name = 'West Dark Death Mountain (Top)'
@@ -420,11 +432,14 @@ def do_blacksmith(entrances, exits, avail):
if avail.world.logic[avail.player] in ['noglitches', 'minorglitches'] and (avail.world.is_tile_swapped(0x29, avail.player) == avail.inverted): if avail.world.logic[avail.player] in ['noglitches', 'minorglitches'] and (avail.world.is_tile_swapped(0x29, avail.player) == avail.inverted):
assumed_inventory.append('Titans Mitts') assumed_inventory.append('Titans Mitts')
blacksmith_options = list()
if not avail.world.is_bombshop_start(avail.player): if not avail.world.is_bombshop_start(avail.player):
links_region = avail.world.get_entrance('Links House Exit', avail.player).connected_region.name links_region = avail.world.get_entrance('Links House Exit', avail.player).connected_region
else: else:
links_region = avail.world.get_entrance('Big Bomb Shop Exit', avail.player).connected_region.name links_region = avail.world.get_entrance('Big Bomb Shop Exit', avail.player).connected_region
blacksmith_options = list(get_accessible_entrances(links_region, avail, assumed_inventory, False, True, True)) if links_region is not None:
links_region = links_region.name
blacksmith_options = list(get_accessible_entrances(links_region, avail, assumed_inventory, False, True, True))
if avail.inverted: if avail.inverted:
dark_sanc = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name dark_sanc = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name
@@ -440,6 +455,9 @@ def do_blacksmith(entrances, exits, avail):
blacksmith_options = [e for e in blacksmith_options if e not in Forbidden_Swap_Entrances] blacksmith_options = [e for e in blacksmith_options if e not in Forbidden_Swap_Entrances]
blacksmith_options = [x for x in blacksmith_options if x in entrances] blacksmith_options = [x for x in blacksmith_options if x in entrances]
if avail.world.shuffle[avail.player] == 'district' and not len(blacksmith_options):
blacksmith_options = [e for e in entrances if e not in Forbidden_Swap_Entrances or not avail.swapped]
assert len(blacksmith_options), 'No available entrances left to place Blacksmith' assert len(blacksmith_options), 'No available entrances left to place Blacksmith'
blacksmith_choice = random.choice(blacksmith_options) blacksmith_choice = random.choice(blacksmith_options)
connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail) connect_entrance(blacksmith_choice, 'Blacksmiths Hut', avail)
@@ -454,11 +472,21 @@ def do_blacksmith(entrances, exits, avail):
def do_standard_connections(avail): def do_standard_connections(avail):
connect_two_way('Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', avail) std_exits = ['Hyrule Castle Exit (South)', 'Hyrule Castle Secret Entrance Exit']
# cannot move uncle cave if not avail.keep_drops_together:
connect_two_way('Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', avail) random.shuffle(std_exits)
connect_entrance('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', avail)
connect_two_way('Links House', 'Links House Exit', avail) connect_two_way('Links House', 'Links House Exit', avail)
connect_entrance('Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', avail)
if avail.coupled:
connect_two_way('Hyrule Castle Entrance (South)', std_exits[0], avail)
# cannot move uncle cave
connect_two_way('Hyrule Castle Secret Entrance Stairs', std_exits[1], avail)
else:
connect_entrance('Hyrule Castle Entrance (South)', std_exits[0], avail)
connect_entrance('Hyrule Castle Secret Entrance Stairs', std_exits[1], avail)
random.shuffle(std_exits)
connect_exit(std_exits[0], 'Hyrule Castle Entrance (South)', avail)
connect_exit(std_exits[1], 'Hyrule Castle Secret Entrance Stairs', avail)
def remove_from_list(t_list, removals): def remove_from_list(t_list, removals):
@@ -466,7 +494,7 @@ def remove_from_list(t_list, removals):
t_list.remove(r) t_list.remove(r)
def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_together): def do_holes_and_linked_drops(entrances, exits, avail, cross_world):
holes_to_shuffle = [x for x in entrances if x in drop_map] holes_to_shuffle = [x for x in entrances if x in drop_map]
if not avail.world.shuffle_ganon: if not avail.world.shuffle_ganon:
@@ -483,7 +511,7 @@ def do_holes_and_linked_drops(entrances, exits, avail, cross_world, keep_togethe
remove_from_list(entrances, ['Pyramid Hole', 'Pyramid Entrance']) remove_from_list(entrances, ['Pyramid Hole', 'Pyramid Entrance'])
remove_from_list(exits, ['Pyramid', 'Pyramid Exit']) remove_from_list(exits, ['Pyramid', 'Pyramid Exit'])
if not keep_together: if not avail.keep_drops_together:
targets = [avail.one_way_map[x] for x in holes_to_shuffle] targets = [avail.one_way_map[x] for x in holes_to_shuffle]
connect_random(holes_to_shuffle, targets, avail) connect_random(holes_to_shuffle, targets, avail)
remove_from_list(entrances, holes_to_shuffle) remove_from_list(entrances, holes_to_shuffle)
@@ -567,6 +595,8 @@ def do_dark_sanc(entrances, exits, avail):
forbidden.extend(Forbidden_Swap_Entrances) forbidden.extend(Forbidden_Swap_Entrances)
if not avail.world.is_bombshop_start(avail.player): if not avail.world.is_bombshop_start(avail.player):
forbidden.append('Links House') forbidden.append('Links House')
else:
forbidden.append('Big Bomb Shop')
if avail.world.owShuffle[avail.player] == 'vanilla': if avail.world.owShuffle[avail.player] == 'vanilla':
choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances] choices = [e for e in avail.world.districts[avail.player]['Northwest Dark World'].entrances if e not in forbidden and e in entrances]
else: else:
@@ -602,7 +632,7 @@ def do_links_house(entrances, exits, avail, cross_world):
forbidden.append('Mimic Cave') forbidden.append('Mimic Cave')
if avail.world.is_bombshop_start(avail.player) and (avail.inverted == avail.world.is_tile_swapped(0x03, avail.player)): if avail.world.is_bombshop_start(avail.player) and (avail.inverted == avail.world.is_tile_swapped(0x03, avail.player)):
forbidden.extend(['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)']) forbidden.extend(['Spectacle Rock Cave', 'Spectacle Rock Cave (Bottom)'])
if avail.inverted: if avail.inverted and avail.world.shuffle[avail.player] != 'district':
dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name dark_sanc_region = avail.world.get_entrance('Dark Sanctuary Hint Exit', avail.player).connected_region.name
forbidden.extend(get_nearby_entrances(avail, dark_sanc_region)) forbidden.extend(get_nearby_entrances(avail, dark_sanc_region))
else: else:
@@ -644,7 +674,7 @@ def do_links_house(entrances, exits, avail, cross_world):
sanc_spawn_can_be_dark = (not avail.inverted and avail.world.doorShuffle[avail.player] in ['partitioned', 'crossed'] sanc_spawn_can_be_dark = (not avail.inverted and avail.world.doorShuffle[avail.player] in ['partitioned', 'crossed']
and avail.world.intensity[avail.player] >= 3) and avail.world.intensity[avail.player] >= 3)
if cross_world and not sanc_spawn_can_be_dark: if (cross_world and not sanc_spawn_can_be_dark) or avail.world.shuffle[avail.player] == 'district':
possible = [e for e in entrance_pool if e not in forbidden] possible = [e for e in entrance_pool if e not in forbidden]
else: else:
world_list = LW_Entrances if not avail.inverted else DW_Entrances world_list = LW_Entrances if not avail.inverted else DW_Entrances
@@ -676,7 +706,7 @@ def do_links_house(entrances, exits, avail, cross_world):
return return
if avail.world.shuffle[avail.player] in ['lite', 'lean']: if avail.world.shuffle[avail.player] in ['lite', 'lean']:
rem_exits = [e for e in avail.exits if e in Connector_Exit_Set and e not in Dungeon_Exit_Set] rem_exits = [e for e in avail.exits if e in Connector_Exit_Set and e not in Dungeon_Exit_Set]
multi_exit_caves = figure_out_connectors(rem_exits) multi_exit_caves = figure_out_connectors(rem_exits, avail)
if cross_world: if cross_world:
possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List] possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List]
possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots] possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots]
@@ -685,7 +715,7 @@ def do_links_house(entrances, exits, avail, cross_world):
possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List and e in world_list] possible_dm_exits = [e for e in avail.entrances if e not in entrances and e in LH_DM_Connector_List and e in world_list]
possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots and e in world_list] possible_exits = [e for e in avail.entrances if e not in entrances and e not in dm_spots and e in world_list]
else: else:
multi_exit_caves = figure_out_connectors(exits) multi_exit_caves = figure_out_connectors(exits, avail)
entrance_pool = entrances if avail.coupled else avail.decoupled_entrances entrance_pool = entrances if avail.coupled else avail.decoupled_entrances
if cross_world: if cross_world:
possible_dm_exits = [e for e in entrances if e in LH_DM_Connector_List] possible_dm_exits = [e for e in entrances if e in LH_DM_Connector_List]
@@ -832,12 +862,22 @@ def get_accessible_entrances(start_region, avail, assumed_inventory=[], cross_wo
return found_entrances return found_entrances
def figure_out_connectors(exits): def figure_out_connectors(exits, avail):
multi_exit_caves = [] multi_exit_caves = []
for item in Connector_List: cave_list = list(Connector_List)
if avail.assumed_loose_caves:
sw_list = ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']
random.shuffle(sw_list)
cave_list.extend([sw_list])
cave_list.extend([[entrance_map[e]] for e in linked_drop_map.values() if 'Inverted ' not in e])
for item in cave_list:
if all(x in exits for x in item): if all(x in exits for x in item):
remove_from_list(exits, item) remove_from_list(exits, item)
multi_exit_caves.append(list(item)) multi_exit_caves.append(list(item))
elif avail.assumed_loose_caves and any(x in exits for x in item):
remaining = [i for i in item if i in exits]
remove_from_list(exits, remaining)
multi_exit_caves.append(list(remaining))
return multi_exit_caves return multi_exit_caves
@@ -1015,7 +1055,7 @@ def figure_out_must_exits_same_world(entrances, exits, avail):
for x in entrances: for x in entrances:
lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x)
multi_exit_caves = figure_out_connectors(exits) multi_exit_caves = figure_out_connectors(exits, avail)
must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit_lw, must_exit_dw = must_exits_helper(avail)
must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances) must_exit_lw = must_exit_filter(avail, must_exit_lw, lw_entrances)
@@ -1025,7 +1065,7 @@ def figure_out_must_exits_same_world(entrances, exits, avail):
def figure_out_must_exits_cross_world(entrances, exits, avail): def figure_out_must_exits_cross_world(entrances, exits, avail):
multi_exit_caves = figure_out_connectors(exits) multi_exit_caves = figure_out_connectors(exits, avail)
must_exit_lw, must_exit_dw = must_exits_helper(avail) must_exit_lw, must_exit_dw = must_exits_helper(avail)
must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances) must_exit = must_exit_filter(avail, must_exit_lw + must_exit_dw, entrances)
@@ -1148,9 +1188,12 @@ def do_fixed_shuffle(avail, entrance_list):
new_x = 'Agahnims Tower Exit' new_x = 'Agahnims Tower Exit'
elif x == 'Agahnims Tower Exit': elif x == 'Agahnims Tower Exit':
new_x = 'Ganons Tower Exit' new_x = 'Ganons Tower Exit'
if avail.world.is_bombshop_start(avail.player):
if x == 'Links House Exit':
new_x = 'Big Bomb Shop'
elif x == 'Big Bomb Shop':
new_x = 'Links House Exit'
lw_exits.add(new_x) lw_exits.add(new_x)
if avail.world.shufflelinks[avail.player] or avail.world.shuffle[avail.player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
lw_exits.update({'Big Bomb Shop'} if avail.world.is_bombshop_start(avail.player) else {'Links House Exit'})
filtered_choices = {i: opt for i, opt in choices.items() if all(t in lw_exits for t in opt[2])} filtered_choices = {i: opt for i, opt in choices.items() if all(t in lw_exits for t in opt[2])}
_, choice = random.choice(list(filtered_choices.items())) _, choice = random.choice(list(filtered_choices.items()))
else: else:
@@ -1361,7 +1404,8 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit):
# find multi exit cave # find multi exit cave
candidates = [] candidates = []
for candidate in cave_options: for candidate in cave_options:
if not isinstance(candidate, str) and len(candidate) > 1 and (candidate in used_caves allow_single = avail.assumed_loose_caves or len(candidate) > 1
if not isinstance(candidate, str) and allow_single and (candidate in used_caves
or len(candidate) < len(entrances) - required_entrances): or len(candidate) < len(entrances) - required_entrances):
if not avail.swapped or (combine_map[exit] not in candidate and not any(e for e in must_exit if combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped if not avail.swapped or (combine_map[exit] not in candidate and not any(e for e in must_exit if combine_map[e] in candidate)): #maybe someday allow these, but we need to disallow mutual locks in Swapped
candidates.append(candidate) candidates.append(candidate)
@@ -1395,6 +1439,10 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit):
if entrance in invalid_connections: if entrance in invalid_connections:
for exit2 in invalid_connections[entrance]: for exit2 in invalid_connections[entrance]:
invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)]) invalid_connections[exit2] = invalid_connections[exit2].union(invalid_connections[exit]).union(invalid_cave_connections[tuple(cave)])
elif len(cave) == 1 and avail.assumed_loose_caves:
#TODO: keep track of caves we use for must exits that are unaccounted here
# the other exits of the cave should NOT be used to satisfy must-exit later
pass
elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit elif cave[-1] == 'Spectacle Rock Cave Exit': # Spectacle rock only has one exit
cave_entrances = [] cave_entrances = []
for cave_exit in rnd_cave[:-1]: for cave_exit in rnd_cave[:-1]:
@@ -1519,14 +1567,12 @@ def find_entrances_and_exits(avail_pool, entrance_pool):
entrances, targets = [], [] entrances, targets = [], []
inverted_substitution(avail_pool, entrance_pool, True) inverted_substitution(avail_pool, entrance_pool, True)
for item in entrance_pool: for item in entrance_pool:
if item == 'Ganons Tower' and not avail_pool.world.shuffle_ganon[avail_pool.player]:
continue
if item in avail_pool.entrances: if item in avail_pool.entrances:
entrances.append(item) entrances.append(item)
if item in entrance_map and entrance_map[item] in avail_pool.exits: if item in avail_pool.default_map and avail_pool.default_map[item] in avail_pool.exits:
targets.append(entrance_map[item]) targets.append(avail_pool.default_map[item])
elif item in single_entrance_map and single_entrance_map[item] in avail_pool.exits: elif item in avail_pool.one_way_map and avail_pool.one_way_map[item] in avail_pool.exits:
targets.append(single_entrance_map[item]) targets.append(avail_pool.one_way_map[item])
return entrances, targets return entrances, targets
@@ -2064,6 +2110,157 @@ modes = {
}, },
} }
}, },
'district': {
'undefined': 'error',
'keep_drops_together': 'off',
'cross_world': 'off',
'pools': {
'northwest_hyrule': {
'special': 'district',
'condition': 'lightworld',
'drops': ['Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'North Fairy Cave Drop',
'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)',
'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole'],
'entrances': ['Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', 'North Fairy Cave',
'Lost Woods Gamble', 'Lumberjack House', 'Old Man Cave (West)', 'Death Mountain Return Cave (West)',
'Fortune Teller (Light)', 'Bonk Rock Cave', 'Graveyard Cave', 'Kings Grave',
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Dark Lumberjack Shop',
'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint',
'Red Shield Shop']
},
'northwest_dark_world': {
'special': 'district',
'condition': 'darkworld',
'drops': ['Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (East)',
'Skull Woods First Section Hole (North)', 'Skull Woods Second Section Hole',
'Lost Woods Hideout Drop', 'Lumberjack Tree Tree', 'Sanctuary Grave', 'North Fairy Cave Drop',
'Kakariko Well Drop', 'Bat Cave Drop'],
'entrances': ['Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
'Skull Woods Second Section Door (West)', 'Skull Woods Final Section', 'Dark Lumberjack Shop',
'Bumper Cave (Bottom)', 'Bumper Cave (Top)', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint',
'Chest Game', 'Thieves Town', 'C-Shaped House', 'Dark World Shop', 'Brewery',
'Red Shield Shop', 'Hammer Peg Cave',
'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Sanctuary', 'North Fairy Cave',
'Kakariko Well Cave', 'Bat Cave Cave', 'Lost Woods Gamble', 'Lumberjack House', 'Fortune Teller (Light)',
'Old Man Cave (West)', 'Death Mountain Return Cave (West)', 'Bonk Rock Cave', 'Graveyard Cave',
'Kings Grave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)', 'Snitch Lady (West)',
'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Bush Covered House', 'Light World Bomb Hut',
'Kakariko Shop', 'Tavern North', 'Tavern (Front)', 'Blacksmiths Hut']
},
'central_hyrule': {
'special': 'district',
'condition': 'lightworld',
'drops': ['Hyrule Castle Secret Entrance Drop', 'Inverted Pyramid Hole',
'Pyramid Hole'],
'entrances': ['Hyrule Castle Secret Entrance Stairs', 'Inverted Pyramid Entrance', 'Agahnims Tower',
'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)', 'Hyrule Castle Entrance (South)',
'Bonk Fairy (Light)', 'Links House', 'Cave 45', 'Light Hype Fairy', 'Dam',
'Pyramid Entrance', 'Pyramid Fairy', 'Bonk Fairy (Dark)', 'Big Bomb Shop', 'Hype Cave', 'Swamp Palace']
},
'kakariko': {
'special': 'district',
'condition': 'lightworld',
'drops': ['Kakariko Well Drop', 'Bat Cave Drop'],
'entrances': ['Kakariko Well Cave', 'Bat Cave Cave', 'Blinds Hideout', 'Elder House (West)', 'Elder House (East)',
'Snitch Lady (West)', 'Snitch Lady (East)', 'Chicken House', 'Sick Kids House', 'Bush Covered House',
'Light World Bomb Hut', 'Kakariko Shop', 'Tavern North', 'Tavern (Front)', 'Blacksmiths Hut',
'Two Brothers House (West)', 'Two Brothers House (East)', 'Library', 'Kakariko Gamble Game',
'Chest Game', 'Thieves Town', 'C-Shaped House', 'Dark World Shop', 'Brewery',
'Hammer Peg Cave', 'Archery Game']
},
'eastern_hyrule': {
'special': 'district',
'condition': 'lightworld',
'entrances': ['Waterfall of Wishing', 'Potion Shop', 'Sahasrahlas Hut', 'Eastern Palace', 'Lake Hylia Fairy',
'Long Fairy Cave',
'Dark Potion Shop', 'Palace of Darkness Hint', 'Palace of Darkness', 'Dark Lake Hylia Fairy',
'East Dark World Hint']
},
'lake_hylia': {
'special': 'district',
'condition': 'lightworld',
'entrances': ['Lake Hylia Fortune Teller', 'Lake Hylia Shop', 'Capacity Upgrade', 'Mini Moldorm Cave',
'Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave',
'Dark Lake Hylia Shop', 'Ice Palace', 'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint',
'Dark Lake Hylia Ledge Spike Cave']
},
'desert': {
'special': 'district',
'condition': 'lightworld',
'entrances': ['Desert Palace Entrance (North)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (South)',
'Desert Palace Entrance (East)', 'Checkerboard Cave', 'Aginahs Cave', 'Desert Fairy', '50 Rupee Cave',
'Mire Shed', 'Misery Mire', 'Mire Fairy', 'Mire Hint']
},
'death_mountain': {
'special': 'district',
'condition': 'lightworld',
'entrances': ['Tower of Hera', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave',
'Death Mountain Return Cave (East)', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Spiral Cave', 'Spiral Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Bottom)',
'Mimic Cave', 'Hookshot Fairy', 'Paradox Cave (Top)', 'Paradox Cave (Middle)', 'Paradox Cave (Bottom)',
'Ganons Tower', 'Dark Death Mountain Fairy', 'Spike Cave', 'Superbunny Cave (Bottom)', 'Superbunny Cave (Top)',
'Dark Death Mountain Shop', 'Hookshot Cave', 'Hookshot Cave Back Entrance',
'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock']
},
'dark_death_mountain': {
'special': 'district',
'condition': 'darkworld',
'entrances': ['Ganons Tower', 'Dark Death Mountain Fairy', 'Spike Cave', 'Superbunny Cave (Bottom)', 'Superbunny Cave (Top)',
'Dark Death Mountain Shop', 'Hookshot Cave', 'Hookshot Cave Back Entrance',
'Dark Death Mountain Ledge (West)', 'Dark Death Mountain Ledge (East)', 'Turtle Rock Isolated Ledge Entrance', 'Turtle Rock',
'Tower of Hera', 'Spectacle Rock Cave Peak', 'Spectacle Rock Cave (Bottom)', 'Spectacle Rock Cave',
'Death Mountain Return Cave (East)', 'Old Man Cave (East)', 'Old Man House (Bottom)', 'Old Man House (Top)',
'Spiral Cave', 'Spiral Cave (Bottom)', 'Fairy Ascension Cave (Top)', 'Fairy Ascension Cave (Bottom)',
'Mimic Cave', 'Hookshot Fairy', 'Paradox Cave (Top)', 'Paradox Cave (Middle)', 'Paradox Cave (Bottom)']
},
'south_dark_world': {
'special': 'district',
'condition': 'darkworld',
'entrances': ['Archery Game', 'Bonk Fairy (Dark)', 'Big Bomb Shop', 'Hype Cave', 'Dark Lake Hylia Shop', 'Ice Palace',
'Dark Lake Hylia Ledge Fairy', 'Dark Lake Hylia Ledge Hint', 'Dark Lake Hylia Ledge Spike Cave',
'Swamp Palace',
'Two Brothers House (West)', 'Two Brothers House (East)', 'Library', 'Kakariko Gamble Game',
'Bonk Fairy (Light)', 'Links House', 'Cave 45', 'Desert Fairy', '50 Rupee Cave', 'Dam',
'Light Hype Fairy', 'Lake Hylia Fortune Teller', 'Lake Hylia Shop', 'Capacity Upgrade',
'Mini Moldorm Cave', 'Ice Rod Cave', 'Good Bee Cave', '20 Rupee Cave']
},
'east_dark_world': {
'special': 'district',
'condition': 'darkworld',
'drops': ['Pyramid Hole',
'Hyrule Castle Secret Entrance Drop', 'Inverted Pyramid Hole'],
'entrances': ['Pyramid Entrance', 'Pyramid Fairy', 'Dark Potion Shop', 'Palace of Darkness Hint', 'Palace of Darkness',
'Dark Lake Hylia Fairy', 'East Dark World Hint',
'Hyrule Castle Secret Entrance Stairs', 'Inverted Pyramid Entrance', 'Waterfall of Wishing', 'Potion Shop',
'Agahnims Tower', 'Hyrule Castle Entrance (West)', 'Hyrule Castle Entrance (East)',
'Hyrule Castle Entrance (South)', 'Sahasrahlas Hut', 'Eastern Palace', 'Lake Hylia Fairy', 'Long Fairy Cave']
},
'mire': {
'special': 'district',
'condition': 'darkworld',
'entrances': ['Mire Shed', 'Misery Mire', 'Mire Fairy', 'Mire Hint',
'Desert Palace Entrance (North)', 'Desert Palace Entrance (West)', 'Desert Palace Entrance (South)',
'Desert Palace Entrance (East)', 'Checkerboard Cave', 'Aginahs Cave']
}
}
},
'swapped': { 'swapped': {
'undefined': 'swap', 'undefined': 'swap',
'keep_drops_together': 'on', 'keep_drops_together': 'on',
@@ -2125,7 +2322,7 @@ drop_map = {
} }
linked_drop_map = { linked_drop_map = {
'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Drop': 'Hyrule Castle Secret Entrance Stairs',
'Kakariko Well Drop': 'Kakariko Well Cave', 'Kakariko Well Drop': 'Kakariko Well Cave',
'Bat Cave Drop': 'Bat Cave Cave', 'Bat Cave Drop': 'Bat Cave Cave',
'North Fairy Cave Drop': 'North Fairy Cave', 'North Fairy Cave Drop': 'North Fairy Cave',
@@ -2136,6 +2333,13 @@ linked_drop_map = {
'Inverted Pyramid Hole': 'Inverted Pyramid Entrance' 'Inverted Pyramid Hole': 'Inverted Pyramid Entrance'
} }
sw_linked_drop_map = {
'Skull Woods Second Section Hole': 'Skull Woods Second Section Door (West)',
'Skull Woods First Section Hole (North)': 'Skull Woods First Section Door',
'Skull Woods First Section Hole (West)': 'Skull Woods First Section Door',
'Skull Woods First Section Hole (East)': 'Skull Woods First Section Door'
}
entrance_map = { entrance_map = {
'Desert Palace Entrance (South)': 'Desert Palace Exit (South)', 'Desert Palace Entrance (South)': 'Desert Palace Exit (South)',
'Desert Palace Entrance (West)': 'Desert Palace Exit (West)', 'Desert Palace Entrance (West)': 'Desert Palace Exit (West)',
@@ -2169,7 +2373,7 @@ entrance_map = {
'Links House': 'Links House Exit', 'Links House': 'Links House Exit',
'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs': 'Hyrule Castle Secret Entrance Exit',
'Kakariko Well Cave': 'Kakariko Well Exit', 'Kakariko Well Cave': 'Kakariko Well Exit',
'Bat Cave Cave': 'Bat Cave Exit', 'Bat Cave Cave': 'Bat Cave Exit',
'North Fairy Cave': 'North Fairy Cave Exit', 'North Fairy Cave': 'North Fairy Cave Exit',
@@ -2244,6 +2448,7 @@ single_entrance_map = {
} }
combine_map = {**entrance_map, **single_entrance_map, **drop_map} combine_map = {**entrance_map, **single_entrance_map, **drop_map}
combine_linked_drop_map = {**linked_drop_map, **sw_linked_drop_map}
LW_Entrances = [] LW_Entrances = []
DW_Entrances = [] DW_Entrances = []