Merge branch 'DoorDevUnstable' into DoorDev
# Conflicts: # Main.py # RELEASENOTES.md # Rom.py # data/base2current.bps
This commit is contained in:
@@ -36,6 +36,7 @@ def main():
|
|||||||
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||||
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
parser.add_argument('--uw_palettes', default='default', choices=['default', 'random', 'blackout'])
|
||||||
parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true')
|
parser.add_argument('--reduce_flashing', help='Reduce some in-game flashing.', action='store_true')
|
||||||
|
parser.add_argument('--shuffle_sfx', help='Shuffles sound sfx', action='store_true')
|
||||||
parser.add_argument('--sprite', help='''\
|
parser.add_argument('--sprite', help='''\
|
||||||
Path to a sprite sheet to use for Link. Needs to be in
|
Path to a sprite sheet to use for Link. Needs to be in
|
||||||
binary format and have a length of 0x7000 (28672) bytes,
|
binary format and have a length of 0x7000 (28672) bytes,
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ def adjust(args):
|
|||||||
args.sprite = None
|
args.sprite = None
|
||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
|
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic,
|
||||||
args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing)
|
args.sprite, args.ow_palettes, args.uw_palettes, args.reduce_flashing, args.shuffle_sfx)
|
||||||
|
|
||||||
output_path.cached_path = args.outputpath
|
output_path.cached_path = args.outputpath
|
||||||
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
rom.write_to_file(output_path('%s.sfc' % outfilebase))
|
||||||
|
|||||||
+484
-61
@@ -77,6 +77,7 @@ class World(object):
|
|||||||
self._room_cache = {}
|
self._room_cache = {}
|
||||||
self.dungeon_layouts = {}
|
self.dungeon_layouts = {}
|
||||||
self.inaccessible_regions = {}
|
self.inaccessible_regions = {}
|
||||||
|
self.enabled_entrances = {}
|
||||||
self.key_logic = {}
|
self.key_logic = {}
|
||||||
self.pool_adjustment = {}
|
self.pool_adjustment = {}
|
||||||
self.key_layout = defaultdict(dict)
|
self.key_layout = defaultdict(dict)
|
||||||
@@ -96,6 +97,7 @@ class World(object):
|
|||||||
set_player_attr('player_names', [])
|
set_player_attr('player_names', [])
|
||||||
set_player_attr('remote_items', False)
|
set_player_attr('remote_items', False)
|
||||||
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
||||||
|
set_player_attr('bottle_refills', ['Bottle (Green Potion)', 'Bottle (Green Potion)'])
|
||||||
set_player_attr('swamp_patch_required', False)
|
set_player_attr('swamp_patch_required', False)
|
||||||
set_player_attr('powder_patch_required', False)
|
set_player_attr('powder_patch_required', False)
|
||||||
set_player_attr('ganon_at_pyramid', True)
|
set_player_attr('ganon_at_pyramid', True)
|
||||||
@@ -114,6 +116,7 @@ class World(object):
|
|||||||
set_player_attr('compassshuffle', False)
|
set_player_attr('compassshuffle', False)
|
||||||
set_player_attr('keyshuffle', False)
|
set_player_attr('keyshuffle', False)
|
||||||
set_player_attr('bigkeyshuffle', False)
|
set_player_attr('bigkeyshuffle', False)
|
||||||
|
set_player_attr('bombbag', False)
|
||||||
set_player_attr('difficulty_requirements', None)
|
set_player_attr('difficulty_requirements', None)
|
||||||
set_player_attr('boss_shuffle', 'none')
|
set_player_attr('boss_shuffle', 'none')
|
||||||
set_player_attr('enemy_shuffle', 'none')
|
set_player_attr('enemy_shuffle', 'none')
|
||||||
@@ -139,6 +142,9 @@ class World(object):
|
|||||||
set_player_attr('standardize_palettes', 'standardize')
|
set_player_attr('standardize_palettes', 'standardize')
|
||||||
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
|
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
|
||||||
|
|
||||||
|
set_player_attr('exp_cache', defaultdict(dict))
|
||||||
|
set_player_attr('enabled_entrances', {})
|
||||||
|
|
||||||
def get_name_string_for_object(self, obj):
|
def get_name_string_for_object(self, obj):
|
||||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
||||||
|
|
||||||
@@ -393,6 +399,10 @@ class World(object):
|
|||||||
def clear_location_cache(self):
|
def clear_location_cache(self):
|
||||||
self._cached_locations = None
|
self._cached_locations = None
|
||||||
|
|
||||||
|
def clear_exp_cache(self):
|
||||||
|
for p in range(1, self.players + 1):
|
||||||
|
self.exp_cache[p].clear()
|
||||||
|
|
||||||
def get_unfilled_locations(self, player=None):
|
def get_unfilled_locations(self, player=None):
|
||||||
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
|
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
|
||||||
|
|
||||||
@@ -425,8 +435,10 @@ class World(object):
|
|||||||
else:
|
else:
|
||||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||||
|
|
||||||
def can_beat_game(self, starting_state=None):
|
def can_beat_game(self, starting_state=None, log_error=False):
|
||||||
if starting_state:
|
if starting_state:
|
||||||
|
if self.has_beaten_game(starting_state):
|
||||||
|
return True
|
||||||
state = starting_state.copy()
|
state = starting_state.copy()
|
||||||
else:
|
else:
|
||||||
state = CollectionState(self)
|
state = CollectionState(self)
|
||||||
@@ -445,6 +457,9 @@ class World(object):
|
|||||||
|
|
||||||
if not sphere:
|
if not sphere:
|
||||||
# ran out of places and did not finish yet, quit
|
# ran out of places and did not finish yet, quit
|
||||||
|
if log_error:
|
||||||
|
missing_locations = ", ".join([x.name for x in prog_locations])
|
||||||
|
logging.getLogger('').error(f'Cannot reach the following locations: {missing_locations}')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
@@ -459,17 +474,25 @@ class World(object):
|
|||||||
|
|
||||||
class CollectionState(object):
|
class CollectionState(object):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, skip_init=False):
|
||||||
self.prog_items = Counter()
|
|
||||||
self.world = parent
|
self.world = parent
|
||||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
if not skip_init:
|
||||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
self.prog_items = Counter()
|
||||||
self.events = []
|
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.path = {}
|
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||||
self.locations_checked = set()
|
self.events = []
|
||||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
self.path = {}
|
||||||
for item in parent.precollected_items:
|
self.locations_checked = set()
|
||||||
self.collect(item, True)
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||||
|
for item in parent.precollected_items:
|
||||||
|
self.collect(item, True)
|
||||||
|
# reached vs. opened in the counter
|
||||||
|
self.door_counter = {player: (Counter(), Counter()) for player in range(1, parent.players + 1)}
|
||||||
|
self.reached_doors = {player: set() for player in range(1, parent.players + 1)}
|
||||||
|
self.opened_doors = {player: set() for player in range(1, parent.players + 1)}
|
||||||
|
self.dungeons_to_check = {player: defaultdict(dict) for player in range(1, parent.players + 1)}
|
||||||
|
self.dungeon_limits = None
|
||||||
|
# self.trace = None
|
||||||
|
|
||||||
def update_reachable_regions(self, player):
|
def update_reachable_regions(self, player):
|
||||||
self.stale[player] = False
|
self.stale[player] = False
|
||||||
@@ -480,68 +503,386 @@ class CollectionState(object):
|
|||||||
start = self.world.get_region('Menu', player)
|
start = self.world.get_region('Menu', player)
|
||||||
if not start in rrp:
|
if not start in rrp:
|
||||||
rrp[start] = CrystalBarrier.Orange
|
rrp[start] = CrystalBarrier.Orange
|
||||||
for exit in start.exits:
|
for conn in start.exits:
|
||||||
bc[exit] = CrystalBarrier.Orange
|
bc[conn] = CrystalBarrier.Orange
|
||||||
|
|
||||||
queue = deque(self.blocked_connections[player].items())
|
queue = deque(self.blocked_connections[player].items())
|
||||||
|
|
||||||
|
self.traverse_world(queue, rrp, bc, player)
|
||||||
|
unresolved_events = [x for y in self.reachable_regions[player] for x in y.locations
|
||||||
|
if x.event and x.item and (x.item.smallkey or x.item.bigkey or x.item.advancement)
|
||||||
|
and x not in self.locations_checked and x.can_reach(self)]
|
||||||
|
unresolved_events = self._do_not_flood_the_keys(unresolved_events)
|
||||||
|
if len(unresolved_events) == 0:
|
||||||
|
self.check_key_doors_in_dungeons(rrp, player)
|
||||||
|
|
||||||
|
def traverse_world(self, queue, rrp, bc, player):
|
||||||
# run BFS on all connections, and keep track of those blocked by missing items
|
# run BFS on all connections, and keep track of those blocked by missing items
|
||||||
while True:
|
while len(queue) > 0:
|
||||||
try:
|
connection, crystal_state = queue.popleft()
|
||||||
connection, crystal_state = queue.popleft()
|
new_region = connection.connected_region
|
||||||
new_region = connection.connected_region
|
if not self.should_visit(new_region, rrp, crystal_state, player):
|
||||||
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
|
if not new_region or not self.dungeon_limits or self.possibly_connected_to_dungeon(new_region, player):
|
||||||
bc.pop(connection, None)
|
bc.pop(connection, None)
|
||||||
elif connection.can_reach(self):
|
elif connection.can_reach(self):
|
||||||
if new_region.type == RegionType.Dungeon:
|
bc.pop(connection, None)
|
||||||
new_crystal_state = crystal_state
|
if new_region.type == RegionType.Dungeon:
|
||||||
for exit in new_region.exits:
|
new_crystal_state = crystal_state
|
||||||
door = exit.door
|
if new_region in rrp:
|
||||||
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
|
new_crystal_state |= rrp[new_region]
|
||||||
new_crystal_state = CrystalBarrier.Either
|
|
||||||
break
|
|
||||||
if new_region in rrp:
|
|
||||||
new_crystal_state |= rrp[new_region]
|
|
||||||
|
|
||||||
rrp[new_region] = new_crystal_state
|
rrp[new_region] = new_crystal_state
|
||||||
|
for conn in new_region.exits:
|
||||||
for exit in new_region.exits:
|
door = conn.door
|
||||||
door = exit.door
|
if door is not None and not door.blocked:
|
||||||
if door is not None and not door.blocked:
|
if self.valid_crystal(door, new_crystal_state):
|
||||||
door_crystal_state = door.crystal if door.crystal else new_crystal_state
|
door_crystal_state = door.crystal if door.crystal else new_crystal_state
|
||||||
bc[exit] = door_crystal_state
|
bc[conn] = door_crystal_state
|
||||||
queue.append((exit, door_crystal_state))
|
queue.append((conn, door_crystal_state))
|
||||||
elif door is None:
|
elif door is None:
|
||||||
queue.append((exit, new_crystal_state))
|
# note: no door in dungeon indicates what exactly? (always traversable)?
|
||||||
else:
|
queue.append((conn, new_crystal_state))
|
||||||
new_crystal_state = CrystalBarrier.Orange
|
else:
|
||||||
rrp[new_region] = new_crystal_state
|
new_crystal_state = CrystalBarrier.Orange
|
||||||
bc.pop(connection, None)
|
rrp[new_region] = new_crystal_state
|
||||||
for exit in new_region.exits:
|
for conn in new_region.exits:
|
||||||
bc[exit] = new_crystal_state
|
bc[conn] = new_crystal_state
|
||||||
queue.append((exit, new_crystal_state))
|
queue.append((conn, new_crystal_state))
|
||||||
|
|
||||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||||
|
|
||||||
# Retry connections if the new region can unblock them
|
# Retry connections if the new region can unblock them
|
||||||
if new_region.name in indirect_connections:
|
if new_region.name in indirect_connections:
|
||||||
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
||||||
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
|
if new_entrance in bc and new_entrance.parent_region in rrp:
|
||||||
queue.append((new_entrance, rrp[new_entrance.parent_region]))
|
new_crystal_state = rrp[new_entrance.parent_region]
|
||||||
except IndexError:
|
if (new_entrance, new_crystal_state) not in queue:
|
||||||
break
|
queue.append((new_entrance, new_crystal_state))
|
||||||
|
# else those connections that are not accessible yet
|
||||||
|
if self.is_small_door(connection):
|
||||||
|
door = connection.door if connection.door.smallKey else connection.door.controller
|
||||||
|
dungeon_name = connection.parent_region.dungeon.name
|
||||||
|
key_logic = self.world.key_logic[player][dungeon_name]
|
||||||
|
if door.name not in self.reached_doors[player]:
|
||||||
|
self.door_counter[player][0][dungeon_name] += 1
|
||||||
|
self.reached_doors[player].add(door.name)
|
||||||
|
if key_logic.sm_doors[door]:
|
||||||
|
self.reached_doors[player].add(key_logic.sm_doors[door].name)
|
||||||
|
if not connection.can_reach(self):
|
||||||
|
checklist_key = 'Universal' if self.world.retro[player] else dungeon_name
|
||||||
|
checklist = self.dungeons_to_check[player][checklist_key]
|
||||||
|
checklist[connection.name] = (connection, crystal_state)
|
||||||
|
elif door.name not in self.opened_doors[player]:
|
||||||
|
opened_doors = self.opened_doors[player]
|
||||||
|
door = connection.door if connection.door.smallKey else connection.door.controller
|
||||||
|
if door.name not in opened_doors:
|
||||||
|
self.door_counter[player][1][dungeon_name] += 1
|
||||||
|
opened_doors.add(door.name)
|
||||||
|
key_logic = self.world.key_logic[player][dungeon_name]
|
||||||
|
if key_logic.sm_doors[door]:
|
||||||
|
opened_doors.add(key_logic.sm_doors[door].name)
|
||||||
|
|
||||||
|
def should_visit(self, new_region, rrp, crystal_state, player):
|
||||||
|
if not new_region:
|
||||||
|
return False
|
||||||
|
if self.dungeon_limits and not self.possibly_connected_to_dungeon(new_region, player):
|
||||||
|
return False
|
||||||
|
if new_region not in rrp:
|
||||||
|
return True
|
||||||
|
if new_region.type != RegionType.Dungeon:
|
||||||
|
return False
|
||||||
|
return (rrp[new_region] & crystal_state) != crystal_state
|
||||||
|
|
||||||
|
def possibly_connected_to_dungeon(self, new_region, player):
|
||||||
|
if new_region.dungeon:
|
||||||
|
return new_region.dungeon.name in self.dungeon_limits
|
||||||
|
else:
|
||||||
|
return new_region.name in self.world.inaccessible_regions[player]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def valid_crystal(door, new_crystal_state):
|
||||||
|
return (not door.crystal or door.crystal == CrystalBarrier.Either or new_crystal_state == CrystalBarrier.Either
|
||||||
|
or new_crystal_state == door.crystal)
|
||||||
|
|
||||||
|
def check_key_doors_in_dungeons(self, rrp, player):
|
||||||
|
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
||||||
|
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
|
||||||
|
continue
|
||||||
|
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
||||||
|
key_total = self.prog_items[(dungeon_keys[dungeon_name], player)] # todo: universal
|
||||||
|
remaining_keys = key_total - self.door_counter[player][1][dungeon_name]
|
||||||
|
if not init_door_candidates or remaining_keys == 0:
|
||||||
|
continue
|
||||||
|
dungeon_doors = {x.name for x in self.world.key_logic[player][dungeon_name].sm_doors.keys()}
|
||||||
|
|
||||||
|
def valid_d_door(x):
|
||||||
|
return x in dungeon_doors
|
||||||
|
|
||||||
|
child_states = deque()
|
||||||
|
child_states.append(self)
|
||||||
|
visited_opened_doors = set()
|
||||||
|
visited_opened_doors.add(frozenset(self.opened_doors[player]))
|
||||||
|
terminal_states, common_regions, common_bc, common_doors = [], {}, {}, set()
|
||||||
|
while len(child_states) > 0:
|
||||||
|
next_child = child_states.popleft()
|
||||||
|
door_candidates = CollectionState.should_explore_child_state(next_child, dungeon_name, player)
|
||||||
|
child_checklist = next_child.dungeons_to_check[player][dungeon_name]
|
||||||
|
if door_candidates:
|
||||||
|
for chosen_door in door_candidates:
|
||||||
|
child_state = next_child.copy()
|
||||||
|
child_queue = deque()
|
||||||
|
child_state.door_counter[player][1][dungeon_name] += 1
|
||||||
|
if isinstance(chosen_door, tuple):
|
||||||
|
child_state.opened_doors[player].add(chosen_door[0])
|
||||||
|
child_state.opened_doors[player].add(chosen_door[1])
|
||||||
|
if chosen_door[0] in child_checklist:
|
||||||
|
child_queue.append(child_checklist[chosen_door[0]])
|
||||||
|
if chosen_door[1] in child_checklist:
|
||||||
|
child_queue.append(child_checklist[chosen_door[1]])
|
||||||
|
else:
|
||||||
|
child_state.opened_doors[player].add(chosen_door)
|
||||||
|
if chosen_door in child_checklist:
|
||||||
|
child_queue.append(child_checklist[chosen_door])
|
||||||
|
if child_state.opened_doors[player] not in visited_opened_doors:
|
||||||
|
done = False
|
||||||
|
while not done:
|
||||||
|
rrp_ = child_state.reachable_regions[player]
|
||||||
|
bc_ = child_state.blocked_connections[player]
|
||||||
|
child_state.set_dungeon_limits(player, dungeon_name)
|
||||||
|
child_queue.extend([(x, y) for x, y in bc_.items()
|
||||||
|
if child_state.possibly_connected_to_dungeon(x.parent_region,
|
||||||
|
player)])
|
||||||
|
child_state.traverse_world(child_queue, rrp_, bc_, player)
|
||||||
|
new_events = child_state.sweep_for_events_once(player)
|
||||||
|
child_state.stale[player] = False
|
||||||
|
if new_events:
|
||||||
|
for conn in bc_:
|
||||||
|
if conn.parent_region.dungeon and conn.parent_region.dungeon.name == dungeon_name:
|
||||||
|
child_queue.append((conn, bc_[conn]))
|
||||||
|
done = not new_events
|
||||||
|
if child_state.opened_doors[player] not in visited_opened_doors:
|
||||||
|
visited_opened_doors.add(frozenset(child_state.opened_doors[player]))
|
||||||
|
child_states.append(child_state)
|
||||||
|
else:
|
||||||
|
terminal_states.append(next_child)
|
||||||
|
common_regions, common_bc, common_doors, first = {}, {}, set(), True
|
||||||
|
bc = self.blocked_connections[player]
|
||||||
|
for term_state in terminal_states:
|
||||||
|
t_rrp = term_state.reachable_regions[player]
|
||||||
|
t_bc = term_state.blocked_connections[player]
|
||||||
|
if first:
|
||||||
|
first = False
|
||||||
|
common_regions = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
|
||||||
|
common_bc = {x: y for x, y in t_bc.items() if x not in bc}
|
||||||
|
common_doors = {x for x in term_state.opened_doors[player] - self.opened_doors[player]
|
||||||
|
if valid_d_door(x)}
|
||||||
|
else:
|
||||||
|
cm_rrp = {x: y for x, y in t_rrp.items() if x not in rrp or y != rrp[x]}
|
||||||
|
common_regions = {k: self.comb_crys(v, cm_rrp[k]) for k, v in common_regions.items()
|
||||||
|
if k in cm_rrp and self.crys_agree(v, cm_rrp[k])}
|
||||||
|
common_bc.update({x: y for x, y in t_bc.items() if x not in bc and x not in common_bc})
|
||||||
|
common_doors &= {x for x in term_state.opened_doors[player] - self.opened_doors[player]
|
||||||
|
if valid_d_door(x)}
|
||||||
|
|
||||||
|
terminal_queue = deque()
|
||||||
|
for door in common_doors:
|
||||||
|
pair = self.find_door_pair(player, dungeon_name, door)
|
||||||
|
if door not in self.reached_doors[player]:
|
||||||
|
self.door_counter[player][0][dungeon_name] += 1
|
||||||
|
self.reached_doors[player].add(door)
|
||||||
|
if pair not in self.reached_doors[player]:
|
||||||
|
self.reached_doors[player].add(pair)
|
||||||
|
self.opened_doors[player].add(door)
|
||||||
|
if door in checklist:
|
||||||
|
terminal_queue.append(checklist[door])
|
||||||
|
if pair not in self.opened_doors[player]:
|
||||||
|
self.door_counter[player][1][dungeon_name] += 1
|
||||||
|
|
||||||
|
self.set_dungeon_limits(player, dungeon_name)
|
||||||
|
rrp_ = self.reachable_regions[player]
|
||||||
|
bc_ = self.blocked_connections[player]
|
||||||
|
for block, crystal in bc_.items():
|
||||||
|
if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player):
|
||||||
|
terminal_queue.append((block, crystal))
|
||||||
|
self.traverse_world(terminal_queue, rrp_, bc_, player)
|
||||||
|
self.dungeon_limits = None
|
||||||
|
|
||||||
|
rrp = self.reachable_regions[player]
|
||||||
|
missing_regions = {x: y for x, y in common_regions.items() if x not in rrp}
|
||||||
|
paths = {}
|
||||||
|
for k in missing_regions:
|
||||||
|
rrp[k] = missing_regions[k]
|
||||||
|
possible_path = terminal_states[0].path[k]
|
||||||
|
self.path[k] = paths[k] = possible_path
|
||||||
|
missing_bc = {}
|
||||||
|
for blocked, crystal in common_bc.items():
|
||||||
|
if (blocked not in bc and blocked.parent_region in rrp
|
||||||
|
and self.should_visit(blocked.connected_region, rrp, crystal, player)):
|
||||||
|
missing_bc[blocked] = crystal
|
||||||
|
for k in missing_bc:
|
||||||
|
bc[k] = missing_bc[k]
|
||||||
|
self.record_dungeon_exploration(player, dungeon_name, checklist,
|
||||||
|
common_doors, missing_regions, missing_bc, paths)
|
||||||
|
checklist.clear()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def comb_crys(a, b):
|
||||||
|
return a if a == b or a != CrystalBarrier.Either else b
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def crys_agree(a, b):
|
||||||
|
return a == b or a == CrystalBarrier.Either or b == CrystalBarrier.Either
|
||||||
|
|
||||||
|
def find_door_pair(self, player, dungeon_name, name):
|
||||||
|
for door in self.world.key_logic[player][dungeon_name].sm_doors.keys():
|
||||||
|
if door.name == name:
|
||||||
|
paired_door = self.world.key_logic[player][dungeon_name].sm_doors[door]
|
||||||
|
return paired_door.name if paired_door else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_dungeon_limits(self, player, dungeon_name):
|
||||||
|
if self.world.retro[player] and self.world.mode[player] == 'standard':
|
||||||
|
self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower']
|
||||||
|
else:
|
||||||
|
self.dungeon_limits = [dungeon_name]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def should_explore_child_state(state, dungeon_name, player):
|
||||||
|
small_key_name = dungeon_keys[dungeon_name]
|
||||||
|
key_total = state.prog_items[(small_key_name, player)]
|
||||||
|
remaining_keys = key_total - state.door_counter[player][1][dungeon_name]
|
||||||
|
unopened_doors = state.door_counter[player][0][dungeon_name] - state.door_counter[player][1][dungeon_name]
|
||||||
|
if remaining_keys > 0 and unopened_doors > 0:
|
||||||
|
key_logic = state.world.key_logic[player][dungeon_name]
|
||||||
|
door_candidates, skip = [], set()
|
||||||
|
for door, paired in key_logic.sm_doors.items():
|
||||||
|
if door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]:
|
||||||
|
if door.name not in skip:
|
||||||
|
if paired:
|
||||||
|
door_candidates.append((door.name, paired.name))
|
||||||
|
skip.add(paired.name)
|
||||||
|
else:
|
||||||
|
door_candidates.append(door.name)
|
||||||
|
return door_candidates
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_rrp(rrp):
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
logger.debug('RRP Checking')
|
||||||
|
for region, packet in rrp.items():
|
||||||
|
new_crystal_state, logic, path = packet
|
||||||
|
logger.debug(f'\nRegion: {region.name} (CS: {str(new_crystal_state)})')
|
||||||
|
for i in range(0, len(logic)):
|
||||||
|
logger.debug(f'{logic[i]}')
|
||||||
|
logger.debug(f'{",".join(str(x) for x in path[i])}')
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
ret = CollectionState(self.world)
|
ret = CollectionState(self.world, skip_init=True)
|
||||||
ret.prog_items = self.prog_items.copy()
|
ret.prog_items = self.prog_items.copy()
|
||||||
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
||||||
ret.events = copy.copy(self.events)
|
ret.events = copy.copy(self.events)
|
||||||
ret.path = copy.copy(self.path)
|
ret.path = copy.copy(self.path)
|
||||||
ret.locations_checked = copy.copy(self.locations_checked)
|
ret.locations_checked = copy.copy(self.locations_checked)
|
||||||
|
ret.stale = {player: self.stale[player] for player in range(1, self.world.players + 1)}
|
||||||
|
ret.door_counter = {player: (copy.copy(self.door_counter[player][0]), copy.copy(self.door_counter[player][1]))
|
||||||
|
for player in range(1, self.world.players + 1)}
|
||||||
|
ret.reached_doors = {player: copy.copy(self.reached_doors[player]) for player in range(1, self.world.players + 1)}
|
||||||
|
ret.opened_doors = {player: copy.copy(self.opened_doors[player]) for player in range(1, self.world.players + 1)}
|
||||||
|
ret.dungeons_to_check = {
|
||||||
|
player: defaultdict(dict, {name: copy.copy(checklist)
|
||||||
|
for name, checklist in self.dungeons_to_check[player].items()})
|
||||||
|
for player in range(1, self.world.players + 1)}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
|
||||||
|
bc = self.blocked_connections[player]
|
||||||
|
ec = self.world.exp_cache[player]
|
||||||
|
prog_set = self.reduce_prog_items(player, dungeon_name)
|
||||||
|
exp_key = (prog_set, frozenset(checklist))
|
||||||
|
if dungeon_name in ec and exp_key in ec[dungeon_name]:
|
||||||
|
# apply
|
||||||
|
common_doors, missing_regions, missing_bc, paths = ec[dungeon_name][exp_key]
|
||||||
|
terminal_queue = deque()
|
||||||
|
for door in common_doors:
|
||||||
|
pair = self.find_door_pair(player, dungeon_name, door)
|
||||||
|
if door not in self.reached_doors[player]:
|
||||||
|
self.door_counter[player][0][dungeon_name] += 1
|
||||||
|
self.reached_doors[player].add(door)
|
||||||
|
if pair not in self.reached_doors[player]:
|
||||||
|
self.reached_doors[player].add(pair)
|
||||||
|
self.opened_doors[player].add(door)
|
||||||
|
if door in checklist:
|
||||||
|
terminal_queue.append(checklist[door])
|
||||||
|
if pair not in self.opened_doors[player]:
|
||||||
|
self.door_counter[player][1][dungeon_name] += 1
|
||||||
|
|
||||||
|
self.set_dungeon_limits(player, dungeon_name)
|
||||||
|
rrp_ = self.reachable_regions[player]
|
||||||
|
bc_ = self.blocked_connections[player]
|
||||||
|
for block, crystal in bc_.items():
|
||||||
|
if (block, crystal) not in terminal_queue and self.possibly_connected_to_dungeon(block.connected_region, player):
|
||||||
|
terminal_queue.append((block, crystal))
|
||||||
|
self.traverse_world(terminal_queue, rrp_, bc_, player)
|
||||||
|
self.dungeon_limits = None
|
||||||
|
|
||||||
|
for k in missing_regions:
|
||||||
|
rrp[k] = missing_regions[k]
|
||||||
|
for r, path in paths.items():
|
||||||
|
self.path[r] = path
|
||||||
|
for k in missing_bc:
|
||||||
|
bc[k] = missing_bc[k]
|
||||||
|
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def record_dungeon_exploration(self, player, dungeon_name, checklist,
|
||||||
|
common_doors, missing_regions, missing_bc, paths):
|
||||||
|
ec = self.world.exp_cache[player]
|
||||||
|
prog_set = self.reduce_prog_items(player, dungeon_name)
|
||||||
|
exp_key = (prog_set, frozenset(checklist))
|
||||||
|
ec[dungeon_name][exp_key] = (common_doors, missing_regions, missing_bc, paths)
|
||||||
|
|
||||||
|
def reduce_prog_items(self, player, dungeon_name):
|
||||||
|
# todo: possibly could include an analysis of dungeon items req. like Hammer, Hookshot, etc
|
||||||
|
# cross dungeon requirements may be necessary for keysanity - which invalidates the above
|
||||||
|
# todo: universal smalls where needed
|
||||||
|
life_count, bottle_count = 0, 0
|
||||||
|
reduced = Counter()
|
||||||
|
for item, cnt in self.prog_items.items():
|
||||||
|
item_name, item_player = item
|
||||||
|
if item_player == player and self.check_if_progressive(item_name):
|
||||||
|
if item_name.startswith('Bottle'): # I think magic requirements can require multiple bottles
|
||||||
|
bottle_count += cnt
|
||||||
|
elif item_name in ['Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart']:
|
||||||
|
if 'Container' in item_name:
|
||||||
|
life_count += 1
|
||||||
|
elif 'Piece of Heart' == item_name:
|
||||||
|
life_count += .25
|
||||||
|
else:
|
||||||
|
reduced[item] = cnt
|
||||||
|
if bottle_count > 0:
|
||||||
|
reduced[('Bottle', player)] = 1
|
||||||
|
if life_count >= 1:
|
||||||
|
reduced[('Heart Container', player)] = 1
|
||||||
|
return frozenset(reduced.items())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_if_progressive(item_name):
|
||||||
|
return (item_name in
|
||||||
|
['Bow', 'Progressive Bow', 'Progressive Bow (Alt)', 'Book of Mudora', 'Hammer', 'Hookshot',
|
||||||
|
'Magic Mirror', 'Ocarina', 'Pegasus Boots', 'Power Glove', 'Cape', 'Mushroom', 'Shovel',
|
||||||
|
'Lamp', 'Magic Powder', 'Moon Pearl', 'Cane of Somaria', 'Fire Rod', 'Flippers', 'Ice Rod',
|
||||||
|
'Titans Mitts', 'Bombos', 'Ether', 'Quake', 'Master Sword', 'Tempered Sword', 'Fighter Sword',
|
||||||
|
'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant',
|
||||||
|
'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5',
|
||||||
|
'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield',
|
||||||
|
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna',
|
||||||
|
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
|
||||||
|
'Magic Upgrade (1/4)']
|
||||||
|
or item_name.startswith(('Bottle', 'Small Key', 'Big Key')))
|
||||||
|
|
||||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||||
try:
|
try:
|
||||||
spot_type = spot.spot_type
|
spot_type = spot.spot_type
|
||||||
@@ -557,6 +898,16 @@ class CollectionState(object):
|
|||||||
|
|
||||||
return spot.can_reach(self)
|
return spot.can_reach(self)
|
||||||
|
|
||||||
|
def sweep_for_events_once(self, player):
|
||||||
|
locations = self.world.get_filled_locations(player)
|
||||||
|
checked_locations = set([l for l in locations if l in self.locations_checked])
|
||||||
|
reachable_events = [location for location in locations if location.event and location.can_reach(self)]
|
||||||
|
reachable_events = self._do_not_flood_the_keys(reachable_events)
|
||||||
|
for event in reachable_events:
|
||||||
|
if event not in checked_locations:
|
||||||
|
self.events.append((event.name, event.player))
|
||||||
|
self.collect(event.item, True, event)
|
||||||
|
return len(reachable_events) > len(checked_locations)
|
||||||
|
|
||||||
def sweep_for_events(self, key_only=False, locations=None):
|
def sweep_for_events(self, key_only=False, locations=None):
|
||||||
# this may need improvement
|
# this may need improvement
|
||||||
@@ -604,6 +955,18 @@ class CollectionState(object):
|
|||||||
or not self.location_can_be_flooded(flood_location))
|
or not self.location_can_be_flooded(flood_location))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_small_door(connection):
|
||||||
|
return connection and connection.door and (connection.door.smallKey or
|
||||||
|
CollectionState.is_controlled_by_small(connection))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_controlled_by_small(connection):
|
||||||
|
return connection.door.controller and connection.door.controller.smallKey
|
||||||
|
|
||||||
|
def is_door_open(self, door_name, player):
|
||||||
|
return door_name in self.opened_doors[player]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def location_can_be_flooded(location):
|
def location_can_be_flooded(location):
|
||||||
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
|
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
|
||||||
@@ -686,8 +1049,7 @@ class CollectionState(object):
|
|||||||
|
|
||||||
# In the future, this can be used to check if the player starts without bombs
|
# In the future, this can be used to check if the player starts without bombs
|
||||||
def can_use_bombs(self, player):
|
def can_use_bombs(self, player):
|
||||||
StartingBombs = True
|
return (not self.world.bombbag[player] or self.has('Bomb Upgrade (+10)', player))
|
||||||
return StartingBombs or self.has('Bomb Upgrade (+10)', player)
|
|
||||||
|
|
||||||
def can_hit_crystal(self, player):
|
def can_hit_crystal(self, player):
|
||||||
return (self.can_use_bombs(player)
|
return (self.can_use_bombs(player)
|
||||||
@@ -719,6 +1081,7 @@ class CollectionState(object):
|
|||||||
def can_get_good_bee(self, player):
|
def can_get_good_bee(self, player):
|
||||||
cave = self.world.get_region('Good Bee Cave', player)
|
cave = self.world.get_region('Good Bee Cave', player)
|
||||||
return (
|
return (
|
||||||
|
self.can_use_bombs(player) and
|
||||||
self.has_bottle(player) and
|
self.has_bottle(player) and
|
||||||
self.has('Bug Catching Net', player) and
|
self.has('Bug Catching Net', player) and
|
||||||
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
|
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
|
||||||
@@ -807,6 +1170,8 @@ class CollectionState(object):
|
|||||||
def collect(self, item, event=False, location=None):
|
def collect(self, item, event=False, location=None):
|
||||||
if location:
|
if location:
|
||||||
self.locations_checked.add(location)
|
self.locations_checked.add(location)
|
||||||
|
if not item:
|
||||||
|
return
|
||||||
changed = False
|
changed = False
|
||||||
if item.name.startswith('Progressive '):
|
if item.name.startswith('Progressive '):
|
||||||
if 'Sword' in item.name:
|
if 'Sword' in item.name:
|
||||||
@@ -1282,6 +1647,7 @@ class Door(object):
|
|||||||
self.bk_shuffle_req = False
|
self.bk_shuffle_req = False
|
||||||
self.standard_restricted = False # flag if portal is not allowed in HC in standard
|
self.standard_restricted = False # flag if portal is not allowed in HC in standard
|
||||||
self.lw_restricted = False # flag if portal is not allowed in DW
|
self.lw_restricted = False # flag if portal is not allowed in DW
|
||||||
|
self.rupee_bow_restricted = False # flag if portal is not allowed in HC in standard+rupee_bow
|
||||||
# self.incognitoPos = -1
|
# self.incognitoPos = -1
|
||||||
# self.sectorLink = False
|
# self.sectorLink = False
|
||||||
|
|
||||||
@@ -1483,6 +1849,7 @@ class Sector(object):
|
|||||||
self.entrance_sector = None
|
self.entrance_sector = None
|
||||||
self.destination_entrance = False
|
self.destination_entrance = False
|
||||||
self.equations = None
|
self.equations = None
|
||||||
|
self.item_logic = set()
|
||||||
|
|
||||||
def region_set(self):
|
def region_set(self):
|
||||||
if self.r_name_set is None:
|
if self.r_name_set is None:
|
||||||
@@ -1735,9 +2102,7 @@ class Location(object):
|
|||||||
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
||||||
|
|
||||||
def can_reach(self, state):
|
def can_reach(self, state):
|
||||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
return self.parent_region.can_reach(state) and self.access_rule(state)
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def forced_big_key(self):
|
def forced_big_key(self):
|
||||||
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
|
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
|
||||||
@@ -1764,6 +2129,12 @@ class Location(object):
|
|||||||
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
||||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.name == other.name and self.player == other.player
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.name, self.player))
|
||||||
|
|
||||||
|
|
||||||
class Item(object):
|
class Item(object):
|
||||||
|
|
||||||
@@ -1807,6 +2178,15 @@ class Item(object):
|
|||||||
def compass(self):
|
def compass(self):
|
||||||
return self.type == 'Compass'
|
return self.type == 'Compass'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dungeon(self):
|
||||||
|
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
|
||||||
|
return None
|
||||||
|
item_dungeon = self.name.split('(')[1][:-1]
|
||||||
|
if item_dungeon == 'Escape':
|
||||||
|
item_dungeon = 'Hyrule Castle'
|
||||||
|
return item_dungeon
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.__unicode__())
|
return str(self.__unicode__())
|
||||||
|
|
||||||
@@ -1893,6 +2273,7 @@ class Spoiler(object):
|
|||||||
self.doorTypes = {}
|
self.doorTypes = {}
|
||||||
self.lobbies = {}
|
self.lobbies = {}
|
||||||
self.medallions = {}
|
self.medallions = {}
|
||||||
|
self.bottles = {}
|
||||||
self.playthrough = {}
|
self.playthrough = {}
|
||||||
self.unreachables = []
|
self.unreachables = []
|
||||||
self.startinventory = []
|
self.startinventory = []
|
||||||
@@ -1936,6 +2317,15 @@ class Spoiler(object):
|
|||||||
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
||||||
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
||||||
|
|
||||||
|
self.bottles = OrderedDict()
|
||||||
|
if self.world.players == 1:
|
||||||
|
self.bottles['Waterfall Bottle'] = self.world.bottle_refills[1][0]
|
||||||
|
self.bottles['Pyramid Bottle'] = self.world.bottle_refills[1][1]
|
||||||
|
else:
|
||||||
|
for player in range(1, self.world.players + 1):
|
||||||
|
self.bottles[f'Waterfall Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][0]
|
||||||
|
self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1]
|
||||||
|
|
||||||
self.startinventory = list(map(str, self.world.precollected_items))
|
self.startinventory = list(map(str, self.world.precollected_items))
|
||||||
|
|
||||||
self.locations = OrderedDict()
|
self.locations = OrderedDict()
|
||||||
@@ -2013,9 +2403,11 @@ class Spoiler(object):
|
|||||||
'logic': self.world.logic,
|
'logic': self.world.logic,
|
||||||
'mode': self.world.mode,
|
'mode': self.world.mode,
|
||||||
'retro': self.world.retro,
|
'retro': self.world.retro,
|
||||||
|
'bombbag': self.world.bombbag,
|
||||||
'weapons': self.world.swords,
|
'weapons': self.world.swords,
|
||||||
'goal': self.world.goal,
|
'goal': self.world.goal,
|
||||||
'shuffle': self.world.shuffle,
|
'shuffle': self.world.shuffle,
|
||||||
|
'shufflelinks': self.world.shufflelinks,
|
||||||
'door_shuffle': self.world.doorShuffle,
|
'door_shuffle': self.world.doorShuffle,
|
||||||
'intensity': self.world.intensity,
|
'intensity': self.world.intensity,
|
||||||
'item_pool': self.world.difficulty,
|
'item_pool': self.world.difficulty,
|
||||||
@@ -2054,6 +2446,7 @@ class Spoiler(object):
|
|||||||
out.update(self.locations)
|
out.update(self.locations)
|
||||||
out['Starting Inventory'] = self.startinventory
|
out['Starting Inventory'] = self.startinventory
|
||||||
out['Special'] = self.medallions
|
out['Special'] = self.medallions
|
||||||
|
out['Bottles'] = self.bottles
|
||||||
if self.hashes:
|
if self.hashes:
|
||||||
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
||||||
if self.shops:
|
if self.shops:
|
||||||
@@ -2090,6 +2483,7 @@ class Spoiler(object):
|
|||||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
|
||||||
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
|
||||||
|
outfile.write(f"Link's House Shuffled: {'Yes' if self.metadata['shufflelinks'][player] else 'No'}\n")
|
||||||
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
||||||
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
|
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
|
||||||
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
|
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
|
||||||
@@ -2111,6 +2505,7 @@ class Spoiler(object):
|
|||||||
outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No'))
|
outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No'))
|
||||||
outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No'))
|
outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No'))
|
||||||
outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n")
|
outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n")
|
||||||
|
outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No'))
|
||||||
if self.doors:
|
if self.doors:
|
||||||
outfile.write('\n\nDoors:\n\n')
|
outfile.write('\n\nDoors:\n\n')
|
||||||
outfile.write('\n'.join(
|
outfile.write('\n'.join(
|
||||||
@@ -2138,6 +2533,9 @@ class Spoiler(object):
|
|||||||
outfile.write('\n\nMedallions:\n')
|
outfile.write('\n\nMedallions:\n')
|
||||||
for dungeon, medallion in self.medallions.items():
|
for dungeon, medallion in self.medallions.items():
|
||||||
outfile.write(f'\n{dungeon}: {medallion} Medallion')
|
outfile.write(f'\n{dungeon}: {medallion} Medallion')
|
||||||
|
outfile.write('\n\nBottle Refills:\n')
|
||||||
|
for fairy, bottle in self.bottles.items():
|
||||||
|
outfile.write(f'\n{fairy}: {bottle}')
|
||||||
if self.startinventory:
|
if self.startinventory:
|
||||||
outfile.write('\n\nStarting Inventory:\n\n')
|
outfile.write('\n\nStarting Inventory:\n\n')
|
||||||
outfile.write('\n'.join(self.startinventory))
|
outfile.write('\n'.join(self.startinventory))
|
||||||
@@ -2154,7 +2552,7 @@ class Spoiler(object):
|
|||||||
|
|
||||||
for player in range(1, self.world.players + 1):
|
for player in range(1, self.world.players + 1):
|
||||||
if self.world.boss_shuffle[player] != 'none':
|
if self.world.boss_shuffle[player] != 'none':
|
||||||
bossmap = self.bosses[player] if self.world.players > 1 else self.bosses
|
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
|
||||||
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n')
|
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n')
|
||||||
outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
|
outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
|
||||||
|
|
||||||
@@ -2197,6 +2595,22 @@ dungeon_names = [
|
|||||||
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
|
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
dungeon_keys = {
|
||||||
|
'Hyrule Castle': 'Small Key (Escape)',
|
||||||
|
'Eastern Palace': 'Small Key (Eastern Palace)',
|
||||||
|
'Desert Palace': 'Small Key (Desert Palace)',
|
||||||
|
'Tower of Hera': 'Small Key (Tower of Hera)',
|
||||||
|
'Agahnims Tower': 'Small Key (Agahnims Tower)',
|
||||||
|
'Palace of Darkness': 'Small Key (Palace of Darkness)',
|
||||||
|
'Swamp Palace': 'Small Key (Swamp Palace)',
|
||||||
|
'Skull Woods': 'Small Key (Skull Woods)',
|
||||||
|
'Thieves Town': 'Small Key (Thieves Town)',
|
||||||
|
'Ice Palace': 'Small Key (Ice Palace)',
|
||||||
|
'Misery Mire': 'Small Key (Misery Mire)',
|
||||||
|
'Turtle Rock': 'Small Key (Turtle Rock)',
|
||||||
|
'Ganons Tower': 'Small Key (Ganons Tower)',
|
||||||
|
'Universal': 'Small Key (Universal)'
|
||||||
|
}
|
||||||
|
|
||||||
class PotItem(FastEnum):
|
class PotItem(FastEnum):
|
||||||
Nothing = 0x0
|
Nothing = 0x0
|
||||||
@@ -2272,7 +2686,7 @@ access_mode = {"items": 0, "locations": 1, "none": 2}
|
|||||||
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3}
|
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3}
|
||||||
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
|
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
|
||||||
|
|
||||||
# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?)
|
# byte 7: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links)
|
||||||
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
|
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
|
||||||
e_dmg = {"default": 0, "shuffled": 1, "random": 2}
|
e_dmg = {"default": 0, "shuffled": 1, "random": 2}
|
||||||
|
|
||||||
@@ -2303,7 +2717,8 @@ class Settings(object):
|
|||||||
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
|
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
|
||||||
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
|
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
|
||||||
|
|
||||||
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)])
|
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
|
||||||
|
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0)])
|
||||||
return base64.b64encode(code, "+-".encode()).decode()
|
return base64.b64encode(code, "+-".encode()).decode()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -2347,3 +2762,11 @@ class Settings(object):
|
|||||||
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
|
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
|
||||||
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
|
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
|
||||||
args.shufflepots[p] = True if settings[7] & 0x4 else False
|
args.shufflepots[p] = True if settings[7] & 0x4 else False
|
||||||
|
args.bombbag[p] = True if settings[7] & 0x2 else False
|
||||||
|
args.shufflelinks[p] = True if settings[7] & 0x1 else False
|
||||||
|
|
||||||
|
|
||||||
|
class KeyRuleType(FastEnum):
|
||||||
|
WorstCase = 0
|
||||||
|
AllowSmall = 1
|
||||||
|
Lock = 2
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def LanmolasDefeatRule(state, player):
|
|||||||
state.has('Fire Rod', player) or
|
state.has('Fire Rod', player) or
|
||||||
state.has('Ice Rod', player) or
|
state.has('Ice Rod', player) or
|
||||||
state.has('Cane of Somaria', player) or
|
state.has('Cane of Somaria', player) or
|
||||||
state.has('Cane of Byrna', player) or
|
(state.has('Cane of Byrna', player) and state.can_use_bombs(player)) or
|
||||||
state.can_shoot_arrows(player))
|
state.can_shoot_arrows(player))
|
||||||
|
|
||||||
def MoldormDefeatRule(state, player):
|
def MoldormDefeatRule(state, player):
|
||||||
@@ -51,7 +51,7 @@ def ArrghusDefeatRule(state, 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
|
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
|
||||||
(state.has('Ice Rod', 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))))
|
||||||
|
|
||||||
|
|
||||||
def MothulaDefeatRule(state, player):
|
def MothulaDefeatRule(state, player):
|
||||||
@@ -92,7 +92,7 @@ def KholdstareDefeatRule(state, player):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def VitreousDefeatRule(state, player):
|
def VitreousDefeatRule(state, player):
|
||||||
return state.can_shoot_arrows(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 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)):
|
||||||
@@ -176,16 +176,16 @@ def place_bosses(world, player):
|
|||||||
|
|
||||||
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
|
if world.boss_shuffle[player] == "simple": # vanilla bosses shuffled
|
||||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||||
else: # all bosses present, the three duplicates chosen at random
|
else: # all bosses present, the three duplicates chosen at random
|
||||||
bosses = all_bosses + [random.choice(placeable_bosses) for _ in range(3)]
|
bosses = all_bosses + random.sample(placeable_bosses, 3)
|
||||||
|
|
||||||
logging.getLogger('').debug('Bosses chosen %s', bosses)
|
logging.getLogger('').debug('Bosses chosen %s', bosses)
|
||||||
|
|
||||||
random.shuffle(bosses)
|
|
||||||
for [loc, level] in boss_locations:
|
for [loc, level] in boss_locations:
|
||||||
loc_text = loc + (' ('+level+')' if level else '')
|
loc_text = loc + (' ('+level+')' if level else '')
|
||||||
boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None)
|
try:
|
||||||
if not boss:
|
boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)])
|
||||||
|
except IndexError:
|
||||||
raise FillError('Could not place boss for location %s' % loc_text)
|
raise FillError('Could not place boss for location %s' % loc_text)
|
||||||
bosses.remove(boss)
|
bosses.remove(boss)
|
||||||
|
|
||||||
|
|||||||
@@ -96,13 +96,14 @@ def parse_cli(argv, no_defaults=False):
|
|||||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
||||||
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||||
|
'bombbag',
|
||||||
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
|
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
|
||||||
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
|
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
|
||||||
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
|
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
|
||||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
|
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
|
||||||
'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code',
|
'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code',
|
||||||
'reduce_flashing']:
|
'reduce_flashing', 'shuffle_sfx']:
|
||||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||||
if player == 1:
|
if player == 1:
|
||||||
setattr(ret, name, {1: value})
|
setattr(ret, name, {1: value})
|
||||||
@@ -126,6 +127,7 @@ def parse_settings():
|
|||||||
settings = {
|
settings = {
|
||||||
"lang": "en",
|
"lang": "en",
|
||||||
"retro": False,
|
"retro": False,
|
||||||
|
"bombbag": False,
|
||||||
"mode": "open",
|
"mode": "open",
|
||||||
"logic": "noglitches",
|
"logic": "noglitches",
|
||||||
"goal": "ganon",
|
"goal": "ganon",
|
||||||
@@ -190,6 +192,7 @@ def parse_settings():
|
|||||||
"ow_palettes": "default",
|
"ow_palettes": "default",
|
||||||
"uw_palettes": "default",
|
"uw_palettes": "default",
|
||||||
"reduce_flashing": False,
|
"reduce_flashing": False,
|
||||||
|
"shuffle_sfx": False,
|
||||||
|
|
||||||
# Spoiler defaults to TRUE
|
# Spoiler defaults to TRUE
|
||||||
# Playthrough defaults to TRUE
|
# Playthrough defaults to TRUE
|
||||||
|
|||||||
+79
-50
@@ -1,22 +1,21 @@
|
|||||||
import RaceRandom as random
|
import RaceRandom as random
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
import logging
|
import logging
|
||||||
import operator as op
|
|
||||||
import time
|
import time
|
||||||
from enum import unique, Flag
|
from enum import unique, Flag
|
||||||
from typing import DefaultDict, Dict, List
|
from typing import DefaultDict, Dict, List
|
||||||
|
|
||||||
from functools import reduce
|
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
|
||||||
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo
|
|
||||||
from Doors import reset_portals
|
from Doors import reset_portals
|
||||||
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
|
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
|
||||||
from Dungeons import dungeon_bigs, dungeon_keys, dungeon_hints
|
from Dungeons import dungeon_bigs, dungeon_hints
|
||||||
from Items import ItemFactory
|
from Items import ItemFactory
|
||||||
from RoomData import DoorKind, PairedDoor, reset_rooms
|
from RoomData import DoorKind, PairedDoor, reset_rooms
|
||||||
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
|
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, pre_validate, determine_required_paths, drop_entrances
|
||||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
||||||
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
|
from DungeonGenerator import dungeon_portals, dungeon_drops, GenerationException
|
||||||
from KeyDoorShuffle import analyze_dungeon, validate_vanilla_key_logic, build_key_layout, validate_key_layout
|
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
|
||||||
|
from Utils import ncr, kth_combination
|
||||||
|
|
||||||
|
|
||||||
def link_doors(world, player):
|
def link_doors(world, player):
|
||||||
@@ -102,6 +101,8 @@ def link_doors_main(world, player):
|
|||||||
connect_portal(portal, world, player)
|
connect_portal(portal, world, player)
|
||||||
if not world.doorShuffle[player] == 'vanilla':
|
if not world.doorShuffle[player] == 'vanilla':
|
||||||
fix_big_key_doors_with_ugly_smalls(world, player)
|
fix_big_key_doors_with_ugly_smalls(world, player)
|
||||||
|
else:
|
||||||
|
unmark_ugly_smalls(world, player)
|
||||||
if world.doorShuffle[player] == 'vanilla':
|
if world.doorShuffle[player] == 'vanilla':
|
||||||
for entrance, ext in open_edges:
|
for entrance, ext in open_edges:
|
||||||
connect_two_way(world, entrance, ext, player)
|
connect_two_way(world, entrance, ext, player)
|
||||||
@@ -214,8 +215,8 @@ def vanilla_key_logic(world, player):
|
|||||||
analyze_dungeon(key_layout, world, player)
|
analyze_dungeon(key_layout, world, player)
|
||||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||||
log_key_logic(builder.name, key_layout.key_logic)
|
log_key_logic(builder.name, key_layout.key_logic)
|
||||||
if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
# if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items' and not world.retro[player] and not world.keydropshuffle[player]:
|
||||||
validate_vanilla_key_logic(world, player)
|
# validate_vanilla_key_logic(world, player)
|
||||||
|
|
||||||
|
|
||||||
# some useful functions
|
# some useful functions
|
||||||
@@ -317,6 +318,13 @@ def connect_one_way(world, entrancename, exitname, player):
|
|||||||
y.dest = x
|
y.dest = x
|
||||||
|
|
||||||
|
|
||||||
|
def unmark_ugly_smalls(world, player):
|
||||||
|
for d in ['Eastern Hint Tile Blocked Path SE', 'Eastern Darkness S', 'Thieves Hallway SE', 'Mire Left Bridge S',
|
||||||
|
'TR Lava Escape SE', 'GT Hidden Spikes SE']:
|
||||||
|
door = world.get_door(d, player)
|
||||||
|
door.smallKey = False
|
||||||
|
|
||||||
|
|
||||||
def fix_big_key_doors_with_ugly_smalls(world, player):
|
def fix_big_key_doors_with_ugly_smalls(world, player):
|
||||||
remove_ugly_small_key_doors(world, player)
|
remove_ugly_small_key_doors(world, player)
|
||||||
unpair_big_key_doors(world, player)
|
unpair_big_key_doors(world, player)
|
||||||
@@ -360,7 +368,8 @@ def choose_portals(world, player):
|
|||||||
|
|
||||||
if world.doorShuffle[player] in ['basic', 'crossed']:
|
if world.doorShuffle[player] in ['basic', 'crossed']:
|
||||||
cross_flag = world.doorShuffle[player] == 'crossed'
|
cross_flag = world.doorShuffle[player] == 'crossed'
|
||||||
bk_shuffle = world.bigkeyshuffle[player]
|
# key drops allow the big key in the right place in Desert Tiles 2
|
||||||
|
bk_shuffle = world.bigkeyshuffle[player] or world.keydropshuffle[player]
|
||||||
std_flag = world.mode[player] == 'standard'
|
std_flag = world.mode[player] == 'standard'
|
||||||
# roast incognito doors
|
# roast incognito doors
|
||||||
world.get_room(0x60, player).delete(5)
|
world.get_room(0x60, player).delete(5)
|
||||||
@@ -407,6 +416,7 @@ def choose_portals(world, player):
|
|||||||
for dungeon, info in shuffled_info:
|
for dungeon, info in shuffled_info:
|
||||||
outstanding_portals = list(dungeon_portals[dungeon])
|
outstanding_portals = list(dungeon_portals[dungeon])
|
||||||
hc_flag = std_flag and dungeon == 'Hyrule Castle'
|
hc_flag = std_flag and dungeon == 'Hyrule Castle'
|
||||||
|
rupee_bow_flag = hc_flag and world.retro[player] # rupee bow
|
||||||
if hc_flag:
|
if hc_flag:
|
||||||
sanc = world.get_portal('Sanctuary', player)
|
sanc = world.get_portal('Sanctuary', player)
|
||||||
sanc.destination = True
|
sanc.destination = True
|
||||||
@@ -416,14 +426,14 @@ def choose_portals(world, player):
|
|||||||
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
|
info.required_passage = {x: y for x, y in info.required_passage.items() if len(y) > 0}
|
||||||
for target_region, possible_portals in info.required_passage.items():
|
for target_region, possible_portals in info.required_passage.items():
|
||||||
candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag,
|
candidates = find_portal_candidates(master_door_list, dungeon, need_passage=True, crossed=cross_flag,
|
||||||
bk_shuffle=bk_shuffle)
|
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
|
||||||
choice, portal = assign_portal(candidates, possible_portals, world, player)
|
choice, portal = assign_portal(candidates, possible_portals, world, player)
|
||||||
portal.destination = True
|
portal.destination = True
|
||||||
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
|
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
|
||||||
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
|
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
|
||||||
for i in range(0, dead_end_choices):
|
for i in range(0, dead_end_choices):
|
||||||
candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True,
|
candidates = find_portal_candidates(master_door_list, dungeon, dead_end_allowed=True,
|
||||||
crossed=cross_flag, bk_shuffle=bk_shuffle)
|
crossed=cross_flag, bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
|
||||||
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
|
possible_portals = outstanding_portals if not info.sole_entrance else [x for x in outstanding_portals if x != info.sole_entrance]
|
||||||
choice, portal = assign_portal(candidates, possible_portals, world, player)
|
choice, portal = assign_portal(candidates, possible_portals, world, player)
|
||||||
if choice.deadEnd:
|
if choice.deadEnd:
|
||||||
@@ -435,7 +445,7 @@ def choose_portals(world, player):
|
|||||||
the_rest = info.total - len(portal_assignment[dungeon])
|
the_rest = info.total - len(portal_assignment[dungeon])
|
||||||
for i in range(0, the_rest):
|
for i in range(0, the_rest):
|
||||||
candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag,
|
candidates = find_portal_candidates(master_door_list, dungeon, crossed=cross_flag,
|
||||||
bk_shuffle=bk_shuffle, standard=hc_flag)
|
bk_shuffle=bk_shuffle, standard=hc_flag, rupee_bow=rupee_bow_flag)
|
||||||
choice, portal = assign_portal(candidates, outstanding_portals, world, player)
|
choice, portal = assign_portal(candidates, outstanding_portals, world, player)
|
||||||
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
|
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
|
||||||
|
|
||||||
@@ -503,6 +513,9 @@ def analyze_portals(world, player):
|
|||||||
raise Exception('please inspect this case')
|
raise Exception('please inspect this case')
|
||||||
if len(reachable_portals) == 1:
|
if len(reachable_portals) == 1:
|
||||||
info.sole_entrance = reachable_portals[0]
|
info.sole_entrance = reachable_portals[0]
|
||||||
|
if world.intensity[player] < 2 and world.doorShuffle[player] == 'basic' and dungeon == 'Desert Palace':
|
||||||
|
if len(inaccessible_portals) == 1 and inaccessible_portals[0] == 'Desert Back':
|
||||||
|
info.required_passage.clear() # can't make a passage at this intensity level, something else must exit
|
||||||
info_map[dungeon] = info
|
info_map[dungeon] = info
|
||||||
|
|
||||||
for dungeon, info in info_map.items():
|
for dungeon, info in info_map.items():
|
||||||
@@ -553,7 +566,7 @@ def disconnect_portal(portal, world, player):
|
|||||||
|
|
||||||
|
|
||||||
def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False,
|
def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allowed=False, crossed=False,
|
||||||
bk_shuffle=False, standard=False):
|
bk_shuffle=False, standard=False, rupee_bow=False):
|
||||||
ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req]
|
ret = [x for x in door_list if bk_shuffle or not x.bk_shuffle_req]
|
||||||
if crossed:
|
if crossed:
|
||||||
ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')]
|
ret = [x for x in ret if not x.dungeonLink or x.dungeonLink == dungeon or x.dungeonLink.startswith('link')]
|
||||||
@@ -565,6 +578,8 @@ def find_portal_candidates(door_list, dungeon, need_passage=False, dead_end_allo
|
|||||||
ret = [x for x in ret if not x.deadEnd]
|
ret = [x for x in ret if not x.deadEnd]
|
||||||
if standard:
|
if standard:
|
||||||
ret = [x for x in ret if not x.standard_restricted]
|
ret = [x for x in ret if not x.standard_restricted]
|
||||||
|
if rupee_bow:
|
||||||
|
ret = [x for x in ret if not x.rupee_bow_restricted]
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -742,7 +757,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map,
|
|||||||
|
|
||||||
def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player):
|
def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player):
|
||||||
entrances_map, potentials, connections = connections_tuple
|
entrances_map, potentials, connections = connections_tuple
|
||||||
enabled_entrances = {}
|
enabled_entrances = world.enabled_entrances[player] = {}
|
||||||
sector_queue = deque(dungeon_builders.values())
|
sector_queue = deque(dungeon_builders.values())
|
||||||
last_key, loops = None, 0
|
last_key, loops = None, 0
|
||||||
logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon"))
|
logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon"))
|
||||||
@@ -1071,6 +1086,7 @@ def assign_cross_keys(dungeon_builders, world, player):
|
|||||||
# Step 3: Initial valid combination find - reduce flex if needed
|
# Step 3: Initial valid combination find - reduce flex if needed
|
||||||
for name, builder in dungeon_builders.items():
|
for name, builder in dungeon_builders.items():
|
||||||
suggested = builder.key_doors_num - builder.key_drop_cnt
|
suggested = builder.key_doors_num - builder.key_drop_cnt
|
||||||
|
builder.total_keys = builder.key_doors_num
|
||||||
find_valid_combination(builder, start_regions_map[name], world, player)
|
find_valid_combination(builder, start_regions_map[name], world, player)
|
||||||
actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt
|
actual_chest_keys = builder.key_doors_num - builder.key_drop_cnt
|
||||||
if actual_chest_keys < suggested:
|
if actual_chest_keys < suggested:
|
||||||
@@ -1087,6 +1103,7 @@ def assign_cross_keys(dungeon_builders, world, player):
|
|||||||
name = builder.name
|
name = builder.name
|
||||||
logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name)
|
logger.debug('Cross Dungeon: Increasing key count by 1 for %s', name)
|
||||||
builder.key_doors_num += 1
|
builder.key_doors_num += 1
|
||||||
|
builder.total_keys = builder.key_doors_num
|
||||||
result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False)
|
result = find_valid_combination(builder, start_regions_map[name], world, player, drop_keys=False)
|
||||||
if result:
|
if result:
|
||||||
remaining -= 1
|
remaining -= 1
|
||||||
@@ -1102,9 +1119,9 @@ def assign_cross_keys(dungeon_builders, world, player):
|
|||||||
logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining)
|
logger.debug('Cross Dungeon: Keys unable to assign in pool %s', remaining)
|
||||||
|
|
||||||
# Last Step: Adjust Small Key Dungeon Pool
|
# Last Step: Adjust Small Key Dungeon Pool
|
||||||
if not world.retro[player]:
|
for name, builder in dungeon_builders.items():
|
||||||
for name, builder in dungeon_builders.items():
|
reassign_key_doors(builder, world, player)
|
||||||
reassign_key_doors(builder, world, player)
|
if not world.retro[player]:
|
||||||
log_key_logic(builder.name, world.key_logic[player][builder.name])
|
log_key_logic(builder.name, world.key_logic[player][builder.name])
|
||||||
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
|
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
|
||||||
dungeon = world.get_dungeon(name, player)
|
dungeon = world.get_dungeon(name, player)
|
||||||
@@ -1345,10 +1362,8 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
|
|||||||
if recombine.master_sector is None:
|
if recombine.master_sector is None:
|
||||||
recombine.master_sector = builder.master_sector
|
recombine.master_sector = builder.master_sector
|
||||||
recombine.master_sector.name = recombine.name
|
recombine.master_sector.name = recombine.name
|
||||||
recombine.pre_open_stonewalls = builder.pre_open_stonewalls
|
|
||||||
else:
|
else:
|
||||||
recombine.master_sector.regions.extend(builder.master_sector.regions)
|
recombine.master_sector.regions.extend(builder.master_sector.regions)
|
||||||
recombine.pre_open_stonewalls.update(builder.pre_open_stonewalls)
|
|
||||||
recombine.layout_starts = list(entrances_map[recombine.name])
|
recombine.layout_starts = list(entrances_map[recombine.name])
|
||||||
dungeon_builders[recombine.name] = recombine
|
dungeon_builders[recombine.name] = recombine
|
||||||
|
|
||||||
@@ -1380,7 +1395,7 @@ def shuffle_key_doors(builder, world, player):
|
|||||||
skips.append(world.get_door(dp.door_a, player))
|
skips.append(world.get_door(dp.door_a, player))
|
||||||
break
|
break
|
||||||
num_key_doors += 1
|
num_key_doors += 1
|
||||||
builder.key_doors_num = num_key_doors
|
builder.key_doors_num = builder.total_keys = num_key_doors
|
||||||
find_small_key_door_candidates(builder, start_regions, world, player)
|
find_small_key_door_candidates(builder, start_regions, world, player)
|
||||||
find_valid_combination(builder, start_regions, world, player)
|
find_valid_combination(builder, start_regions, world, player)
|
||||||
reassign_key_doors(builder, world, player)
|
reassign_key_doors(builder, world, player)
|
||||||
@@ -1443,7 +1458,16 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
|
|||||||
random.shuffle(sample_list)
|
random.shuffle(sample_list)
|
||||||
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
|
proposal = kth_combination(sample_list[itr], builder.candidates, builder.key_doors_num)
|
||||||
|
|
||||||
|
# eliminate start region if portal marked as destination
|
||||||
|
excluded = {}
|
||||||
|
for region in start_regions:
|
||||||
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
||||||
|
if portal and portal.destination:
|
||||||
|
excluded[region] = None
|
||||||
|
start_regions = [x for x in start_regions if x not in excluded.keys()]
|
||||||
|
|
||||||
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
|
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
|
||||||
|
determine_prize_lock(key_layout, world, player)
|
||||||
while not validate_key_layout(key_layout, world, player):
|
while not validate_key_layout(key_layout, world, player):
|
||||||
itr += 1
|
itr += 1
|
||||||
stop_early = False
|
stop_early = False
|
||||||
@@ -1561,7 +1585,7 @@ def find_key_door_candidates(region, checked, world, player):
|
|||||||
if d2.type == DoorType.Normal:
|
if d2.type == DoorType.Normal:
|
||||||
room_b = world.get_room(d2.roomIndex, player)
|
room_b = world.get_room(d2.roomIndex, player)
|
||||||
pos_b, kind_b = room_b.doorList[d2.doorListPos]
|
pos_b, kind_b = room_b.doorList[d2.doorListPos]
|
||||||
valid = kind in okay_normals and kind_b in okay_normals
|
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||||
else:
|
else:
|
||||||
valid = kind in okay_normals
|
valid = kind in okay_normals
|
||||||
if valid and 0 <= d2.doorListPos < 4:
|
if valid and 0 <= d2.doorListPos < 4:
|
||||||
@@ -1578,26 +1602,10 @@ def find_key_door_candidates(region, checked, world, player):
|
|||||||
return candidates, checked_doors
|
return candidates, checked_doors
|
||||||
|
|
||||||
|
|
||||||
def kth_combination(k, l, r):
|
def valid_key_door_pair(door1, door2):
|
||||||
if r == 0:
|
if door1.roomIndex != door2.roomIndex:
|
||||||
return []
|
return True
|
||||||
elif len(l) == r:
|
return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1
|
||||||
return l
|
|
||||||
else:
|
|
||||||
i = ncr(len(l)-1, r-1)
|
|
||||||
if k < i:
|
|
||||||
return l[0:1] + kth_combination(k, l[1:], r-1)
|
|
||||||
else:
|
|
||||||
return kth_combination(k-i, l[1:], r)
|
|
||||||
|
|
||||||
|
|
||||||
def ncr(n, r):
|
|
||||||
if r == 0:
|
|
||||||
return 1
|
|
||||||
r = min(r, n-r)
|
|
||||||
numerator = reduce(op.mul, range(n, n-r, -1), 1)
|
|
||||||
denominator = reduce(op.mul, range(1, r+1), 1)
|
|
||||||
return numerator / denominator
|
|
||||||
|
|
||||||
|
|
||||||
def reassign_key_doors(builder, world, player):
|
def reassign_key_doors(builder, world, player):
|
||||||
@@ -1828,6 +1836,10 @@ def find_inaccessible_regions(world, player):
|
|||||||
if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'):
|
if connect.type is not RegionType.Dungeon or connect.name.endswith(' Portal'):
|
||||||
queue.append(connect)
|
queue.append(connect)
|
||||||
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
|
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
|
||||||
|
if world.mode[player] == 'inverted':
|
||||||
|
ledge = world.get_region('Hyrule Castle Ledge', player)
|
||||||
|
if any(x for x in ledge.exits if x.connected_region.name == 'Agahnims Tower Portal'):
|
||||||
|
world.inaccessible_regions[player].append('Hyrule Castle Ledge')
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
logger.debug('Inaccessible Regions:')
|
logger.debug('Inaccessible Regions:')
|
||||||
for r in world.inaccessible_regions[player]:
|
for r in world.inaccessible_regions[player]:
|
||||||
@@ -1912,14 +1924,18 @@ def check_required_paths(paths, world, player):
|
|||||||
if dungeon_name in world.dungeon_layouts[player].keys():
|
if dungeon_name in world.dungeon_layouts[player].keys():
|
||||||
builder = world.dungeon_layouts[player][dungeon_name]
|
builder = world.dungeon_layouts[player][dungeon_name]
|
||||||
if len(paths[dungeon_name]) > 0:
|
if len(paths[dungeon_name]) > 0:
|
||||||
states_to_explore = defaultdict(list)
|
states_to_explore = {}
|
||||||
for path in paths[dungeon_name]:
|
for path in paths[dungeon_name]:
|
||||||
if type(path) is tuple:
|
if type(path) is tuple:
|
||||||
states_to_explore[tuple([path[0]])] = path[1]
|
states_to_explore[tuple([path[0]])] = (path[1], 'any')
|
||||||
else:
|
else:
|
||||||
states_to_explore[tuple(builder.path_entrances)].append(path)
|
common_starts = tuple(builder.path_entrances)
|
||||||
|
if common_starts not in states_to_explore:
|
||||||
|
states_to_explore[common_starts] = ([], 'all')
|
||||||
|
states_to_explore[common_starts][0].append(path)
|
||||||
cached_initial_state = None
|
cached_initial_state = None
|
||||||
for start_regs, dest_regs in states_to_explore.items():
|
for start_regs, info in states_to_explore.items():
|
||||||
|
dest_regs, path_type = info
|
||||||
if type(dest_regs) is not list:
|
if type(dest_regs) is not list:
|
||||||
dest_regs = [dest_regs]
|
dest_regs = [dest_regs]
|
||||||
check_paths = convert_regions(dest_regs, world, player)
|
check_paths = convert_regions(dest_regs, world, player)
|
||||||
@@ -1936,11 +1952,17 @@ def check_required_paths(paths, world, player):
|
|||||||
cached_initial_state = state
|
cached_initial_state = state
|
||||||
else:
|
else:
|
||||||
state = cached_initial_state
|
state = cached_initial_state
|
||||||
valid, bad_region = check_if_regions_visited(state, check_paths)
|
if path_type == 'any':
|
||||||
|
valid, bad_region = check_if_any_regions_visited(state, check_paths)
|
||||||
|
else:
|
||||||
|
valid, bad_region = check_if_all_regions_visited(state, check_paths)
|
||||||
if not valid:
|
if not valid:
|
||||||
if check_for_pinball_fix(state, bad_region, world, player):
|
if check_for_pinball_fix(state, bad_region, world, player):
|
||||||
explore_state(state, world, player)
|
explore_state(state, world, player)
|
||||||
valid, bad_region = check_if_regions_visited(state, check_paths)
|
if path_type == 'any':
|
||||||
|
valid, bad_region = check_if_any_regions_visited(state, check_paths)
|
||||||
|
else:
|
||||||
|
valid, bad_region = check_if_all_regions_visited(state, check_paths)
|
||||||
if not valid:
|
if not valid:
|
||||||
raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name))
|
raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name))
|
||||||
|
|
||||||
@@ -1980,7 +2002,7 @@ def explore_state_not_inaccessible(state, world, player):
|
|||||||
state.add_all_doors_check_unattached(connect_region, world, player)
|
state.add_all_doors_check_unattached(connect_region, world, player)
|
||||||
|
|
||||||
|
|
||||||
def check_if_regions_visited(state, check_paths):
|
def check_if_any_regions_visited(state, check_paths):
|
||||||
valid = False
|
valid = False
|
||||||
breaking_region = None
|
breaking_region = None
|
||||||
for region_target in check_paths:
|
for region_target in check_paths:
|
||||||
@@ -1992,6 +2014,13 @@ def check_if_regions_visited(state, check_paths):
|
|||||||
return valid, breaking_region
|
return valid, breaking_region
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_all_regions_visited(state, check_paths):
|
||||||
|
for region_target in check_paths:
|
||||||
|
if not state.visited_at_all(region_target):
|
||||||
|
return False, region_target
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
def check_for_pinball_fix(state, bad_region, world, player):
|
def check_for_pinball_fix(state, bad_region, world, player):
|
||||||
pinball_region = world.get_region('Skull Pinball', player)
|
pinball_region = world.get_region('Skull Pinball', player)
|
||||||
# todo: lobby shuffle
|
# todo: lobby shuffle
|
||||||
@@ -2015,10 +2044,10 @@ class DROptions(Flag):
|
|||||||
Town_Portal = 0x02 # If on, Players will start with mirror scroll
|
Town_Portal = 0x02 # If on, Players will start with mirror scroll
|
||||||
Map_Info = 0x04
|
Map_Info = 0x04
|
||||||
Debug = 0x08
|
Debug = 0x08
|
||||||
Rails = 0x10 # If on, draws rails
|
Fix_EG = 0x10 # used to be Rails = 0x10 # Unused bit now
|
||||||
OriginalPalettes = 0x20
|
OriginalPalettes = 0x20
|
||||||
Open_PoD_Wall = 0x40 # If on, pre opens the PoD wall, no bow required
|
# Open_PoD_Wall = 0x40 # No longer pre-opening pod wall - unused
|
||||||
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required
|
# Open_Desert_Wall = 0x80 # No longer pre-opening desert wall - unused
|
||||||
Hide_Total = 0x100
|
Hide_Total = 0x100
|
||||||
DarkWorld_Spawns = 0x200
|
DarkWorld_Spawns = 0x200
|
||||||
|
|
||||||
|
|||||||
@@ -1490,8 +1490,11 @@ def create_doors(world, player):
|
|||||||
world.get_door('GT Petting Zoo SE', player).dead_end()
|
world.get_door('GT Petting Zoo SE', player).dead_end()
|
||||||
world.get_door('GT DMs Room SW', player).dead_end()
|
world.get_door('GT DMs Room SW', player).dead_end()
|
||||||
world.get_door("GT Bob\'s Room SE", player).passage = False
|
world.get_door("GT Bob\'s Room SE", player).passage = False
|
||||||
world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note (todo)
|
world.get_door('Desert Tiles 2 SE', player).bk_shuffle_req = True # key-drop note: allows this to be a portal
|
||||||
world.get_door('Swamp Lobby S', player).standard_restricted = True # key-drop note (todo)
|
world.get_door('Swamp Lobby S', player).standard_restricted = True
|
||||||
|
world.get_door('PoD Mimics 2 SW', player).rupee_bow_restricted = True # bow statue
|
||||||
|
# enemizer logic could get rid of the following restriction
|
||||||
|
world.get_door('PoD Pit Room S', player).rupee_bow_restricted = True # so mimics 1 shouldn't be required
|
||||||
|
|
||||||
# can't unlink from boss right now
|
# can't unlink from boss right now
|
||||||
world.get_door('Hera Lobby S', player).dungeonLink = 'Tower of Hera'
|
world.get_door('Hera Lobby S', player).dungeonLink = 'Tower of Hera'
|
||||||
|
|||||||
+108
-69
@@ -39,14 +39,15 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
|||||||
proposed_map = {}
|
proposed_map = {}
|
||||||
doors_to_connect = {}
|
doors_to_connect = {}
|
||||||
all_regions = set()
|
all_regions = set()
|
||||||
bk_needed = False
|
|
||||||
bk_special = False
|
bk_special = False
|
||||||
for sector in builder.sectors:
|
for sector in builder.sectors:
|
||||||
for door in sector.outstanding_doors:
|
for door in sector.outstanding_doors:
|
||||||
doors_to_connect[door.name] = door
|
doors_to_connect[door.name] = door
|
||||||
all_regions.update(sector.regions)
|
all_regions.update(sector.regions)
|
||||||
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
bk_special |= check_for_special(sector.regions)
|
||||||
bk_special = bk_special or check_for_special(sector)
|
bk_needed = False
|
||||||
|
for sector in builder.sectors:
|
||||||
|
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
||||||
paths = determine_paths_for_dungeon(world, player, all_regions, builder.name)
|
paths = determine_paths_for_dungeon(world, player, all_regions, builder.name)
|
||||||
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions,
|
dungeon, hangers, hooks = gen_dungeon_info(builder.name, builder.sectors, entrance_regions, all_regions,
|
||||||
proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
|
proposed_map, doors_to_connect, bk_needed, bk_special, world, player)
|
||||||
@@ -55,24 +56,6 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
|||||||
|
|
||||||
|
|
||||||
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
|
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
|
||||||
stonewalls = check_for_stonewalls(builder)
|
|
||||||
sector = generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player)
|
|
||||||
for stonewall in stonewalls:
|
|
||||||
if not stonewall_valid(stonewall):
|
|
||||||
builder.pre_open_stonewalls.add(stonewall)
|
|
||||||
return sector
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_stonewalls(builder):
|
|
||||||
stonewalls = set()
|
|
||||||
for sector in builder.sectors:
|
|
||||||
for door in sector.outstanding_doors:
|
|
||||||
if door.stonewall:
|
|
||||||
stonewalls.add(door)
|
|
||||||
return stonewalls
|
|
||||||
|
|
||||||
|
|
||||||
def generate_dungeon_main(builder, entrance_region_names, split_dungeon, world, player):
|
|
||||||
if builder.valid_proposal: # we made this earlier in gen, just use it
|
if builder.valid_proposal: # we made this earlier in gen, just use it
|
||||||
proposed_map = builder.valid_proposal
|
proposed_map = builder.valid_proposal
|
||||||
else:
|
else:
|
||||||
@@ -101,19 +84,37 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
|||||||
excluded = {}
|
excluded = {}
|
||||||
for region in entrance_regions:
|
for region in entrance_regions:
|
||||||
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
||||||
if portal and portal.destination:
|
if portal:
|
||||||
excluded[region] = None
|
if portal.destination:
|
||||||
|
excluded[region] = None
|
||||||
|
elif len(entrance_regions) > 1:
|
||||||
|
p_region = portal.door.entrance.connected_region
|
||||||
|
access_region = next(x.parent_region for x in p_region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||||
|
if (access_region.name in world.inaccessible_regions[player] and
|
||||||
|
region.name not in world.enabled_entrances[player]):
|
||||||
|
excluded[region] = None
|
||||||
|
elif len(region.entrances) == 1: # for holes
|
||||||
|
access_region = next(x.parent_region for x in region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||||
|
or x.parent_region.name == 'Sewer Drop')
|
||||||
|
if access_region.name == 'Sewer Drop':
|
||||||
|
access_region = next(x.parent_region for x in access_region.entrances)
|
||||||
|
if (access_region.name in world.inaccessible_regions[player] and
|
||||||
|
region.name not in world.enabled_entrances[player]):
|
||||||
|
excluded[region] = None
|
||||||
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
||||||
doors_to_connect = {}
|
doors_to_connect = {}
|
||||||
all_regions = set()
|
all_regions = set()
|
||||||
bk_needed = False
|
|
||||||
bk_special = False
|
bk_special = False
|
||||||
for sector in builder.sectors:
|
for sector in builder.sectors:
|
||||||
for door in sector.outstanding_doors:
|
for door in sector.outstanding_doors:
|
||||||
doors_to_connect[door.name] = door
|
doors_to_connect[door.name] = door
|
||||||
all_regions.update(sector.regions)
|
all_regions.update(sector.regions)
|
||||||
bk_needed = bk_needed or determine_if_bk_needed(sector, split_dungeon, world, player)
|
bk_special |= check_for_special(sector.regions)
|
||||||
bk_special = bk_special or check_for_special(sector)
|
bk_needed = False
|
||||||
|
for sector in builder.sectors:
|
||||||
|
bk_needed |= determine_if_bk_needed(sector, split_dungeon, bk_special, world, player)
|
||||||
proposed_map = {}
|
proposed_map = {}
|
||||||
choices_master = [[]]
|
choices_master = [[]]
|
||||||
depth = 0
|
depth = 0
|
||||||
@@ -187,8 +188,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
|||||||
return proposed_map
|
return proposed_map
|
||||||
|
|
||||||
|
|
||||||
def determine_if_bk_needed(sector, split_dungeon, world, player):
|
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
|
||||||
if not split_dungeon:
|
if not split_dungeon or bk_special:
|
||||||
for region in sector.regions:
|
for region in sector.regions:
|
||||||
for ext in region.exits:
|
for ext in region.exits:
|
||||||
door = world.check_for_door(ext.name, player)
|
door = world.check_for_door(ext.name, player)
|
||||||
@@ -197,8 +198,8 @@ def determine_if_bk_needed(sector, split_dungeon, world, player):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_for_special(sector):
|
def check_for_special(regions):
|
||||||
for region in sector.regions:
|
for region in regions:
|
||||||
for loc in region.locations:
|
for loc in region.locations:
|
||||||
if loc.forced_big_key():
|
if loc.forced_big_key():
|
||||||
return True
|
return True
|
||||||
@@ -417,6 +418,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a
|
|||||||
# origin has no more hooks, but not all doors have been proposed
|
# origin has no more hooks, but not all doors have been proposed
|
||||||
if not world.bigkeyshuffle[player]:
|
if not world.bigkeyshuffle[player]:
|
||||||
possible_bks = len(dungeon['Origin'].possible_bk_locations)
|
possible_bks = len(dungeon['Origin'].possible_bk_locations)
|
||||||
|
if bk_special and check_for_special(dungeon['Origin'].visited_regions):
|
||||||
|
possible_bks = 1
|
||||||
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
|
true_origin_hooks = [x for x in dungeon['Origin'].hooks.keys() if not x.bigKey or possible_bks > 0 or not bk_needed]
|
||||||
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
|
if len(true_origin_hooks) == 0 and len(proposed_map.keys()) < len(doors_to_connect):
|
||||||
return False
|
return False
|
||||||
@@ -450,7 +453,8 @@ def check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, a
|
|||||||
bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special)
|
bk_possible = not bk_needed or (world.bigkeyshuffle[player] and not bk_special)
|
||||||
for piece in dungeon.values():
|
for piece in dungeon.values():
|
||||||
all_visited.update(piece.visited_regions)
|
all_visited.update(piece.visited_regions)
|
||||||
if not bk_possible and len(piece.possible_bk_locations) > 0:
|
if ((not bk_possible and len(piece.possible_bk_locations) > 0) or
|
||||||
|
(bk_special and check_for_special(piece.visited_regions))):
|
||||||
bk_possible = True
|
bk_possible = True
|
||||||
if len(all_regions.difference(all_visited)) > 0:
|
if len(all_regions.difference(all_visited)) > 0:
|
||||||
return False
|
return False
|
||||||
@@ -572,7 +576,8 @@ def determine_paths_for_dungeon(world, player, all_regions, name):
|
|||||||
paths.append(boss)
|
paths.append(boss)
|
||||||
if 'Thieves Boss' in all_r_names:
|
if 'Thieves Boss' in all_r_names:
|
||||||
paths.append('Thieves Boss')
|
paths.append('Thieves Boss')
|
||||||
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
|
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
||||||
|
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
|
||||||
for drop_check in drop_path_checks:
|
for drop_check in drop_path_checks:
|
||||||
if drop_check in all_r_names:
|
if drop_check in all_r_names:
|
||||||
paths.append((drop_check, non_hole_portals))
|
paths.append((drop_check, non_hole_portals))
|
||||||
@@ -598,35 +603,6 @@ def winnow_hangers(hangers, hooks):
|
|||||||
hangers[hanger].remove(door)
|
hangers[hanger].remove(door)
|
||||||
|
|
||||||
|
|
||||||
def stonewall_valid(stonewall):
|
|
||||||
bad_door = stonewall.dest
|
|
||||||
if bad_door.blocked:
|
|
||||||
return True # great we're done with this one
|
|
||||||
loop_region = stonewall.entrance.parent_region
|
|
||||||
start_regions = [bad_door.entrance.parent_region]
|
|
||||||
if bad_door.dependents:
|
|
||||||
for dep in bad_door.dependents:
|
|
||||||
start_regions.append(dep.entrance.parent_region)
|
|
||||||
queue = deque(start_regions)
|
|
||||||
visited = set(start_regions)
|
|
||||||
while len(queue) > 0:
|
|
||||||
region = queue.popleft()
|
|
||||||
if region == loop_region:
|
|
||||||
return False # guaranteed loop
|
|
||||||
possible_entrances = list(region.entrances)
|
|
||||||
for entrance in possible_entrances:
|
|
||||||
parent = entrance.parent_region
|
|
||||||
if parent.type != RegionType.Dungeon:
|
|
||||||
return False # you can get stuck from an entrance
|
|
||||||
else:
|
|
||||||
door = entrance.door
|
|
||||||
if (door is None or (door != stonewall and not door.blocked)) and parent not in visited:
|
|
||||||
visited.add(parent)
|
|
||||||
queue.append(parent)
|
|
||||||
# we didn't find anything bad
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception):
|
def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception):
|
||||||
# todo: info about dungeon events - not sure about that
|
# todo: info about dungeon events - not sure about that
|
||||||
graph_piece = GraphPiece()
|
graph_piece = GraphPiece()
|
||||||
@@ -807,6 +783,10 @@ class ExplorationState(object):
|
|||||||
self.dungeon = dungeon
|
self.dungeon = dungeon
|
||||||
self.pinball_used = False
|
self.pinball_used = False
|
||||||
|
|
||||||
|
self.prize_door_set = {}
|
||||||
|
self.prize_doors = []
|
||||||
|
self.prize_doors_opened = False
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
ret = ExplorationState(dungeon=self.dungeon)
|
ret = ExplorationState(dungeon=self.dungeon)
|
||||||
ret.unattached_doors = list(self.unattached_doors)
|
ret.unattached_doors = list(self.unattached_doors)
|
||||||
@@ -833,6 +813,10 @@ class ExplorationState(object):
|
|||||||
ret.non_door_entrances = list(self.non_door_entrances)
|
ret.non_door_entrances = list(self.non_door_entrances)
|
||||||
ret.dungeon = self.dungeon
|
ret.dungeon = self.dungeon
|
||||||
ret.pinball_used = self.pinball_used
|
ret.pinball_used = self.pinball_used
|
||||||
|
|
||||||
|
ret.prize_door_set = dict(self.prize_door_set)
|
||||||
|
ret.prize_doors = list(self.prize_doors)
|
||||||
|
ret.prize_doors_opened = self.prize_doors_opened
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def next_avail_door(self):
|
def next_avail_door(self):
|
||||||
@@ -842,6 +826,8 @@ class ExplorationState(object):
|
|||||||
return exp_door
|
return exp_door
|
||||||
|
|
||||||
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False):
|
def visit_region(self, region, key_region=None, key_checks=False, bk_Flag=False):
|
||||||
|
if region.type != RegionType.Dungeon:
|
||||||
|
self.crystal = CrystalBarrier.Orange
|
||||||
if self.crystal == CrystalBarrier.Either:
|
if self.crystal == CrystalBarrier.Either:
|
||||||
if region not in self.visited_blue:
|
if region not in self.visited_blue:
|
||||||
self.visited_blue.append(region)
|
self.visited_blue.append(region)
|
||||||
@@ -868,6 +854,8 @@ class ExplorationState(object):
|
|||||||
if location.name in flooded_keys_reverse.keys() and self.location_found(
|
if location.name in flooded_keys_reverse.keys() and self.location_found(
|
||||||
flooded_keys_reverse[location.name]):
|
flooded_keys_reverse[location.name]):
|
||||||
self.perform_event(flooded_keys_reverse[location.name], key_region)
|
self.perform_event(flooded_keys_reverse[location.name], key_region)
|
||||||
|
if '- Prize' in location.name:
|
||||||
|
self.prize_received = True
|
||||||
|
|
||||||
def flooded_key_check(self, location):
|
def flooded_key_check(self, location):
|
||||||
if location.name not in flooded_keys.keys():
|
if location.name not in flooded_keys.keys():
|
||||||
@@ -1103,9 +1091,9 @@ def valid_region_to_explore_in_regions(region, all_regions, world, player):
|
|||||||
def valid_region_to_explore(region, name, world, player):
|
def valid_region_to_explore(region, name, world, player):
|
||||||
if region is None:
|
if region is None:
|
||||||
return False
|
return False
|
||||||
return (region.type == RegionType.Dungeon and region.dungeon.name in name)\
|
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
|
||||||
or region.name in world.inaccessible_regions[player]\
|
or region.name in world.inaccessible_regions[player]
|
||||||
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard')
|
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||||
|
|
||||||
|
|
||||||
def get_doors(world, region, player):
|
def get_doors(world, region, player):
|
||||||
@@ -1172,9 +1160,8 @@ class DungeonBuilder(object):
|
|||||||
self.path_entrances = None # used for pathing/key doors, I think
|
self.path_entrances = None # used for pathing/key doors, I think
|
||||||
self.split_flag = False
|
self.split_flag = False
|
||||||
|
|
||||||
self.pre_open_stonewalls = set() # used by stonewall system
|
|
||||||
|
|
||||||
self.candidates = None
|
self.candidates = None
|
||||||
|
self.total_keys = None
|
||||||
self.key_doors_num = None
|
self.key_doors_num = None
|
||||||
self.combo_size = None
|
self.combo_size = None
|
||||||
self.flex = 0
|
self.flex = 0
|
||||||
@@ -1249,6 +1236,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
|||||||
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
||||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
|
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
|
||||||
candidate_sectors, global_pole)
|
candidate_sectors, global_pole)
|
||||||
|
if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
||||||
|
assign_sector(find_sector("Thieves Blind's Cell", candidate_sectors), current_dungeon,
|
||||||
|
candidate_sectors, global_pole)
|
||||||
entrances_map, potentials, connections = connections_tuple
|
entrances_map, potentials, connections = connections_tuple
|
||||||
accessible_sectors, reverse_d_map = set(), {}
|
accessible_sectors, reverse_d_map = set(), {}
|
||||||
for key in dungeon_entrances.keys():
|
for key in dungeon_entrances.keys():
|
||||||
@@ -1290,13 +1280,16 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
|||||||
sanc_builder = random.choice(lw_builders)
|
sanc_builder = random.choice(lw_builders)
|
||||||
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
||||||
|
|
||||||
|
bow_sectors, retro_std_flag = {}, world.retro[player] and world.mode[player] == 'standard'
|
||||||
free_location_sectors = {}
|
free_location_sectors = {}
|
||||||
crystal_switches = {}
|
crystal_switches = {}
|
||||||
crystal_barriers = {}
|
crystal_barriers = {}
|
||||||
polarized_sectors = {}
|
polarized_sectors = {}
|
||||||
neutral_sectors = {}
|
neutral_sectors = {}
|
||||||
for sector in candidate_sectors:
|
for sector in candidate_sectors:
|
||||||
if sector.chest_locations > 0:
|
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
|
||||||
|
bow_sectors[sector] = None
|
||||||
|
elif sector.chest_locations > 0:
|
||||||
free_location_sectors[sector] = None
|
free_location_sectors[sector] = None
|
||||||
elif sector.c_switch:
|
elif sector.c_switch:
|
||||||
crystal_switches[sector] = None
|
crystal_switches[sector] = None
|
||||||
@@ -1306,6 +1299,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
|||||||
neutral_sectors[sector] = None
|
neutral_sectors[sector] = None
|
||||||
else:
|
else:
|
||||||
polarized_sectors[sector] = None
|
polarized_sectors[sector] = None
|
||||||
|
if bow_sectors:
|
||||||
|
assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
|
||||||
assign_location_sectors(dungeon_map, free_location_sectors, global_pole)
|
assign_location_sectors(dungeon_map, free_location_sectors, global_pole)
|
||||||
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
|
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole)
|
||||||
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
ensure_crystal_switches_reachable(dungeon_map, leftover, polarized_sectors, crystal_barriers, global_pole)
|
||||||
@@ -1471,6 +1466,9 @@ def define_sector_features(sectors):
|
|||||||
sector.blue_barrier = True
|
sector.blue_barrier = True
|
||||||
if door.bigKey:
|
if door.bigKey:
|
||||||
sector.bk_required = True
|
sector.bk_required = True
|
||||||
|
if region.name in ['PoD Mimics 2', 'PoD Bow Statue Right', 'PoD Mimics 1', 'GT Mimics 1', 'GT Mimics 2',
|
||||||
|
'Eastern Single Eyegore', 'Eastern Duo Eyegores']:
|
||||||
|
sector.item_logic.add('Bow')
|
||||||
|
|
||||||
|
|
||||||
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
||||||
@@ -1521,6 +1519,19 @@ def find_sector(r_name, sectors):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
|
||||||
|
sector_list = list(bow_sectors)
|
||||||
|
random.shuffle(sector_list)
|
||||||
|
population = []
|
||||||
|
for name in dungeon_map:
|
||||||
|
if name != 'Hyrule Castle':
|
||||||
|
population.append(name)
|
||||||
|
choices = random.choices(population, k=len(sector_list))
|
||||||
|
for i, choice in enumerate(choices):
|
||||||
|
builder = dungeon_map[choice]
|
||||||
|
assign_sector(sector_list[i], builder, bow_sectors, global_pole)
|
||||||
|
|
||||||
|
|
||||||
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
|
def assign_location_sectors(dungeon_map, free_location_sectors, global_pole):
|
||||||
valid = False
|
valid = False
|
||||||
choices = None
|
choices = None
|
||||||
@@ -3274,6 +3285,7 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
|||||||
possible_regions.add(portal.door.entrance.parent_region.name)
|
possible_regions.add(portal.door.entrance.parent_region.name)
|
||||||
if builder.name in dungeon_drops.keys():
|
if builder.name in dungeon_drops.keys():
|
||||||
possible_regions.update(dungeon_drops[builder.name])
|
possible_regions.update(dungeon_drops[builder.name])
|
||||||
|
independents = find_independent_entrances(possible_regions, world, player)
|
||||||
for name, split_build in builder.split_dungeon_map.items():
|
for name, split_build in builder.split_dungeon_map.items():
|
||||||
name_bits = name.split(" ")
|
name_bits = name.split(" ")
|
||||||
orig_name = " ".join(name_bits[:-1])
|
orig_name = " ".join(name_bits[:-1])
|
||||||
@@ -3286,7 +3298,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
|||||||
if r_name not in entrance_regions:
|
if r_name not in entrance_regions:
|
||||||
entrance_regions.append(r_name)
|
entrance_regions.append(r_name)
|
||||||
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid]
|
# entrance_regions = [x for x in entrance_regions if x not in split_check_entrance_invalid]
|
||||||
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, True, world, player)
|
split = any(x for x in independents if x not in entrance_regions)
|
||||||
|
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
|
||||||
# record split proposals
|
# record split proposals
|
||||||
builder.valid_proposal[name] = proposal
|
builder.valid_proposal[name] = proposal
|
||||||
builder.exception_list = list(sector_list)
|
builder.exception_list = list(sector_list)
|
||||||
@@ -3301,6 +3314,32 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
|||||||
return len(unreached_doors) == 0, unreached_doors
|
return len(unreached_doors) == 0, unreached_doors
|
||||||
|
|
||||||
|
|
||||||
|
def find_independent_entrances(entrance_regions, world, player):
|
||||||
|
independents = set()
|
||||||
|
for region in entrance_regions:
|
||||||
|
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == region), None)
|
||||||
|
if portal:
|
||||||
|
if portal.destination:
|
||||||
|
continue
|
||||||
|
elif len(entrance_regions) > 1:
|
||||||
|
p_region = portal.door.entrance.connected_region
|
||||||
|
access_region = next(x.parent_region for x in p_region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||||
|
if access_region.name in world.inaccessible_regions[player]:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
r = world.get_region(region, player)
|
||||||
|
access_region = next(x.parent_region for x in r.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||||
|
or x.parent_region.name == 'Sewer Drop')
|
||||||
|
if access_region.name == 'Sewer Drop':
|
||||||
|
access_region = next(x.parent_region for x in access_region.entrances)
|
||||||
|
if access_region.name in world.inaccessible_regions[player]:
|
||||||
|
continue
|
||||||
|
independents.add(region)
|
||||||
|
return independents
|
||||||
|
|
||||||
|
|
||||||
def resolve_equations(builder, sector_list):
|
def resolve_equations(builder, sector_list):
|
||||||
unreached_doors = defaultdict(list)
|
unreached_doors = defaultdict(list)
|
||||||
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}
|
equations = {x: y for x, y in copy_door_equations(builder, sector_list).items() if len(y) > 0}
|
||||||
@@ -3823,7 +3862,7 @@ dungeon_boss_sectors = {
|
|||||||
'Palace of Darkness': ['PoD Boss'],
|
'Palace of Darkness': ['PoD Boss'],
|
||||||
'Swamp Palace': ['Swamp Boss'],
|
'Swamp Palace': ['Swamp Boss'],
|
||||||
'Skull Woods': ['Skull Boss'],
|
'Skull Woods': ['Skull Boss'],
|
||||||
'Thieves Town': ['Thieves Blind\'s Cell', 'Thieves Boss'],
|
'Thieves Town': ['Thieves Boss'],
|
||||||
'Ice Palace': ['Ice Boss'],
|
'Ice Palace': ['Ice Boss'],
|
||||||
'Misery Mire': ['Mire Boss'],
|
'Misery Mire': ['Mire Boss'],
|
||||||
'Turtle Rock': ['TR Boss'],
|
'Turtle Rock': ['TR Boss'],
|
||||||
|
|||||||
+33
@@ -375,6 +375,38 @@ flexible_starts = {
|
|||||||
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
|
'Skull Woods': ['Skull Left Drop', 'Skull Pinball']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DungeonInfo:
|
||||||
|
|
||||||
|
def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize=None):
|
||||||
|
# todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name):
|
||||||
|
self.free_items = free
|
||||||
|
self.key_num = keys
|
||||||
|
self.bk_present = bk
|
||||||
|
self.map_present = map
|
||||||
|
self.compass_present = compass
|
||||||
|
self.bk_drops = bk_drop
|
||||||
|
self.key_drops = drops
|
||||||
|
self.prize = prize
|
||||||
|
|
||||||
|
|
||||||
|
dungeon_table = {
|
||||||
|
'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None),
|
||||||
|
'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize'),
|
||||||
|
'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize'),
|
||||||
|
'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize'),
|
||||||
|
'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None),
|
||||||
|
'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize'),
|
||||||
|
'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize'),
|
||||||
|
'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize'),
|
||||||
|
'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize"),
|
||||||
|
'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize'),
|
||||||
|
'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize'),
|
||||||
|
'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize'),
|
||||||
|
'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dungeon_keys = {
|
dungeon_keys = {
|
||||||
'Hyrule Castle': 'Small Key (Escape)',
|
'Hyrule Castle': 'Small Key (Escape)',
|
||||||
'Eastern Palace': 'Small Key (Eastern Palace)',
|
'Eastern Palace': 'Small Key (Eastern Palace)',
|
||||||
@@ -407,6 +439,7 @@ dungeon_bigs = {
|
|||||||
'Ganons Tower': 'Big Key (Ganons Tower)'
|
'Ganons Tower': 'Big Key (Ganons Tower)'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dungeon_hints = {
|
dungeon_hints = {
|
||||||
'Hyrule Castle': 'in Hyrule Castle',
|
'Hyrule Castle': 'in Hyrule Castle',
|
||||||
'Eastern Palace': 'in Eastern Palace',
|
'Eastern Palace': 'in Eastern Palace',
|
||||||
|
|||||||
+71
-17
@@ -109,6 +109,7 @@ def link_entrances(world, player):
|
|||||||
links_house_doors = [i for i in LW_Single_Cave_Doors if i not in Isolated_LH_Doors_Open]
|
links_house_doors = [i for i in LW_Single_Cave_Doors if i not in Isolated_LH_Doors_Open]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Links House Exit', player)
|
connect_two_way(world, links_house, 'Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in bomb_shop_doors:
|
if links_house in bomb_shop_doors:
|
||||||
bomb_shop_doors.remove(links_house)
|
bomb_shop_doors.remove(links_house)
|
||||||
if links_house in blacksmith_doors:
|
if links_house in blacksmith_doors:
|
||||||
@@ -173,6 +174,7 @@ def link_entrances(world, player):
|
|||||||
links_house_doors = [i for i in lw_entrances if i not in Isolated_LH_Doors_Open]
|
links_house_doors = [i for i in lw_entrances if i not in Isolated_LH_Doors_Open]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Links House Exit', player)
|
connect_two_way(world, links_house, 'Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in lw_entrances:
|
if links_house in lw_entrances:
|
||||||
lw_entrances.remove(links_house)
|
lw_entrances.remove(links_house)
|
||||||
|
|
||||||
@@ -215,6 +217,13 @@ def link_entrances(world, player):
|
|||||||
if bomb_shop in dw_entrances:
|
if bomb_shop in dw_entrances:
|
||||||
dw_entrances.remove(bomb_shop)
|
dw_entrances.remove(bomb_shop)
|
||||||
|
|
||||||
|
# standard mode cannot have Bonk Fairy Light be a connector in case of starting boots
|
||||||
|
# or boots are in links house, etc.
|
||||||
|
removed = False
|
||||||
|
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in lw_entrances:
|
||||||
|
lw_entrances.remove('Bonk Fairy (Light)')
|
||||||
|
removed = True
|
||||||
|
|
||||||
# place the old man cave's entrance somewhere in the light world
|
# place the old man cave's entrance somewhere in the light world
|
||||||
random.shuffle(lw_entrances)
|
random.shuffle(lw_entrances)
|
||||||
old_man_entrance = lw_entrances.pop()
|
old_man_entrance = lw_entrances.pop()
|
||||||
@@ -226,6 +235,8 @@ def link_entrances(world, player):
|
|||||||
|
|
||||||
# now scramble the rest
|
# now scramble the rest
|
||||||
connect_caves(world, lw_entrances, dw_entrances, caves, player)
|
connect_caves(world, lw_entrances, dw_entrances, caves, player)
|
||||||
|
if removed:
|
||||||
|
lw_entrances.append('Bonk Fairy (Light)')
|
||||||
|
|
||||||
# scramble holes
|
# scramble holes
|
||||||
scramble_holes(world, player)
|
scramble_holes(world, player)
|
||||||
@@ -328,6 +339,7 @@ def link_entrances(world, player):
|
|||||||
links_house_doors = [i for i in lw_entrances + lw_must_exits if i not in Isolated_LH_Doors_Open]
|
links_house_doors = [i for i in lw_entrances + lw_must_exits if i not in Isolated_LH_Doors_Open]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Links House Exit', player)
|
connect_two_way(world, links_house, 'Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in lw_entrances:
|
if links_house in lw_entrances:
|
||||||
lw_entrances.remove(links_house)
|
lw_entrances.remove(links_house)
|
||||||
if links_house in lw_must_exits:
|
if links_house in lw_must_exits:
|
||||||
@@ -395,14 +407,23 @@ def link_entrances(world, player):
|
|||||||
if bomb_shop in dw_entrances:
|
if bomb_shop in dw_entrances:
|
||||||
dw_entrances.remove(bomb_shop)
|
dw_entrances.remove(bomb_shop)
|
||||||
|
|
||||||
|
# standard mode cannot have Bonk Fairy Light be a connector in case of
|
||||||
|
# starting boots or boots are in links house, etc.
|
||||||
|
removed = False
|
||||||
|
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in lw_entrances:
|
||||||
|
lw_entrances.remove('Bonk Fairy (Light)')
|
||||||
|
removed = True
|
||||||
|
|
||||||
# place the old man cave's entrance somewhere in the light world
|
# place the old man cave's entrance somewhere in the light world
|
||||||
old_man_entrance = lw_entrances.pop()
|
old_man_entrance = lw_entrances.pop()
|
||||||
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
|
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
|
||||||
|
|
||||||
|
|
||||||
# now scramble the rest
|
# now scramble the rest
|
||||||
connect_caves(world, lw_entrances, dw_entrances, caves, player)
|
connect_caves(world, lw_entrances, dw_entrances, caves, player)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
lw_entrances.append('Bonk Fairy (Light)')
|
||||||
|
|
||||||
# scramble holes
|
# scramble holes
|
||||||
scramble_holes(world, player)
|
scramble_holes(world, player)
|
||||||
|
|
||||||
@@ -449,6 +470,7 @@ def link_entrances(world, player):
|
|||||||
links_house_doors = [i for i in links_house_doors if i not in exclusions]
|
links_house_doors = [i for i in links_house_doors if i not in exclusions]
|
||||||
links_house = random.choice(list(links_house_doors))
|
links_house = random.choice(list(links_house_doors))
|
||||||
connect_two_way(world, links_house, 'Links House Exit', player)
|
connect_two_way(world, links_house, 'Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in entrances:
|
if links_house in entrances:
|
||||||
entrances.remove(links_house)
|
entrances.remove(links_house)
|
||||||
elif links_house in must_exits:
|
elif links_house in must_exits:
|
||||||
@@ -487,16 +509,24 @@ def link_entrances(world, player):
|
|||||||
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
|
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
|
||||||
entrances.remove(bomb_shop)
|
entrances.remove(bomb_shop)
|
||||||
|
|
||||||
|
# standard mode cannot have Bonk Fairy Light be a connector in case of
|
||||||
|
# starting boots or boots are in links house, etc.
|
||||||
|
removed = False
|
||||||
|
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in entrances:
|
||||||
|
entrances.remove('Bonk Fairy (Light)')
|
||||||
|
removed = True
|
||||||
|
|
||||||
# place the old man cave's entrance somewhere
|
# place the old man cave's entrance somewhere
|
||||||
random.shuffle(entrances)
|
random.shuffle(entrances)
|
||||||
old_man_entrance = entrances.pop()
|
old_man_entrance = entrances.pop()
|
||||||
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
|
connect_two_way(world, old_man_entrance, 'Old Man Cave Exit (West)', player)
|
||||||
|
|
||||||
|
|
||||||
# now scramble the rest
|
# now scramble the rest
|
||||||
connect_caves(world, entrances, [], caves, player)
|
connect_caves(world, entrances, [], caves, player)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
entrances.append('Bonk Fairy (Light)')
|
||||||
|
|
||||||
# scramble holes
|
# scramble holes
|
||||||
scramble_holes(world, player)
|
scramble_holes(world, player)
|
||||||
|
|
||||||
@@ -911,6 +941,7 @@ def link_entrances(world, player):
|
|||||||
links_house_doors = [i for i in links_house_doors if i not in exclusions]
|
links_house_doors = [i for i in links_house_doors if i not in exclusions]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Links House Exit', player)
|
connect_two_way(world, links_house, 'Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
exit_pool.remove(links_house)
|
exit_pool.remove(links_house)
|
||||||
doors.remove(links_house)
|
doors.remove(links_house)
|
||||||
|
|
||||||
@@ -971,6 +1002,13 @@ def link_entrances(world, player):
|
|||||||
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
|
connect_entrance(world, bomb_shop, 'Big Bomb Shop', player)
|
||||||
doors.remove(bomb_shop)
|
doors.remove(bomb_shop)
|
||||||
|
|
||||||
|
# standard mode cannot have Bonk Fairy Light be a connector in case of
|
||||||
|
# starting boots or boots are in links house, etc.
|
||||||
|
removed = False
|
||||||
|
if world.mode[player] == 'standard' and 'Bonk Fairy (Light)' in doors:
|
||||||
|
doors.remove('Bonk Fairy (Light)')
|
||||||
|
removed = True
|
||||||
|
|
||||||
# handle remaining caves
|
# handle remaining caves
|
||||||
for cave in caves:
|
for cave in caves:
|
||||||
if isinstance(cave, str):
|
if isinstance(cave, str):
|
||||||
@@ -980,6 +1018,9 @@ def link_entrances(world, player):
|
|||||||
connect_exit(world, exit, exit_pool.pop(), player)
|
connect_exit(world, exit, exit_pool.pop(), player)
|
||||||
connect_entrance(world, doors.pop(), exit, player)
|
connect_entrance(world, doors.pop(), exit, player)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
doors.append('Bonk Fairy (Light)')
|
||||||
|
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, doors, door_targets, player)
|
connect_doors(world, doors, door_targets, player)
|
||||||
elif world.shuffle[player] == 'insanity_legacy':
|
elif world.shuffle[player] == 'insanity_legacy':
|
||||||
@@ -1265,6 +1306,7 @@ def link_inverted_entrances(world, player):
|
|||||||
links_house_doors = [i for i in DW_Single_Cave_Doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
links_house_doors = [i for i in DW_Single_Cave_Doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in bomb_shop_doors:
|
if links_house in bomb_shop_doors:
|
||||||
bomb_shop_doors.remove(links_house)
|
bomb_shop_doors.remove(links_house)
|
||||||
if links_house in blacksmith_doors:
|
if links_house in blacksmith_doors:
|
||||||
@@ -1345,6 +1387,7 @@ def link_inverted_entrances(world, player):
|
|||||||
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in dw_entrances:
|
if links_house in dw_entrances:
|
||||||
dw_entrances.remove(links_house)
|
dw_entrances.remove(links_house)
|
||||||
|
|
||||||
@@ -1474,6 +1517,7 @@ def link_inverted_entrances(world, player):
|
|||||||
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
links_house_doors = [i for i in dw_entrances if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in dw_entrances:
|
if links_house in dw_entrances:
|
||||||
dw_entrances.remove(links_house)
|
dw_entrances.remove(links_house)
|
||||||
|
|
||||||
@@ -1611,6 +1655,7 @@ def link_inverted_entrances(world, player):
|
|||||||
links_house_doors = [i for i in entrances + must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
links_house_doors = [i for i in entrances + must_exits if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
if links_house in entrances:
|
if links_house in entrances:
|
||||||
entrances.remove(links_house)
|
entrances.remove(links_house)
|
||||||
elif links_house in must_exits:
|
elif links_house in must_exits:
|
||||||
@@ -1744,6 +1789,7 @@ def link_inverted_entrances(world, player):
|
|||||||
links_house_doors = [i for i in doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
links_house_doors = [i for i in doors if i not in Inverted_Dark_Sanctuary_Doors + Isolated_LH_Doors]
|
||||||
links_house = random.choice(links_house_doors)
|
links_house = random.choice(links_house_doors)
|
||||||
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
connect_two_way(world, links_house, 'Inverted Links House Exit', player)
|
||||||
|
connect_exit(world, 'Chris Houlihan Room Exit', links_house, player) # should match link's house
|
||||||
doors.remove(links_house)
|
doors.remove(links_house)
|
||||||
exit_pool.remove(links_house)
|
exit_pool.remove(links_house)
|
||||||
|
|
||||||
@@ -2120,10 +2166,11 @@ def connect_doors(world, doors, targets, player):
|
|||||||
"""This works inplace"""
|
"""This works inplace"""
|
||||||
random.shuffle(doors)
|
random.shuffle(doors)
|
||||||
random.shuffle(targets)
|
random.shuffle(targets)
|
||||||
while doors:
|
placing = min(len(doors), len(targets))
|
||||||
door = doors.pop()
|
for door, target in zip(doors, targets):
|
||||||
target = targets.pop()
|
|
||||||
connect_entrance(world, door, target, player)
|
connect_entrance(world, door, target, player)
|
||||||
|
doors[:] = doors[placing:]
|
||||||
|
targets[:] = targets[placing:]
|
||||||
|
|
||||||
|
|
||||||
def skull_woods_shuffle(world, player):
|
def skull_woods_shuffle(world, player):
|
||||||
@@ -2373,7 +2420,7 @@ Cave_Exits_Base = [['Elder House Exit (East)', 'Elder House Exit (West)'],
|
|||||||
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'],
|
['Death Mountain Return Cave Exit (West)', 'Death Mountain Return Cave Exit (East)'],
|
||||||
['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'],
|
['Fairy Ascension Cave Exit (Bottom)', 'Fairy Ascension Cave Exit (Top)'],
|
||||||
['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'],
|
['Bumper Cave Exit (Top)', 'Bumper Cave Exit (Bottom)'],
|
||||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']]
|
['Hookshot Cave Back Exit', 'Hookshot Cave Front Exit']]
|
||||||
|
|
||||||
Cave_Exits_Base += [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'),
|
Cave_Exits_Base += [('Superbunny Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)'),
|
||||||
('Spiral Cave Exit (Top)', 'Spiral Cave Exit')]
|
('Spiral Cave Exit (Top)', 'Spiral Cave Exit')]
|
||||||
@@ -3115,6 +3162,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
|||||||
('Dark Death Mountain Drop (West)', 'Dark Death Mountain (West Bottom)'),
|
('Dark Death Mountain Drop (West)', 'Dark Death Mountain (West Bottom)'),
|
||||||
('East Death Mountain (Top) Mirror Spot', 'East Death Mountain (Top)'),
|
('East Death Mountain (Top) Mirror Spot', 'East Death Mountain (Top)'),
|
||||||
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
|
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
|
||||||
|
('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'),
|
||||||
|
('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'),
|
||||||
|
('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'),
|
||||||
|
('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'),
|
||||||
('Turtle Rock Teleporter', 'Turtle Rock (Top)'),
|
('Turtle Rock Teleporter', 'Turtle Rock (Top)'),
|
||||||
('Turtle Rock Drop', 'Dark Death Mountain (Top)'),
|
('Turtle Rock Drop', 'Dark Death Mountain (Top)'),
|
||||||
('Floating Island Drop', 'Dark Death Mountain (Top)'),
|
('Floating Island Drop', 'Dark Death Mountain (Top)'),
|
||||||
@@ -3233,6 +3284,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
|||||||
('Turtle Rock Tail Drop', 'Turtle Rock (Top)'),
|
('Turtle Rock Tail Drop', 'Turtle Rock (Top)'),
|
||||||
('Turtle Rock Drop', 'Dark Death Mountain'),
|
('Turtle Rock Drop', 'Dark Death Mountain'),
|
||||||
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
|
('Superbunny Cave Climb', 'Superbunny Cave (Top)'),
|
||||||
|
('Hookshot Cave Front to Middle', 'Hookshot Cave (Middle)'),
|
||||||
|
('Hookshot Cave Middle to Front', 'Hookshot Cave (Front)'),
|
||||||
|
('Hookshot Cave Middle to Back', 'Hookshot Cave (Back)'),
|
||||||
|
('Hookshot Cave Back to Middle', 'Hookshot Cave (Middle)'),
|
||||||
('Desert Ledge Drop', 'Light World'),
|
('Desert Ledge Drop', 'Light World'),
|
||||||
('Floating Island Drop', 'Dark Death Mountain'),
|
('Floating Island Drop', 'Dark Death Mountain'),
|
||||||
('Dark Lake Hylia Central Island Teleporter', 'Lake Hylia Central Island'),
|
('Dark Lake Hylia Central Island Teleporter', 'Lake Hylia Central Island'),
|
||||||
@@ -3284,7 +3339,6 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
|||||||
('EDDM Flute', 'The Sky'),
|
('EDDM Flute', 'The Sky'),
|
||||||
('Dark Grassy Lawn Flute', 'The Sky'),
|
('Dark Grassy Lawn Flute', 'The Sky'),
|
||||||
('Hammer Peg Area Flute', 'The Sky'),
|
('Hammer Peg Area Flute', 'The Sky'),
|
||||||
('Chris Houlihan Room Exit', 'Pyramid Ledge'),
|
|
||||||
('Bush Covered Lawn Inner Bushes', 'Light World'),
|
('Bush Covered Lawn Inner Bushes', 'Light World'),
|
||||||
('Bush Covered Lawn Outer Bushes', 'Bush Covered Lawn'),
|
('Bush Covered Lawn Outer Bushes', 'Bush Covered Lawn'),
|
||||||
('Bush Covered Lawn Mirror Spot', 'Dark Grassy Lawn'),
|
('Bush Covered Lawn Mirror Spot', 'Dark Grassy Lawn'),
|
||||||
@@ -3428,16 +3482,16 @@ default_connections = [('Links House', 'Links House'),
|
|||||||
('Dark Desert Hint', 'Dark Desert Hint'),
|
('Dark Desert Hint', 'Dark Desert Hint'),
|
||||||
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
|
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
|
||||||
('Spike Cave', 'Spike Cave'),
|
('Spike Cave', 'Spike Cave'),
|
||||||
('Hookshot Cave', 'Hookshot Cave'),
|
('Hookshot Cave', 'Hookshot Cave (Front)'),
|
||||||
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
|
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
|
||||||
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
|
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
|
||||||
('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'),
|
('Dark Death Mountain Fairy', 'Dark Death Mountain Healer Fairy'),
|
||||||
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
|
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
|
||||||
('Superbunny Cave Exit (Top)', 'Dark Death Mountain (Top)'),
|
('Superbunny Cave Exit (Top)', 'Dark Death Mountain (Top)'),
|
||||||
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
|
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
|
||||||
('Hookshot Cave Exit (South)', 'Dark Death Mountain (Top)'),
|
('Hookshot Cave Front Exit', 'Dark Death Mountain (Top)'),
|
||||||
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
|
('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'),
|
||||||
('Hookshot Cave Back Entrance', 'Hookshot Cave'),
|
('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'),
|
||||||
('Mimic Cave', 'Mimic Cave'),
|
('Mimic Cave', 'Mimic Cave'),
|
||||||
|
|
||||||
('Pyramid Hole', 'Pyramid'),
|
('Pyramid Hole', 'Pyramid'),
|
||||||
@@ -3562,13 +3616,13 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
|
|||||||
('Dark Desert Hint', 'Dark Desert Hint'),
|
('Dark Desert Hint', 'Dark Desert Hint'),
|
||||||
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
|
('Dark Desert Fairy', 'Dark Desert Healer Fairy'),
|
||||||
('Spike Cave', 'Spike Cave'),
|
('Spike Cave', 'Spike Cave'),
|
||||||
('Hookshot Cave', 'Hookshot Cave'),
|
('Hookshot Cave', 'Hookshot Cave (Front)'),
|
||||||
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
|
('Superbunny Cave (Top)', 'Superbunny Cave (Top)'),
|
||||||
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
|
('Cave Shop (Dark Death Mountain)', 'Cave Shop (Dark Death Mountain)'),
|
||||||
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
|
('Superbunny Cave (Bottom)', 'Superbunny Cave (Bottom)'),
|
||||||
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
|
('Superbunny Cave Exit (Bottom)', 'Dark Death Mountain (East Bottom)'),
|
||||||
('Hookshot Cave Exit (North)', 'Death Mountain Floating Island (Dark World)'),
|
('Hookshot Cave Back Exit', 'Death Mountain Floating Island (Dark World)'),
|
||||||
('Hookshot Cave Back Entrance', 'Hookshot Cave'),
|
('Hookshot Cave Back Entrance', 'Hookshot Cave (Back)'),
|
||||||
('Mimic Cave', 'Mimic Cave'),
|
('Mimic Cave', 'Mimic Cave'),
|
||||||
('Inverted Pyramid Hole', 'Pyramid'),
|
('Inverted Pyramid Hole', 'Pyramid'),
|
||||||
('Inverted Links House', 'Inverted Links House'),
|
('Inverted Links House', 'Inverted Links House'),
|
||||||
@@ -3589,7 +3643,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
|
|||||||
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'),
|
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave'),
|
||||||
('Death Mountain Return Cave Exit (West)', 'Death Mountain'),
|
('Death Mountain Return Cave Exit (West)', 'Death Mountain'),
|
||||||
('Death Mountain Return Cave Exit (East)', 'Death Mountain'),
|
('Death Mountain Return Cave Exit (East)', 'Death Mountain'),
|
||||||
('Hookshot Cave Exit (South)', 'Dark Death Mountain'),
|
('Hookshot Cave Front Exit', 'Dark Death Mountain'),
|
||||||
('Superbunny Cave Exit (Top)', 'Dark Death Mountain'),
|
('Superbunny Cave Exit (Top)', 'Dark Death Mountain'),
|
||||||
('Pyramid Exit', 'Light World'),
|
('Pyramid Exit', 'Light World'),
|
||||||
('Inverted Pyramid Entrance', 'Bottom of Pyramid')]
|
('Inverted Pyramid Entrance', 'Bottom of Pyramid')]
|
||||||
@@ -3937,8 +3991,8 @@ exit_ids = {'Links House Exit': (0x01, 0x00),
|
|||||||
'Bumper Cave Exit (Bottom)': (0x16, 0x17),
|
'Bumper Cave Exit (Bottom)': (0x16, 0x17),
|
||||||
'Superbunny Cave Exit (Top)': (0x14, 0x15),
|
'Superbunny Cave Exit (Top)': (0x14, 0x15),
|
||||||
'Superbunny Cave Exit (Bottom)': (0x13, 0x14),
|
'Superbunny Cave Exit (Bottom)': (0x13, 0x14),
|
||||||
'Hookshot Cave Exit (South)': (0x3A, 0x3B),
|
'Hookshot Cave Front Exit': (0x3A, 0x3B),
|
||||||
'Hookshot Cave Exit (North)': (0x3B, 0x3C),
|
'Hookshot Cave Back Exit': (0x3B, 0x3C),
|
||||||
'Ganons Tower Exit': (0x37, 0x38),
|
'Ganons Tower Exit': (0x37, 0x38),
|
||||||
'Inverted Ganons Tower Exit': (0x37, 0x38),
|
'Inverted Ganons Tower Exit': (0x37, 0x38),
|
||||||
'Pyramid Exit': (0x36, 0x37),
|
'Pyramid Exit': (0x36, 0x37),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import RaceRandom as random
|
import RaceRandom as random
|
||||||
|
import collections
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
@@ -211,7 +213,7 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
|||||||
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
and valid_key_placement(item_to_place, location, itempool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||||
spot_to_fill = location
|
spot_to_fill = location
|
||||||
break
|
break
|
||||||
elif item_to_place.smallkey or item_to_place.bigkey:
|
if item_to_place.smallkey or item_to_place.bigkey:
|
||||||
location.item = None
|
location.item = None
|
||||||
|
|
||||||
if spot_to_fill is None:
|
if spot_to_fill is None:
|
||||||
@@ -221,7 +223,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
|||||||
if world.accessibility[item_to_place.player] != 'none':
|
if world.accessibility[item_to_place.player] != 'none':
|
||||||
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
logging.getLogger('').warning('Not all items placed. Game beatable anyway. (Could not place %s)' % item_to_place)
|
||||||
continue
|
continue
|
||||||
raise FillError('No more spots to place %s' % item_to_place)
|
spot_to_fill = last_ditch_placement(item_to_place, locations, world, maximum_exploration_state,
|
||||||
|
base_state, itempool, keys_in_itempool, single_player_placement)
|
||||||
|
if spot_to_fill is None:
|
||||||
|
raise FillError('No more spots to place %s' % item_to_place)
|
||||||
|
|
||||||
world.push_item(spot_to_fill, item_to_place, False)
|
world.push_item(spot_to_fill, item_to_place, False)
|
||||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||||
@@ -240,7 +245,11 @@ def valid_key_placement(item, location, itempool, world):
|
|||||||
return True
|
return True
|
||||||
key_logic = world.key_logic[item.player][dungeon.name]
|
key_logic = world.key_logic[item.player][dungeon.name]
|
||||||
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
|
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
|
||||||
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None)
|
prize_loc = None
|
||||||
|
if key_logic.prize_location:
|
||||||
|
prize_loc = world.get_location(key_logic.prize_location, location.player)
|
||||||
|
cr_count = world.crystals_needed_for_gt[location.player]
|
||||||
|
return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count)
|
||||||
else:
|
else:
|
||||||
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
|
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
|
||||||
or (item.bigkey and not world.bigkeyshuffle[item.player]))
|
or (item.bigkey and not world.bigkeyshuffle[item.player]))
|
||||||
@@ -250,9 +259,7 @@ def valid_key_placement(item, location, itempool, world):
|
|||||||
def track_outside_keys(item, location, world):
|
def track_outside_keys(item, location, world):
|
||||||
if not item.smallkey:
|
if not item.smallkey:
|
||||||
return
|
return
|
||||||
item_dungeon = item.name.split('(')[1][:-1]
|
item_dungeon = item.dungeon
|
||||||
if item_dungeon == 'Escape':
|
|
||||||
item_dungeon = 'Hyrule Castle'
|
|
||||||
if location.player == item.player:
|
if location.player == item.player:
|
||||||
loc_dungeon = location.parent_region.dungeon
|
loc_dungeon = location.parent_region.dungeon
|
||||||
if loc_dungeon and loc_dungeon.name == item_dungeon:
|
if loc_dungeon and loc_dungeon.name == item_dungeon:
|
||||||
@@ -260,6 +267,80 @@ def track_outside_keys(item, location, world):
|
|||||||
world.key_logic[item.player][item_dungeon].outside_keys += 1
|
world.key_logic[item.player][item_dungeon].outside_keys += 1
|
||||||
|
|
||||||
|
|
||||||
|
def last_ditch_placement(item_to_place, locations, world, state, base_state, itempool,
|
||||||
|
keys_in_itempool=None, single_player_placement=False):
|
||||||
|
def location_preference(loc):
|
||||||
|
if not loc.item.advancement:
|
||||||
|
return 1
|
||||||
|
if loc.item.type and loc.item.type != 'Sword':
|
||||||
|
if loc.item.type in ['Map', 'Compass']:
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return 3
|
||||||
|
return 4
|
||||||
|
|
||||||
|
if item_to_place.type == 'Crystal':
|
||||||
|
possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal']
|
||||||
|
else:
|
||||||
|
possible_swaps = [x for x in state.locations_checked
|
||||||
|
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item]
|
||||||
|
swap_locations = sorted(possible_swaps, key=location_preference)
|
||||||
|
|
||||||
|
for location in swap_locations:
|
||||||
|
old_item = location.item
|
||||||
|
new_pool = list(itempool) + [old_item]
|
||||||
|
new_spot = find_spot_for_item(item_to_place, [location], world, base_state, new_pool,
|
||||||
|
keys_in_itempool, single_player_placement)
|
||||||
|
if new_spot:
|
||||||
|
restore_item = new_spot.item
|
||||||
|
new_spot.item = item_to_place
|
||||||
|
swap_spot = find_spot_for_item(old_item, locations, world, base_state, itempool,
|
||||||
|
keys_in_itempool, single_player_placement)
|
||||||
|
if swap_spot:
|
||||||
|
logging.getLogger('').debug(f'Swapping {old_item} for {item_to_place}')
|
||||||
|
world.push_item(swap_spot, old_item, False)
|
||||||
|
swap_spot.event = True
|
||||||
|
locations.remove(swap_spot)
|
||||||
|
locations.append(new_spot)
|
||||||
|
return new_spot
|
||||||
|
else:
|
||||||
|
new_spot.item = restore_item
|
||||||
|
else:
|
||||||
|
location.item = old_item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_spot_for_item(item_to_place, locations, world, base_state, pool,
|
||||||
|
keys_in_itempool=None, single_player_placement=False):
|
||||||
|
def sweep_from_pool():
|
||||||
|
new_state = base_state.copy()
|
||||||
|
for item in pool:
|
||||||
|
new_state.collect(item, True)
|
||||||
|
new_state.sweep_for_events()
|
||||||
|
return new_state
|
||||||
|
for location in locations:
|
||||||
|
maximum_exploration_state = sweep_from_pool()
|
||||||
|
perform_access_check = True
|
||||||
|
old_item = None
|
||||||
|
if world.accessibility[item_to_place.player] == 'none':
|
||||||
|
perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not world.has_beaten_game(maximum_exploration_state)
|
||||||
|
|
||||||
|
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||||
|
old_item = location.item
|
||||||
|
location.item = item_to_place
|
||||||
|
test_state = maximum_exploration_state.copy()
|
||||||
|
test_state.stale[item_to_place.player] = True
|
||||||
|
else:
|
||||||
|
test_state = maximum_exploration_state
|
||||||
|
if (not single_player_placement or location.player == item_to_place.player) \
|
||||||
|
and location.can_fill(test_state, item_to_place, perform_access_check) \
|
||||||
|
and valid_key_placement(item_to_place, location, pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool, world):
|
||||||
|
return location
|
||||||
|
if item_to_place.smallkey or item_to_place.bigkey:
|
||||||
|
location.item = old_item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
|
||||||
# If not passed in, then get a shuffled list of locations to fill in
|
# If not passed in, then get a shuffled list of locations to fill in
|
||||||
if not fill_locations:
|
if not fill_locations:
|
||||||
@@ -420,9 +501,8 @@ def sell_keys(world, player):
|
|||||||
|
|
||||||
def balance_multiworld_progression(world):
|
def balance_multiworld_progression(world):
|
||||||
state = CollectionState(world)
|
state = CollectionState(world)
|
||||||
checked_locations = []
|
checked_locations = set()
|
||||||
unchecked_locations = world.get_locations().copy()
|
unchecked_locations = set(world.get_locations())
|
||||||
random.shuffle(unchecked_locations)
|
|
||||||
|
|
||||||
reachable_locations_count = {}
|
reachable_locations_count = {}
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
@@ -430,7 +510,7 @@ def balance_multiworld_progression(world):
|
|||||||
|
|
||||||
def get_sphere_locations(sphere_state, locations):
|
def get_sphere_locations(sphere_state, locations):
|
||||||
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
sphere_state.sweep_for_events(key_only=True, locations=locations)
|
||||||
return [loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)]
|
return {loc for loc in locations if sphere_state.can_reach(loc) and sphere_state.not_flooding_a_key(sphere_state.world, loc)}
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
sphere_locations = get_sphere_locations(state, unchecked_locations)
|
||||||
@@ -441,38 +521,42 @@ def balance_multiworld_progression(world):
|
|||||||
if checked_locations:
|
if checked_locations:
|
||||||
threshold = max(reachable_locations_count.values()) - 20
|
threshold = max(reachable_locations_count.values()) - 20
|
||||||
|
|
||||||
balancing_players = [player for player, reachables in reachable_locations_count.items() if reachables < threshold]
|
balancing_players = {player for player, reachables in reachable_locations_count.items() if reachables < threshold}
|
||||||
if balancing_players is not None and len(balancing_players) > 0:
|
if balancing_players:
|
||||||
balancing_state = state.copy()
|
balancing_state = state.copy()
|
||||||
balancing_unchecked_locations = unchecked_locations.copy()
|
balancing_unchecked_locations = unchecked_locations.copy()
|
||||||
balancing_reachables = reachable_locations_count.copy()
|
balancing_reachables = reachable_locations_count.copy()
|
||||||
balancing_sphere = sphere_locations.copy()
|
balancing_sphere = sphere_locations.copy()
|
||||||
candidate_items = []
|
candidate_items = collections.defaultdict(set)
|
||||||
while True:
|
while True:
|
||||||
for location in balancing_sphere:
|
for location in balancing_sphere:
|
||||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||||
balancing_state.collect(location.item, True, location)
|
balancing_state.collect(location.item, True, location)
|
||||||
if location.item.player in balancing_players and not location.locked:
|
player = location.item.player
|
||||||
candidate_items.append(location)
|
if player in balancing_players and not location.locked and location.player != player:
|
||||||
|
candidate_items[player].add(location)
|
||||||
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
|
||||||
for location in balancing_sphere:
|
for location in balancing_sphere:
|
||||||
balancing_unchecked_locations.remove(location)
|
balancing_unchecked_locations.remove(location)
|
||||||
balancing_reachables[location.player] += 1
|
balancing_reachables[location.player] += 1
|
||||||
if world.has_beaten_game(balancing_state) or all([reachables >= threshold for reachables in balancing_reachables.values()]):
|
if world.has_beaten_game(balancing_state) or all(reachables >= threshold for reachables in balancing_reachables.values()):
|
||||||
break
|
break
|
||||||
elif not balancing_sphere:
|
elif not balancing_sphere:
|
||||||
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
|
||||||
|
|
||||||
unlocked_locations = [l for l in unchecked_locations if l not in balancing_unchecked_locations]
|
unlocked_locations = collections.defaultdict(set)
|
||||||
|
for l in unchecked_locations:
|
||||||
|
if l not in balancing_unchecked_locations:
|
||||||
|
unlocked_locations[l.player].add(l)
|
||||||
items_to_replace = []
|
items_to_replace = []
|
||||||
for player in balancing_players:
|
for player in balancing_players:
|
||||||
locations_to_test = [l for l in unlocked_locations if l.player == player]
|
locations_to_test = unlocked_locations[player]
|
||||||
# only replace items that end up in another player's world
|
items_to_test = candidate_items[player]
|
||||||
items_to_test = [l for l in candidate_items if l.item.player == player and l.player != player]
|
|
||||||
while items_to_test:
|
while items_to_test:
|
||||||
testing = items_to_test.pop()
|
testing = items_to_test.pop()
|
||||||
reducing_state = state.copy()
|
reducing_state = state.copy()
|
||||||
for location in [*[l for l in items_to_replace if l.item.player == player], *items_to_test]:
|
for location in itertools.chain((l for l in items_to_replace if l.item.player == player),
|
||||||
|
items_to_test):
|
||||||
reducing_state.collect(location.item, True, location)
|
reducing_state.collect(location.item, True, location)
|
||||||
|
|
||||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||||
@@ -486,33 +570,44 @@ def balance_multiworld_progression(world):
|
|||||||
items_to_replace.append(testing)
|
items_to_replace.append(testing)
|
||||||
|
|
||||||
replaced_items = False
|
replaced_items = False
|
||||||
replacement_locations = [l for l in checked_locations if not l.event and not l.locked]
|
# sort then shuffle to maintain deterministic behaviour,
|
||||||
|
# while allowing use of set for better algorithm growth behaviour elsewhere
|
||||||
|
replacement_locations = sorted((l for l in checked_locations if not l.event and not l.locked),
|
||||||
|
key=lambda loc: (loc.name, loc.player))
|
||||||
|
random.shuffle(replacement_locations)
|
||||||
|
items_to_replace.sort(key=lambda item: (item.name, item.player))
|
||||||
|
random.shuffle(items_to_replace)
|
||||||
while replacement_locations and items_to_replace:
|
while replacement_locations and items_to_replace:
|
||||||
new_location = replacement_locations.pop()
|
|
||||||
old_location = items_to_replace.pop()
|
old_location = items_to_replace.pop()
|
||||||
|
for new_location in replacement_locations:
|
||||||
|
if (new_location.can_fill(state, old_location.item, False) and
|
||||||
|
old_location.can_fill(state, new_location.item, False)):
|
||||||
|
replacement_locations.remove(new_location)
|
||||||
|
new_location.item, old_location.item = old_location.item, new_location.item
|
||||||
|
if world.shopsanity[new_location.player]:
|
||||||
|
check_shop_swap(new_location)
|
||||||
|
if world.shopsanity[old_location.player]:
|
||||||
|
check_shop_swap(old_location)
|
||||||
|
new_location.event, old_location.event = True, False
|
||||||
|
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
|
||||||
|
f"displacing {old_location.item} into {old_location}")
|
||||||
|
state.collect(new_location.item, True, new_location)
|
||||||
|
replaced_items = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logging.warning(f"Could not Progression Balance {old_location.item}")
|
||||||
|
|
||||||
while not new_location.can_fill(state, old_location.item, False) or (new_location.item and not old_location.can_fill(state, new_location.item, False)):
|
|
||||||
replacement_locations.insert(0, new_location)
|
|
||||||
new_location = replacement_locations.pop()
|
|
||||||
|
|
||||||
new_location.item, old_location.item = old_location.item, new_location.item
|
|
||||||
if world.shopsanity[new_location.player]:
|
|
||||||
check_shop_swap(new_location)
|
|
||||||
if world.shopsanity[old_location.player]:
|
|
||||||
check_shop_swap(old_location)
|
|
||||||
new_location.event, old_location.event = True, False
|
|
||||||
state.collect(new_location.item, True, new_location)
|
|
||||||
replaced_items = True
|
|
||||||
if replaced_items:
|
if replaced_items:
|
||||||
for location in get_sphere_locations(state, [l for l in unlocked_locations if l.player in balancing_players]):
|
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
|
||||||
|
for location in get_sphere_locations(state, unlocked):
|
||||||
unchecked_locations.remove(location)
|
unchecked_locations.remove(location)
|
||||||
reachable_locations_count[location.player] += 1
|
reachable_locations_count[location.player] += 1
|
||||||
sphere_locations.append(location)
|
sphere_locations.add(location)
|
||||||
|
|
||||||
for location in sphere_locations:
|
for location in sphere_locations:
|
||||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||||
state.collect(location.item, True, location)
|
state.collect(location.item, True, location)
|
||||||
checked_locations.extend(sphere_locations)
|
checked_locations |= sphere_locations
|
||||||
|
|
||||||
if world.has_beaten_game(state):
|
if world.has_beaten_game(state):
|
||||||
break
|
break
|
||||||
@@ -553,7 +648,7 @@ def balance_money_progression(world):
|
|||||||
base_value = sum(rupee_rooms.values())
|
base_value = sum(rupee_rooms.values())
|
||||||
available_money = {player: base_value for player in range(1, world.players+1)}
|
available_money = {player: base_value for player in range(1, world.players+1)}
|
||||||
for loc in world.get_locations():
|
for loc in world.get_locations():
|
||||||
if loc.item.name in rupee_chart:
|
if loc.item and loc.item.name in rupee_chart:
|
||||||
available_money[loc.item.player] += rupee_chart[loc.item.name]
|
available_money[loc.item.player] += rupee_chart[loc.item.name]
|
||||||
|
|
||||||
total_price = {player: 0 for player in range(1, world.players+1)}
|
total_price = {player: 0 for player in range(1, world.players+1)}
|
||||||
@@ -618,7 +713,7 @@ def balance_money_progression(world):
|
|||||||
slot = shop_to_location_table[location.parent_region.name].index(location.name)
|
slot = shop_to_location_table[location.parent_region.name].index(location.name)
|
||||||
shop = location.parent_region.shop
|
shop = location.parent_region.shop
|
||||||
shop_item = shop.inventory[slot]
|
shop_item = shop.inventory[slot]
|
||||||
if interesting_item(location, location.item, world, location.item.player):
|
if location.item and interesting_item(location, location.item, world, location.item.player):
|
||||||
if location.item.name.startswith('Rupee') and loc_player == location.item.player:
|
if location.item.name.startswith('Rupee') and loc_player == location.item.player:
|
||||||
if shop_item['price'] < rupee_chart[location.item.name]:
|
if shop_item['price'] < rupee_chart[location.item.name]:
|
||||||
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
|
wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block
|
||||||
@@ -638,20 +733,21 @@ def balance_money_progression(world):
|
|||||||
if location_free:
|
if location_free:
|
||||||
state.collect(location.item, True, location)
|
state.collect(location.item, True, location)
|
||||||
unchecked_locations.remove(location)
|
unchecked_locations.remove(location)
|
||||||
if location.item.name.startswith('Rupee'):
|
if location.item:
|
||||||
wallet[location.item.player] += rupee_chart[location.item.name]
|
if location.item.name.startswith('Rupee'):
|
||||||
if location.item.name != 'Rupees (300)':
|
wallet[location.item.player] += rupee_chart[location.item.name]
|
||||||
|
if location.item.name != 'Rupees (300)':
|
||||||
|
balance_locations[location.item.player].add(location)
|
||||||
|
if interesting_item(location, location.item, world, location.item.player):
|
||||||
|
checked_locations.append(location)
|
||||||
|
elif location.item.name in acceptable_balancers:
|
||||||
balance_locations[location.item.player].add(location)
|
balance_locations[location.item.player].add(location)
|
||||||
if interesting_item(location, location.item, world, location.item.player):
|
|
||||||
checked_locations.append(location)
|
|
||||||
elif location.item.name in acceptable_balancers:
|
|
||||||
balance_locations[location.item.player].add(location)
|
|
||||||
for room, income in rupee_rooms.items():
|
for room, income in rupee_rooms.items():
|
||||||
for player in range(1, world.players+1):
|
for player in range(1, world.players+1):
|
||||||
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
|
if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]:
|
||||||
wallet[player] += income
|
wallet[player] += income
|
||||||
rooms_visited[player].add(room)
|
rooms_visited[player].add(room)
|
||||||
if checked_locations:
|
if checked_locations or len(unchecked_locations) == 0:
|
||||||
if world.has_beaten_game(state):
|
if world.has_beaten_game(state):
|
||||||
done = True
|
done = True
|
||||||
continue
|
continue
|
||||||
@@ -661,14 +757,17 @@ def balance_money_progression(world):
|
|||||||
solvent = set()
|
solvent = set()
|
||||||
insolvent = set()
|
insolvent = set()
|
||||||
for player in range(1, world.players+1):
|
for player in range(1, world.players+1):
|
||||||
if wallet[player] >= sphere_costs[player] > 0:
|
if wallet[player] >= sphere_costs[player] >= 0:
|
||||||
solvent.add(player)
|
solvent.add(player)
|
||||||
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
|
if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]:
|
||||||
insolvent.add(player)
|
insolvent.add(player)
|
||||||
if len(solvent) == 0:
|
if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
|
||||||
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
|
if len(insolvent) > 0:
|
||||||
difference = sphere_costs[target_player]-wallet[target_player]
|
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p])
|
||||||
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
|
difference = sphere_costs[target_player]-wallet[target_player]
|
||||||
|
logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
|
||||||
|
else:
|
||||||
|
difference = 0
|
||||||
while difference > 0:
|
while difference > 0:
|
||||||
swap_targets = [x for x in unchecked_locations if x not in sphere_locations and x.item.name.startswith('Rupees') and x.item.player == target_player]
|
swap_targets = [x for x in unchecked_locations if x not in sphere_locations and x.item.name.startswith('Rupees') and x.item.player == target_player]
|
||||||
if len(swap_targets) == 0:
|
if len(swap_targets) == 0:
|
||||||
@@ -705,10 +804,10 @@ def balance_money_progression(world):
|
|||||||
for player in solvent:
|
for player in solvent:
|
||||||
wallet[player] -= sphere_costs[player]
|
wallet[player] -= sphere_costs[player]
|
||||||
for location in locked_by_money[player]:
|
for location in locked_by_money[player]:
|
||||||
if location == 'Kiki':
|
if isinstance(location, str) and location == 'Kiki':
|
||||||
kiki_paid[player] = True
|
kiki_paid[player] = True
|
||||||
else:
|
else:
|
||||||
state.collect(location.item, True, location)
|
state.collect(location.item, True, location)
|
||||||
unchecked_locations.remove(location)
|
unchecked_locations.remove(location)
|
||||||
if location.item.name.startswith('Rupee'):
|
if location.item and location.item.name.startswith('Rupee'):
|
||||||
wallet[location.item.player] += rupee_chart[location.item.name]
|
wallet[location.item.player] += rupee_chart[location.item.name]
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ from source.classes.BabelFish import BabelFish
|
|||||||
from source.classes.Empty import Empty
|
from source.classes.Empty import Empty
|
||||||
|
|
||||||
|
|
||||||
|
def check_python_version(fish):
|
||||||
|
import sys
|
||||||
|
version = sys.version_info
|
||||||
|
if version.major < 3 or version.minor < 7:
|
||||||
|
messagebox.showinfo("Door Shuffle " + ESVersion, fish.translate("cli","cli","old.python.version") % sys.version)
|
||||||
|
|
||||||
|
|
||||||
def guiMain(args=None):
|
def guiMain(args=None):
|
||||||
# Save settings to file
|
# Save settings to file
|
||||||
def save_settings(args):
|
def save_settings(args):
|
||||||
@@ -188,6 +195,8 @@ def guiMain(args=None):
|
|||||||
# load adjust settings into options
|
# load adjust settings into options
|
||||||
loadadjustargs(self, self.settings)
|
loadadjustargs(self, self.settings)
|
||||||
|
|
||||||
|
check_python_version(self.fish)
|
||||||
|
|
||||||
# run main window
|
# run main window
|
||||||
mainWindow.mainloop()
|
mainWindow.mainloop()
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -199,8 +199,11 @@ def create_inverted_regions(world, player):
|
|||||||
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||||
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||||
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||||
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
|
create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
|
||||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
|
['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']),
|
||||||
|
create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']),
|
||||||
|
create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']),
|
||||||
|
|
||||||
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
|
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance']),
|
||||||
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
create_cave_region(player, 'Mimic Cave', 'Mimic Cave', ['Mimic Cave']),
|
||||||
|
|
||||||
|
|||||||
+66
-26
@@ -4,7 +4,6 @@ import math
|
|||||||
import RaceRandom as random
|
import RaceRandom as random
|
||||||
|
|
||||||
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState
|
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState
|
||||||
from Bosses import place_bosses
|
|
||||||
from Dungeons import get_dungeon_item_pool
|
from Dungeons import get_dungeon_item_pool
|
||||||
from EntranceShuffle import connect_entrance
|
from EntranceShuffle import connect_entrance
|
||||||
from Regions import shop_to_location_table, retro_shops, shop_table_by_location
|
from Regions import shop_to_location_table, retro_shops, shop_table_by_location
|
||||||
@@ -37,7 +36,7 @@ Difficulty = namedtuple('Difficulty',
|
|||||||
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
|
['baseitems', 'bottles', 'bottle_count', 'same_bottle', 'progressiveshield',
|
||||||
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
|
'basicshield', 'progressivearmor', 'basicarmor', 'swordless',
|
||||||
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
|
'progressivesword', 'basicsword', 'basicbow', 'timedohko', 'timedother',
|
||||||
'retro',
|
'retro', 'bombbag',
|
||||||
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
|
'extras', 'progressive_sword_limit', 'progressive_shield_limit',
|
||||||
'progressive_armor_limit', 'progressive_bottle_limit',
|
'progressive_armor_limit', 'progressive_bottle_limit',
|
||||||
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
|
'progressive_bow_limit', 'heart_piece_limit', 'boss_heart_container_limit'])
|
||||||
@@ -61,6 +60,7 @@ difficulties = {
|
|||||||
timedohko = ['Green Clock'] * 25,
|
timedohko = ['Green Clock'] * 25,
|
||||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||||
retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
|
retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
|
||||||
|
bombbag = ['Bomb Upgrade (+10)'] * 2,
|
||||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||||
progressive_sword_limit = 4,
|
progressive_sword_limit = 4,
|
||||||
progressive_shield_limit = 3,
|
progressive_shield_limit = 3,
|
||||||
@@ -86,6 +86,7 @@ difficulties = {
|
|||||||
timedohko = ['Green Clock'] * 25,
|
timedohko = ['Green Clock'] * 25,
|
||||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||||
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
|
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
|
||||||
|
bombbag = ['Bomb Upgrade (+10)'] * 2,
|
||||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||||
progressive_sword_limit = 3,
|
progressive_sword_limit = 3,
|
||||||
progressive_shield_limit = 2,
|
progressive_shield_limit = 2,
|
||||||
@@ -111,6 +112,7 @@ difficulties = {
|
|||||||
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
timedohko = ['Green Clock'] * 20 + ['Red Clock'] * 5,
|
||||||
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
|
||||||
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
|
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
|
||||||
|
bombbag = ['Bomb Upgrade (+10)'] * 2,
|
||||||
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
|
||||||
progressive_sword_limit = 2,
|
progressive_sword_limit = 2,
|
||||||
progressive_shield_limit = 1,
|
progressive_shield_limit = 1,
|
||||||
@@ -251,10 +253,10 @@ def generate_itempool(world, player):
|
|||||||
|
|
||||||
# set up item pool
|
# set up item pool
|
||||||
if world.custom:
|
if world.custom:
|
||||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray)
|
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray)
|
||||||
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
|
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
|
||||||
else:
|
else:
|
||||||
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player], world.logic[player])
|
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player])
|
||||||
|
|
||||||
if player in world.pool_adjustment.keys():
|
if player in world.pool_adjustment.keys():
|
||||||
amt = world.pool_adjustment[player]
|
amt = world.pool_adjustment[player]
|
||||||
@@ -284,7 +286,7 @@ def generate_itempool(world, player):
|
|||||||
if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
||||||
if item not in possible_weapons:
|
if item not in possible_weapons:
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
if item in ['Bombs (10)']:
|
if not world.bombbag[player] and item in ['Bombs (10)']:
|
||||||
if item not in possible_weapons and world.doorShuffle[player] != 'crossed':
|
if item not in possible_weapons and world.doorShuffle[player] != 'crossed':
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
starting_weapon = random.choice(possible_weapons)
|
starting_weapon = random.choice(possible_weapons)
|
||||||
@@ -315,6 +317,11 @@ def generate_itempool(world, player):
|
|||||||
p_item = next(item for item in items if item.name == potion and item.player == player)
|
p_item = next(item for item in items if item.name == potion and item.player == player)
|
||||||
p_item.priority = True # don't beemize one of each potion
|
p_item.priority = True # don't beemize one of each potion
|
||||||
|
|
||||||
|
if world.bombbag[player]:
|
||||||
|
for item in items:
|
||||||
|
if item.name == 'Bomb Upgrade (+10)' and item.player == player:
|
||||||
|
item.advancement = True
|
||||||
|
|
||||||
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
|
world.lamps_needed_for_dark_rooms = lamps_needed_for_dark_rooms
|
||||||
|
|
||||||
if clock_mode is not None:
|
if clock_mode is not None:
|
||||||
@@ -339,11 +346,11 @@ def generate_itempool(world, player):
|
|||||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||||
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0):
|
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray[player]["heartcontainer"] == 0):
|
||||||
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
|
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
|
||||||
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4):
|
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[player]["heartpiece"] < 4):
|
||||||
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
|
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
|
||||||
for hp in adv_heart_pieces:
|
for i in range(4):
|
||||||
hp.advancement = True
|
next(adv_heart_pieces).advancement = True
|
||||||
|
|
||||||
beeweights = {'0': {None: 100},
|
beeweights = {'0': {None: 100},
|
||||||
'1': {None: 75, 'trap': 25},
|
'1': {None: 75, 'trap': 25},
|
||||||
@@ -363,7 +370,15 @@ def generate_itempool(world, player):
|
|||||||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||||
world.required_medallions[player] = (mm_medallion, tr_medallion)
|
world.required_medallions[player] = (mm_medallion, tr_medallion)
|
||||||
|
|
||||||
place_bosses(world, player)
|
# shuffle bottle refills
|
||||||
|
if world.difficulty[player] in ['hard', 'expert']:
|
||||||
|
waterfall_bottle = hardbottles[random.randint(0, 5)]
|
||||||
|
pyramid_bottle = hardbottles[random.randint(0, 5)]
|
||||||
|
else:
|
||||||
|
waterfall_bottle = normalbottles[random.randint(0, 6)]
|
||||||
|
pyramid_bottle = normalbottles[random.randint(0, 6)]
|
||||||
|
world.bottle_refills[player] = (waterfall_bottle, pyramid_bottle)
|
||||||
|
|
||||||
set_up_shops(world, player)
|
set_up_shops(world, player)
|
||||||
|
|
||||||
if world.retro[player]:
|
if world.retro[player]:
|
||||||
@@ -520,6 +535,15 @@ def set_up_shops(world, player):
|
|||||||
rss.locked = True
|
rss.locked = True
|
||||||
cap_shop = world.get_region('Capacity Upgrade', player).shop
|
cap_shop = world.get_region('Capacity Upgrade', player).shop
|
||||||
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
|
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
|
||||||
|
if world.bombbag[player]:
|
||||||
|
if world.shopsanity[player]:
|
||||||
|
removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player]
|
||||||
|
for remove in removals:
|
||||||
|
world.itempool.remove(remove)
|
||||||
|
world.itempool.append(ItemFactory('Rupees (50)', player)) # replace the bomb upgrade
|
||||||
|
else:
|
||||||
|
cap_shop = world.get_region('Capacity Upgrade', player).shop
|
||||||
|
cap_shop.inventory[0] = cap_shop.inventory[1] # remove bomb capacity upgrades in bombbag
|
||||||
|
|
||||||
|
|
||||||
def customize_shops(world, player):
|
def customize_shops(world, player):
|
||||||
@@ -561,7 +585,7 @@ def customize_shops(world, player):
|
|||||||
shop.shopkeeper_config = shopkeeper
|
shop.shopkeeper_config = shopkeeper
|
||||||
# handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades
|
# handle capacity upgrades - randomly choose a bomb bunch or arrow bunch to become capacity upgrades
|
||||||
if world.difficulty[player] == 'normal':
|
if world.difficulty[player] == 'normal':
|
||||||
if not found_bomb_upgrade and len(possible_replacements) > 0:
|
if not found_bomb_upgrade and len(possible_replacements) > 0 and not world.bombbag[player]:
|
||||||
choices = []
|
choices = []
|
||||||
for shop, idx, loc, item in possible_replacements:
|
for shop, idx, loc, item in possible_replacements:
|
||||||
if item.name in ['Bombs (3)', 'Bombs (10)']:
|
if item.name in ['Bombs (3)', 'Bombs (10)']:
|
||||||
@@ -587,6 +611,7 @@ def customize_shops(world, player):
|
|||||||
upgrade.location = loc
|
upgrade.location = loc
|
||||||
change_shop_items_to_rupees(world, player, shops_to_customize)
|
change_shop_items_to_rupees(world, player, shops_to_customize)
|
||||||
balance_prices(world, player)
|
balance_prices(world, player)
|
||||||
|
check_hints(world, player)
|
||||||
|
|
||||||
|
|
||||||
def randomize_price(price):
|
def randomize_price(price):
|
||||||
@@ -690,6 +715,15 @@ def balance_prices(world, player):
|
|||||||
# loc.parent_region.shop.inventory[slot]['price'] = new_price
|
# loc.parent_region.shop.inventory[slot]['price'] = new_price
|
||||||
|
|
||||||
|
|
||||||
|
def check_hints(world, player):
|
||||||
|
if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']:
|
||||||
|
for shop, location_list in shop_to_location_table.items():
|
||||||
|
if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']:
|
||||||
|
continue # near the queen, near potions, and near 7 chests are fine
|
||||||
|
for loc_name in location_list: # other shops are indistinguishable in ER
|
||||||
|
world.get_location(loc_name, player).hint_text = f'for sale'
|
||||||
|
|
||||||
|
|
||||||
repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart',
|
repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart',
|
||||||
'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion']
|
'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion']
|
||||||
|
|
||||||
@@ -709,7 +743,7 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)'
|
|||||||
'Rupees (100)': 100, 'Rupees (300)': 300}
|
'Rupees (100)': 100, 'Rupees (300)': 300}
|
||||||
|
|
||||||
|
|
||||||
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, door_shuffle, logic):
|
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic):
|
||||||
pool = []
|
pool = []
|
||||||
placed_items = {}
|
placed_items = {}
|
||||||
precollected_items = []
|
precollected_items = []
|
||||||
@@ -756,6 +790,11 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
|||||||
diff = difficulties[difficulty]
|
diff = difficulties[difficulty]
|
||||||
pool.extend(diff.baseitems)
|
pool.extend(diff.baseitems)
|
||||||
|
|
||||||
|
if bombbag:
|
||||||
|
pool = [item.replace('Bomb Upgrade (+5)','Rupees (5)') for item in pool]
|
||||||
|
pool = [item.replace('Bomb Upgrade (+10)','Rupees (5)') for item in pool]
|
||||||
|
pool.extend(diff.bombbag)
|
||||||
|
|
||||||
# expert+ difficulties produce the same contents for
|
# expert+ difficulties produce the same contents for
|
||||||
# all bottles, since only one bottle is available
|
# all bottles, since only one bottle is available
|
||||||
if diff.same_bottle:
|
if diff.same_bottle:
|
||||||
@@ -850,7 +889,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
|||||||
pool.extend(['Small Key (Universal)'])
|
pool.extend(['Small Key (Universal)'])
|
||||||
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
|
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
|
||||||
|
|
||||||
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, customitemarray):
|
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray):
|
||||||
if isinstance(customitemarray,dict) and 1 in customitemarray:
|
if isinstance(customitemarray,dict) and 1 in customitemarray:
|
||||||
customitemarray = customitemarray[1]
|
customitemarray = customitemarray[1]
|
||||||
pool = []
|
pool = []
|
||||||
@@ -966,20 +1005,21 @@ def test():
|
|||||||
for shuffle in ['full', 'insanity_legacy']:
|
for shuffle in ['full', 'insanity_legacy']:
|
||||||
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
|
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
|
||||||
for retro in [True, False]:
|
for retro in [True, False]:
|
||||||
for door_shuffle in ['basic', 'crossed', 'vanilla']:
|
for bombbag in [True, False]:
|
||||||
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, door_shuffle, logic)
|
for door_shuffle in ['basic', 'crossed', 'vanilla']:
|
||||||
count = len(out[0]) + len(out[1])
|
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic)
|
||||||
|
count = len(out[0]) + len(out[1])
|
||||||
|
|
||||||
correct_count = total_items_to_place
|
correct_count = total_items_to_place
|
||||||
if goal == 'pedestal' and swords != 'vanilla':
|
if goal == 'pedestal' and swords != 'vanilla':
|
||||||
# pedestal goals generate one extra item
|
# pedestal goals generate one extra item
|
||||||
correct_count += 1
|
correct_count += 1
|
||||||
if retro:
|
if retro:
|
||||||
correct_count += 28
|
correct_count += 28
|
||||||
try:
|
try:
|
||||||
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro))
|
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag))
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test()
|
test()
|
||||||
|
|||||||
+427
-108
@@ -2,9 +2,9 @@ import itertools
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
|
||||||
from BaseClasses import DoorType
|
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
|
||||||
from Regions import dungeon_events
|
from Regions import dungeon_events
|
||||||
from Dungeons import dungeon_keys, dungeon_bigs
|
from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table
|
||||||
from DungeonGenerator import ExplorationState, special_big_key_doors
|
from DungeonGenerator import ExplorationState, special_big_key_doors
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +25,9 @@ class KeyLayout(object):
|
|||||||
self.all_locations = set()
|
self.all_locations = set()
|
||||||
self.item_locations = set()
|
self.item_locations = set()
|
||||||
|
|
||||||
|
self.found_doors = set()
|
||||||
|
self.prize_relevant = None
|
||||||
|
self.prize_can_lock = None # if true, then you may need to beat the bo
|
||||||
# bk special?
|
# bk special?
|
||||||
# bk required? True if big chests or big doors exists
|
# bk required? True if big chests or big doors exists
|
||||||
|
|
||||||
@@ -35,6 +38,7 @@ class KeyLayout(object):
|
|||||||
self.max_chests = calc_max_chests(builder, self, world, player)
|
self.max_chests = calc_max_chests(builder, self, world, player)
|
||||||
self.all_locations = set()
|
self.all_locations = set()
|
||||||
self.item_locations = set()
|
self.item_locations = set()
|
||||||
|
self.prize_relevant = None
|
||||||
|
|
||||||
|
|
||||||
class KeyLogic(object):
|
class KeyLogic(object):
|
||||||
@@ -54,10 +58,12 @@ class KeyLogic(object):
|
|||||||
self.location_rules = {}
|
self.location_rules = {}
|
||||||
self.outside_keys = 0
|
self.outside_keys = 0
|
||||||
self.dungeon = dungeon_name
|
self.dungeon = dungeon_name
|
||||||
|
self.sm_doors = {}
|
||||||
|
self.prize_location = None
|
||||||
|
|
||||||
def check_placement(self, unplaced_keys, big_key_loc=None):
|
def check_placement(self, unplaced_keys, big_key_loc=None, prize_loc=None, cr_count=7):
|
||||||
for rule in self.placement_rules:
|
for rule in self.placement_rules:
|
||||||
if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
|
if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc, prize_loc, cr_count):
|
||||||
return False
|
return False
|
||||||
if big_key_loc:
|
if big_key_loc:
|
||||||
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
|
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
|
||||||
@@ -65,6 +71,15 @@ class KeyLogic(object):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.door_rules.clear()
|
||||||
|
self.bk_restricted.clear()
|
||||||
|
self.bk_locked.clear()
|
||||||
|
self.sm_restricted.clear()
|
||||||
|
self.bk_doors.clear()
|
||||||
|
self.bk_chests.clear()
|
||||||
|
self.placement_rules.clear()
|
||||||
|
|
||||||
|
|
||||||
class DoorRules(object):
|
class DoorRules(object):
|
||||||
|
|
||||||
@@ -79,6 +94,8 @@ class DoorRules(object):
|
|||||||
self.small_location = None
|
self.small_location = None
|
||||||
self.opposite = None
|
self.opposite = None
|
||||||
|
|
||||||
|
self.new_rules = {} # keyed by type, or type+lock_item -> number
|
||||||
|
|
||||||
|
|
||||||
class LocationRule(object):
|
class LocationRule(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -101,9 +118,11 @@ class PlacementRule(object):
|
|||||||
self.needed_keys_w_bk = None
|
self.needed_keys_w_bk = None
|
||||||
self.needed_keys_wo_bk = None
|
self.needed_keys_wo_bk = None
|
||||||
self.check_locations_w_bk = None
|
self.check_locations_w_bk = None
|
||||||
|
self.special_bk_avail = False
|
||||||
self.check_locations_wo_bk = None
|
self.check_locations_wo_bk = None
|
||||||
self.bk_relevant = True
|
self.bk_relevant = True
|
||||||
self.key_reduced = False
|
self.key_reduced = False
|
||||||
|
self.prize_relevance = None
|
||||||
|
|
||||||
def contradicts(self, rule, unplaced_keys, big_key_loc):
|
def contradicts(self, rule, unplaced_keys, big_key_loc):
|
||||||
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
|
bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False
|
||||||
@@ -112,6 +131,10 @@ class PlacementRule(object):
|
|||||||
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
|
rule_locations = rule.check_locations_wo_bk if rule_blocked else rule.check_locations_w_bk
|
||||||
if check_locations is None or rule_locations is None:
|
if check_locations is None or rule_locations is None:
|
||||||
return False
|
return False
|
||||||
|
if not bk_blocked and big_key_loc not in check_locations: # bk is not available, so rule doesn't apply
|
||||||
|
return False
|
||||||
|
if not rule_blocked and big_key_loc not in rule_locations: # bk is not available, so rule doesn't apply
|
||||||
|
return False
|
||||||
check_locations = check_locations - {big_key_loc}
|
check_locations = check_locations - {big_key_loc}
|
||||||
rule_locations = rule_locations - {big_key_loc}
|
rule_locations = rule_locations - {big_key_loc}
|
||||||
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
|
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
|
||||||
@@ -134,13 +157,30 @@ class PlacementRule(object):
|
|||||||
left -= rule_needed
|
left -= rule_needed
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_satisfiable(self, outside_keys, unplaced_keys):
|
def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc, prize_location, cr_count):
|
||||||
|
if self.prize_relevance and prize_location:
|
||||||
|
if self.prize_relevance == 'BigBomb':
|
||||||
|
if prize_location.item.name not in ['Crystal 5', 'Crystal 6']:
|
||||||
|
return True
|
||||||
|
elif self.prize_relevance == 'GT':
|
||||||
|
if 'Crystal' not in prize_location.item.name or cr_count < 7:
|
||||||
|
return True
|
||||||
bk_blocked = False
|
bk_blocked = False
|
||||||
if self.bk_conditional_set:
|
if self.bk_conditional_set:
|
||||||
for loc in self.bk_conditional_set:
|
for loc in self.bk_conditional_set:
|
||||||
if loc.item and loc.item.bigkey:
|
if loc.item and loc.item.bigkey:
|
||||||
bk_blocked = True
|
bk_blocked = True
|
||||||
break
|
break
|
||||||
|
elif len(self.check_locations_w_bk) > self.needed_keys_w_bk:
|
||||||
|
def loc_has_bk(l):
|
||||||
|
return (big_key_loc is not None and big_key_loc == l) or (l.item and l.item.bigkey)
|
||||||
|
|
||||||
|
# todo: sometimes the bk avail rule doesn't mean the bk must be avail or this rule is invalid
|
||||||
|
# but sometimes it certainly does
|
||||||
|
# check threshold vs len(check_loc) maybe to determine bk isn't relevant?
|
||||||
|
bk_found = self.special_bk_avail or any(loc for loc in self.check_locations_w_bk if loc_has_bk(loc))
|
||||||
|
if not bk_found:
|
||||||
|
return True
|
||||||
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
|
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
|
||||||
if not bk_blocked and check_locations is None:
|
if not bk_blocked and check_locations is None:
|
||||||
return True
|
return True
|
||||||
@@ -170,6 +210,8 @@ class KeyCounter(object):
|
|||||||
self.important_location = False
|
self.important_location = False
|
||||||
self.other_locations = {}
|
self.other_locations = {}
|
||||||
self.important_locations = {}
|
self.important_locations = {}
|
||||||
|
self.prize_doors_opened = False
|
||||||
|
self.prize_received = False
|
||||||
|
|
||||||
def used_smalls_loc(self, reserve=0):
|
def used_smalls_loc(self, reserve=0):
|
||||||
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
|
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
|
||||||
@@ -209,16 +251,30 @@ def calc_max_chests(builder, key_layout, world, player):
|
|||||||
|
|
||||||
|
|
||||||
def analyze_dungeon(key_layout, world, player):
|
def analyze_dungeon(key_layout, world, player):
|
||||||
|
key_layout.key_logic.reset()
|
||||||
key_layout.key_counters = create_key_counters(key_layout, world, player)
|
key_layout.key_counters = create_key_counters(key_layout, world, player)
|
||||||
key_logic = key_layout.key_logic
|
key_logic = key_layout.key_logic
|
||||||
|
for door in key_layout.proposal:
|
||||||
|
if isinstance(door, tuple):
|
||||||
|
key_logic.sm_doors[door[0]] = door[1]
|
||||||
|
key_logic.sm_doors[door[1]] = door[0]
|
||||||
|
else:
|
||||||
|
if door.dest and door.type != DoorType.SpiralStairs:
|
||||||
|
key_logic.sm_doors[door] = door.dest
|
||||||
|
key_logic.sm_doors[door.dest] = door
|
||||||
|
else:
|
||||||
|
key_logic.sm_doors[door] = None
|
||||||
|
|
||||||
find_bk_locked_sections(key_layout, world, player)
|
find_bk_locked_sections(key_layout, world, player)
|
||||||
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||||
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
|
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
|
||||||
|
key_logic.prize_location = dungeon_table[key_layout.sector.name].prize
|
||||||
if world.retro[player] and world.mode[player] != 'standard':
|
if world.retro[player] and world.mode[player] != 'standard':
|
||||||
return
|
return
|
||||||
|
|
||||||
original_key_counter = find_counter({}, False, key_layout)
|
original_key_counter = find_counter({}, False, key_layout, False)
|
||||||
|
if key_layout.big_key_special and forced_big_key_avail(original_key_counter.other_locations) is not None:
|
||||||
|
original_key_counter = find_counter({}, True, key_layout, False)
|
||||||
queue = deque([(None, original_key_counter)])
|
queue = deque([(None, original_key_counter)])
|
||||||
doors_completed = set()
|
doors_completed = set()
|
||||||
visited_cid = set()
|
visited_cid = set()
|
||||||
@@ -237,28 +293,39 @@ def analyze_dungeon(key_layout, world, player):
|
|||||||
if not big_avail:
|
if not big_avail:
|
||||||
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
|
if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs:
|
||||||
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
|
key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations))
|
||||||
|
# note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop
|
||||||
|
# I don't like this exception here or there
|
||||||
|
elif available < possible_smalls and avail_bigs and non_big_locs > 0:
|
||||||
|
max_ctr = find_max_counter(key_layout)
|
||||||
|
bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations]
|
||||||
|
key_logic.bk_restricted.update(filter_big_chest(bk_lockdown))
|
||||||
# try to relax the rules here? - smallest requirement that doesn't force a softlock
|
# try to relax the rules here? - smallest requirement that doesn't force a softlock
|
||||||
child_queue = deque()
|
child_queue = deque()
|
||||||
for child in key_counter.child_doors.keys():
|
for child in key_counter.child_doors.keys():
|
||||||
if not child.bigKey or not key_layout.big_key_special or big_avail:
|
if can_open_door_by_counter(child, key_counter, key_layout, world, player):
|
||||||
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
|
odd_counter = create_odd_key_counter(child, key_counter, key_layout, world, player)
|
||||||
empty_flag = empty_counter(odd_counter)
|
empty_flag = empty_counter(odd_counter)
|
||||||
child_queue.append((child, odd_counter, empty_flag))
|
child_queue.append((child, odd_counter, empty_flag))
|
||||||
while len(child_queue) > 0:
|
while len(child_queue) > 0:
|
||||||
child, odd_counter, empty_flag = child_queue.popleft()
|
child, odd_counter, empty_flag = child_queue.popleft()
|
||||||
if not child.bigKey and child not in doors_completed:
|
prize_flag = key_counter.prize_doors_opened
|
||||||
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
|
if child in key_layout.flat_prop and child not in doors_completed:
|
||||||
rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
best_counter = find_best_counter(child, key_layout, odd_counter, False, empty_flag)
|
||||||
|
rule = create_rule(best_counter, key_counter, world, player)
|
||||||
|
create_worst_case_rule(rule, best_counter, world, player)
|
||||||
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
|
||||||
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
|
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
|
||||||
key_logic.door_rules[child.name] = rule
|
key_logic.door_rules[child.name] = rule
|
||||||
|
elif not child.bigKey and child not in doors_completed:
|
||||||
|
prize_flag = True
|
||||||
doors_completed.add(child)
|
doors_completed.add(child)
|
||||||
next_counter = find_next_counter(child, key_counter, key_layout)
|
next_counter = find_next_counter(child, key_counter, key_layout, prize_flag)
|
||||||
ctr_id = cid(next_counter, key_layout)
|
ctr_id = cid(next_counter, key_layout)
|
||||||
if ctr_id not in visited_cid:
|
if ctr_id not in visited_cid:
|
||||||
queue.append((child, next_counter))
|
queue.append((child, next_counter))
|
||||||
visited_cid.add(ctr_id)
|
visited_cid.add(ctr_id)
|
||||||
check_rules(original_key_counter, key_layout, world, player)
|
# todo: why is this commented out?
|
||||||
|
# check_rules(original_key_counter, key_layout, world, player)
|
||||||
|
|
||||||
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
|
# Flip bk rules if more restrictive, to prevent placing a big key in a softlocking location
|
||||||
for rule in key_logic.door_rules.values():
|
for rule in key_logic.door_rules.values():
|
||||||
@@ -274,6 +341,8 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
|||||||
key_logic = key_layout.key_logic
|
key_logic = key_layout.key_logic
|
||||||
max_ctr = find_max_counter(key_layout)
|
max_ctr = find_max_counter(key_layout)
|
||||||
for code, key_counter in key_layout.key_counters.items():
|
for code, key_counter in key_layout.key_counters.items():
|
||||||
|
if skip_key_counter_due_to_prize(key_layout, key_counter):
|
||||||
|
continue # we have the prize, we are not concerned about this case
|
||||||
accessible_loc = set()
|
accessible_loc = set()
|
||||||
accessible_loc.update(key_counter.free_locations)
|
accessible_loc.update(key_counter.free_locations)
|
||||||
accessible_loc.update(key_counter.key_only_locations)
|
accessible_loc.update(key_counter.key_only_locations)
|
||||||
@@ -294,13 +363,16 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
|||||||
else:
|
else:
|
||||||
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
||||||
rule.check_locations_w_bk = accessible_loc
|
rule.check_locations_w_bk = accessible_loc
|
||||||
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
if key_layout.big_key_special:
|
||||||
|
rule.special_bk_avail = forced_big_key_avail(key_counter.important_locations) is not None
|
||||||
|
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||||
else:
|
else:
|
||||||
if big_key_progress(key_counter) and only_sm_doors(key_counter):
|
if big_key_progress(key_counter) and only_sm_doors(key_counter):
|
||||||
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
|
create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, accessible_loc, min_keys, world, player)
|
||||||
rule.bk_conditional_set = blocked_loc
|
rule.bk_conditional_set = blocked_loc
|
||||||
rule.needed_keys_wo_bk = min_keys
|
rule.needed_keys_wo_bk = min_keys
|
||||||
rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
|
rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc))
|
||||||
|
rule.prize_relevance = key_layout.prize_relevant if rule_prize_relevant(key_counter) else None
|
||||||
if valid_rule:
|
if valid_rule:
|
||||||
key_logic.placement_rules.append(rule)
|
key_logic.placement_rules.append(rule)
|
||||||
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
|
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
|
||||||
@@ -308,6 +380,14 @@ def create_exhaustive_placement_rules(key_layout, world, player):
|
|||||||
refine_location_rules(key_layout)
|
refine_location_rules(key_layout)
|
||||||
|
|
||||||
|
|
||||||
|
def rule_prize_relevant(key_counter):
|
||||||
|
return not key_counter.prize_doors_opened and not key_counter.prize_received
|
||||||
|
|
||||||
|
|
||||||
|
def skip_key_counter_due_to_prize(key_layout, key_counter):
|
||||||
|
return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened
|
||||||
|
|
||||||
|
|
||||||
def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
|
def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, player):
|
||||||
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
|
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
|
||||||
blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
|
blocked_others = set(max_ctr.other_locations).difference(set(ctr.other_locations))
|
||||||
@@ -320,6 +400,7 @@ def placement_self_lock_adjustment(rule, max_ctr, blocked_loc, ctr, world, playe
|
|||||||
rule.needed_keys_w_bk -= 1
|
rule.needed_keys_w_bk -= 1
|
||||||
|
|
||||||
|
|
||||||
|
# this rule is suspect - commented out usages for now
|
||||||
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
|
def check_sm_restriction_needed(key_layout, max_ctr, rule, blocked):
|
||||||
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
|
if rule.needed_keys_w_bk == key_layout.max_chests + len(max_ctr.key_only_locations):
|
||||||
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
|
key_layout.key_logic.sm_restricted.update(blocked.difference(max_ctr.key_only_locations))
|
||||||
@@ -402,7 +483,7 @@ def refine_placement_rules(key_layout, max_ctr):
|
|||||||
if rule.needed_keys_wo_bk == 0:
|
if rule.needed_keys_wo_bk == 0:
|
||||||
rules_to_remove.append(rule)
|
rules_to_remove.append(rule)
|
||||||
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
|
if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests:
|
||||||
if len(rule.bk_conditional_set) > 0:
|
if not rule.prize_relevance and len(rule.bk_conditional_set) > 0:
|
||||||
key_logic.bk_restricted.update(rule.bk_conditional_set)
|
key_logic.bk_restricted.update(rule.bk_conditional_set)
|
||||||
rules_to_remove.append(rule)
|
rules_to_remove.append(rule)
|
||||||
changed = True # impossible for bk to be here, I think
|
changed = True # impossible for bk to be here, I think
|
||||||
@@ -413,7 +494,8 @@ def refine_placement_rules(key_layout, max_ctr):
|
|||||||
rule_b = temp
|
rule_b = temp
|
||||||
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
|
if rule_a.bk_conditional_set and rule_b.check_locations_w_bk:
|
||||||
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
|
common_needed = min(rule_a.needed_keys_wo_bk, rule_b.needed_keys_w_bk)
|
||||||
if len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk) < common_needed:
|
common_locs = len(rule_b.check_locations_w_bk & rule_a.check_locations_wo_bk)
|
||||||
|
if (common_needed - common_locs) * 2 > key_layout.max_chests:
|
||||||
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
|
key_logic.bk_restricted.update(rule_a.bk_conditional_set)
|
||||||
rules_to_remove.append(rule_a)
|
rules_to_remove.append(rule_a)
|
||||||
changed = True
|
changed = True
|
||||||
@@ -478,7 +560,7 @@ def create_inclusive_rule(key_layout, max_ctr, code, key_counter, blocked_loc, a
|
|||||||
else:
|
else:
|
||||||
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
placement_self_lock_adjustment(rule, max_ctr, blocked_loc, key_counter, world, player)
|
||||||
rule.check_locations_w_bk = accessible_loc
|
rule.check_locations_w_bk = accessible_loc
|
||||||
check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
# check_sm_restriction_needed(key_layout, max_ctr, rule, blocked_loc)
|
||||||
key_logic.placement_rules.append(rule)
|
key_logic.placement_rules.append(rule)
|
||||||
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
|
adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr)
|
||||||
|
|
||||||
@@ -538,6 +620,8 @@ def relative_empty_counter(odd_counter, key_counter):
|
|||||||
return False
|
return False
|
||||||
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
|
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
|
||||||
return False
|
return False
|
||||||
|
if len(set(odd_counter.other_locations).difference(key_counter.other_locations)) > 0:
|
||||||
|
return False
|
||||||
# important only
|
# important only
|
||||||
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
|
if len(set(odd_counter.important_locations).difference(key_counter.important_locations)) > 0:
|
||||||
return False
|
return False
|
||||||
@@ -594,33 +678,50 @@ def unique_child_door_2(child, key_counter):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
|
# def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
|
||||||
ignored_doors = {door, door.dest} if door is not None else {}
|
# ignored_doors = {door, door.dest} if door is not None else {}
|
||||||
finished = False
|
# finished = False
|
||||||
opened_doors = dict(key_counter.open_doors)
|
# opened_doors = dict(key_counter.open_doors)
|
||||||
bk_opened = key_counter.big_key_opened
|
# bk_opened = key_counter.big_key_opened
|
||||||
# new_counter = key_counter
|
# # new_counter = key_counter
|
||||||
last_counter = key_counter
|
# last_counter = key_counter
|
||||||
while not finished:
|
# while not finished:
|
||||||
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
|
# door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk)
|
||||||
if door_set is None or len(door_set) == 0:
|
# if door_set is None or len(door_set) == 0:
|
||||||
finished = True
|
# finished = True
|
||||||
continue
|
# continue
|
||||||
for new_door in door_set:
|
# for new_door in door_set:
|
||||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
# proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||||
bk_open = bk_opened or new_door.bigKey
|
# bk_open = bk_opened or new_door.bigKey
|
||||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
# new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
||||||
bk_open = new_counter.big_key_opened
|
# bk_open = new_counter.big_key_opened
|
||||||
# this means the new_door invalidates the door / leads to the same stuff
|
# # this means the new_door invalidates the door / leads to the same stuff
|
||||||
if not empty_flag and relative_empty_counter(odd_counter, new_counter):
|
# if not empty_flag and relative_empty_counter(odd_counter, new_counter):
|
||||||
ignored_doors.add(new_door)
|
# ignored_doors.add(new_door)
|
||||||
elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
|
# elif empty_flag or key_wasted(new_door, door, last_counter, new_counter, key_layout, world, player):
|
||||||
last_counter = new_counter
|
# last_counter = new_counter
|
||||||
opened_doors = proposed_doors
|
# opened_doors = proposed_doors
|
||||||
bk_opened = bk_open
|
# bk_opened = bk_open
|
||||||
else:
|
# else:
|
||||||
ignored_doors.add(new_door)
|
# ignored_doors.add(new_door)
|
||||||
return last_counter
|
# return last_counter
|
||||||
|
|
||||||
|
|
||||||
|
def find_best_counter(door, key_layout, odd_counter, skip_bk, empty_flag):
|
||||||
|
best, best_ctr, locations = 0, None, 0
|
||||||
|
for code, counter in key_layout.key_counters.items():
|
||||||
|
if door not in counter.open_doors:
|
||||||
|
if best_ctr is None or counter.used_keys > best or (counter.used_keys == best and count_locations(counter) > locations):
|
||||||
|
if not skip_bk or not counter.big_key_opened:
|
||||||
|
if empty_flag or not relative_empty_counter(odd_counter, counter):
|
||||||
|
best = counter.used_keys
|
||||||
|
best_ctr = counter
|
||||||
|
locations = count_locations(counter)
|
||||||
|
return best_ctr
|
||||||
|
|
||||||
|
|
||||||
|
def count_locations(ctr):
|
||||||
|
return len(ctr.free_locations) + len(ctr.key_only_locations) + len(ctr.other_locations) + len(ctr.important_locations)
|
||||||
|
|
||||||
|
|
||||||
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
|
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
|
||||||
@@ -638,7 +739,7 @@ def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): #
|
|||||||
for new_door in door_set:
|
for new_door in door_set:
|
||||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||||
bk_open = bk_opened or new_door.bigKey
|
bk_open = bk_opened or new_door.bigKey
|
||||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
new_counter = find_counter(proposed_doors, bk_open, key_layout, key_counter.prize_doors_opened)
|
||||||
bk_open = new_counter.big_key_opened
|
bk_open = new_counter.big_key_opened
|
||||||
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
|
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
|
||||||
ignored_doors.add(new_door)
|
ignored_doors.add(new_door)
|
||||||
@@ -690,7 +791,7 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
|
|||||||
for new_child in new_children:
|
for new_child in new_children:
|
||||||
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
|
proposed_doors = {**opened_doors, **dict.fromkeys([new_child, new_child.dest])}
|
||||||
bk_open = bk_opened or new_door.bigKey
|
bk_open = bk_opened or new_door.bigKey
|
||||||
new_counter = find_counter(proposed_doors, bk_open, key_layout)
|
new_counter = find_counter(proposed_doors, bk_open, key_layout, current_counter.prize_doors_opened)
|
||||||
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
|
if key_wasted(new_child, old_door, current_counter, new_counter, key_layout, world, player):
|
||||||
wasted_keys += 1
|
wasted_keys += 1
|
||||||
if new_avail - wasted_keys < old_avail:
|
if new_avail - wasted_keys < old_avail:
|
||||||
@@ -698,10 +799,11 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world,
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def find_next_counter(new_door, old_counter, key_layout):
|
def find_next_counter(new_door, old_counter, key_layout, prize_flag=None):
|
||||||
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
|
proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])}
|
||||||
bk_open = old_counter.big_key_opened or new_door.bigKey
|
bk_open = old_counter.big_key_opened or new_door.bigKey
|
||||||
return find_counter(proposed_doors, bk_open, key_layout)
|
prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened
|
||||||
|
return find_counter(proposed_doors, bk_open, key_layout, prize_flag)
|
||||||
|
|
||||||
|
|
||||||
def check_special_locations(locations):
|
def check_special_locations(locations):
|
||||||
@@ -717,7 +819,7 @@ def calc_avail_keys(key_counter, world, player):
|
|||||||
return raw_avail - key_counter.used_keys
|
return raw_avail - key_counter.used_keys
|
||||||
|
|
||||||
|
|
||||||
def create_rule(key_counter, prev_counter, key_layout, world, player):
|
def create_rule(key_counter, prev_counter, world, player):
|
||||||
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
|
# prev_chest_keys = available_chest_small_keys(prev_counter, world)
|
||||||
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
|
# prev_avail = prev_chest_keys + len(prev_counter.key_only_locations)
|
||||||
chest_keys = available_chest_small_keys(key_counter, world, player)
|
chest_keys = available_chest_small_keys(key_counter, world, player)
|
||||||
@@ -736,6 +838,11 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
|
|||||||
return DoorRules(rule_num, is_valid)
|
return DoorRules(rule_num, is_valid)
|
||||||
|
|
||||||
|
|
||||||
|
def create_worst_case_rule(rules, key_counter, world, player):
|
||||||
|
required_keys = key_counter.used_keys + 1 # this makes more sense, if key_counter has wasted all keys
|
||||||
|
rules.new_rules[KeyRuleType.WorstCase] = required_keys
|
||||||
|
|
||||||
|
|
||||||
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
|
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
|
||||||
if world.accessibility[player] != 'locations':
|
if world.accessibility[player] != 'locations':
|
||||||
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
|
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
|
||||||
@@ -744,11 +851,12 @@ def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, playe
|
|||||||
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
|
if len(counter.free_locations) == 1 and len(counter.key_only_locations) == 0 and not counter.important_location:
|
||||||
rule.allow_small = True
|
rule.allow_small = True
|
||||||
rule.small_location = next(iter(counter.free_locations))
|
rule.small_location = next(iter(counter.free_locations))
|
||||||
|
rule.new_rules[KeyRuleType.AllowSmall] = rule.new_rules[KeyRuleType.WorstCase] - 1
|
||||||
|
|
||||||
|
|
||||||
def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
||||||
# open all doors in counter
|
# open all doors in counter
|
||||||
counter = open_all_counter(parent_counter, key_layout, door=door)
|
counter = open_all_counter(parent_counter, key_layout, world, player, door=door)
|
||||||
max_counter = find_max_counter(key_layout)
|
max_counter = find_max_counter(key_layout)
|
||||||
# find the difference
|
# find the difference
|
||||||
inverted_counter = KeyCounter(key_layout.max_chests)
|
inverted_counter = KeyCounter(key_layout.max_chests)
|
||||||
@@ -764,7 +872,7 @@ def find_inverted_counter(door, parent_counter, key_layout, world, player):
|
|||||||
return inverted_counter
|
return inverted_counter
|
||||||
|
|
||||||
|
|
||||||
def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
|
def open_all_counter(parent_counter, key_layout, world, player, door=None, skipBk=False):
|
||||||
changed = True
|
changed = True
|
||||||
counter = parent_counter
|
counter = parent_counter
|
||||||
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
|
proposed_doors = dict.fromkeys(parent_counter.open_doors.keys())
|
||||||
@@ -776,14 +884,12 @@ def open_all_counter(parent_counter, key_layout, door=None, skipBk=False):
|
|||||||
if skipBk:
|
if skipBk:
|
||||||
if not child.bigKey:
|
if not child.bigKey:
|
||||||
doors_to_open[child] = None
|
doors_to_open[child] = None
|
||||||
elif not child.bigKey or not key_layout.big_key_special or counter.big_key_opened:
|
elif can_open_door_by_counter(child, counter, key_layout, world, player):
|
||||||
doors_to_open[child] = None
|
doors_to_open[child] = None
|
||||||
if len(doors_to_open.keys()) > 0:
|
if len(doors_to_open.keys()) > 0:
|
||||||
proposed_doors = {**proposed_doors, **doors_to_open}
|
proposed_doors = {**proposed_doors, **doors_to_open}
|
||||||
bk_hint = counter.big_key_opened
|
bk_hint = counter.big_key_opened or any(d.bigKey for d in doors_to_open.keys())
|
||||||
for d in doors_to_open.keys():
|
counter = find_counter(proposed_doors, bk_hint, key_layout, True)
|
||||||
bk_hint = bk_hint or d.bigKey
|
|
||||||
counter = find_counter(proposed_doors, bk_hint, key_layout)
|
|
||||||
changed = True
|
changed = True
|
||||||
return counter
|
return counter
|
||||||
|
|
||||||
@@ -804,7 +910,7 @@ def open_some_counter(parent_counter, key_layout, ignored_doors):
|
|||||||
bk_hint = counter.big_key_opened
|
bk_hint = counter.big_key_opened
|
||||||
for d in doors_to_open.keys():
|
for d in doors_to_open.keys():
|
||||||
bk_hint = bk_hint or d.bigKey
|
bk_hint = bk_hint or d.bigKey
|
||||||
counter = find_counter(proposed_doors, bk_hint, key_layout)
|
counter = find_counter(proposed_doors, bk_hint, key_layout, parent_counter.prize_doors_opened)
|
||||||
changed = True
|
changed = True
|
||||||
return counter
|
return counter
|
||||||
|
|
||||||
@@ -845,16 +951,16 @@ def big_key_drop_available(key_counter):
|
|||||||
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
|
def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_layout, world, player):
|
||||||
if key_counter.big_key_opened:
|
if key_counter.big_key_opened:
|
||||||
return
|
return
|
||||||
best_counter = find_best_counter(door, odd_counter, key_counter, key_layout, world, player, True, empty_flag)
|
best_counter = find_best_counter(door, key_layout, odd_counter, True, empty_flag)
|
||||||
bk_rule = create_rule(best_counter, key_counter, key_layout, world, player)
|
bk_rule = create_rule(best_counter, key_counter, world, player)
|
||||||
if bk_rule.small_key_num >= rule.small_key_num:
|
if bk_rule.small_key_num >= rule.small_key_num:
|
||||||
return
|
return
|
||||||
door_open = find_next_counter(door, best_counter, key_layout)
|
door_open = find_next_counter(door, best_counter, key_layout)
|
||||||
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
|
ignored_doors = dict_intersection(best_counter.child_doors, door_open.child_doors)
|
||||||
dest_ignored = []
|
dest_ignored = []
|
||||||
for door in ignored_doors.keys():
|
for d in ignored_doors.keys():
|
||||||
if door.dest not in ignored_doors:
|
if d.dest not in ignored_doors:
|
||||||
dest_ignored.append(door.dest)
|
dest_ignored.append(d.dest)
|
||||||
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
|
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
|
||||||
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
|
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
|
||||||
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
|
unique_loc = dict_difference(post_counter.free_locations, best_counter.free_locations)
|
||||||
@@ -862,8 +968,8 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
|
|||||||
if len(unique_loc) > 0: # and bk_rule.is_valid
|
if len(unique_loc) > 0: # and bk_rule.is_valid
|
||||||
rule.alternate_small_key = bk_rule.small_key_num
|
rule.alternate_small_key = bk_rule.small_key_num
|
||||||
rule.alternate_big_key_loc.update(unique_loc)
|
rule.alternate_big_key_loc.update(unique_loc)
|
||||||
# elif not bk_rule.is_valid:
|
if not door.bigKey:
|
||||||
# key_layout.key_logic.bk_restricted.update(unique_loc)
|
rule.new_rules[(KeyRuleType.Lock, key_layout.key_logic.bk_name)] = best_counter.used_keys + 1
|
||||||
|
|
||||||
|
|
||||||
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
|
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
|
||||||
@@ -887,12 +993,19 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c
|
|||||||
return worst_counter, post_counter, bk_rule_num
|
return worst_counter, post_counter, bk_rule_num
|
||||||
|
|
||||||
|
|
||||||
def open_a_door(door, child_state, flat_proposal):
|
def open_a_door(door, child_state, flat_proposal, world, player):
|
||||||
if door.bigKey or door.name in special_big_key_doors:
|
if door.bigKey or door.name in special_big_key_doors:
|
||||||
child_state.big_key_opened = True
|
child_state.big_key_opened = True
|
||||||
child_state.avail_doors.extend(child_state.big_doors)
|
child_state.avail_doors.extend(child_state.big_doors)
|
||||||
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
|
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
|
||||||
child_state.big_doors.clear()
|
child_state.big_doors.clear()
|
||||||
|
elif door in child_state.prize_door_set:
|
||||||
|
child_state.prize_doors_opened = True
|
||||||
|
for exp_door in child_state.prize_doors:
|
||||||
|
new_region = exp_door.door.entrance.parent_region
|
||||||
|
child_state.visit_region(new_region, key_checks=True)
|
||||||
|
child_state.add_all_doors_check_keys(new_region, flat_proposal, world, player)
|
||||||
|
child_state.prize_doors.clear()
|
||||||
else:
|
else:
|
||||||
child_state.opened_doors.append(door)
|
child_state.opened_doors.append(door)
|
||||||
doors_to_open = [x for x in child_state.small_doors if x.door == door]
|
doors_to_open = [x for x in child_state.small_doors if x.door == door]
|
||||||
@@ -935,6 +1048,7 @@ def only_sm_doors(key_counter):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# doesn't count dest doors
|
# doesn't count dest doors
|
||||||
def count_unique_small_doors(key_counter, proposal):
|
def count_unique_small_doors(key_counter, proposal):
|
||||||
cnt = 0
|
cnt = 0
|
||||||
@@ -949,7 +1063,7 @@ def count_unique_small_doors(key_counter, proposal):
|
|||||||
|
|
||||||
|
|
||||||
def exist_relevant_big_doors(key_counter, key_layout):
|
def exist_relevant_big_doors(key_counter, key_layout):
|
||||||
bk_counter = find_counter(key_counter.open_doors, True, key_layout, False)
|
bk_counter = find_counter(key_counter.open_doors, True, key_layout, key_counter.prize_doors_opened, False)
|
||||||
if bk_counter is not None:
|
if bk_counter is not None:
|
||||||
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
|
diff = dict_difference(bk_counter.free_locations, key_counter.free_locations)
|
||||||
if len(diff) > 0:
|
if len(diff) > 0:
|
||||||
@@ -982,14 +1096,6 @@ def filter_big_chest(locations):
|
|||||||
return [x for x in locations if '- Big Chest' not in x.name]
|
return [x for x in locations if '- Big Chest' not in x.name]
|
||||||
|
|
||||||
|
|
||||||
def count_locations_exclude_logic(locations, key_logic):
|
|
||||||
cnt = 0
|
|
||||||
for loc in locations:
|
|
||||||
if not location_is_bk_locked(loc, key_logic) and not loc.forced_item and not prize_or_event(loc):
|
|
||||||
cnt += 1
|
|
||||||
return cnt
|
|
||||||
|
|
||||||
|
|
||||||
def location_is_bk_locked(loc, key_logic):
|
def location_is_bk_locked(loc, key_logic):
|
||||||
return loc in key_logic.bk_chests or loc in key_logic.bk_locked
|
return loc in key_logic.bk_chests or loc in key_logic.bk_locked
|
||||||
|
|
||||||
@@ -998,18 +1104,36 @@ def prize_or_event(loc):
|
|||||||
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||||
|
|
||||||
|
|
||||||
def count_free_locations(state):
|
def boss_unavail(loc, world, player):
|
||||||
|
# todo: ambrosia
|
||||||
|
# return world.bossdrops[player] == 'ambrosia' and "- Boss" in loc.name
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def blind_boss_unavail(loc, state, world, player):
|
||||||
|
if loc.name == "Thieves' Town - Boss":
|
||||||
|
# todo: check attic
|
||||||
|
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
||||||
|
(not any(x for x in state.found_locations if x.name == 'Suspicious Maiden') or
|
||||||
|
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
|
||||||
|
not any(x for x in state.found_locations if x.name == 'Attic Cracked Floor'))))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def count_free_locations(state, world, player):
|
||||||
cnt = 0
|
cnt = 0
|
||||||
for loc in state.found_locations:
|
for loc in state.found_locations:
|
||||||
if not prize_or_event(loc) and not loc.forced_item:
|
if (not prize_or_event(loc) and not loc.forced_item and not boss_unavail(loc, world, player)
|
||||||
|
and not blind_boss_unavail(loc, state, world, player)):
|
||||||
cnt += 1
|
cnt += 1
|
||||||
return cnt
|
return cnt
|
||||||
|
|
||||||
|
|
||||||
def count_locations_exclude_big_chest(state):
|
def count_locations_exclude_big_chest(state, world, player):
|
||||||
cnt = 0
|
cnt = 0
|
||||||
for loc in state.found_locations:
|
for loc in state.found_locations:
|
||||||
if '- Big Chest' not in loc.name and not loc.forced_item and not prize_or_event(loc):
|
if ('- Big Chest' not in loc.name and not loc.forced_item and not boss_unavail(loc, world, player)
|
||||||
|
and not prize_or_event(loc) and not blind_boss_unavail(loc, state, world, player)):
|
||||||
cnt += 1
|
cnt += 1
|
||||||
return cnt
|
return cnt
|
||||||
|
|
||||||
@@ -1197,7 +1321,7 @@ def check_rules_deep(original_counter, key_layout, world, player):
|
|||||||
elif not door.bigKey:
|
elif not door.bigKey:
|
||||||
can_open = True
|
can_open = True
|
||||||
if can_open:
|
if can_open:
|
||||||
can_progress = smalls_opened or not big_maybe_not_found
|
can_progress = (big_avail or not big_maybe_not_found) if door.bigKey else smalls_opened
|
||||||
next_counter = find_next_counter(door, counter, key_layout)
|
next_counter = find_next_counter(door, counter, key_layout)
|
||||||
c_id = cid(next_counter, key_layout)
|
c_id = cid(next_counter, key_layout)
|
||||||
if c_id not in completed:
|
if c_id not in completed:
|
||||||
@@ -1265,6 +1389,22 @@ def check_bk_special(regions, world, player):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def forced_big_key_avail(locations):
|
||||||
|
for loc in locations:
|
||||||
|
if loc.forced_big_key():
|
||||||
|
return loc
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def prize_relevance(key_layout, dungeon_entrance):
|
||||||
|
if len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_table[key_layout.key_logic.dungeon].prize:
|
||||||
|
if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']:
|
||||||
|
return 'GT'
|
||||||
|
elif dungeon_entrance.name == 'Pyramid Fairy':
|
||||||
|
return 'BigBomb'
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Soft lock stuff
|
# Soft lock stuff
|
||||||
def validate_key_layout(key_layout, world, player):
|
def validate_key_layout(key_layout, world, player):
|
||||||
# retro is all good - except for hyrule castle in standard mode
|
# retro is all good - except for hyrule castle in standard mode
|
||||||
@@ -1275,8 +1415,15 @@ def validate_key_layout(key_layout, world, player):
|
|||||||
state.key_locations = key_layout.max_chests
|
state.key_locations = key_layout.max_chests
|
||||||
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
|
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
|
||||||
for region in key_layout.start_regions:
|
for region in key_layout.start_regions:
|
||||||
state.visit_region(region, key_checks=True)
|
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||||
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
|
||||||
|
if prize_relevant_flag:
|
||||||
|
state.append_door_to_list(portal_door, state.prize_doors)
|
||||||
|
state.prize_door_set[portal_door] = dungeon_entrance
|
||||||
|
key_layout.prize_relevant = prize_relevant_flag
|
||||||
|
else:
|
||||||
|
state.visit_region(region, key_checks=True)
|
||||||
|
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
||||||
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
|
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
|
||||||
|
|
||||||
|
|
||||||
@@ -1287,7 +1434,10 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
|||||||
if not smalls_avail and num_bigs == 0:
|
if not smalls_avail and num_bigs == 0:
|
||||||
return True # I think that's the end
|
return True # I think that's the end
|
||||||
# todo: fix state to separate out these types
|
# todo: fix state to separate out these types
|
||||||
ttl_locations = count_free_locations(state) if state.big_key_opened else count_locations_exclude_big_chest(state)
|
if state.big_key_opened:
|
||||||
|
ttl_locations = count_free_locations(state, world, player)
|
||||||
|
else:
|
||||||
|
ttl_locations = count_locations_exclude_big_chest(state, world, player)
|
||||||
ttl_small_key_only = count_small_key_only_locations(state)
|
ttl_small_key_only = count_small_key_only_locations(state)
|
||||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||||
@@ -1298,17 +1448,23 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
|||||||
found_forced_bk = state.found_forced_bk()
|
found_forced_bk = state.found_forced_bk()
|
||||||
smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations)
|
smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations)
|
||||||
bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
|
bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk)
|
||||||
if smalls_done and bk_done:
|
# prize door should not be opened if the boss is reachable - but not reached yet
|
||||||
|
allow_for_prize_lock = (key_layout.prize_can_lock and
|
||||||
|
not any(x for x in state.found_locations if '- Prize' in x.name))
|
||||||
|
prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock
|
||||||
|
if smalls_done and bk_done and prize_done:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
# todo: pretty sure you should OR these paths together, maybe when there's one location and it can
|
||||||
|
# either be small or big key
|
||||||
if smalls_avail and available_small_locations > 0:
|
if smalls_avail and available_small_locations > 0:
|
||||||
for exp_door in state.small_doors:
|
for exp_door in state.small_doors:
|
||||||
state_copy = state.copy()
|
state_copy = state.copy()
|
||||||
open_a_door(exp_door.door, state_copy, flat_proposal)
|
open_a_door(exp_door.door, state_copy, flat_proposal, world, player)
|
||||||
state_copy.used_smalls += 1
|
state_copy.used_smalls += 1
|
||||||
if state_copy.used_smalls > ttl_small_key_only:
|
if state_copy.used_smalls > ttl_small_key_only:
|
||||||
state_copy.used_locations += 1
|
state_copy.used_locations += 1
|
||||||
code = state_id(state_copy, flat_proposal)
|
code = validate_id(state_copy, flat_proposal)
|
||||||
if code not in checked_states.keys():
|
if code not in checked_states.keys():
|
||||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||||
state, available_small_locations, world, player)
|
state, available_small_locations, world, player)
|
||||||
@@ -1319,10 +1475,24 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
|||||||
return False
|
return False
|
||||||
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
|
if not state.big_key_opened and (available_big_locations >= num_bigs > 0 or (found_forced_bk and num_bigs > 0)):
|
||||||
state_copy = state.copy()
|
state_copy = state.copy()
|
||||||
open_a_door(state.big_doors[0].door, state_copy, flat_proposal)
|
open_a_door(state.big_doors[0].door, state_copy, flat_proposal, world, player)
|
||||||
if not found_forced_bk:
|
if not found_forced_bk:
|
||||||
state_copy.used_locations += 1
|
state_copy.used_locations += 1
|
||||||
code = state_id(state_copy, flat_proposal)
|
code = validate_id(state_copy, flat_proposal)
|
||||||
|
if code not in checked_states.keys():
|
||||||
|
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||||
|
state, available_small_locations, world, player)
|
||||||
|
checked_states[code] = valid
|
||||||
|
else:
|
||||||
|
valid = checked_states[code]
|
||||||
|
if not valid:
|
||||||
|
return False
|
||||||
|
# todo: feel like you only open these if the boss is available???
|
||||||
|
# todo: or if a crystal isn't valid placement on this boss
|
||||||
|
if not state.prize_doors_opened and key_layout.prize_relevant:
|
||||||
|
state_copy = state.copy()
|
||||||
|
open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player)
|
||||||
|
code = validate_id(state_copy, flat_proposal)
|
||||||
if code not in checked_states.keys():
|
if code not in checked_states.keys():
|
||||||
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
valid = validate_key_layout_sub_loop(key_layout, state_copy, checked_states, flat_proposal,
|
||||||
state, available_small_locations, world, player)
|
state, available_small_locations, world, player)
|
||||||
@@ -1341,7 +1511,7 @@ def invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, p
|
|||||||
state_copy = state.copy()
|
state_copy = state.copy()
|
||||||
while len(new_bk_doors) > 0:
|
while len(new_bk_doors) > 0:
|
||||||
for door in new_bk_doors:
|
for door in new_bk_doors:
|
||||||
open_a_door(door.door, state_copy, key_layout.flat_prop)
|
open_a_door(door.door, state_copy, key_layout.flat_prop, world, player)
|
||||||
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
|
new_bk_doors = set(state_copy.big_doors).difference(set(prev_state.big_doors))
|
||||||
expand_key_state(state_copy, key_layout.flat_prop, world, player)
|
expand_key_state(state_copy, key_layout.flat_prop, world, player)
|
||||||
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
|
new_locations = set(state_copy.found_locations).difference(set(prev_state.found_locations))
|
||||||
@@ -1365,6 +1535,39 @@ def enough_small_locations(state, avail_small_loc):
|
|||||||
return avail_small_loc >= len(unique_d_set)
|
return avail_small_loc >= len(unique_d_set)
|
||||||
|
|
||||||
|
|
||||||
|
def determine_prize_lock(key_layout, world, player):
|
||||||
|
if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle'))
|
||||||
|
or world.logic[player] == 'nologic'):
|
||||||
|
return # done, doesn't matter what
|
||||||
|
flat_proposal = key_layout.flat_prop
|
||||||
|
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||||
|
state.key_locations = key_layout.max_chests
|
||||||
|
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
|
||||||
|
prize_lock_possible = False
|
||||||
|
for region in key_layout.start_regions:
|
||||||
|
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||||
|
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
|
||||||
|
if prize_relevant_flag:
|
||||||
|
state.append_door_to_list(portal_door, state.prize_doors)
|
||||||
|
state.prize_door_set[portal_door] = dungeon_entrance
|
||||||
|
key_layout.prize_relevant = prize_relevant_flag
|
||||||
|
prize_lock_possible = True
|
||||||
|
else:
|
||||||
|
state.visit_region(region, key_checks=True)
|
||||||
|
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
||||||
|
if not prize_lock_possible:
|
||||||
|
return # done, no prize entrances to worry about
|
||||||
|
expand_key_state(state, flat_proposal, world, player)
|
||||||
|
while len(state.small_doors) > 0 or len(state.big_doors) > 0:
|
||||||
|
if len(state.big_doors) > 0:
|
||||||
|
open_a_door(state.big_doors[0].door, state, flat_proposal, world, player)
|
||||||
|
elif len(state.small_doors) > 0:
|
||||||
|
open_a_door(state.small_doors[0].door, state, flat_proposal, world, player)
|
||||||
|
expand_key_state(state, flat_proposal, world, player)
|
||||||
|
if any(x for x in state.found_locations if '- Prize' in x.name):
|
||||||
|
key_layout.prize_can_lock = True
|
||||||
|
|
||||||
|
|
||||||
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||||
if not world.keyshuffle[player] and not world.retro[player]:
|
if not world.keyshuffle[player] and not world.retro[player]:
|
||||||
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
|
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
|
||||||
@@ -1373,29 +1576,54 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
|||||||
return state.key_locations - state.used_smalls
|
return state.key_locations - state.used_smalls
|
||||||
|
|
||||||
|
|
||||||
|
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
|
||||||
|
if not world.keyshuffle[player] and not world.retro[player]:
|
||||||
|
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||||
|
avail_chest_keys = min(free_locations - bk_adj, layout.max_chests)
|
||||||
|
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
|
||||||
|
return layout.max_chests + len(counter.key_only_locations) - counter.used_keys
|
||||||
|
|
||||||
|
|
||||||
def cnt_avail_big_locations(ttl_locations, state, world, player):
|
def cnt_avail_big_locations(ttl_locations, state, world, player):
|
||||||
if not world.bigkeyshuffle[player]:
|
if not world.bigkeyshuffle[player]:
|
||||||
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
|
return max(0, ttl_locations - state.used_locations) if not state.big_key_special else 0
|
||||||
return 1 if not state.big_key_special else 0
|
return 1 if not state.big_key_special else 0
|
||||||
|
|
||||||
|
|
||||||
|
def cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player):
|
||||||
|
if not world.bigkeyshuffle[player]:
|
||||||
|
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||||
|
used_locations = max(0, counter.used_keys - len(counter.key_only_locations)) + bk_adj
|
||||||
|
return max(0, ttl_locations - used_locations) if not layout.big_key_special else 0
|
||||||
|
return 1 if not layout.big_key_special else 0
|
||||||
|
|
||||||
|
|
||||||
def create_key_counters(key_layout, world, player):
|
def create_key_counters(key_layout, world, player):
|
||||||
key_counters = {}
|
key_counters = {}
|
||||||
|
key_layout.found_doors.clear()
|
||||||
flat_proposal = key_layout.flat_prop
|
flat_proposal = key_layout.flat_prop
|
||||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||||
if world.doorShuffle[player] == 'vanilla':
|
if world.doorShuffle[player] == 'vanilla':
|
||||||
state.key_locations = len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
builder = world.dungeon_layouts[player][key_layout.sector.name]
|
||||||
|
state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
|
||||||
else:
|
else:
|
||||||
state.key_locations = world.dungeon_layouts[player][key_layout.sector.name].key_doors_num
|
builder = world.dungeon_layouts[player][key_layout.sector.name]
|
||||||
state.big_key_special, special_region = False, None
|
state.key_locations = builder.total_keys - builder.key_drop_cnt
|
||||||
|
state.big_key_special = False
|
||||||
for region in key_layout.sector.regions:
|
for region in key_layout.sector.regions:
|
||||||
for location in region.locations:
|
for location in region.locations:
|
||||||
if location.forced_big_key():
|
if location.forced_big_key():
|
||||||
state.big_key_special = True
|
state.big_key_special = True
|
||||||
special_region = region
|
|
||||||
for region in key_layout.start_regions:
|
for region in key_layout.start_regions:
|
||||||
state.visit_region(region, key_checks=True)
|
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||||
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance)
|
||||||
|
if prize_relevant_flag:
|
||||||
|
state.append_door_to_list(portal_door, state.prize_doors)
|
||||||
|
state.prize_door_set[portal_door] = dungeon_entrance
|
||||||
|
key_layout.prize_relevant = prize_relevant_flag
|
||||||
|
else:
|
||||||
|
state.visit_region(region, key_checks=True)
|
||||||
|
state.add_all_doors_check_keys(region, flat_proposal, world, player)
|
||||||
expand_key_state(state, flat_proposal, world, player)
|
expand_key_state(state, flat_proposal, world, player)
|
||||||
code = state_id(state, key_layout.flat_prop)
|
code = state_id(state, key_layout.flat_prop)
|
||||||
key_counters[code] = create_key_counter(state, key_layout, world, player)
|
key_counters[code] = create_key_counter(state, key_layout, world, player)
|
||||||
@@ -1403,12 +1631,15 @@ def create_key_counters(key_layout, world, player):
|
|||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
next_key_counter, parent_state = queue.popleft()
|
next_key_counter, parent_state = queue.popleft()
|
||||||
for door in next_key_counter.child_doors:
|
for door in next_key_counter.child_doors:
|
||||||
|
key_layout.found_doors.add(door)
|
||||||
|
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
|
||||||
|
key_layout.found_doors.add(door.dest)
|
||||||
child_state = parent_state.copy()
|
child_state = parent_state.copy()
|
||||||
if door.bigKey or door.name in special_big_key_doors:
|
if door.bigKey or door.name in special_big_key_doors:
|
||||||
key_layout.key_logic.bk_doors.add(door)
|
key_layout.key_logic.bk_doors.add(door)
|
||||||
# open the door, if possible
|
# open the door, if possible
|
||||||
if not door.bigKey or not child_state.big_key_special or child_state.visited_at_all(special_region):
|
if can_open_door(door, child_state, world, player):
|
||||||
open_a_door(door, child_state, flat_proposal)
|
open_a_door(door, child_state, flat_proposal, world, player)
|
||||||
expand_key_state(child_state, flat_proposal, world, player)
|
expand_key_state(child_state, flat_proposal, world, player)
|
||||||
code = state_id(child_state, key_layout.flat_prop)
|
code = state_id(child_state, key_layout.flat_prop)
|
||||||
if code not in key_counters.keys():
|
if code not in key_counters.keys():
|
||||||
@@ -1418,9 +1649,57 @@ def create_key_counters(key_layout, world, player):
|
|||||||
return key_counters
|
return key_counters
|
||||||
|
|
||||||
|
|
||||||
|
def find_outside_connection(region):
|
||||||
|
portal = next((x for x in region.entrances if ' Portal' in x.parent_region.name), None)
|
||||||
|
if portal:
|
||||||
|
dungeon_entrance = next(x for x in portal.parent_region.entrances
|
||||||
|
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||||
|
portal_entrance = next(x for x in portal.parent_region.entrances if x.parent_region == region)
|
||||||
|
return dungeon_entrance, portal_entrance.door
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def can_open_door(door, state, world, player):
|
||||||
|
if state.big_key_opened:
|
||||||
|
ttl_locations = count_free_locations(state, world, player)
|
||||||
|
else:
|
||||||
|
ttl_locations = count_locations_exclude_big_chest(state, world, player)
|
||||||
|
if door.smallKey:
|
||||||
|
ttl_small_key_only = count_small_key_only_locations(state)
|
||||||
|
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state, world, player)
|
||||||
|
return available_small_locations > 0
|
||||||
|
elif door.bigKey:
|
||||||
|
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||||
|
found_forced_bk = state.found_forced_bk()
|
||||||
|
return not state.big_key_opened and (available_big_locations > 0 or found_forced_bk)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player):
|
||||||
|
if counter.big_key_opened:
|
||||||
|
ttl_locations = len(counter.free_locations)
|
||||||
|
else:
|
||||||
|
ttl_locations = len([x for x in counter.free_locations if '- Big Chest' not in x.name])
|
||||||
|
|
||||||
|
if door.smallKey:
|
||||||
|
# ttl_small_key_only = len(counter.key_only_locations)
|
||||||
|
return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0
|
||||||
|
elif door.bigKey:
|
||||||
|
if counter.big_key_opened:
|
||||||
|
return False
|
||||||
|
if layout.big_key_special:
|
||||||
|
return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey)
|
||||||
|
else:
|
||||||
|
available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player)
|
||||||
|
return available_big_locations > 0
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create_key_counter(state, key_layout, world, player):
|
def create_key_counter(state, key_layout, world, player):
|
||||||
key_counter = KeyCounter(key_layout.max_chests)
|
key_counter = KeyCounter(key_layout.max_chests)
|
||||||
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors)))
|
key_counter.child_doors.update(dict.fromkeys(unique_doors(state.small_doors+state.big_doors+state.prize_doors)))
|
||||||
for loc in state.found_locations:
|
for loc in state.found_locations:
|
||||||
if important_location(loc, world, player):
|
if important_location(loc, world, player):
|
||||||
key_counter.important_location = True
|
key_counter.important_location = True
|
||||||
@@ -1437,6 +1716,10 @@ def create_key_counter(state, key_layout, world, player):
|
|||||||
key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
|
key_counter.open_doors.update(dict.fromkeys(state.opened_doors))
|
||||||
key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
|
key_counter.used_keys = count_unique_sm_doors(state.opened_doors)
|
||||||
key_counter.big_key_opened = state.big_key_opened
|
key_counter.big_key_opened = state.big_key_opened
|
||||||
|
if len(state.prize_door_set) > 0 and state.prize_doors_opened:
|
||||||
|
key_counter.prize_doors_opened = True
|
||||||
|
if any(x for x in key_counter.important_locations if '- Prize' in x.name):
|
||||||
|
key_counter.prize_received = True
|
||||||
return key_counter
|
return key_counter
|
||||||
|
|
||||||
|
|
||||||
@@ -1487,11 +1770,23 @@ def state_id(state, flat_proposal):
|
|||||||
s_id = '1' if state.big_key_opened else '0'
|
s_id = '1' if state.big_key_opened else '0'
|
||||||
for d in flat_proposal:
|
for d in flat_proposal:
|
||||||
s_id += '1' if d in state.opened_doors else '0'
|
s_id += '1' if d in state.opened_doors else '0'
|
||||||
|
if len(state.prize_door_set) > 0:
|
||||||
|
s_id += '1' if state.prize_doors_opened else '0'
|
||||||
return s_id
|
return s_id
|
||||||
|
|
||||||
|
|
||||||
def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
|
def validate_id(state, flat_proposal):
|
||||||
counter = find_counter_hint(opened_doors, bk_hint, key_layout)
|
s_id = '1' if state.big_key_opened else '0'
|
||||||
|
for d in flat_proposal:
|
||||||
|
s_id += '1' if d in state.opened_doors else '0'
|
||||||
|
if len(state.prize_door_set) > 0:
|
||||||
|
s_id += '1' if state.prize_doors_opened else '0'
|
||||||
|
s_id += str(state.used_locations)
|
||||||
|
return s_id
|
||||||
|
|
||||||
|
|
||||||
|
def find_counter(opened_doors, bk_hint, key_layout, prize_flag, raise_on_error=True):
|
||||||
|
counter = find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag)
|
||||||
if counter is not None:
|
if counter is not None:
|
||||||
return counter
|
return counter
|
||||||
more_doors = []
|
more_doors = []
|
||||||
@@ -1500,43 +1795,47 @@ def find_counter(opened_doors, bk_hint, key_layout, raise_on_error=True):
|
|||||||
if door.dest not in opened_doors.keys():
|
if door.dest not in opened_doors.keys():
|
||||||
more_doors.append(door.dest)
|
more_doors.append(door.dest)
|
||||||
if len(more_doors) > len(opened_doors.keys()):
|
if len(more_doors) > len(opened_doors.keys()):
|
||||||
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout)
|
counter = find_counter_hint(dict.fromkeys(more_doors), bk_hint, key_layout, prize_flag)
|
||||||
if counter is not None:
|
if counter is not None:
|
||||||
return counter
|
return counter
|
||||||
if raise_on_error:
|
if raise_on_error:
|
||||||
raise Exception('Unable to find door permutation. Init CID: %s' % counter_id(opened_doors, bk_hint, key_layout.flat_prop))
|
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||||
|
raise Exception(f'Unable to find door permutation. Init CID: {cid}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_counter_hint(opened_doors, bk_hint, key_layout):
|
def find_counter_hint(opened_doors, bk_hint, key_layout, prize_flag):
|
||||||
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop)
|
cid = counter_id(opened_doors, bk_hint, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||||
if cid in key_layout.key_counters.keys():
|
if cid in key_layout.key_counters.keys():
|
||||||
return key_layout.key_counters[cid]
|
return key_layout.key_counters[cid]
|
||||||
if not bk_hint:
|
if not bk_hint:
|
||||||
cid = counter_id(opened_doors, True, key_layout.flat_prop)
|
cid = counter_id(opened_doors, True, key_layout.flat_prop, key_layout.prize_relevant, prize_flag)
|
||||||
if cid in key_layout.key_counters.keys():
|
if cid in key_layout.key_counters.keys():
|
||||||
return key_layout.key_counters[cid]
|
return key_layout.key_counters[cid]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_max_counter(key_layout):
|
def find_max_counter(key_layout):
|
||||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), False, key_layout)
|
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), False, key_layout, True)
|
||||||
if max_counter is None:
|
if max_counter is None:
|
||||||
raise Exception("Max Counter is none - something is amiss")
|
raise Exception("Max Counter is none - something is amiss")
|
||||||
if len(max_counter.child_doors) > 0:
|
if len(max_counter.child_doors) > 0:
|
||||||
max_counter = find_counter_hint(dict.fromkeys(key_layout.flat_prop), True, key_layout)
|
max_counter = find_counter_hint(dict.fromkeys(key_layout.found_doors), True, key_layout, True)
|
||||||
return max_counter
|
return max_counter
|
||||||
|
|
||||||
|
|
||||||
def counter_id(opened_doors, bk_unlocked, flat_proposal):
|
def counter_id(opened_doors, bk_unlocked, flat_proposal, prize_relevant, prize_flag):
|
||||||
s_id = '1' if bk_unlocked else '0'
|
s_id = '1' if bk_unlocked else '0'
|
||||||
for d in flat_proposal:
|
for d in flat_proposal:
|
||||||
s_id += '1' if d in opened_doors.keys() else '0'
|
s_id += '1' if d in opened_doors.keys() else '0'
|
||||||
|
if prize_relevant:
|
||||||
|
s_id += '1' if prize_flag else '0'
|
||||||
return s_id
|
return s_id
|
||||||
|
|
||||||
|
|
||||||
def cid(counter, key_layout):
|
def cid(counter, key_layout):
|
||||||
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop)
|
return counter_id(counter.open_doors, counter.big_key_opened, key_layout.flat_prop,
|
||||||
|
key_layout.prize_relevant, counter.prize_doors_opened)
|
||||||
|
|
||||||
|
|
||||||
# class SoftLockException(Exception):
|
# class SoftLockException(Exception):
|
||||||
@@ -1746,8 +2045,20 @@ def validate_key_placement(key_layout, world, player):
|
|||||||
found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
|
found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
|
||||||
found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
|
found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
|
||||||
len(counter.key_only_locations) + keys_outside
|
len(counter.key_only_locations) + keys_outside
|
||||||
|
if key_layout.prize_relevant:
|
||||||
|
found_prize = any(x for x in counter.important_locations if '- Prize' in x.name)
|
||||||
|
if not found_prize and dungeon_table[key_layout.sector.name].prize:
|
||||||
|
prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player)
|
||||||
|
if key_layout.prize_relevant == 'BigBomb':
|
||||||
|
found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6']
|
||||||
|
elif key_layout.prize_relevant == 'GT':
|
||||||
|
found_prize = 'Crystal' not in prize_loc.item.name or world.crystals_needed_for_gt[player] < 7
|
||||||
|
else:
|
||||||
|
found_prize = False
|
||||||
can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
|
can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
|
||||||
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors)
|
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors) or \
|
||||||
|
self_locked_child_door(key_layout, counter) or \
|
||||||
|
(key_layout.prize_relevant and not counter.prize_doors_opened and found_prize)
|
||||||
if not can_progress:
|
if not can_progress:
|
||||||
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
|
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
|
||||||
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]
|
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item.name != bigkey_name) or "- Boss" in l.name]
|
||||||
@@ -1761,3 +2072,11 @@ def validate_key_placement(key_layout, world, player):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def self_locked_child_door(key_layout, counter):
|
||||||
|
if len(counter.child_doors) == 1:
|
||||||
|
door = next(iter(counter.child_doors.keys()))
|
||||||
|
return door.smallKey and key_layout.key_logic.door_rules[door.name].allow_small
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import time
|
|||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings
|
from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings
|
||||||
|
from Bosses import place_bosses
|
||||||
from Items import ItemFactory
|
from Items import ItemFactory
|
||||||
from KeyDoorShuffle import validate_key_placement
|
from KeyDoorShuffle import validate_key_placement
|
||||||
from OverworldGlitchRules import create_owg_connections
|
from OverworldGlitchRules import create_owg_connections
|
||||||
@@ -28,14 +29,24 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
|
|||||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
|
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
|
||||||
from Utils import output_path, parse_player_names
|
from Utils import output_path, parse_player_names
|
||||||
|
|
||||||
__version__ = '0.4.1-dev'
|
__version__ = '1.0.0-dev'
|
||||||
|
|
||||||
|
from source.classes.BabelFish import BabelFish
|
||||||
|
|
||||||
|
|
||||||
class EnemizerError(RuntimeError):
|
class EnemizerError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_python_version():
|
||||||
|
import sys
|
||||||
|
version = sys.version_info
|
||||||
|
if version.major < 3 or version.minor < 7:
|
||||||
|
logging.warning(BabelFish().translate("cli","cli","old.python.version"), sys.version)
|
||||||
|
|
||||||
|
|
||||||
def main(args, seed=None, fish=None):
|
def main(args, seed=None, fish=None):
|
||||||
|
check_python_version()
|
||||||
if args.outputpath:
|
if args.outputpath:
|
||||||
os.makedirs(args.outputpath, exist_ok=True)
|
os.makedirs(args.outputpath, exist_ok=True)
|
||||||
output_path.cached_path = args.outputpath
|
output_path.cached_path = args.outputpath
|
||||||
@@ -69,6 +80,7 @@ def main(args, seed=None, fish=None):
|
|||||||
world.compassshuffle = args.compassshuffle.copy()
|
world.compassshuffle = args.compassshuffle.copy()
|
||||||
world.keyshuffle = args.keyshuffle.copy()
|
world.keyshuffle = args.keyshuffle.copy()
|
||||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||||
|
world.bombbag = args.bombbag.copy()
|
||||||
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
||||||
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
|
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
|
||||||
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
||||||
@@ -135,6 +147,7 @@ def main(args, seed=None, fish=None):
|
|||||||
create_rooms(world, player)
|
create_rooms(world, player)
|
||||||
create_dungeons(world, player)
|
create_dungeons(world, player)
|
||||||
adjust_locations(world, player)
|
adjust_locations(world, player)
|
||||||
|
place_bosses(world, player)
|
||||||
|
|
||||||
if any(world.potshuffle.values()):
|
if any(world.potshuffle.values()):
|
||||||
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
||||||
@@ -233,7 +246,7 @@ def main(args, seed=None, fish=None):
|
|||||||
balance_multiworld_progression(world)
|
balance_multiworld_progression(world)
|
||||||
|
|
||||||
# if we only check for beatable, we can do this sanity check first before creating the rom
|
# if we only check for beatable, we can do this sanity check first before creating the rom
|
||||||
if not world.can_beat_game():
|
if not world.can_beat_game(log_error=True):
|
||||||
raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
|
raise RuntimeError(world.fish.translate("cli","cli","cannot.beat.game"))
|
||||||
|
|
||||||
for player in range(1, world.players+1):
|
for player in range(1, world.players+1):
|
||||||
@@ -258,11 +271,11 @@ def main(args, seed=None, fish=None):
|
|||||||
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
rom = JsonRom() if args.jsonout or use_enemizer else LocalRom(args.rom)
|
||||||
|
|
||||||
if use_enemizer and (args.enemizercli or not args.jsonout):
|
if use_enemizer and (args.enemizercli or not args.jsonout):
|
||||||
base_patch = LocalRom(args.rom) # update base2current.json (side effect)
|
local_rom = LocalRom(args.rom) # update base2current.json (side effect)
|
||||||
if args.rom and not(os.path.isfile(args.rom)):
|
if args.rom and not(os.path.isfile(args.rom)):
|
||||||
raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom)
|
raise RuntimeError("Could not find valid base rom for enemizing at expected path %s." % args.rom)
|
||||||
if os.path.exists(args.enemizercli):
|
if os.path.exists(args.enemizercli):
|
||||||
patch_enemizer(world, player, rom, args.rom, args.enemizercli, sprite_random_on_hit)
|
patch_enemizer(world, player, rom, local_rom, args.enemizercli, sprite_random_on_hit)
|
||||||
enemized = True
|
enemized = True
|
||||||
if not args.jsonout:
|
if not args.jsonout:
|
||||||
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
|
||||||
@@ -282,7 +295,8 @@ def main(args, seed=None, fish=None):
|
|||||||
|
|
||||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||||
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player])
|
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player],
|
||||||
|
args.shuffle_sfx[player])
|
||||||
|
|
||||||
if args.jsonout:
|
if args.jsonout:
|
||||||
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||||
@@ -350,6 +364,7 @@ def copy_world(world):
|
|||||||
ret.player_names = copy.deepcopy(world.player_names)
|
ret.player_names = copy.deepcopy(world.player_names)
|
||||||
ret.remote_items = world.remote_items.copy()
|
ret.remote_items = world.remote_items.copy()
|
||||||
ret.required_medallions = world.required_medallions.copy()
|
ret.required_medallions = world.required_medallions.copy()
|
||||||
|
ret.bottle_refills = world.bottle_refills.copy()
|
||||||
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
ret.swamp_patch_required = world.swamp_patch_required.copy()
|
||||||
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
ret.ganon_at_pyramid = world.ganon_at_pyramid.copy()
|
||||||
ret.powder_patch_required = world.powder_patch_required.copy()
|
ret.powder_patch_required = world.powder_patch_required.copy()
|
||||||
@@ -372,6 +387,7 @@ def copy_world(world):
|
|||||||
ret.compassshuffle = world.compassshuffle.copy()
|
ret.compassshuffle = world.compassshuffle.copy()
|
||||||
ret.keyshuffle = world.keyshuffle.copy()
|
ret.keyshuffle = world.keyshuffle.copy()
|
||||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||||
|
ret.bombbag = world.bombbag.copy()
|
||||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||||
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
|
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
|
||||||
@@ -389,6 +405,8 @@ def copy_world(world):
|
|||||||
ret.mixed_travel = world.mixed_travel.copy()
|
ret.mixed_travel = world.mixed_travel.copy()
|
||||||
ret.standardize_palettes = world.standardize_palettes.copy()
|
ret.standardize_palettes = world.standardize_palettes.copy()
|
||||||
|
|
||||||
|
ret.exp_cache = world.exp_cache.copy()
|
||||||
|
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
if world.mode[player] != 'inverted':
|
if world.mode[player] != 'inverted':
|
||||||
create_regions(ret, player)
|
create_regions(ret, player)
|
||||||
@@ -448,6 +466,7 @@ def copy_world(world):
|
|||||||
# these need to be modified properly by set_rules
|
# these need to be modified properly by set_rules
|
||||||
new_location.access_rule = lambda state: True
|
new_location.access_rule = lambda state: True
|
||||||
new_location.item_rule = lambda state: True
|
new_location.item_rule = lambda state: True
|
||||||
|
new_location.forced_item = location.forced_item
|
||||||
|
|
||||||
# copy remaining itempool. No item in itempool should have an assigned location
|
# copy remaining itempool. No item in itempool should have an assigned location
|
||||||
for item in world.itempool:
|
for item in world.itempool:
|
||||||
@@ -525,11 +544,11 @@ def create_playthrough(world):
|
|||||||
while sphere_candidates:
|
while sphere_candidates:
|
||||||
state.sweep_for_events(key_only=True)
|
state.sweep_for_events(key_only=True)
|
||||||
|
|
||||||
sphere = []
|
sphere = set()
|
||||||
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||||
for location in sphere_candidates:
|
for location in sphere_candidates:
|
||||||
if state.can_reach(location) and state.not_flooding_a_key(world, location):
|
if state.can_reach(location) and state.not_flooding_a_key(world, location):
|
||||||
sphere.append(location)
|
sphere.add(location)
|
||||||
|
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
sphere_candidates.remove(location)
|
sphere_candidates.remove(location)
|
||||||
@@ -550,21 +569,24 @@ def create_playthrough(world):
|
|||||||
|
|
||||||
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
|
# in the second phase, we cull each sphere such that the game is still beatable, reducing each range of influence to the bare minimum required inside it
|
||||||
for num, sphere in reversed(list(enumerate(collection_spheres))):
|
for num, sphere in reversed(list(enumerate(collection_spheres))):
|
||||||
to_delete = []
|
to_delete = set()
|
||||||
for location in sphere:
|
for location in sphere:
|
||||||
# we remove the item at location and check if game is still beatable
|
# we remove the item at location and check if game is still beatable
|
||||||
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
logging.getLogger('').debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
|
||||||
old_item = location.item
|
old_item = location.item
|
||||||
location.item = None
|
location.item = None
|
||||||
|
# todo: this is not very efficient, but I'm not sure how else to do it for this backwards logic
|
||||||
|
# world.clear_exp_cache()
|
||||||
if world.can_beat_game(state_cache[num]):
|
if world.can_beat_game(state_cache[num]):
|
||||||
to_delete.append(location)
|
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
|
||||||
|
to_delete.add(location)
|
||||||
else:
|
else:
|
||||||
# still required, got to keep it around
|
# still required, got to keep it around
|
||||||
|
# logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is required')
|
||||||
location.item = old_item
|
location.item = old_item
|
||||||
|
|
||||||
# cull entries in spheres for spoiler walkthrough at end
|
# cull entries in spheres for spoiler walkthrough at end
|
||||||
for location in to_delete:
|
sphere -= to_delete
|
||||||
sphere.remove(location)
|
|
||||||
|
|
||||||
# second phase, sphere 0
|
# second phase, sphere 0
|
||||||
for item in [i for i in world.precollected_items if i.advancement]:
|
for item in [i for i in world.precollected_items if i.advancement]:
|
||||||
@@ -580,7 +602,7 @@ def create_playthrough(world):
|
|||||||
# used to access it was deemed not required.) So we need to do one final sphere collection pass
|
# used to access it was deemed not required.) So we need to do one final sphere collection pass
|
||||||
# to build up the correct spheres
|
# to build up the correct spheres
|
||||||
|
|
||||||
required_locations = [item for sphere in collection_spheres for item in sphere]
|
required_locations = {item for sphere in collection_spheres for item in sphere}
|
||||||
state = CollectionState(world)
|
state = CollectionState(world)
|
||||||
collection_spheres = []
|
collection_spheres = []
|
||||||
while required_locations:
|
while required_locations:
|
||||||
@@ -596,7 +618,10 @@ def create_playthrough(world):
|
|||||||
|
|
||||||
logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
|
logging.getLogger('').debug(world.fish.translate("cli","cli","building.final.spheres"), len(collection_spheres), len(sphere), len(required_locations))
|
||||||
if not sphere:
|
if not sphere:
|
||||||
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
|
if world.has_beaten_game(state):
|
||||||
|
required_locations.clear()
|
||||||
|
else:
|
||||||
|
raise RuntimeError(world.fish.translate("cli","cli","cannot.reach.required"))
|
||||||
|
|
||||||
# store the required locations for statistical analysis
|
# store the required locations for statistical analysis
|
||||||
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
|
old_world.required_locations = [(location.name, location.player) for sphere in collection_spheres for location in sphere]
|
||||||
@@ -617,7 +642,7 @@ def create_playthrough(world):
|
|||||||
old_world.spoiler.paths = dict()
|
old_world.spoiler.paths = dict()
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
old_world.spoiler.paths.update({location.gen_name(): get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
|
||||||
for _, path in dict(old_world.spoiler.paths).items():
|
for path in dict(old_world.spoiler.paths).values():
|
||||||
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
|
||||||
if world.mode[player] != 'inverted':
|
if world.mode[player] != 'inverted':
|
||||||
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
old_world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
|
||||||
@@ -625,6 +650,6 @@ def create_playthrough(world):
|
|||||||
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
old_world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||||
|
|
||||||
# we can finally output our playthrough
|
# we can finally output our playthrough
|
||||||
old_world.spoiler.playthrough = OrderedDict([("0", [str(item) for item in world.precollected_items if item.advancement])])
|
old_world.spoiler.playthrough = {"0": [str(item) for item in world.precollected_items if item.advancement]}
|
||||||
for i, sphere in enumerate(collection_spheres):
|
for i, sphere in enumerate(collection_spheres):
|
||||||
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}
|
old_world.spoiler.playthrough[str(i + 1)] = {location.gen_name(): str(location.item) for location in sphere}
|
||||||
|
|||||||
+1
-1
@@ -846,7 +846,7 @@ async def track_locations(ctx : Context, roomid, roomdata):
|
|||||||
new_locations.append(Regions.lookup_name_to_id[location])
|
new_locations.append(Regions.lookup_name_to_id[location])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if roomid in location_shop_ids:
|
if ctx.shop_mode or ctx.retro_mode:
|
||||||
misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN)
|
misc_data = await snes_read(ctx, SHOP_ADDR, SHOP_SRAM_LEN)
|
||||||
for cnt, b in enumerate(misc_data):
|
for cnt, b in enumerate(misc_data):
|
||||||
my_check = Regions.shop_table_by_location_id[0x400000 + cnt]
|
my_check = Regions.shop_table_by_location_id[0x400000 + cnt]
|
||||||
|
|||||||
+45
-25
@@ -31,6 +31,7 @@ def main():
|
|||||||
parser.add_argument('--rom')
|
parser.add_argument('--rom')
|
||||||
parser.add_argument('--enemizercli')
|
parser.add_argument('--enemizercli')
|
||||||
parser.add_argument('--outputpath')
|
parser.add_argument('--outputpath')
|
||||||
|
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||||
for player in range(1, multiargs.multi + 1):
|
for player in range(1, multiargs.multi + 1):
|
||||||
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -63,6 +64,7 @@ def main():
|
|||||||
erargs.race = True
|
erargs.race = True
|
||||||
erargs.outputname = seedname
|
erargs.outputname = seedname
|
||||||
erargs.outputpath = args.outputpath
|
erargs.outputpath = args.outputpath
|
||||||
|
erargs.loglevel = args.loglevel
|
||||||
|
|
||||||
if args.rom:
|
if args.rom:
|
||||||
erargs.rom = args.rom
|
erargs.rom = args.rom
|
||||||
@@ -97,7 +99,8 @@ def get_weights(path):
|
|||||||
raise Exception(f'Failed to read weights file: {e}')
|
raise Exception(f'Failed to read weights file: {e}')
|
||||||
|
|
||||||
def roll_settings(weights):
|
def roll_settings(weights):
|
||||||
def get_choice(option, root=weights):
|
def get_choice(option, root=None):
|
||||||
|
root = weights if root is None else root
|
||||||
if option not in root:
|
if option not in root:
|
||||||
return None
|
return None
|
||||||
if type(root[option]) is not dict:
|
if type(root[option]) is not dict:
|
||||||
@@ -112,13 +115,24 @@ def roll_settings(weights):
|
|||||||
return default
|
return default
|
||||||
return choice
|
return choice
|
||||||
|
|
||||||
|
while True:
|
||||||
|
subweights = weights.get('subweights', {})
|
||||||
|
if len(subweights) == 0:
|
||||||
|
break
|
||||||
|
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
|
||||||
|
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
|
||||||
|
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
|
||||||
|
subweights['subweights'] = subweights.get('subweights', {})
|
||||||
|
weights = {**weights, **subweights}
|
||||||
|
|
||||||
ret = argparse.Namespace()
|
ret = argparse.Namespace()
|
||||||
|
|
||||||
glitches_required = get_choice('glitches_required')
|
glitches_required = get_choice('glitches_required')
|
||||||
if glitches_required not in ['none', 'no_logic']:
|
if glitches_required is not None:
|
||||||
print("Only NMG and No Logic supported")
|
if glitches_required not in ['none', 'no_logic']:
|
||||||
glitches_required = 'none'
|
print("Only NMG and No Logic supported")
|
||||||
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
|
glitches_required = 'none'
|
||||||
|
ret.logic = {'none': 'noglitches', 'no_logic': 'nologic'}[glitches_required]
|
||||||
|
|
||||||
item_placement = get_choice('item_placement')
|
item_placement = get_choice('item_placement')
|
||||||
# not supported in ER
|
# not supported in ER
|
||||||
@@ -150,39 +164,44 @@ def roll_settings(weights):
|
|||||||
ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize'
|
ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize'
|
||||||
|
|
||||||
goal = get_choice('goals')
|
goal = get_choice('goals')
|
||||||
ret.goal = {'ganon': 'ganon',
|
if goal is not None:
|
||||||
'fast_ganon': 'crystals',
|
ret.goal = {'ganon': 'ganon',
|
||||||
'dungeons': 'dungeons',
|
'fast_ganon': 'crystals',
|
||||||
'pedestal': 'pedestal',
|
'dungeons': 'dungeons',
|
||||||
'triforce-hunt': 'triforcehunt'
|
'pedestal': 'pedestal',
|
||||||
}[goal]
|
'triforce-hunt': 'triforcehunt'
|
||||||
|
}[goal]
|
||||||
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
|
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
|
||||||
|
|
||||||
ret.crystals_gt = get_choice('tower_open')
|
ret.crystals_gt = get_choice('tower_open')
|
||||||
|
|
||||||
ret.crystals_ganon = get_choice('ganon_open')
|
ret.crystals_ganon = get_choice('ganon_open')
|
||||||
|
|
||||||
if ret.goal == 'triforcehunt':
|
goal_min = get_choice_default('triforce_goal_min', default=20)
|
||||||
goal_min = get_choice_default('triforce_goal_min', default=20)
|
goal_max = get_choice_default('triforce_goal_max', default=20)
|
||||||
goal_max = get_choice_default('triforce_goal_max', default=20)
|
pool_min = get_choice_default('triforce_pool_min', default=30)
|
||||||
pool_min = get_choice_default('triforce_pool_min', default=30)
|
pool_max = get_choice_default('triforce_pool_max', default=30)
|
||||||
pool_max = get_choice_default('triforce_pool_max', default=30)
|
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
|
||||||
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
|
min_diff = get_choice_default('triforce_min_difference', default=10)
|
||||||
min_diff = get_choice_default('triforce_min_difference', default=10)
|
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
|
||||||
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
|
|
||||||
ret.mode = get_choice('world_state')
|
ret.mode = get_choice('world_state')
|
||||||
if ret.mode == 'retro':
|
if ret.mode == 'retro':
|
||||||
ret.mode = 'open'
|
ret.mode = 'open'
|
||||||
ret.retro = True
|
ret.retro = True
|
||||||
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
|
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
|
||||||
|
|
||||||
|
ret.bombbag = get_choice('bombbag') == 'on'
|
||||||
|
|
||||||
ret.hints = get_choice('hints') == 'on'
|
ret.hints = get_choice('hints') == 'on'
|
||||||
|
|
||||||
ret.swords = {'randomized': 'random',
|
swords = get_choice('weapons')
|
||||||
'assured': 'assured',
|
if swords is not None:
|
||||||
'vanilla': 'vanilla',
|
ret.swords = {'randomized': 'random',
|
||||||
'swordless': 'swordless'
|
'assured': 'assured',
|
||||||
}[get_choice('weapons')]
|
'vanilla': 'vanilla',
|
||||||
|
'swordless': 'swordless'
|
||||||
|
}[swords]
|
||||||
|
|
||||||
ret.difficulty = get_choice('item_pool')
|
ret.difficulty = get_choice('item_pool')
|
||||||
|
|
||||||
@@ -232,6 +251,7 @@ def roll_settings(weights):
|
|||||||
ret.heartbeep = get_choice('heartbeep', romweights)
|
ret.heartbeep = get_choice('heartbeep', romweights)
|
||||||
ret.ow_palettes = get_choice('ow_palettes', romweights)
|
ret.ow_palettes = get_choice('ow_palettes', romweights)
|
||||||
ret.uw_palettes = get_choice('uw_palettes', romweights)
|
ret.uw_palettes = get_choice('uw_palettes', romweights)
|
||||||
|
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -51,7 +51,7 @@ vanilla_pots = {
|
|||||||
43: [Pot(16, 5, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 5, PotItem.Switch, 'PoD Sexy Statue'), Pot(16, 6, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 6, PotItem.Bomb, 'PoD Sexy Statue'), Pot(16, 7, PotItem.Heart, 'PoD Sexy Statue'),
|
43: [Pot(16, 5, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 5, PotItem.Switch, 'PoD Sexy Statue'), Pot(16, 6, PotItem.Heart, 'PoD Sexy Statue'), Pot(44, 6, PotItem.Bomb, 'PoD Sexy Statue'), Pot(16, 7, PotItem.Heart, 'PoD Sexy Statue'),
|
||||||
Pot(44, 7, PotItem.Bomb, 'PoD Sexy Statue'), Pot(146, 21, PotItem.Bomb, 'PoD Map Balcony'), Pot(170, 21, PotItem.FiveArrows, 'PoD Map Balcony'), Pot(146, 22, PotItem.Bomb, 'PoD Map Balcony'),
|
Pot(44, 7, PotItem.Bomb, 'PoD Sexy Statue'), Pot(146, 21, PotItem.Bomb, 'PoD Map Balcony'), Pot(170, 21, PotItem.FiveArrows, 'PoD Map Balcony'), Pot(146, 22, PotItem.Bomb, 'PoD Map Balcony'),
|
||||||
Pot(170, 22, PotItem.FiveArrows, 'PoD Map Balcony')],
|
Pot(170, 22, PotItem.FiveArrows, 'PoD Map Balcony')],
|
||||||
44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave')],
|
44: [Pot(108, 24, PotItem.Heart, 'Hookshot Cave (Middle)'), Pot(112, 24, PotItem.Heart, 'Hookshot Cave (Middle)')],
|
||||||
47: [Pot(28, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(32, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(28, 9, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(32, 9, PotItem.FiveRupees, 'Kakariko Well (top)'),
|
47: [Pot(28, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(32, 7, PotItem.Heart, 'Kakariko Well (top)'), Pot(28, 9, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(32, 9, PotItem.FiveRupees, 'Kakariko Well (top)'),
|
||||||
Pot(172, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(180, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(104, 27, PotItem.Heart, 'Kakariko Well (bottom)'), Pot(104, 28, PotItem.Heart, 'Kakariko Well (bottom)')],
|
Pot(172, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(180, 19, PotItem.FiveRupees, 'Kakariko Well (top)'), Pot(104, 27, PotItem.Heart, 'Kakariko Well (bottom)'), Pot(104, 28, PotItem.Heart, 'Kakariko Well (bottom)')],
|
||||||
49: [Pot(92, 28, PotItem.Bomb, 'Hera Beetles'), Pot(96, 28, PotItem.Nothing, 'Hera Beetles')],
|
49: [Pot(92, 28, PotItem.Bomb, 'Hera Beetles'), Pot(96, 28, PotItem.Nothing, 'Hera Beetles')],
|
||||||
@@ -66,8 +66,8 @@ vanilla_pots = {
|
|||||||
55: [Pot(60, 6, PotItem.Key, 'Swamp Trench 1 Alcove'), Pot(48, 20, PotItem.Nothing, 'Swamp Trench 1 Key Ledge')],
|
55: [Pot(60, 6, PotItem.Key, 'Swamp Trench 1 Alcove'), Pot(48, 20, PotItem.Nothing, 'Swamp Trench 1 Key Ledge')],
|
||||||
56: [Pot(164, 12, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 13, PotItem.FiveRupees, 'Swamp Pot Row'), Pot(164, 18, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 19, PotItem.Key, 'Swamp Pot Row')],
|
56: [Pot(164, 12, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 13, PotItem.FiveRupees, 'Swamp Pot Row'), Pot(164, 18, PotItem.Bomb, 'Swamp Pot Row'), Pot(164, 19, PotItem.Key, 'Swamp Pot Row')],
|
||||||
57: [Pot(12, 20, PotItem.Heart, 'Skull Spike Corner'), Pot(48, 28, PotItem.FiveArrows, 'Skull Spike Corner'), Pot(100, 22, PotItem.SmallMagic, 'Skull Final Drop'), Pot(100, 26, PotItem.FiveArrows, 'Skull Final Drop')],
|
57: [Pot(12, 20, PotItem.Heart, 'Skull Spike Corner'), Pot(48, 28, PotItem.FiveArrows, 'Skull Spike Corner'), Pot(100, 22, PotItem.SmallMagic, 'Skull Final Drop'), Pot(100, 26, PotItem.FiveArrows, 'Skull Final Drop')],
|
||||||
60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave'),
|
60: [Pot(24, 8, PotItem.SmallMagic, 'Hookshot Cave (Front)'), Pot(64, 12, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(20, 14, PotItem.OneRupee, 'Hookshot Cave (Front)'), Pot(20, 19, PotItem.Nothing, 'Hookshot Cave (Front)'),
|
||||||
Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave')],
|
Pot(68, 18, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(96, 19, PotItem.Heart, 'Hookshot Cave (Front)'), Pot(64, 20, PotItem.FiveRupees, 'Hookshot Cave (Front)'), Pot(64, 26, PotItem.FiveRupees, 'Hookshot Cave (Front)')],
|
||||||
61: [Pot(76, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(112, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(24, 22, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(40, 22, PotItem.FiveArrows, 'GT Crystal Inner Circle'),
|
61: [Pot(76, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(112, 12, PotItem.Bomb, 'GT Mini Helmasaur Room'), Pot(24, 22, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(40, 22, PotItem.FiveArrows, 'GT Crystal Inner Circle'),
|
||||||
Pot(32, 24, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(20, 26, PotItem.FiveRupees, 'GT Crystal Inner Circle'), Pot(36, 26, PotItem.BigMagic, 'GT Crystal Inner Circle')],
|
Pot(32, 24, PotItem.Heart, 'GT Crystal Inner Circle'), Pot(20, 26, PotItem.FiveRupees, 'GT Crystal Inner Circle'), Pot(36, 26, PotItem.BigMagic, 'GT Crystal Inner Circle')],
|
||||||
62: [Pot(96, 6, PotItem.Bomb, 'Ice Stalfos Hint'), Pot(100, 6, PotItem.SmallMagic, 'Ice Stalfos Hint'), Pot(88, 10, PotItem.Heart, 'Ice Stalfos Hint'), Pot(92, 10, PotItem.SmallMagic, 'Ice Stalfos Hint')],
|
62: [Pot(96, 6, PotItem.Bomb, 'Ice Stalfos Hint'), Pot(100, 6, PotItem.SmallMagic, 'Ice Stalfos Hint'), Pot(88, 10, PotItem.Heart, 'Ice Stalfos Hint'), Pot(92, 10, PotItem.SmallMagic, 'Ice Stalfos Hint')],
|
||||||
|
|||||||
+86
-2
@@ -1,4 +1,88 @@
|
|||||||
### Bug Fix
|
# New Features
|
||||||
|
|
||||||
Fix for boss music in non-DR modes (Thanks codemann8)
|
## Shuffle SFX
|
||||||
|
|
||||||
|
Shuffles a large portion of the sounds effects. Can be used with the adjuster.
|
||||||
|
|
||||||
|
CLI: ```--shuffle_sfx```
|
||||||
|
|
||||||
|
## Bomb Logic
|
||||||
|
|
||||||
|
When enabling this option, you do not start with bomb capacity but rather you must find 1 of 2 bomb bags. (They are represented by the +10 capacity item.) Bomb capacity upgrades are otherwise unavailable.
|
||||||
|
|
||||||
|
CLI: ```--bombbag```
|
||||||
|
|
||||||
|
|
||||||
|
# Bug Fixes and Notes.
|
||||||
|
|
||||||
|
* 0.5.1.7
|
||||||
|
* Baserom update
|
||||||
|
* Fix for Inverted Mode: Dark Lake Hylia shop defaults to selling a blue potion
|
||||||
|
* Fix for Ijwu's enemizer: Boss door in Thieves' Town no longer closes after the maiden hint if Blind is shuffled to Theives' Town in boss shuffle + crossed mode
|
||||||
|
* No logic now sets the AllowAccidentalMajorGlitches flag in the rom appropriately
|
||||||
|
* Houlihan room now exits wherever Link's House is shuffled to
|
||||||
|
* Rom fixes from Catobat and Codemann8. Thanks!
|
||||||
|
* 0.5.1.6
|
||||||
|
* Rules fixes for TT (Boss and Cell) can now have TT Big Key if not otherwise required (boss shuffle + crossed dungeon)
|
||||||
|
* BUg fix for money balancing
|
||||||
|
* Add some bomb assumptions for bosses in bombbag mode
|
||||||
|
* 0.5.1.5
|
||||||
|
* Fix for hard pool capacity upgrades missing
|
||||||
|
* Bonk Fairy (Light) is no longer in logic for ER Standard and is forbidden to be a connector, so rain state isn't exitable
|
||||||
|
* Bug fix for retro + enemizer and arrows appearing under pots
|
||||||
|
* Added bombbag and shufflelinks to settings code
|
||||||
|
* Catobat fixes:
|
||||||
|
* Fairy refills in spoiler
|
||||||
|
* Subweights support in mystery
|
||||||
|
* More defaults for mystery weights
|
||||||
|
* Less camera jank for straight stair transitions
|
||||||
|
* Bug with Straight stairs with vanilla doors where Link's walking animation stopped early is fixed
|
||||||
|
* 0.5.1.4
|
||||||
|
* Revert quadrant glitch fix for baserom
|
||||||
|
* Fix for inverted
|
||||||
|
* 0.5.1.3
|
||||||
|
* Certain lobbies forbidden in standard when rupee bow is enabled
|
||||||
|
* PoD EG disarmed when mirroring (except in nologic)
|
||||||
|
* Fixed issue with key logic
|
||||||
|
* Updated baserom
|
||||||
|
* 0.5.1.2
|
||||||
|
* Allowed Blind's Cell to be shuffled anywhere if Blind is not the boss of Thieves Town
|
||||||
|
* Remove unique annotation from a FastEnum that was causing problems
|
||||||
|
* Updated prevent mixed_travel setting to prevent more mixed travel
|
||||||
|
* Prevent key door loops on the same supertile where you could have spent 2 keys on one logical door
|
||||||
|
* Promoted dynamic soft-lock prevention on "stonewalls" from experimental to be the primary prevention (Stonewalls are now never pre-opened)
|
||||||
|
* Fix to money balancing algorithm with small item_pool, thanks Catobat
|
||||||
|
* Many fixes and refinements to key logic and generation
|
||||||
|
* 0.5.1.1
|
||||||
|
* Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore
|
||||||
|
* Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame)
|
||||||
|
* Fix for bias in boss shuffle "full"
|
||||||
|
* Fix for certain lone big chests in keysanity (allowed you to get contents without big key)
|
||||||
|
* Fix for pinball checking
|
||||||
|
* Fix for multi-entrance dungeons
|
||||||
|
* 2 fixes for big key placement logic
|
||||||
|
* ensure big key is placed early if the validator assumes it)
|
||||||
|
* Open big key doors appropriately when generating rules and big key is forced somewhere
|
||||||
|
* Updated cutoff entrances for intensity 3
|
||||||
|
* 0.5.1.0
|
||||||
|
* Large logic refactor introducing a new method of key logic
|
||||||
|
* Some performance optimization
|
||||||
|
* Some outstanding bug fixes (boss shuffle "full" picks three unique bosses to be duplicated, e.g.)
|
||||||
|
* 0.5.0.3
|
||||||
|
* Fixed a bug in retro+vanilla and big key placement
|
||||||
|
* Fixed a problem with shops not registering in the Multiclient until you visit one
|
||||||
|
* Fixed a bug in the Mystery code with sfx
|
||||||
|
* 0.5.0.2
|
||||||
|
* --shuffle_sfx option added
|
||||||
|
* 0.5.0.1
|
||||||
|
* --bombbag option added
|
||||||
|
* 0.5.0.0
|
||||||
|
* Handles headered roms for enemizer (Thanks compiling)
|
||||||
|
* Warning added for earlier version of python (Thanks compiling)
|
||||||
|
* Minor logic issue for defeating Aga in standard (Thanks compiling)
|
||||||
|
* Fix for boss music in non-DR modes (Thanks codemann8)
|
||||||
|
|
||||||
|
# Known Issues
|
||||||
|
|
||||||
|
* Shopsanity Issues
|
||||||
|
* Forfeit in Multiworld not granting all shop items
|
||||||
+9
-5
@@ -190,8 +190,11 @@ def create_regions(world, player):
|
|||||||
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
create_cave_region(player, 'Superbunny Cave (Top)', 'a connector', ['Superbunny Cave - Top', 'Superbunny Cave - Bottom'], ['Superbunny Cave Exit (Top)']),
|
||||||
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
create_cave_region(player, 'Superbunny Cave (Bottom)', 'a connector', None, ['Superbunny Cave Climb', 'Superbunny Cave Exit (Bottom)']),
|
||||||
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
create_cave_region(player, 'Spike Cave', 'Spike Cave', ['Spike Cave']),
|
||||||
create_cave_region(player, 'Hookshot Cave', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
|
create_cave_region(player, 'Hookshot Cave (Front)', 'a connector', ['Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left'],
|
||||||
['Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)']),
|
['Hookshot Cave Front to Middle', 'Hookshot Cave Front Exit']),
|
||||||
|
create_cave_region(player, 'Hookshot Cave (Back)', 'a connector', None, ['Hookshot Cave Back to Middle', 'Hookshot Cave Back Exit']),
|
||||||
|
create_cave_region(player, 'Hookshot Cave (Middle)', 'a connector', None, ['Hookshot Cave Middle to Back', 'Hookshot Cave Middle to Front']),
|
||||||
|
|
||||||
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
|
create_dw_region(player, 'Death Mountain Floating Island (Dark World)', None, ['Floating Island Drop', 'Hookshot Cave Back Entrance', 'Floating Island Mirror Spot']),
|
||||||
create_lw_region(player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
create_lw_region(player, 'Death Mountain Floating Island (Light World)', ['Floating Island']),
|
||||||
create_dw_region(player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
create_dw_region(player, 'Turtle Rock (Top)', None, ['Turtle Rock Drop']),
|
||||||
@@ -954,6 +957,7 @@ def create_shops(world, player):
|
|||||||
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
|
if world.mode[player] == 'inverted' and region_name == 'Dark Lake Hylia Shop':
|
||||||
locked = True
|
locked = True
|
||||||
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
inventory = [('Blue Potion', 160), ('Blue Shield', 50), ('Bombs (10)', 50)]
|
||||||
|
custom = True
|
||||||
region = world.get_region(region_name, player)
|
region = world.get_region(region_name, player)
|
||||||
shop = Shop(region, room_id, type, shopkeeper, custom, locked, sram)
|
shop = Shop(region, room_id, type, shopkeeper, custom, locked, sram)
|
||||||
region.shop = shop
|
region.shop = shop
|
||||||
@@ -1371,9 +1375,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'),
|
|||||||
'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'),
|
'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'),
|
||||||
'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'),
|
'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'),
|
||||||
'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'),
|
'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'),
|
||||||
'Potion Shop - Left': (None, None, False, 'for sale near the witch'),
|
'Potion Shop - Left': (None, None, False, 'for sale near potions'),
|
||||||
'Potion Shop - Middle': (None, None, False, 'for sale near the witch'),
|
'Potion Shop - Middle': (None, None, False, 'for sale near potions'),
|
||||||
'Potion Shop - Right': (None, None, False, 'for sale near the witch'),
|
'Potion Shop - Right': (None, None, False, 'for sale near potions'),
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_
|
|||||||
from Items import ItemFactory
|
from Items import ItemFactory
|
||||||
from EntranceShuffle import door_addresses, exit_ids
|
from EntranceShuffle import door_addresses, exit_ids
|
||||||
|
|
||||||
|
from source.classes.SFX import randomize_sfx
|
||||||
|
|
||||||
|
|
||||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||||
RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da'
|
RANDOMIZERBASEHASH = '5112ddd931bda3d9979097bc39a5e768'
|
||||||
|
|
||||||
|
|
||||||
class JsonRom(object):
|
class JsonRom(object):
|
||||||
@@ -86,10 +88,12 @@ class LocalRom(object):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.hash = hash
|
self.hash = hash
|
||||||
self.orig_buffer = None
|
self.orig_buffer = None
|
||||||
|
self.file = file
|
||||||
|
self.has_smc_header = False
|
||||||
if not os.path.isfile(file):
|
if not os.path.isfile(file):
|
||||||
raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file)
|
raise RuntimeError("Could not find valid local base rom for patching at expected path %s." % file)
|
||||||
with open(file, 'rb') as stream:
|
with open(file, 'rb') as stream:
|
||||||
self.buffer = read_rom(stream)
|
self.buffer, self.has_smc_header = read_rom(stream)
|
||||||
if patch:
|
if patch:
|
||||||
self.patch_base_rom()
|
self.patch_base_rom()
|
||||||
self.orig_buffer = self.buffer.copy()
|
self.orig_buffer = self.buffer.copy()
|
||||||
@@ -98,8 +102,7 @@ class LocalRom(object):
|
|||||||
self.buffer[address] = value
|
self.buffer[address] = value
|
||||||
|
|
||||||
def write_bytes(self, startaddress, values):
|
def write_bytes(self, startaddress, values):
|
||||||
for i, value in enumerate(values):
|
self.buffer[startaddress:startaddress + len(values)] = values
|
||||||
self.write_byte(startaddress + i, value)
|
|
||||||
|
|
||||||
def write_to_file(self, file):
|
def write_to_file(self, file):
|
||||||
with open(file, 'wb') as outfile:
|
with open(file, 'wb') as outfile:
|
||||||
@@ -187,12 +190,21 @@ def write_int32s(rom, startaddress, values):
|
|||||||
def read_rom(stream):
|
def read_rom(stream):
|
||||||
"Reads rom into bytearray and strips off any smc header"
|
"Reads rom into bytearray and strips off any smc header"
|
||||||
buffer = bytearray(stream.read())
|
buffer = bytearray(stream.read())
|
||||||
|
has_smc_header = False
|
||||||
if len(buffer)%0x400 == 0x200:
|
if len(buffer)%0x400 == 0x200:
|
||||||
buffer = buffer[0x200:]
|
buffer = buffer[0x200:]
|
||||||
return buffer
|
has_smc_header = True
|
||||||
|
return buffer, has_smc_header
|
||||||
|
|
||||||
def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_on_hit):
|
def patch_enemizer(world, player, rom, local_rom, enemizercli, random_sprite_on_hit):
|
||||||
baserom_path = os.path.abspath(baserom_path)
|
baserom_path = os.path.abspath(local_rom.file)
|
||||||
|
unheadered_path = None
|
||||||
|
if local_rom.has_smc_header:
|
||||||
|
headered_path = baserom_path
|
||||||
|
unheadered_path = baserom_path = os.path.abspath(output_path('unheadered_rom.sfc'))
|
||||||
|
with open(headered_path, 'rb') as headered:
|
||||||
|
with open(baserom_path, 'wb') as unheadered:
|
||||||
|
unheadered.write(headered.read()[0x200:])
|
||||||
basepatch_path = os.path.abspath(local_path(os.path.join("data","base2current.json")))
|
basepatch_path = os.path.abspath(local_path(os.path.join("data","base2current.json")))
|
||||||
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
|
enemizer_basepatch_path = os.path.join(os.path.dirname(enemizercli), "enemizerBasePatch.json")
|
||||||
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
|
randopatch_path = os.path.abspath(output_path('enemizer_randopatch.json'))
|
||||||
@@ -321,6 +333,7 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
|
|||||||
rom.write_bytes(0xEA081, [0x5c, 0x00, 0x80, 0xb7, 0xc9, 0x6, 0xf0, 0x24,
|
rom.write_bytes(0xEA081, [0x5c, 0x00, 0x80, 0xb7, 0xc9, 0x6, 0xf0, 0x24,
|
||||||
0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d])
|
0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d])
|
||||||
rom.write_byte(0x200101, 0) # Do not close boss room door on entry.
|
rom.write_byte(0x200101, 0) # Do not close boss room door on entry.
|
||||||
|
rom.write_byte(0x1B0101, 0) # Do not close boss room door on entry. (for Ijwu's enemizer)
|
||||||
|
|
||||||
if random_sprite_on_hit:
|
if random_sprite_on_hit:
|
||||||
_populate_sprite_table()
|
_populate_sprite_table()
|
||||||
@@ -336,6 +349,12 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_
|
|||||||
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
rom.write_bytes(0x307000 + (i * 0x8000), sprite.palette)
|
||||||
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
|
||||||
|
|
||||||
|
if local_rom.has_smc_header:
|
||||||
|
try:
|
||||||
|
os.remove(unheadered_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.remove(randopatch_path)
|
os.remove(randopatch_path)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -653,11 +672,25 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
dr_flags |= DROptions.Debug
|
dr_flags |= DROptions.Debug
|
||||||
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
|
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
|
||||||
and world.mixed_travel[player] == 'prevent':
|
and world.mixed_travel[player] == 'prevent':
|
||||||
dr_flags |= DROptions.Rails
|
# PoD Falling Bridge or Hammjump
|
||||||
|
# 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05
|
||||||
|
# 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01
|
||||||
|
rom.write_bytes(0xfa607, [0x2d, 0x79, 0x69, 0x14, 0x99, 0x5d])
|
||||||
|
# PoD Arena
|
||||||
|
# 1FA573: db $D4, $B2, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 2C } | Size: 02
|
||||||
|
# 1FA576: db $D4, $CE, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 33 } | Size: 01
|
||||||
|
# 1FA579: db $D9, $AE, $69 ; 0x0069: Vertical Rail ↕ | { 36, 2B } | Size: 06
|
||||||
|
rom.write_bytes(0xfa573, [0xd4, 0xb2, 0x22, 0xd4, 0xce, 0x22, 0xd9, 0xae, 0x69])
|
||||||
|
# Mire BK Pond
|
||||||
|
# 1FB1FC: db $C8, $9D, $69 ; 0x0069: Vertical Rail ↕ | { 32, 27 } | Size: 01
|
||||||
|
# 1FB1FF: db $B4, $AC, $5D ; 0x005D: Large Horizontal Rail ↔ | { 2D, 2B } | Size: 00
|
||||||
|
rom.write_bytes(0xfb1fc, [0xc8, 0x9d, 0x69, 0xb4, 0xac, 0x5d])
|
||||||
if world.standardize_palettes[player] == 'original':
|
if world.standardize_palettes[player] == 'original':
|
||||||
dr_flags |= DROptions.OriginalPalettes
|
dr_flags |= DROptions.OriginalPalettes
|
||||||
if world.experimental[player]:
|
if world.experimental[player]:
|
||||||
dr_flags |= DROptions.DarkWorld_Spawns
|
dr_flags |= DROptions.DarkWorld_Spawns
|
||||||
|
if world.logic[player] != 'nologic':
|
||||||
|
dr_flags |= DROptions.Fix_EG
|
||||||
|
|
||||||
|
|
||||||
# fix hc big key problems (map and compass too)
|
# fix hc big key problems (map and compass too)
|
||||||
@@ -724,13 +757,6 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player))
|
rom.write_bytes(paired_door.address_a(world, player), paired_door.rom_data_a(world, player))
|
||||||
rom.write_bytes(paired_door.address_b(world, player), paired_door.rom_data_b(world, player))
|
rom.write_bytes(paired_door.address_b(world, player), paired_door.rom_data_b(world, player))
|
||||||
if world.doorShuffle[player] != 'vanilla':
|
if world.doorShuffle[player] != 'vanilla':
|
||||||
if not world.experimental[player]:
|
|
||||||
for builder in world.dungeon_layouts[player].values():
|
|
||||||
for stonewall in builder.pre_open_stonewalls:
|
|
||||||
if stonewall.name == 'Desert Wall Slide NW':
|
|
||||||
dr_flags |= DROptions.Open_Desert_Wall
|
|
||||||
elif stonewall.name == 'PoD Bow Statue Down Ladder':
|
|
||||||
dr_flags |= DROptions.Open_PoD_Wall
|
|
||||||
for name, pair in boss_indicator.items():
|
for name, pair in boss_indicator.items():
|
||||||
dungeon_id, boss_door = pair
|
dungeon_id, boss_door = pair
|
||||||
opposite_door = world.get_door(boss_door, player).dest
|
opposite_door = world.get_door(boss_door, player).dest
|
||||||
@@ -1032,7 +1058,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
rom.write_bytes(0x184000, [
|
rom.write_bytes(0x184000, [
|
||||||
# original_item, limit, replacement_item, filler
|
# original_item, limit, replacement_item, filler
|
||||||
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
|
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
|
||||||
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
|
0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10)
|
||||||
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
||||||
0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
||||||
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
||||||
@@ -1041,12 +1067,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
])
|
])
|
||||||
|
|
||||||
# set Fountain bottle exchange items
|
# set Fountain bottle exchange items
|
||||||
if world.difficulty[player] in ['hard', 'expert']:
|
rom.write_byte(0x348FF, ItemFactory(world.bottle_refills[player][0], player).code)
|
||||||
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)])
|
rom.write_byte(0x3493B, ItemFactory(world.bottle_refills[player][1], player).code)
|
||||||
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x48][random.randint(0, 5)])
|
|
||||||
else:
|
|
||||||
rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
|
|
||||||
rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)])
|
|
||||||
|
|
||||||
#enable Fat Fairy Chests
|
#enable Fat Fairy Chests
|
||||||
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
|
rom.write_bytes(0x1FC16, [0xB1, 0xC6, 0xF9, 0xC9, 0xC6, 0xF9])
|
||||||
@@ -1169,7 +1191,10 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
equip[0x36C] = 0x18
|
equip[0x36C] = 0x18
|
||||||
equip[0x36D] = 0x18
|
equip[0x36D] = 0x18
|
||||||
equip[0x379] = 0x68
|
equip[0x379] = 0x68
|
||||||
starting_max_bombs = 10
|
if world.bombbag[player]:
|
||||||
|
starting_max_bombs = 0
|
||||||
|
else:
|
||||||
|
starting_max_bombs = 10
|
||||||
starting_max_arrows = 30
|
starting_max_arrows = 30
|
||||||
|
|
||||||
startingstate = CollectionState(world)
|
startingstate = CollectionState(world)
|
||||||
@@ -1426,6 +1451,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
|
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
|
||||||
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost
|
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost
|
||||||
rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots
|
rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots
|
||||||
|
if enemized:
|
||||||
|
rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1)
|
||||||
rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant
|
rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant
|
||||||
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
|
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
|
||||||
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
|
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
|
||||||
@@ -1436,6 +1463,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
||||||
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
|
rom.write_byte(0x1800A4, 0x01 if world.logic[player] != 'nologic' else 0x00) # enable POD EG fix
|
||||||
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
||||||
|
rom.write_byte(0x180358, 0x01 if world.logic[player] == 'nologic' else 0x00)
|
||||||
|
|
||||||
# remove shield from uncle
|
# remove shield from uncle
|
||||||
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
|
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
|
||||||
@@ -1461,7 +1489,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
|||||||
rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x180188, [0, 0, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x18018B, [0, 0, 10]) # Mantle respawn refills (magic, bombs, arrows)
|
||||||
bow_max, bow_small = 70, 10
|
bow_max, bow_small = 70, 10
|
||||||
elif uncle_location.item is not None and uncle_location.item.name in ['Bombs (10)']:
|
elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']:
|
||||||
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
|
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
|
||||||
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x180188, [0, 3, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
@@ -1591,13 +1619,16 @@ def write_custom_shops(rom, world, player):
|
|||||||
loc_item = ItemFactory(item['item'], player)
|
loc_item = ItemFactory(item['item'], player)
|
||||||
if (not world.shopsanity[player] and shop.region.name == 'Capacity Upgrade'
|
if (not world.shopsanity[player] and shop.region.name == 'Capacity Upgrade'
|
||||||
and world.difficulty[player] != 'normal'):
|
and world.difficulty[player] != 'normal'):
|
||||||
continue # skip cap upgrades except in normal/shopsanity
|
# really should be 5A instead of B0 -- surprise!!!
|
||||||
item_id = loc_item.code
|
item_id, price, replace, replace_price, item_max = 0xB0, [0, 0], 0xFF, [0, 0], 1
|
||||||
price = int16_as_bytes(item['price'])
|
else:
|
||||||
replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF
|
item_id = loc_item.code
|
||||||
replace_price = int16_as_bytes(item['replacement_price'])
|
price = int16_as_bytes(item['price'])
|
||||||
|
replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF
|
||||||
|
replace_price = int16_as_bytes(item['replacement_price'])
|
||||||
|
item_max = item['max']
|
||||||
item_player = 0 if item['player'] == player else item['player']
|
item_player = 0 if item['player'] == player else item['player']
|
||||||
item_data = [shop_id, item_id] + price + [item['max'], replace] + replace_price + [item_player]
|
item_data = [shop_id, item_id] + price + [item_max, replace] + replace_price + [item_player]
|
||||||
items_data.extend(item_data)
|
items_data.extend(item_data)
|
||||||
|
|
||||||
rom.write_bytes(0x184800, shop_data)
|
rom.write_bytes(0x184800, shop_data)
|
||||||
@@ -1625,7 +1656,7 @@ def hud_format_text(text):
|
|||||||
|
|
||||||
|
|
||||||
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite,
|
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite,
|
||||||
ow_palettes, uw_palettes, reduce_flashing):
|
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx):
|
||||||
|
|
||||||
if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer:
|
if not os.path.exists("data/sprites/official/001.link.1.zspr") and rom.orig_buffer:
|
||||||
dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380],
|
dump_zspr(rom.orig_buffer[0x80000:0x87000], rom.orig_buffer[0xdd308:0xdd380],
|
||||||
@@ -1728,6 +1759,9 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
|
|||||||
elif uw_palettes == 'blackout':
|
elif uw_palettes == 'blackout':
|
||||||
blackout_uw_palettes(rom)
|
blackout_uw_palettes(rom)
|
||||||
|
|
||||||
|
if shuffle_sfx:
|
||||||
|
randomize_sfx(rom)
|
||||||
|
|
||||||
if isinstance(rom, LocalRom):
|
if isinstance(rom, LocalRom):
|
||||||
rom.write_crc()
|
rom.write_crc()
|
||||||
|
|
||||||
@@ -2275,6 +2309,12 @@ def set_inverted_mode(world, player, rom):
|
|||||||
write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8)
|
write_int16(rom, snes_to_pc(0x02E8D5), 0x07C8)
|
||||||
write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8)
|
write_int16(rom, snes_to_pc(0x02E8F7), 0x01F8)
|
||||||
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC428), 0x00) # remove diggable light world portals
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC43A), 0x00)
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC590), 0x00)
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC5A1), 0x00)
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC5B1), 0x00)
|
||||||
|
rom.write_byte(snes_to_pc(0x1BC5C7), 0x00)
|
||||||
# the following bytes should only be written in vanilla
|
# the following bytes should only be written in vanilla
|
||||||
# or they'll overwrite the randomizer's shuffles
|
# or they'll overwrite the randomizer's shuffles
|
||||||
if world.shuffle[player] == 'vanilla':
|
if world.shuffle[player] == 'vanilla':
|
||||||
@@ -2771,7 +2811,7 @@ def write_pots_to_rom(rom, pot_contents):
|
|||||||
pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing]
|
pots = [pot for pot in pot_contents[i] if pot.item != PotItem.Nothing]
|
||||||
if len(pots) > 0:
|
if len(pots) > 0:
|
||||||
write_int16(rom, pot_item_room_table_lookup + 2*i, n)
|
write_int16(rom, pot_item_room_table_lookup + 2*i, n)
|
||||||
rom.write_bytes(n, itertools.chain(*((pot.x,pot.y,pot.item) for pot in pots)))
|
rom.write_bytes(n, list(itertools.chain.from_iterable(((pot.x, pot.y, pot.item) for pot in pots))))
|
||||||
n += 3*len(pots) + 2
|
n += 3*len(pots) + 2
|
||||||
rom.write_bytes(n - 2, [0xFF,0xFF])
|
rom.write_bytes(n - 2, [0xFF,0xFF])
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
import OverworldGlitchRules
|
import OverworldGlitchRules
|
||||||
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier
|
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
|
||||||
from RoomData import DoorKind
|
from RoomData import DoorKind
|
||||||
from OverworldGlitchRules import overworld_glitches_rules
|
from OverworldGlitchRules import overworld_glitches_rules
|
||||||
|
|
||||||
@@ -274,9 +274,11 @@ def global_rules(world, player):
|
|||||||
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: state.has('Hammer', player))
|
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: state.has('Hammer', player))
|
||||||
for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']:
|
for entrance in ['Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Block Path', 'Thieves Conveyor Bridge Block Path']:
|
||||||
set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player))
|
set_rule(world.get_entrance(entrance, player), lambda state: state.can_lift_rocks(player))
|
||||||
for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
|
|
||||||
forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player)
|
# I think these rules are unnecessary now - testing needed
|
||||||
forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player)
|
# for location in ['Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss']:
|
||||||
|
# forbid_item(world.get_location(location, player), 'Big Key (Thieves Town)', player)
|
||||||
|
# forbid_item(world.get_location('Thieves\' Town - Blind\'s Cell', player), 'Big Key (Thieves Town)', player)
|
||||||
for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']:
|
for location in ['Suspicious Maiden', 'Thieves\' Town - Blind\'s Cell']:
|
||||||
set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player))
|
set_rule(world.get_location(location, player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||||
set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player))
|
set_rule(world.get_location('Revealing Light', player), lambda state: state.has('Shining Light', player) and state.has('Maiden Rescued', player))
|
||||||
@@ -304,7 +306,11 @@ def global_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
||||||
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
||||||
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
|
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
|
||||||
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
|
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or
|
||||||
|
(state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs
|
||||||
|
(state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish
|
||||||
|
state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player)) # need to defeat wizzrobes, bombs don't work ...
|
||||||
|
# byrna could work with sufficient magic
|
||||||
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
set_rule(world.get_location('Misery Mire - Spike Chest', player), lambda state: (state.world.can_take_damage and state.has_hearts(player, 4)) or state.has('Cane of Byrna', player) or state.has('Cape', player))
|
||||||
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
|
set_rule(world.get_entrance('Mire Left Bridge Hook Path', player), lambda state: state.has('Hookshot', player))
|
||||||
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
|
set_rule(world.get_entrance('Mire Tile Room NW', player), lambda state: state.has_fire_source(player))
|
||||||
@@ -561,7 +567,8 @@ def global_rules(world, player):
|
|||||||
def bomb_rules(world, player):
|
def bomb_rules(world, player):
|
||||||
bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended.
|
bonkable_doors = ['Two Brothers House Exit (West)', 'Two Brothers House Exit (East)'] # Technically this is incorrectly defined, but functionally the same as what is intended.
|
||||||
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',
|
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',
|
||||||
'Hookshot Cave Exit (South)', 'Hookshot Cave Exit (North)', 'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery']
|
'Hookshot Cave Back to Middle', 'Hookshot Cave Front to Middle', 'Hookshot Cave Middle to Front','Hookshot Cave Middle to Back',
|
||||||
|
'Dark Lake Hylia Ledge Fairy', 'Hype Cave', 'Brewery']
|
||||||
for entrance in bonkable_doors:
|
for entrance in bonkable_doors:
|
||||||
add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player))
|
add_rule(world.get_entrance(entrance, player), lambda state: state.can_use_bombs(player) or state.has_Boots(player))
|
||||||
for entrance in bombable_doors:
|
for entrance in bombable_doors:
|
||||||
@@ -576,9 +583,10 @@ def bomb_rules(world, player):
|
|||||||
for location in bombable_items:
|
for location in bombable_items:
|
||||||
add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player))
|
add_rule(world.get_location(location, player), lambda state: state.can_use_bombs(player))
|
||||||
|
|
||||||
cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy']
|
cave_kill_locations = ['Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Generous Guy', 'Spiral Cave']
|
||||||
for location in cave_kill_locations:
|
for location in cave_kill_locations:
|
||||||
add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player))
|
add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player))
|
||||||
|
add_rule(world.get_entrance('Spiral Cave (top to bottom)', player), lambda state: state.can_kill_most_things(player) or state.can_use_bombs(player))
|
||||||
|
|
||||||
paradox_switch_chests = ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle']
|
paradox_switch_chests = ['Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle']
|
||||||
for location in paradox_switch_chests:
|
for location in paradox_switch_chests:
|
||||||
@@ -706,7 +714,8 @@ def default_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
|
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player))
|
||||||
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
|
set_rule(world.get_entrance('East Death Mountain Teleporter', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||||
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
|
# can erase block - overridden in noglitches
|
||||||
|
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has_Mirror(player))
|
||||||
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
||||||
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player))
|
set_rule(world.get_entrance('Turtle Rock Teleporter', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player))
|
||||||
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player))
|
||||||
@@ -843,7 +852,8 @@ def inverted_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has_Pearl(player))
|
set_rule(world.get_entrance('Broken Bridge (East)', player), lambda state: state.has('Hookshot', player) and state.has_Pearl(player))
|
||||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player))
|
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East Bottom)', player), lambda state: state.can_lift_heavy_rocks(player))
|
||||||
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player))
|
set_rule(world.get_entrance('Fairy Ascension Rocks', player), lambda state: state.can_lift_heavy_rocks(player) and state.has_Pearl(player))
|
||||||
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has('Mirror', player)) # can erase block
|
# can erase block - overridden in noglitches
|
||||||
|
set_rule(world.get_entrance('Paradox Cave Push Block Reverse', player), lambda state: state.has_Mirror(player))
|
||||||
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player))
|
set_rule(world.get_entrance('Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player))
|
||||||
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has_Pearl(player)) # bunny cannot use hammer
|
set_rule(world.get_entrance('Dark Death Mountain Teleporter (East)', player), lambda state: state.can_lift_heavy_rocks(player) and state.has('Hammer', player) and state.has_Pearl(player)) # bunny cannot use hammer
|
||||||
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player)) # bunny can not use hammer
|
set_rule(world.get_entrance('East Death Mountain (Top)', player), lambda state: state.has('Hammer', player) and state.has_Pearl(player)) # bunny can not use hammer
|
||||||
@@ -1149,7 +1159,7 @@ def standard_rules(world, player):
|
|||||||
set_rule(entrance, lambda state: state.has('Zelda Delivered', player))
|
set_rule(entrance, lambda state: state.has('Zelda Delivered', player))
|
||||||
set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player))
|
set_rule(world.get_entrance('Sanctuary Exit', player), lambda state: state.has('Zelda Delivered', player))
|
||||||
# zelda should be saved before agahnim is in play
|
# zelda should be saved before agahnim is in play
|
||||||
set_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
|
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
|
||||||
|
|
||||||
# too restrictive for crossed?
|
# too restrictive for crossed?
|
||||||
def uncle_item_rule(item):
|
def uncle_item_rule(item):
|
||||||
@@ -1160,7 +1170,7 @@ def standard_rules(world, player):
|
|||||||
|
|
||||||
def bomb_escape_rule():
|
def bomb_escape_rule():
|
||||||
loc = world.get_location("Link's Uncle", player)
|
loc = world.get_location("Link's Uncle", player)
|
||||||
return loc.item and loc.item.name == 'Bombs (10)'
|
return loc.item and loc.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']
|
||||||
|
|
||||||
def standard_escape_rule(state):
|
def standard_escape_rule(state):
|
||||||
return state.can_kill_most_things(player) or bomb_escape_rule()
|
return state.can_kill_most_things(player) or bomb_escape_rule()
|
||||||
@@ -1206,7 +1216,7 @@ def standard_rules(world, player):
|
|||||||
'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)',
|
'North Fairy Cave', 'North Fairy Cave Drop', 'Lost Woods Gamble', 'Snitch Lady (East)',
|
||||||
'Snitch Lady (West)', 'Tavern (Front)', 'Bush Covered House', 'Light World Bomb Hut',
|
'Snitch Lady (West)', 'Tavern (Front)', 'Bush Covered House', 'Light World Bomb Hut',
|
||||||
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)',
|
'Kakariko Shop', 'Long Fairy Cave', 'Good Bee Cave', '20 Rupee Cave', 'Cave Shop (Lake Hylia)',
|
||||||
'Waterfall of Wishing', 'Hyrule Castle Main Gate', '50 Rupee Cave',
|
'Waterfall of Wishing', 'Hyrule Castle Main Gate', '50 Rupee Cave', 'Bonk Fairy (Light)',
|
||||||
'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy',
|
'Fortune Teller (Light)', 'Lake Hylia Fairy', 'Light Hype Fairy', 'Desert Fairy',
|
||||||
'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']:
|
'Lumberjack House', 'Lake Hylia Fortune Teller', 'Kakariko Gamble Game', 'Top of Pyramid']:
|
||||||
add_rule(world.get_entrance(entrance, player), lambda state: state.has('Zelda Delivered', player))
|
add_rule(world.get_entrance(entrance, player), lambda state: state.has('Zelda Delivered', player))
|
||||||
@@ -1671,7 +1681,7 @@ def set_bunny_rules(world, player, inverted):
|
|||||||
|
|
||||||
# regions for the exits of multi-entrace caves/drops that bunny cannot pass
|
# regions for the exits of multi-entrace caves/drops that bunny cannot pass
|
||||||
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
|
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
|
||||||
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave',
|
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)',
|
||||||
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
|
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
|
||||||
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
|
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
|
||||||
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
|
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
|
||||||
@@ -1939,14 +1949,12 @@ bunny_impassible_doors = {
|
|||||||
def add_key_logic_rules(world, player):
|
def add_key_logic_rules(world, player):
|
||||||
key_logic = world.key_logic[player]
|
key_logic = world.key_logic[player]
|
||||||
for d_name, d_logic in key_logic.items():
|
for d_name, d_logic in key_logic.items():
|
||||||
for door_name, keys in d_logic.door_rules.items():
|
for door_name, rule in d_logic.door_rules.items():
|
||||||
spot = world.get_entrance(door_name, player)
|
door_entrance = world.get_entrance(door_name, player)
|
||||||
if not world.retro[player] or world.mode[player] != 'standard' or not retro_in_hc(spot):
|
add_rule(door_entrance, eval_small_key_door(door_name, d_name, player))
|
||||||
rule = create_advanced_key_rule(d_logic, player, keys)
|
if door_entrance.door.dependents:
|
||||||
if keys.opposite:
|
for dep in door_entrance.door.dependents:
|
||||||
rule = or_rule(rule, create_advanced_key_rule(d_logic, player, keys.opposite))
|
add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player))
|
||||||
add_rule(spot, rule)
|
|
||||||
|
|
||||||
for location in d_logic.bk_restricted:
|
for location in d_logic.bk_restricted:
|
||||||
if not location.forced_item:
|
if not location.forced_item:
|
||||||
forbid_item(location, d_logic.bk_name, player)
|
forbid_item(location, d_logic.bk_name, player)
|
||||||
@@ -1955,7 +1963,10 @@ def add_key_logic_rules(world, player):
|
|||||||
for door in d_logic.bk_doors:
|
for door in d_logic.bk_doors:
|
||||||
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
|
add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player))
|
||||||
for chest in d_logic.bk_chests:
|
for chest in d_logic.bk_chests:
|
||||||
add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player))
|
big_chest = world.get_location(chest.name, player)
|
||||||
|
add_rule(big_chest, create_rule(d_logic.bk_name, player))
|
||||||
|
if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1:
|
||||||
|
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
|
||||||
if world.retro[player]:
|
if world.retro[player]:
|
||||||
for d_name, layout in world.key_layout[player].items():
|
for d_name, layout in world.key_layout[player].items():
|
||||||
for door in layout.flat_prop:
|
for door in layout.flat_prop:
|
||||||
@@ -1963,6 +1974,40 @@ def add_key_logic_rules(world, player):
|
|||||||
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1))
|
add_rule(door.entrance, create_key_rule('Small Key (Universal)', player, 1))
|
||||||
|
|
||||||
|
|
||||||
|
def eval_small_key_door_main(state, door_name, dungeon, player):
|
||||||
|
if state.is_door_open(door_name, player):
|
||||||
|
return True
|
||||||
|
key_logic = state.world.key_logic[player][dungeon]
|
||||||
|
door_rule = key_logic.door_rules[door_name]
|
||||||
|
door_openable = False
|
||||||
|
for ruleType, number in door_rule.new_rules.items():
|
||||||
|
if door_openable:
|
||||||
|
return True
|
||||||
|
if ruleType == KeyRuleType.WorstCase:
|
||||||
|
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
|
||||||
|
elif ruleType == KeyRuleType.AllowSmall:
|
||||||
|
if (door_rule.small_location.item and door_rule.small_location.item.name == key_logic.small_key_name
|
||||||
|
and door_rule.small_location.item.player == player):
|
||||||
|
return True # always okay if allow small is on
|
||||||
|
elif isinstance(ruleType, tuple):
|
||||||
|
lock, lock_item = ruleType
|
||||||
|
# this doesn't track logical locks yet, i.e. hammer locks the item and hammer is there, but the item isn't
|
||||||
|
for loc in door_rule.alternate_big_key_loc:
|
||||||
|
spot = state.world.get_location(loc, player)
|
||||||
|
if spot.item and spot.item.name == lock_item:
|
||||||
|
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
|
||||||
|
break
|
||||||
|
return door_openable
|
||||||
|
|
||||||
|
|
||||||
|
def eval_small_key_door(door_name, dungeon, player):
|
||||||
|
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
||||||
|
|
||||||
|
|
||||||
|
def allow_big_key_in_big_chest(bk_name, player):
|
||||||
|
return lambda state, item: item.name == bk_name and item.player == player
|
||||||
|
|
||||||
|
|
||||||
def retro_in_hc(spot):
|
def retro_in_hc(spot):
|
||||||
return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False
|
return spot.parent_region.dungeon.name == 'Hyrule Castle' if spot.parent_region.dungeon else False
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import operator as op
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
def int16_as_bytes(value):
|
def int16_as_bytes(value):
|
||||||
@@ -116,6 +118,28 @@ def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Jap
|
|||||||
return "New Rom Hash: " + basemd5.hexdigest()
|
return "New Rom Hash: " + basemd5.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def kth_combination(k, l, r):
|
||||||
|
if r == 0:
|
||||||
|
return []
|
||||||
|
elif len(l) == r:
|
||||||
|
return l
|
||||||
|
else:
|
||||||
|
i = ncr(len(l)-1, r-1)
|
||||||
|
if k < i:
|
||||||
|
return l[0:1] + kth_combination(k, l[1:], r-1)
|
||||||
|
else:
|
||||||
|
return kth_combination(k-i, l[1:], r)
|
||||||
|
|
||||||
|
|
||||||
|
def ncr(n, r):
|
||||||
|
if r == 0:
|
||||||
|
return 1
|
||||||
|
r = min(r, n-r)
|
||||||
|
numerator = reduce(op.mul, range(n, n-r, -1), 1)
|
||||||
|
denominator = reduce(op.mul, range(1, r+1), 1)
|
||||||
|
return numerator / denominator
|
||||||
|
|
||||||
|
|
||||||
entrance_offsets = {
|
entrance_offsets = {
|
||||||
'Sanctuary': 0x2,
|
'Sanctuary': 0x2,
|
||||||
'HC West': 0x3,
|
'HC West': 0x3,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
; Normal doors use $FE to store the trap door indicator
|
; Normal doors use $FE to store the trap door indicator
|
||||||
; Normal doors use $045e to store Y coordinate when transitioning to in-room stairs
|
; Normal doors use $045e to store Y coordinate when transitioning to in-room stairs
|
||||||
; Normal doors use $045f to determine the order in which supertile quadrants are drawn
|
; Normal doors use $045f to determine the order in which supertile quadrants are drawn
|
||||||
|
; Straight stairs use $046d to store X coordinate on animation start
|
||||||
; Spiral doors use $045e to store stair type
|
; Spiral doors use $045e to store stair type
|
||||||
; Gfx uses $b1 to for sub-sub-sub-module thing
|
; Gfx uses $b1 to for sub-sub-sub-module thing
|
||||||
|
|
||||||
|
|||||||
@@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c
|
|||||||
dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052
|
dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052
|
||||||
; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross
|
; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross
|
||||||
dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e
|
dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e
|
||||||
|
; Sewer Drop, Mire Cross, GT Crystal Circles
|
||||||
|
dw $0011, $00b2, $003d
|
||||||
dw $ffff
|
dw $ffff
|
||||||
|
|
||||||
; dungeon tables
|
; dungeon tables
|
||||||
@@ -679,6 +681,8 @@ db $00,$07,$20,$20,$07,$07,$07,$07,$07,$20,$20,$07,$20,$20,$20,$20
|
|||||||
db $07,$07,$02,$02,$02,$02,$07,$07,$07,$20,$20,$07,$20,$20,$20,$07
|
db $07,$07,$02,$02,$02,$02,$07,$07,$07,$20,$20,$07,$20,$20,$20,$07
|
||||||
|
|
||||||
;27f300
|
;27f300
|
||||||
|
DungeonTilesets:
|
||||||
|
db $04,$04,$05,$12,$04,$08,$07,$0C,$09,$0B,$05,$0A,$0D,$0E,$06,$06
|
||||||
|
|
||||||
;
|
;
|
||||||
;org $27ff00
|
;org $27ff00
|
||||||
|
|||||||
+11
-3
@@ -76,6 +76,9 @@ nop : jsl OverridePaletteHeader
|
|||||||
org $02817e ; Bank02.asm : 414 (LDA $02811E, X)
|
org $02817e ; Bank02.asm : 414 (LDA $02811E, X)
|
||||||
jsl FixAnimatedTiles
|
jsl FixAnimatedTiles
|
||||||
|
|
||||||
|
org $0aef43 ; UnderworldMap_RecoverGFX
|
||||||
|
jsl FixCloseDungeonMap
|
||||||
|
|
||||||
org $028a06 ; Bank02.asm : 1941 Dungeon_ResetTorchBackgroundAndPlayer
|
org $028a06 ; Bank02.asm : 1941 Dungeon_ResetTorchBackgroundAndPlayer
|
||||||
JSL FixWallmasterLamp
|
JSL FixWallmasterLamp
|
||||||
|
|
||||||
@@ -125,6 +128,11 @@ org $07a955 ; <- Bank07.asm : around 6564 (JP is a bit different) (STZ $05FC : S
|
|||||||
jsl BlockEraseFix
|
jsl BlockEraseFix
|
||||||
nop #2
|
nop #2
|
||||||
|
|
||||||
|
org $02A0A8
|
||||||
|
Mirror_SaveRoomData:
|
||||||
|
org $07A95B ; < bank_07.asm ; #_07A95B: JSL Mirror_SaveRoomData
|
||||||
|
jsl EGFixOnMirror
|
||||||
|
|
||||||
org $02b82a
|
org $02b82a
|
||||||
jsl FixShopCode
|
jsl FixShopCode
|
||||||
|
|
||||||
@@ -181,9 +189,9 @@ Main_ShowTextMessage:
|
|||||||
|
|
||||||
; Conditionally disable UW music changes in Door Rando
|
; Conditionally disable UW music changes in Door Rando
|
||||||
org $028ADB ; <- Bank02.asm:2088-2095 (LDX.b #$14 : LDA $A0 ...)
|
org $028ADB ; <- Bank02.asm:2088-2095 (LDX.b #$14 : LDA $A0 ...)
|
||||||
JSL.l Underworld_DoorDown_Entry : CPX #$10
|
JSL.l Underworld_DoorDown_Entry : CPX #$FF
|
||||||
db $B0, $21 ; BCS $028B04
|
BEQ + : db $80, $1C ; BRA $028B04
|
||||||
BRA + : NOP #6 : +
|
NOP #6 : +
|
||||||
|
|
||||||
org $02C3F2 ; <- Bank02.asm:10521 Unused call
|
org $02C3F2 ; <- Bank02.asm:10521 Unused call
|
||||||
Underworld_DoorDown_Call:
|
Underworld_DoorDown_Call:
|
||||||
|
|||||||
+10
@@ -45,6 +45,16 @@ FixAnimatedTiles:
|
|||||||
+ LDA $02802E, X ; what we wrote over
|
+ LDA $02802E, X ; what we wrote over
|
||||||
RTL
|
RTL
|
||||||
|
|
||||||
|
FixCloseDungeonMap:
|
||||||
|
LDA.l DRMode : CMP #$02 : BNE .vanilla
|
||||||
|
LDA $040C : BMI .vanilla
|
||||||
|
LSR : TAX
|
||||||
|
LDA.l DungeonTilesets,x
|
||||||
|
RTL
|
||||||
|
.vanilla
|
||||||
|
LDA $7EC20E
|
||||||
|
RTL
|
||||||
|
|
||||||
FixWallmasterLamp:
|
FixWallmasterLamp:
|
||||||
ORA $0458
|
ORA $0458
|
||||||
STY $1C : STA $1D : RTL ; what we wrote over
|
STY $1C : STA $1D : RTL ; what we wrote over
|
||||||
|
|||||||
+13
-7
@@ -73,8 +73,9 @@ TrapDoorFixer:
|
|||||||
rts
|
rts
|
||||||
|
|
||||||
Cleanup:
|
Cleanup:
|
||||||
stz $047a
|
lda.l DRFlags : and #$10 : beq +
|
||||||
inc $11
|
stz $047a
|
||||||
|
+ inc $11
|
||||||
lda $ef
|
lda $ef
|
||||||
rts
|
rts
|
||||||
|
|
||||||
@@ -149,15 +150,14 @@ LoadRoomVert:
|
|||||||
.notEdge
|
.notEdge
|
||||||
lda $01 : and #$03 : cmp #$03 : bne .normal
|
lda $01 : and #$03 : cmp #$03 : bne .normal
|
||||||
jsr ScrollToInroomStairs
|
jsr ScrollToInroomStairs
|
||||||
|
stz $046d
|
||||||
bra .end
|
bra .end
|
||||||
.normal
|
.normal
|
||||||
ldy #$01 : jsr ShiftVariablesMainDir
|
ldy #$01 : jsr ShiftVariablesMainDir
|
||||||
jsr PrepScrollToNormal
|
jsr PrepScrollToNormal
|
||||||
.scroll
|
.scroll
|
||||||
lda $01 : and #$40 : pha
|
lda $01 : and #$40 : sta $046d
|
||||||
jsr ScrollX
|
jsr ScrollX
|
||||||
pla : beq .end
|
|
||||||
ldy #$00 : jsr ApplyScroll
|
|
||||||
.end
|
.end
|
||||||
plb ; restore db register
|
plb ; restore db register
|
||||||
rts
|
rts
|
||||||
@@ -290,6 +290,11 @@ StraightStairsAdj:
|
|||||||
stx $0464 : sty $012e ; what we wrote over
|
stx $0464 : sty $012e ; what we wrote over
|
||||||
lda.l DRMode : beq +
|
lda.l DRMode : beq +
|
||||||
lda $045e : bne .toInroom
|
lda $045e : bne .toInroom
|
||||||
|
lda $046d : beq .noScroll
|
||||||
|
sta $22
|
||||||
|
ldy #$00 : jsr ApplyScroll
|
||||||
|
stz $046d
|
||||||
|
.noScroll
|
||||||
jsr GetTileAttribute : tax
|
jsr GetTileAttribute : tax
|
||||||
lda $11 : cmp #$12 : beq .goingNorth
|
lda $11 : cmp #$12 : beq .goingNorth
|
||||||
lda $a2 : cmp #$51 : bne ++
|
lda $a2 : cmp #$51 : bne ++
|
||||||
@@ -337,9 +342,10 @@ db $d0, $f6, $10, $1a, $f0, $00
|
|||||||
|
|
||||||
StraightStairsFix:
|
StraightStairsFix:
|
||||||
{
|
{
|
||||||
|
pha
|
||||||
lda.l DRMode : bne +
|
lda.l DRMode : bne +
|
||||||
!add $20 : sta $20 ;what we wrote over
|
pla : !add $20 : sta $20 : rtl ;what we wrote over
|
||||||
+ rtl
|
+ pla : rtl
|
||||||
}
|
}
|
||||||
|
|
||||||
StraightStairLayerFix:
|
StraightStairLayerFix:
|
||||||
|
|||||||
+7
-5
@@ -35,11 +35,7 @@ rtl
|
|||||||
|
|
||||||
OnFileLoadOverride:
|
OnFileLoadOverride:
|
||||||
jsl OnFileLoad ; what I wrote over
|
jsl OnFileLoad ; what I wrote over
|
||||||
lda.l DRFlags : and #$80 : beq + ;flag is off
|
+ lda.l DRFlags : and #$02 : beq + ; Mirror Scroll
|
||||||
lda $7ef086 : ora #$80 : sta $7ef086
|
|
||||||
+ lda.l DRFlags : and #$40 : beq + ;flag is off
|
|
||||||
lda $7ef036 : ora #$80 : sta $7ef036
|
|
||||||
+ lda.l DRFlags : and #$02 : beq +
|
|
||||||
lda $7ef353 : bne +
|
lda $7ef353 : bne +
|
||||||
lda #$01 : sta $7ef353
|
lda #$01 : sta $7ef353
|
||||||
+ rtl
|
+ rtl
|
||||||
@@ -51,6 +47,12 @@ MirrorCheckOverride:
|
|||||||
rtl
|
rtl
|
||||||
+ lda.l DRScroll : rtl
|
+ lda.l DRScroll : rtl
|
||||||
|
|
||||||
|
EGFixOnMirror:
|
||||||
|
lda.l DRFlags : and #$10 : beq +
|
||||||
|
stz $047a
|
||||||
|
+ jsl Mirror_SaveRoomData
|
||||||
|
rtl
|
||||||
|
|
||||||
BlockEraseFix:
|
BlockEraseFix:
|
||||||
lda $7ef353 : and #$02 : beq +
|
lda $7ef353 : and #$02 : beq +
|
||||||
stz $05fc : stz $05fd
|
stz $05fc : stz $05fd
|
||||||
|
|||||||
+5
-1
@@ -168,7 +168,11 @@ ScrollX: ;change the X offset variables
|
|||||||
|
|
||||||
pla : sta $00
|
pla : sta $00
|
||||||
sep #$30
|
sep #$30
|
||||||
lda $04 : sta $22
|
lda $04 : ldx $046d : bne .straight
|
||||||
|
sta $22 : bra +
|
||||||
|
.straight
|
||||||
|
sta $046d ; set X position later
|
||||||
|
+
|
||||||
lda $00 : sta $23 : sta $0609 : sta $060d
|
lda $00 : sta $23 : sta $0609 : sta $060d
|
||||||
lda $01 : sta $a9
|
lda $01 : sta $a9
|
||||||
lda $0e : asl : ora $ac : sta $ac
|
lda $0e : asl : ora $ac : sta $ac
|
||||||
|
|||||||
Binary file not shown.
@@ -16,6 +16,9 @@
|
|||||||
pot_shuffle:
|
pot_shuffle:
|
||||||
on: 1
|
on: 1
|
||||||
off: 3
|
off: 3
|
||||||
|
bombbag:
|
||||||
|
on: 1
|
||||||
|
off: 4
|
||||||
entrance_shuffle:
|
entrance_shuffle:
|
||||||
none: 15
|
none: 15
|
||||||
dungeonssimple: 3
|
dungeonssimple: 3
|
||||||
@@ -132,3 +135,6 @@
|
|||||||
half: 0
|
half: 0
|
||||||
quarter: 1
|
quarter: 1
|
||||||
off: 0
|
off: 0
|
||||||
|
shuffle_sfx:
|
||||||
|
on: 1
|
||||||
|
off: 1
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
description: Example for subweights
|
||||||
|
glitches_required: none
|
||||||
|
world_state: open
|
||||||
|
goals: ganon
|
||||||
|
weapons: randomized
|
||||||
|
entrance_shuffle: none
|
||||||
|
intensity: 3
|
||||||
|
subweights:
|
||||||
|
vanilla:
|
||||||
|
chance: 25
|
||||||
|
weights:
|
||||||
|
door_shuffle: vanilla
|
||||||
|
keydropshuffle:
|
||||||
|
on: 40
|
||||||
|
off: 60
|
||||||
|
basic:
|
||||||
|
chance: 25
|
||||||
|
weights:
|
||||||
|
door_shuffle: basic
|
||||||
|
keydropshuffle:
|
||||||
|
on: 70
|
||||||
|
off: 30
|
||||||
|
crossed:
|
||||||
|
chance: 25
|
||||||
|
weights:
|
||||||
|
door_shuffle: crossed
|
||||||
|
keydropshuffle:
|
||||||
|
on: 90
|
||||||
|
off: 10
|
||||||
|
chaos:
|
||||||
|
chance: 25
|
||||||
|
weights:
|
||||||
|
door_shuffle: crossed
|
||||||
|
entrance_shuffle:
|
||||||
|
none: 30
|
||||||
|
crossed: 70
|
||||||
|
keydropshuffle:
|
||||||
|
on: 90
|
||||||
|
off: 10
|
||||||
|
shopsanity:
|
||||||
|
on: 50
|
||||||
|
off: 50
|
||||||
|
bombbag:
|
||||||
|
on: 25
|
||||||
|
off: 75
|
||||||
|
subweights:
|
||||||
|
normal:
|
||||||
|
chance: 40
|
||||||
|
weights: {}
|
||||||
|
swordless:
|
||||||
|
chance: 20
|
||||||
|
weights:
|
||||||
|
weapons: swordless
|
||||||
|
keysanity:
|
||||||
|
chance: 40
|
||||||
|
weights:
|
||||||
|
dungeon_items: full
|
||||||
@@ -199,6 +199,10 @@
|
|||||||
"action": "store_true",
|
"action": "store_true",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
},
|
},
|
||||||
|
"shuffle_sfx": {
|
||||||
|
"action": "store_true",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
"mapshuffle": {
|
"mapshuffle": {
|
||||||
"action": "store_true",
|
"action": "store_true",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
@@ -220,6 +224,10 @@
|
|||||||
"type": "bool",
|
"type": "bool",
|
||||||
"help": "suppress"
|
"help": "suppress"
|
||||||
},
|
},
|
||||||
|
"bombbag": {
|
||||||
|
"action": "store_true",
|
||||||
|
"type": "bool"
|
||||||
|
},
|
||||||
"retro": {
|
"retro": {
|
||||||
"action": "store_true",
|
"action": "store_true",
|
||||||
"type": "bool"
|
"type": "bool"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"cli": {
|
"cli": {
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"app.title": "ALttP Door Randomizer Version %s - Seed: %s, Code: %s",
|
"app.title": "ALttP Door Randomizer Version %s : --seed %s --code %s",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"seed": "Seed",
|
"seed": "Seed",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
@@ -52,7 +52,8 @@
|
|||||||
"enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.",
|
"enemizer.nothing.applied": "No Enemizer options will be applied until this is resolved.",
|
||||||
"building.collection.spheres": "Building up collection spheres",
|
"building.collection.spheres": "Building up collection spheres",
|
||||||
"building.calculating.spheres": "Calculated sphere %i, containing %i of %i progress items.",
|
"building.calculating.spheres": "Calculated sphere %i, containing %i of %i progress items.",
|
||||||
"building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items."
|
"building.final.spheres": "Calculated final sphere %i, containing %i of %i progress items.",
|
||||||
|
"old.python.version": "Door Rando may have issues with python versions earlier than 3.7. Detected version: %s"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"lang": [ "App Language, if available, defaults to English" ],
|
"lang": [ "App Language, if available, defaults to English" ],
|
||||||
@@ -263,6 +264,7 @@
|
|||||||
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
|
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
|
||||||
],
|
],
|
||||||
"pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"],
|
"pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"],
|
||||||
|
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
|
||||||
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
|
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
|
||||||
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
|
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
|
||||||
"custom": [ "Not supported." ],
|
"custom": [ "Not supported." ],
|
||||||
@@ -295,6 +297,7 @@
|
|||||||
"sprite that will be extracted."
|
"sprite that will be extracted."
|
||||||
],
|
],
|
||||||
"reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ],
|
"reduce_flashing": [ "Reduce some in-game flashing (default: %(default)s)" ],
|
||||||
|
"shuffle_sfx": [ "Shuffle sounds effects (default: %(default)s)" ],
|
||||||
"create_rom": [ "Create an output rom file. (default: %(default)s)" ],
|
"create_rom": [ "Create an output rom file. (default: %(default)s)" ],
|
||||||
"gui": [ "Launch the GUI. (default: %(default)s)" ],
|
"gui": [ "Launch the GUI. (default: %(default)s)" ],
|
||||||
"jsonout": [
|
"jsonout": [
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"checkboxes": {
|
"checkboxes": {
|
||||||
"nobgm": { "type": "checkbox" },
|
"nobgm": { "type": "checkbox" },
|
||||||
"quickswap": { "type": "checkbox" },
|
"quickswap": { "type": "checkbox" },
|
||||||
"reduce_flashing": {"type": "checkbox"}
|
"reduce_flashing": {"type": "checkbox"},
|
||||||
|
"shuffle_sfx": {"type": "checkbox"}
|
||||||
},
|
},
|
||||||
"leftAdjustFrame": {
|
"leftAdjustFrame": {
|
||||||
"heartcolor": {
|
"heartcolor": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"adjust.nobgm": "Disable Music & MSU-1",
|
"adjust.nobgm": "Disable Music & MSU-1",
|
||||||
"adjust.quickswap": "L/R Quickswapping",
|
"adjust.quickswap": "L/R Quickswapping",
|
||||||
"adjust.reduce_flashing": "Reduce Flashing",
|
"adjust.reduce_flashing": "Reduce Flashing",
|
||||||
|
"adjust.shuffle_sfx": "Shuffle Sound Effects",
|
||||||
|
|
||||||
"adjust.heartcolor": "Heart Color",
|
"adjust.heartcolor": "Heart Color",
|
||||||
"adjust.heartcolor.red": "Red",
|
"adjust.heartcolor.red": "Red",
|
||||||
@@ -134,6 +135,7 @@
|
|||||||
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
|
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
|
||||||
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
|
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
|
||||||
"randomizer.gameoptions.reduce_flashing": "Reduce Flashing",
|
"randomizer.gameoptions.reduce_flashing": "Reduce Flashing",
|
||||||
|
"randomizer.gameoptions.shuffle_sfx": "Shuffle Sound Effects",
|
||||||
|
|
||||||
"randomizer.gameoptions.heartcolor": "Heart Color",
|
"randomizer.gameoptions.heartcolor": "Heart Color",
|
||||||
"randomizer.gameoptions.heartcolor.red": "Red",
|
"randomizer.gameoptions.heartcolor.red": "Red",
|
||||||
@@ -190,6 +192,7 @@
|
|||||||
"randomizer.item.hints": "Include Helpful Hints",
|
"randomizer.item.hints": "Include Helpful Hints",
|
||||||
"randomizer.item.retro": "Retro mode (universal keys)",
|
"randomizer.item.retro": "Retro mode (universal keys)",
|
||||||
"randomizer.item.pseudoboots": "Start with Pseudo Boots",
|
"randomizer.item.pseudoboots": "Start with Pseudo Boots",
|
||||||
|
"randomizer.item.bombbag": "Bombbag",
|
||||||
|
|
||||||
"randomizer.item.worldstate": "World State",
|
"randomizer.item.worldstate": "World State",
|
||||||
"randomizer.item.worldstate.standard": "Standard",
|
"randomizer.item.worldstate.standard": "Standard",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"checkboxes": {
|
"checkboxes": {
|
||||||
"nobgm": { "type": "checkbox" },
|
"nobgm": { "type": "checkbox" },
|
||||||
"quickswap": { "type": "checkbox" },
|
"quickswap": { "type": "checkbox" },
|
||||||
"reduce_flashing": {"type": "checkbox"}
|
"reduce_flashing": {"type": "checkbox"},
|
||||||
|
"shuffle_sfx": {"type": "checkbox"}
|
||||||
},
|
},
|
||||||
"leftRomOptionsFrame": {
|
"leftRomOptionsFrame": {
|
||||||
"heartcolor": {
|
"heartcolor": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"checkboxes": {
|
"checkboxes": {
|
||||||
"retro": { "type": "checkbox" },
|
"retro": { "type": "checkbox" },
|
||||||
|
"bombbag": { "type": "checkbox" },
|
||||||
"shopsanity": { "type": "checkbox" },
|
"shopsanity": { "type": "checkbox" },
|
||||||
"hints": {
|
"hints": {
|
||||||
"type": "checkbox"
|
"type": "checkbox"
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import random
|
||||||
|
from Utils import int16_as_bytes
|
||||||
|
|
||||||
|
|
||||||
|
class SFX(object):
|
||||||
|
|
||||||
|
def __init__(self, name, sfx_set, orig_id, addr, chain, accomp=False):
|
||||||
|
self.name = name
|
||||||
|
self.sfx_set = sfx_set
|
||||||
|
self.orig_id = orig_id
|
||||||
|
self.addr = addr
|
||||||
|
self.chain = chain
|
||||||
|
self.accomp = accomp
|
||||||
|
|
||||||
|
self.target_set = None
|
||||||
|
self.target_id = None
|
||||||
|
self.target_chain = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_sfx_data():
|
||||||
|
sfx_pool = [SFX('Slash1', 0x02, 0x01, 0x2614, []), SFX('Slash2', 0x02, 0x02, 0x2625, []),
|
||||||
|
SFX('Slash3', 0x02, 0x03, 0x2634, []), SFX('Slash4', 0x02, 0x04, 0x2643, []),
|
||||||
|
SFX('Wall clink', 0x02, 0x05, 0x25DD, []), SFX('Bombable door clink', 0x02, 0x06, 0x25D7, []),
|
||||||
|
SFX('Fwoosh shooting', 0x02, 0x07, 0x25B7, []), SFX('Arrow hitting wall', 0x02, 0x08, 0x25E3, []),
|
||||||
|
SFX('Boomerang whooshing', 0x02, 0x09, 0x25AD, []), SFX('Hookshot', 0x02, 0x0A, 0x25C7, []),
|
||||||
|
SFX('Placing bomb', 0x02, 0x0B, 0x2478, []),
|
||||||
|
SFX('Bomb exploding/Quake/Bombos/Exploding wall', 0x02, 0x0C, 0x269C, []),
|
||||||
|
SFX('Powder', 0x02, 0x0D, 0x2414, [0x3f]), SFX('Fire rod shot', 0x02, 0x0E, 0x2404, []),
|
||||||
|
SFX('Ice rod shot', 0x02, 0x0F, 0x24C3, []), SFX('Hammer use', 0x02, 0x10, 0x23FA, []),
|
||||||
|
SFX('Hammering peg', 0x02, 0x11, 0x23F0, []), SFX('Digging', 0x02, 0x12, 0x23CD, []),
|
||||||
|
SFX('Flute use', 0x02, 0x13, 0x23A0, [0x3e]), SFX('Cape on', 0x02, 0x14, 0x2380, []),
|
||||||
|
SFX('Cape off/Wallmaster grab', 0x02, 0x15, 0x2390, []), SFX('Staircase', 0x02, 0x16, 0x232C, []),
|
||||||
|
SFX('Staircase', 0x02, 0x17, 0x2344, []), SFX('Staircase', 0x02, 0x18, 0x2356, []),
|
||||||
|
SFX('Staircase', 0x02, 0x19, 0x236E, []), SFX('Tall grass/Hammer hitting bush', 0x02, 0x1A, 0x2316, []),
|
||||||
|
SFX('Mire shallow water', 0x02, 0x1B, 0x2307, []), SFX('Shallow water', 0x02, 0x1C, 0x2301, []),
|
||||||
|
SFX('Lifting object', 0x02, 0x1D, 0x22BB, []), SFX('Cutting grass', 0x02, 0x1E, 0x2577, []),
|
||||||
|
SFX('Item breaking', 0x02, 0x1F, 0x22E9, []), SFX('Item falling in pit', 0x02, 0x20, 0x22DA, []),
|
||||||
|
SFX('Bomb hitting ground/General bang', 0x02, 0x21, 0x22CF, []),
|
||||||
|
SFX('Pushing object/Armos bounce', 0x02, 0x22, 0x2107, []), SFX('Boots dust', 0x02, 0x23, 0x22B1, []),
|
||||||
|
SFX('Splashing', 0x02, 0x24, 0x22A5, [0x3d]), SFX('Mire shallow water again?', 0x02, 0x25, 0x2296, []),
|
||||||
|
SFX('Link taking damage', 0x02, 0x26, 0x2844, []), SFX('Fainting', 0x02, 0x27, 0x2252, []),
|
||||||
|
SFX('Item splash', 0x02, 0x28, 0x2287, []), SFX('Rupee refill', 0x02, 0x29, 0x243F, [0x3b]),
|
||||||
|
SFX('Fire rod shot hitting wall/Bombos spell', 0x02, 0x2A, 0x2033, []),
|
||||||
|
SFX('Heart beep/Text box', 0x02, 0x2B, 0x1FF2, []), SFX('Sword up', 0x02, 0x2C, 0x1FD9, [0x3a]),
|
||||||
|
SFX('Magic drain', 0x02, 0x2D, 0x20A6, []), SFX('GT opening', 0x02, 0x2E, 0x1FCA, [0x39]),
|
||||||
|
SFX('GT opening/Water drain', 0x02, 0x2F, 0x1F47, [0x38]), SFX('Cucco', 0x02, 0x30, 0x1EF1, []),
|
||||||
|
SFX('Fairy', 0x02, 0x31, 0x20CE, []), SFX('Bug net', 0x02, 0x32, 0x1D47, []),
|
||||||
|
SFX('Teleport2', 0x02, 0x33, 0x1CDC, [], True), SFX('Teleport1', 0x02, 0x34, 0x1F6F, [0x33]),
|
||||||
|
SFX('Quake/Vitreous/Zora king/Armos/Pyramid/Lanmo', 0x02, 0x35, 0x1C67, [0x36]),
|
||||||
|
SFX('Mire entrance (extends above)', 0x02, 0x36, 0x1C64, [], True),
|
||||||
|
SFX('Spin charged', 0x02, 0x37, 0x1A43, []), SFX('Water sound', 0x02, 0x38, 0x1F6F, [], True),
|
||||||
|
SFX('GT opening thunder', 0x02, 0x39, 0x1F9C, [], True), SFX('Sword up', 0x02, 0x3A, 0x1FE7, [], True),
|
||||||
|
SFX('Quiet rupees', 0x02, 0x3B, 0x2462, [], True), SFX('Error beep', 0x02, 0x3C, 0x1A37, []),
|
||||||
|
SFX('Big splash', 0x02, 0x3D, 0x22AB, [], True), SFX('Flute again', 0x02, 0x3E, 0x23B5, [], True),
|
||||||
|
SFX('Powder paired', 0x02, 0x3F, 0x2435, [], True),
|
||||||
|
|
||||||
|
SFX('Sword beam', 0x03, 0x01, 0x1A18, []),
|
||||||
|
SFX('TR opening', 0x03, 0x02, 0x254E, []), SFX('Pyramid hole', 0x03, 0x03, 0x224A, []),
|
||||||
|
SFX('Angry soldier', 0x03, 0x04, 0x220E, []), SFX('Lynel shot/Javelin toss', 0x03, 0x05, 0x25B7, []),
|
||||||
|
SFX('BNC swing/Phantom ganon/Helma tail/Arrghus swoosh', 0x03, 0x06, 0x21F5, []),
|
||||||
|
SFX('Cannon fire', 0x03, 0x07, 0x223D, []), SFX('Damage to enemy; $0BEX.4=1', 0x03, 0x08, 0x21E6, []),
|
||||||
|
SFX('Enemy death', 0x03, 0x09, 0x21C1, []), SFX('Collecting rupee', 0x03, 0x0A, 0x21A9, []),
|
||||||
|
SFX('Collecting heart', 0x03, 0x0B, 0x2198, []),
|
||||||
|
SFX('Non-blank text character', 0x03, 0x0C, 0x218E, []),
|
||||||
|
SFX('HUD heart (used explicitly by sanc heart?)', 0x03, 0x0D, 0x21B5, []),
|
||||||
|
SFX('Opening chest', 0x03, 0x0E, 0x2182, []),
|
||||||
|
SFX('♪Do do do doooooo♫', 0x03, 0x0F, 0x24B9, [0x3C, 0x3D, 0x3E, 0x3F]),
|
||||||
|
SFX('Opening/Closing map (paired)', 0x03, 0x10, 0x216D, [0x3b]),
|
||||||
|
SFX('Opening item menu/Bomb shop guy breathing', 0x03, 0x11, 0x214F, []),
|
||||||
|
SFX('Closing item menu/Bomb shop guy breathing', 0x03, 0x12, 0x215E, []),
|
||||||
|
SFX('Throwing object (sprites use it as well)/Stalfos jump', 0x03, 0x13, 0x213B, []),
|
||||||
|
SFX('Key door/Trinecks/Dash key landing/Stalfos Knight collapse', 0x03, 0x14, 0x246C, []),
|
||||||
|
SFX('Door closing/OW door opening/Chest opening (w/ $29 in $012E)', 0x03, 0x15, 0x212F, []),
|
||||||
|
SFX('Armos Knight thud', 0x03, 0x16, 0x2123, []), SFX('Rat squeak', 0x03, 0x17, 0x25A6, []),
|
||||||
|
SFX('Dragging/Mantle moving', 0x03, 0x18, 0x20DD, []),
|
||||||
|
SFX('Fireball/Laser shot; Somehow used by Trinexx???', 0x03, 0x19, 0x250A, []),
|
||||||
|
SFX('Chest reveal jingle ', 0x03, 0x1A, 0x1E8A, [0x38]),
|
||||||
|
SFX('Puzzle jingle', 0x03, 0x1B, 0x20B6, [0x3a]), SFX('Damage to enemy', 0x03, 0x1C, 0x1A62, []),
|
||||||
|
SFX('Potion refill/Magic drain', 0x03, 0x1D, 0x20A6, []),
|
||||||
|
SFX('Flapping (Duck/Cucco swarm/Ganon bats/Keese/Raven/Vulture)', 0x03, 0x1E, 0x2091, []),
|
||||||
|
SFX('Link falling', 0x03, 0x1F, 0x204B, []), SFX('Menu/Text cursor moved', 0x03, 0x20, 0x276C, []),
|
||||||
|
SFX('Damage to boss', 0x03, 0x21, 0x27E2, []), SFX('Boss dying/Deleting file', 0x03, 0x22, 0x26CF, []),
|
||||||
|
SFX('Spin attack/Medallion swoosh', 0x03, 0x23, 0x2001, [0x39]),
|
||||||
|
SFX('OW map perspective change', 0x03, 0x24, 0x2043, []),
|
||||||
|
SFX('Pressure switch', 0x03, 0x25, 0x1E9D, []),
|
||||||
|
SFX('Lightning/Game over/Laser/Ganon bat/Trinexx lunge', 0x03, 0x26, 0x1E7B, []),
|
||||||
|
SFX('Agahnim charge', 0x03, 0x27, 0x1E40, []), SFX('Agahnim/Ganon teleport', 0x03, 0x28, 0x26F7, []),
|
||||||
|
SFX('Agahnim shot', 0x03, 0x29, 0x1E21, []),
|
||||||
|
SFX('Somaria/Byrna/Ether spell/Helma fire ball', 0x03, 0x2A, 0x1E12, []),
|
||||||
|
SFX('Electrocution', 0x03, 0x2B, 0x1DF3, []), SFX('Bees', 0x03, 0x2C, 0x1DC0, []),
|
||||||
|
SFX('Milestone, also via text', 0x03, 0x2D, 0x1DA9, [0x37]),
|
||||||
|
SFX('Collecting heart container', 0x03, 0x2E, 0x1D5D, [0x35, 0x34]),
|
||||||
|
SFX('Collecting absorbable key', 0x03, 0x2F, 0x1D80, [0x33]),
|
||||||
|
SFX('Byrna spark/Item plop/Magic bat zap/Blob emerge', 0x03, 0x30, 0x1B53, []),
|
||||||
|
SFX('Sprite falling/Moldorm shuffle', 0x03, 0x31, 0x1ACA, []),
|
||||||
|
SFX('Bumper boing/Somaria punt/Blob transmutation/Sprite boings', 0x03, 0x32, 0x1A78, []),
|
||||||
|
SFX('Jingle (paired $2F→$33)', 0x03, 0x33, 0x1D93, [], True),
|
||||||
|
SFX('Depressing jingle (paired $2E→$35→$34)', 0x03, 0x34, 0x1D66, [], True),
|
||||||
|
SFX('Ugly jingle (paired $2E→$35→$34)', 0x03, 0x35, 0x1D73, [], True),
|
||||||
|
SFX('Wizzrobe shot/Helma fireball split/Mothula beam/Blue balls', 0x03, 0x36, 0x1AA7, []),
|
||||||
|
SFX('Dinky jingle (paired $2D→$37)', 0x03, 0x37, 0x1DB4, [], True),
|
||||||
|
SFX('Apathetic jingle (paired $1A→$38)', 0x03, 0x38, 0x1E93, [], True),
|
||||||
|
SFX('Quiet swish (paired $23→$39)', 0x03, 0x39, 0x2017, [], True),
|
||||||
|
SFX('Defective jingle (paired $1B→$3A)', 0x03, 0x3A, 0x20C0, [], True),
|
||||||
|
SFX('Petulant jingle (paired $10→$3B)', 0x03, 0x3B, 0x2176, [], True),
|
||||||
|
SFX('Triumphant jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3C, 0x248A, [], True),
|
||||||
|
SFX('Less triumphant jingle ($0F→$3C→$3D→$3E→$3F)', 0x03, 0x3D, 0x2494, [], True),
|
||||||
|
SFX('"You tried, I guess" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3E, 0x249E, [], True),
|
||||||
|
SFX('"You didn\'t really try" jingle (paired $0F→$3C→$3D→$3E→$3F)', 0x03, 0x3F, 0x2480, [], True)]
|
||||||
|
return sfx_pool
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_sfx_data():
|
||||||
|
sfx_pool = init_sfx_data()
|
||||||
|
sfx_map = {2: {}, 3: {}}
|
||||||
|
accompaniment_map = {2: set(), 3: set()}
|
||||||
|
candidates = []
|
||||||
|
for sfx in sfx_pool:
|
||||||
|
sfx_map[sfx.sfx_set][sfx.orig_id] = sfx
|
||||||
|
if not sfx.accomp:
|
||||||
|
candidates.append((sfx.sfx_set, sfx.orig_id))
|
||||||
|
else:
|
||||||
|
accompaniment_map[sfx.sfx_set].add(sfx.orig_id)
|
||||||
|
chained_sfx = [x for x in sfx_pool if len(x.chain) > 0]
|
||||||
|
|
||||||
|
random.shuffle(candidates)
|
||||||
|
|
||||||
|
# place chained sfx first
|
||||||
|
random.shuffle(chained_sfx) # todo: sort largest to smallest
|
||||||
|
chained_sfx = sorted(chained_sfx, key=lambda x: len(x.chain), reverse=True)
|
||||||
|
for chained in chained_sfx:
|
||||||
|
chosen_slot = next(x for x in candidates if len(accompaniment_map[x[0]]) - len(chained.chain) >= 0)
|
||||||
|
if chosen_slot is None:
|
||||||
|
raise Exception('Something went wrong with sfx chains')
|
||||||
|
chosen_set, chosen_id = chosen_slot
|
||||||
|
chained.target_set, chained.target_id = chosen_slot
|
||||||
|
chained.target_chain = []
|
||||||
|
for downstream in chained.chain:
|
||||||
|
next_slot = accompaniment_map[chosen_set].pop()
|
||||||
|
ds_acc = sfx_map[chained.sfx_set][downstream]
|
||||||
|
ds_acc.target_set, ds_acc.target_id = chosen_set, next_slot
|
||||||
|
chained.target_chain.append(next_slot)
|
||||||
|
candidates.remove(chosen_slot)
|
||||||
|
sfx_pool.remove(chained)
|
||||||
|
|
||||||
|
unchained_sfx = [x for x in sfx_pool if not x.accomp]
|
||||||
|
# do the rest
|
||||||
|
for sfx in unchained_sfx:
|
||||||
|
chosen_slot = candidates.pop()
|
||||||
|
sfx.target_set, sfx.target_id = chosen_slot
|
||||||
|
|
||||||
|
return sfx_map
|
||||||
|
|
||||||
|
|
||||||
|
sfx_table = {
|
||||||
|
2: 0x1a8c29,
|
||||||
|
3: 0x1A8D25
|
||||||
|
}
|
||||||
|
|
||||||
|
# 0x1a8c29
|
||||||
|
# d8059
|
||||||
|
|
||||||
|
sfx_accompaniment_table = {
|
||||||
|
2: 0x1A8CA7,
|
||||||
|
3: 0x1A8DA3
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def randomize_sfx(rom):
|
||||||
|
sfx_map = shuffle_sfx_data()
|
||||||
|
|
||||||
|
for shuffled_sfx in sfx_map.values():
|
||||||
|
for sfx in shuffled_sfx.values():
|
||||||
|
base_address = sfx_table[sfx.target_set]
|
||||||
|
rom.write_bytes(base_address + (sfx.target_id * 2) - 2, int16_as_bytes(sfx.addr))
|
||||||
|
ac_base = sfx_accompaniment_table[sfx.target_set]
|
||||||
|
last = sfx.target_id
|
||||||
|
if sfx.target_chain:
|
||||||
|
for chained in sfx.target_chain:
|
||||||
|
rom.write_byte(ac_base + last - 1, chained)
|
||||||
|
last = chained
|
||||||
|
rom.write_byte(ac_base + last - 1, 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@ SETTINGSTOPROCESS = {
|
|||||||
"item": {
|
"item": {
|
||||||
"hints": "hints",
|
"hints": "hints",
|
||||||
"retro": "retro",
|
"retro": "retro",
|
||||||
|
"bombbag": "bombbag",
|
||||||
"shopsanity": "shopsanity",
|
"shopsanity": "shopsanity",
|
||||||
"pseudoboots": "pseudoboots",
|
"pseudoboots": "pseudoboots",
|
||||||
"worldstate": "mode",
|
"worldstate": "mode",
|
||||||
@@ -107,7 +108,8 @@ SETTINGSTOPROCESS = {
|
|||||||
"menuspeed": "fastmenu",
|
"menuspeed": "fastmenu",
|
||||||
"owpalettes": "ow_palettes",
|
"owpalettes": "ow_palettes",
|
||||||
"uwpalettes": "uw_palettes",
|
"uwpalettes": "uw_palettes",
|
||||||
"reduce_flashing": "reduce_flashing"
|
"reduce_flashing": "reduce_flashing",
|
||||||
|
"shuffle_sfx": "shuffle_sfx",
|
||||||
},
|
},
|
||||||
"generation": {
|
"generation": {
|
||||||
"createspoiler": "create_spoiler",
|
"createspoiler": "create_spoiler",
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ def adjust_page(top, parent, settings):
|
|||||||
"quickswap": "quickswap",
|
"quickswap": "quickswap",
|
||||||
"nobgm": "disablemusic",
|
"nobgm": "disablemusic",
|
||||||
"reduce_flashing": "reduce_flashing",
|
"reduce_flashing": "reduce_flashing",
|
||||||
|
"shuffle_sfx": "shuffle_sfx",
|
||||||
}
|
}
|
||||||
guiargs = Namespace()
|
guiargs = Namespace()
|
||||||
for option in options:
|
for option in options:
|
||||||
|
|||||||
Reference in New Issue
Block a user