Merged in DR v1.2.0.20
This commit is contained in:
@@ -96,6 +96,7 @@ class World(object):
|
||||
self.rooms = []
|
||||
self._room_cache = {}
|
||||
self.dungeon_layouts = {}
|
||||
self.dungeon_pool = {}
|
||||
self.inaccessible_regions = {}
|
||||
self.enabled_entrances = {}
|
||||
self.key_logic = {}
|
||||
@@ -164,6 +165,7 @@ class World(object):
|
||||
set_player_attr('colorizepots', True)
|
||||
set_player_attr('pot_pool', {})
|
||||
set_player_attr('decoupledoors', False)
|
||||
set_player_attr('door_self_loops', False)
|
||||
set_player_attr('door_type_mode', 'original')
|
||||
set_player_attr('trap_door_mode', 'optional')
|
||||
set_player_attr('key_logic_algorithm', 'default')
|
||||
@@ -2023,6 +2025,7 @@ class Door(object):
|
||||
self.dest = None
|
||||
self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed)
|
||||
self.blocked_orig = False
|
||||
self.trapped = False
|
||||
self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue)
|
||||
self.smallKey = False # There's a small key door on this side
|
||||
self.bigKey = False # There's a big key door on this side
|
||||
@@ -2133,7 +2136,7 @@ class Door(object):
|
||||
return self
|
||||
|
||||
def no_exit(self):
|
||||
self.blocked = self.blocked_orig = True
|
||||
self.blocked = self.blocked_orig = self.trapped = True
|
||||
return self
|
||||
|
||||
def no_entrance(self):
|
||||
@@ -2869,6 +2872,7 @@ class Spoiler(object):
|
||||
'trap_door_mode': self.world.trap_door_mode,
|
||||
'key_logic': self.world.key_logic_algorithm,
|
||||
'decoupledoors': self.world.decoupledoors,
|
||||
'door_self_loops': self.world.door_self_loops,
|
||||
'dungeon_counters': self.world.dungeon_counters,
|
||||
'item_pool': self.world.difficulty,
|
||||
'item_functionality': self.world.difficulty_adjustments,
|
||||
@@ -3115,6 +3119,7 @@ class Spoiler(object):
|
||||
outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player])
|
||||
outfile.write('Trap Door Mode:'.ljust(line_width) + '%s\n' % self.metadata['trap_door_mode'][player])
|
||||
outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player]))
|
||||
outfile.write('Spiral Stairs Self-Loop:'.ljust(line_width) + '%s\n' % yn(self.metadata['door_self_loops'][player]))
|
||||
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player]))
|
||||
outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player])
|
||||
outfile.write('\n')
|
||||
@@ -3396,7 +3401,7 @@ class Pot(object):
|
||||
|
||||
|
||||
# byte 0: DDDE EEEE (DR, ER)
|
||||
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3}
|
||||
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4}
|
||||
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
|
||||
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10}
|
||||
|
||||
@@ -3421,10 +3426,10 @@ mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
|
||||
pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7,
|
||||
'clustered': 8, 'nonempty': 9}
|
||||
|
||||
# byte 5: CCCC CTTX (crystals gt, ctr2, experimental)
|
||||
# byte 5: SCCC CTTX (self-loop doors, crystals gt, ctr2, experimental)
|
||||
counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
|
||||
|
||||
# byte 6: CCCC CPAA (crystals ganon, pyramid, access
|
||||
# byte 6: ?CCC CPAA (crystals ganon, pyramid, access
|
||||
access_mode = {"items": 0, "locations": 1, "none": 2}
|
||||
|
||||
# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies)
|
||||
@@ -3489,7 +3494,8 @@ class Settings(object):
|
||||
|
||||
(0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
|
||||
|
||||
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
|
||||
(0x80 if w.door_self_loops[p] else 0)
|
||||
| ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
|
||||
| (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0),
|
||||
|
||||
((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3)
|
||||
@@ -3515,8 +3521,8 @@ class Settings(object):
|
||||
(flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4
|
||||
| take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]),
|
||||
|
||||
((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 6
|
||||
| trap_door_mode[w.trap_door_mode[p]] << 4 | key_logic_algo[w.key_logic_algorithm[p]]),
|
||||
((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5
|
||||
| trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]),
|
||||
])
|
||||
return base64.b64encode(code, "+-".encode()).decode()
|
||||
|
||||
@@ -3554,12 +3560,13 @@ class Settings(object):
|
||||
args.dropshuffle[p] = True if settings[4] & 0x10 else False
|
||||
args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F]
|
||||
|
||||
args.door_self_loops[p] = True if settings[5] & 0x80 else False
|
||||
args.dungeon_counters[p] = r(counter_mode)[(settings[5] & 0x6) >> 1]
|
||||
cgt = (settings[5] & 0xf8) >> 3
|
||||
cgt = (settings[5] & 0x78) >> 3
|
||||
args.crystals_gt[p] = "random" if cgt == 8 else cgt
|
||||
args.experimental[p] = True if settings[5] & 0x1 else False
|
||||
|
||||
cgan = (settings[6] & 0xf8) >> 3
|
||||
cgan = (settings[6] & 0x78) >> 3
|
||||
args.crystals_ganon[p] = "random" if cgan == 8 else cgan
|
||||
args.openpyramid[p] = True if settings[6] & 0x4 else False
|
||||
|
||||
@@ -3599,8 +3606,8 @@ class Settings(object):
|
||||
|
||||
if len(settings) > 14:
|
||||
args.pseudoboots[p] = True if settings[14] & 0x80 else False
|
||||
args.overworld_map[p] = r(overworld_map_mode)[(settings[14] & 0x60) >> 6]
|
||||
args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x14) >> 4]
|
||||
args.overworld_map[p] = r(overworld_map_mode)[(settings[14] & 0x60) >> 5]
|
||||
args.trap_door_mode[p] = r(trap_door_mode)[(settings[14] & 0x18) >> 3]
|
||||
args.key_logic_algorithm[p] = r(key_logic_algo)[settings[14] & 0x07]
|
||||
|
||||
|
||||
|
||||
3
CLI.py
3
CLI.py
@@ -142,7 +142,7 @@ def parse_cli(argv, no_defaults=False):
|
||||
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
|
||||
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
|
||||
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
|
||||
'bonk_drops', 'trap_door_mode', 'key_logic_algorithm']:
|
||||
'bonk_drops', 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
@@ -227,6 +227,7 @@ def parse_settings():
|
||||
"trap_door_mode": "optional",
|
||||
"key_logic_algorithm": "default",
|
||||
"decoupledoors": False,
|
||||
"door_self_loops": False,
|
||||
"experimental": False,
|
||||
"dungeon_counters": "default",
|
||||
"mixed_travel": "prevent",
|
||||
|
||||
101
DoorShuffle.py
101
DoorShuffle.py
@@ -88,6 +88,7 @@ def link_doors_prep(world, player):
|
||||
|
||||
find_inaccessible_regions(world, player)
|
||||
|
||||
create_dungeon_pool(world, player)
|
||||
if world.intensity[player] >= 3 and world.doorShuffle[player] != 'vanilla':
|
||||
choose_portals(world, player)
|
||||
else:
|
||||
@@ -133,6 +134,20 @@ def create_dungeon_pool(world, player):
|
||||
pool = None
|
||||
if world.doorShuffle[player] == 'basic':
|
||||
pool = [([name], regions) for name, regions in dungeon_regions.items()]
|
||||
elif world.doorShuffle[player] == 'paired':
|
||||
dungeon_pool = list(dungeon_regions.keys())
|
||||
groups = []
|
||||
while dungeon_pool:
|
||||
if len(dungeon_pool) == 3:
|
||||
groups.append(list(dungeon_pool))
|
||||
dungeon_pool.clear()
|
||||
else:
|
||||
choice_a = random.choice(dungeon_pool)
|
||||
dungeon_pool.remove(choice_a)
|
||||
choice_b = random.choice(dungeon_pool)
|
||||
dungeon_pool.remove(choice_b)
|
||||
groups.append([choice_a, choice_b])
|
||||
pool = [(group, list(chain.from_iterable([dungeon_regions[d] for d in group]))) for group in groups]
|
||||
elif world.doorShuffle[player] == 'partitioned':
|
||||
groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'],
|
||||
['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'],
|
||||
@@ -143,38 +158,17 @@ def create_dungeon_pool(world, player):
|
||||
elif world.doorShuffle[player] != 'vanilla':
|
||||
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
|
||||
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
|
||||
return pool
|
||||
world.dungeon_pool[player] = pool
|
||||
|
||||
|
||||
def link_doors_main(world, player):
|
||||
pool = create_dungeon_pool(world, player)
|
||||
pool = world.dungeon_pool[player]
|
||||
if pool:
|
||||
main_dungeon_pool(pool, world, player)
|
||||
if world.doorShuffle[player] != 'vanilla':
|
||||
create_door_spoiler(world, player)
|
||||
|
||||
|
||||
# todo: I think this function is not necessary
|
||||
def mark_regions(world, player):
|
||||
# traverse dungeons and make sure dungeon property is assigned
|
||||
player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
|
||||
for dungeon in player_dungeons:
|
||||
queue = deque(dungeon.regions)
|
||||
while len(queue) > 0:
|
||||
region = world.get_region(queue.popleft(), player)
|
||||
if region.name not in dungeon.regions:
|
||||
dungeon.regions.append(region.name)
|
||||
region.dungeon = dungeon
|
||||
for ext in region.exits:
|
||||
d = world.check_for_door(ext.name, player)
|
||||
connected = ext.connected_region
|
||||
if d is not None and connected is not None:
|
||||
if d.dest is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue:
|
||||
queue.append(connected) # needs to be added
|
||||
elif connected is not None and connected.name not in dungeon.regions and connected.type == RegionType.Dungeon and connected.name not in queue:
|
||||
queue.append(connected) # needs to be added
|
||||
|
||||
|
||||
def create_door_spoiler(world, player):
|
||||
logger = logging.getLogger('')
|
||||
shuffled_door_types = [DoorType.Normal, DoorType.SpiralStairs]
|
||||
@@ -438,17 +432,7 @@ def pair_existing_key_doors(world, player, door_a, door_b):
|
||||
def choose_portals(world, player):
|
||||
if world.doorShuffle[player] != ['vanilla']:
|
||||
shuffle_flag = world.doorShuffle[player] != 'basic'
|
||||
allowed = {}
|
||||
if world.doorShuffle[player] == 'basic':
|
||||
allowed = {name: {name} for name in dungeon_regions}
|
||||
elif world.doorShuffle[player] == 'partitioned':
|
||||
groups = [['Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower'],
|
||||
['Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town'],
|
||||
['Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower']]
|
||||
allowed = {name: set(group) for group in groups for name in group}
|
||||
elif world.doorShuffle[player] == 'crossed':
|
||||
all_dungeons = set(dungeon_regions.keys())
|
||||
allowed = {name: all_dungeons for name in dungeon_regions}
|
||||
allowed = {name: set(group[0]) for group in world.dungeon_pool[player] for name in group[0]}
|
||||
|
||||
# key drops allow the big key in the right place in Desert Tiles 2
|
||||
bk_shuffle = world.bigkeyshuffle[player] or world.pottery[player] not in ['none', 'cave']
|
||||
@@ -593,7 +577,7 @@ def customizer_portals(master_door_list, world, player):
|
||||
assigned_doors.add(door)
|
||||
# restricts connected doors to the customized portals
|
||||
if assigned_doors:
|
||||
pool = create_dungeon_pool(world, player)
|
||||
pool = world.dungeon_pool[player]
|
||||
if pool:
|
||||
pool_map = {}
|
||||
for pool, region_list in pool:
|
||||
@@ -1860,12 +1844,12 @@ def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, wo
|
||||
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, all_custom[dungeon])
|
||||
remaining -= len(custom_trap_doors[dungeon])
|
||||
ttl += len(builder.candidates.trap)
|
||||
if ttl == 0:
|
||||
if ttl == 0 and all(len(custom_trap_doors[dungeon]) == 0 for dungeon in pool):
|
||||
continue
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
proportion = len(builder.candidates.trap)
|
||||
calc = int(round(proportion * door_type_pool.traps/ttl))
|
||||
calc = 0 if ttl == 0 else int(round(proportion * door_type_pool.traps/ttl))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
@@ -1997,6 +1981,9 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_
|
||||
remaining = max(0, remaining)
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if ttl == 0:
|
||||
calculated = 0
|
||||
else:
|
||||
calculated = int(round(builder.key_doors_num*total_keys/ttl))
|
||||
max_keys = max(0, builder.location_cnt - calc_used_dungeon_items(builder, world, player))
|
||||
cand_len = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
|
||||
@@ -2227,9 +2214,10 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
|
||||
sample_list = build_sample_list(combinations, 1000)
|
||||
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
|
||||
proposal.extend(custom_trap_doors)
|
||||
filtered_proposal = [x for x in proposal if x.name not in trap_door_exceptions]
|
||||
|
||||
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
|
||||
while not validate_trap_layout(proposal, builder, start_regions, paths, world, player):
|
||||
while not validate_trap_layout(filtered_proposal, builder, start_regions, paths, world, player):
|
||||
itr += 1
|
||||
if itr >= len(sample_list):
|
||||
if not drop:
|
||||
@@ -2264,6 +2252,12 @@ def filter_start_regions(builder, start_regions, world, player):
|
||||
portal_entrance_region = portal.door.entrance.parent_region.name
|
||||
if portal_entrance_region not in builder.path_entrances:
|
||||
excluded[region] = None
|
||||
if not portal:
|
||||
drop_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'), None)
|
||||
if drop_region and drop_region.name in world.inaccessible_regions[player]:
|
||||
excluded[region] = None
|
||||
if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'):
|
||||
excluded[region] = None
|
||||
if portal is None:
|
||||
@@ -2359,10 +2353,12 @@ def reassign_trap_doors(trap_map, world, player):
|
||||
elif kind in [DoorKind.Trap2, DoorKind.TrapTriggerable]:
|
||||
room.change(d.doorListPos, DoorKind.Normal)
|
||||
d.blocked = False
|
||||
d.trapped = False
|
||||
# connect_one_way(world, d.name, d.dest.name, player)
|
||||
elif d.type is DoorType.Normal and d not in traps:
|
||||
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
|
||||
d.blocked = False
|
||||
d.trapped = False
|
||||
for d in traps:
|
||||
change_door_to_trap(d, world, player)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
|
||||
@@ -2400,24 +2396,45 @@ def change_door_to_trap(d, world, player):
|
||||
elif d.direction in [Direction.North, Direction.West]:
|
||||
new_kind = DoorKind.TrapTriggerable
|
||||
if new_kind:
|
||||
d.blocked = True
|
||||
d.blocked = is_trap_door_blocked(d)
|
||||
d.trapped = True
|
||||
pos = 3 if d.type == DoorType.Normal else 4
|
||||
verify_door_list_pos(d, room, world, player, pos)
|
||||
d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1, 3: 0x8}[d.doorListPos]
|
||||
room.change(d.doorListPos, new_kind)
|
||||
if d.entrance.connected_region is not None:
|
||||
if d.entrance.connected_region is not None and d.blocked:
|
||||
d.entrance.connected_region.entrances.remove(d.entrance)
|
||||
d.entrance.connected_region = None
|
||||
elif d.type is DoorType.Normal:
|
||||
d.blocked = True
|
||||
d.blocked = is_trap_door_blocked(d)
|
||||
d.trapped = True
|
||||
verify_door_list_pos(d, room, world, player, pos=3)
|
||||
d.trapFlag = {0: 0x4, 1: 0x2, 2: 0x1}[d.doorListPos]
|
||||
room.change(d.doorListPos, DoorKind.Trap)
|
||||
if d.entrance.connected_region is not None:
|
||||
if d.entrance.connected_region is not None and d.blocked:
|
||||
d.entrance.connected_region.entrances.remove(d.entrance)
|
||||
d.entrance.connected_region = None
|
||||
|
||||
|
||||
trap_door_exceptions = {
|
||||
'PoD Mimics 2 SW', 'TR Twin Pokeys NW', 'Thieves Blocked Entry SW', 'Hyrule Dungeon Armory Interior Key Door N',
|
||||
'Desert Compass Key Door WN', 'TR Tile Room SE', 'Mire Cross SW', 'Tower Circle of Pots ES',
|
||||
'Eastern Single Eyegore ES', 'Eastern Duo Eyegores SE', 'Swamp Push Statue S',
|
||||
'Skull 2 East Lobby WS', 'GT Hope Room WN', 'Eastern Courtyard Ledge S', 'Ice Lobby SE', 'GT Speed Torch WN',
|
||||
'Ice Switch Room ES', 'Ice Switch Room NE', 'Skull Torch Room WS', 'GT Speed Torch NE', 'GT Speed Torch WS',
|
||||
'GT Torch Cross WN', 'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Torches WN', 'PoD Lobby N', 'PoD Middle Cage S',
|
||||
'Ice Bomb Jump NW', 'GT Hidden Spikes SE', 'Ice Tall Hint EN', 'GT Conveyor Cross EN', 'Eastern Pot Switch WN',
|
||||
'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Eastern Dark Square Key Door WN', 'Eastern Lobby NW',
|
||||
'Eastern Lobby NE', 'Ice Cross Bottom SE', 'Desert Back Lobby S', 'Desert West S',
|
||||
'Desert West Lobby ES', 'Mire Hidden Shooters SE', 'Mire Hidden Shooters ES', 'Mire Hidden Shooters WS',
|
||||
'Tower Dark Pits EN', 'Tower Dark Maze ES', 'TR Tongue Pull WS',
|
||||
}
|
||||
|
||||
|
||||
def is_trap_door_blocked(door):
|
||||
return door.name not in trap_door_exceptions
|
||||
|
||||
|
||||
def find_big_key_candidates(builder, start_regions, used, world, player):
|
||||
if world.door_type_mode[player] != 'original': # big, all, chaos
|
||||
# traverse dungeon and find candidates
|
||||
|
||||
@@ -1677,10 +1677,24 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
|
||||
random.shuffle(sector_list)
|
||||
orig_location_set = build_orig_location_set(dungeon_map)
|
||||
num_dungeon_items = requested_dungeon_items(world, player)
|
||||
locations_to_distribute = sum(sector.chest_locations for sector in free_location_sectors.keys())
|
||||
reserved_per_dungeon = {d_name: count_reserved_locations(world, player, orig_location_set[d_name])
|
||||
for d_name in dungeon_map.keys()}
|
||||
base_free, found_enough = 2, False
|
||||
while not found_enough:
|
||||
needed = sum(max(0, max(base_free, reserved_per_dungeon[d]) + num_dungeon_items - len(orig_location_set[d]))
|
||||
for d in dungeon_map.keys())
|
||||
if needed > locations_to_distribute:
|
||||
if base_free == 0:
|
||||
raise Exception('Unable to meet minimum requirements, check for customizer problems')
|
||||
base_free -= 1
|
||||
else:
|
||||
found_enough = True
|
||||
d_idx = {builder.name: i for i, builder in enumerate(dungeon_map.values())}
|
||||
next_sector = sector_list.pop()
|
||||
while not valid:
|
||||
choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set, world, player)
|
||||
choice, totals, location_set = weighted_random_location(dungeon_map, choices, orig_location_set,
|
||||
base_free, world, player)
|
||||
if not choice:
|
||||
break
|
||||
choices[choice].append(next_sector)
|
||||
@@ -1691,7 +1705,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
|
||||
valid = True
|
||||
for d_name, idx in d_idx.items():
|
||||
free_items = count_reserved_locations(world, player, location_set[d_name])
|
||||
target = max(free_items, 2) + num_dungeon_items
|
||||
target = max(free_items, base_free) + num_dungeon_items
|
||||
if totals[idx] < target:
|
||||
valid = False
|
||||
break
|
||||
@@ -1699,7 +1713,6 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
|
||||
if len(sector_list) == 0:
|
||||
choices = defaultdict(list)
|
||||
sector_list = list(free_location_sectors)
|
||||
else:
|
||||
next_sector = sector_list.pop()
|
||||
else:
|
||||
choices[choice].remove(next_sector)
|
||||
@@ -1709,7 +1722,7 @@ def assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_p
|
||||
return free_location_sectors
|
||||
|
||||
|
||||
def weighted_random_location(dungeon_map, choices, orig_location_set, world, player):
|
||||
def weighted_random_location(dungeon_map, choices, orig_location_set, base_free, world, player):
|
||||
population = []
|
||||
totals = []
|
||||
location_set = {x: set(y) for x, y in orig_location_set.items()}
|
||||
@@ -1720,7 +1733,7 @@ def weighted_random_location(dungeon_map, choices, orig_location_set, world, pla
|
||||
builder_set = location_set[dungeon_builder.name]
|
||||
builder_set.update(set().union(*(s.chest_location_set for s in choices[dungeon_builder])))
|
||||
free_items = count_reserved_locations(world, player, builder_set)
|
||||
target = max(free_items, 2) + num_dungeon_items
|
||||
target = max(free_items, base_free) + num_dungeon_items
|
||||
if ttl < target:
|
||||
population.append(dungeon_builder)
|
||||
choice = random.choice(population) if len(population) > 0 else None
|
||||
@@ -1775,7 +1788,7 @@ def count_reserved_locations(world, player, proposed_set):
|
||||
return 2
|
||||
|
||||
|
||||
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole, assign_one=False):
|
||||
def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers, global_pole):
|
||||
population = []
|
||||
some_c_switches_present = False
|
||||
for name, builder in dungeon_map.items():
|
||||
@@ -1784,7 +1797,7 @@ def assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barrier
|
||||
if builder.c_switch_present and not builder.c_locked:
|
||||
some_c_switches_present = True
|
||||
if len(population) == 0: # nothing needs a switch
|
||||
if assign_one and not some_c_switches_present: # something should have one
|
||||
if len(crystal_barriers) > 0 and not some_c_switches_present: # something should have one
|
||||
if len(crystal_switches) == 0:
|
||||
raise GenerationException('No crystal switches to assign. Ref %s' % next(iter(dungeon_map.keys())))
|
||||
valid, builder_choice, switch_choice = False, None, None
|
||||
@@ -3139,8 +3152,7 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info):
|
||||
check_for_forced_assignments(dungeon_map, candidate_sectors, global_pole)
|
||||
check_for_forced_crystal(dungeon_map, candidate_sectors, global_pole)
|
||||
crystal_switches, crystal_barriers, neutral_sectors, polarized_sectors = categorize_sectors(candidate_sectors)
|
||||
leftover = assign_crystal_switch_sectors(dungeon_map, crystal_switches, crystal_barriers,
|
||||
global_pole, len(crystal_barriers) > 0)
|
||||
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)
|
||||
for sector in leftover:
|
||||
if sector.polarity().is_neutral():
|
||||
|
||||
4
Main.py
4
Main.py
@@ -36,7 +36,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
|
||||
version_number = '1.2.0.19'
|
||||
version_number = '1.2.0.20'
|
||||
version_branch = '-u'
|
||||
__version__ = f'{version_number}{version_branch}'
|
||||
|
||||
@@ -122,6 +122,7 @@ def main(args, seed=None, fish=None):
|
||||
world.trap_door_mode = args.trap_door_mode.copy()
|
||||
world.key_logic_algorithm = args.key_logic_algorithm.copy()
|
||||
world.decoupledoors = args.decoupledoors.copy()
|
||||
world.door_self_loops = args.door_self_loops.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.fish = fish
|
||||
@@ -519,6 +520,7 @@ def copy_world(world):
|
||||
ret.beemizer = world.beemizer.copy()
|
||||
ret.intensity = world.intensity.copy()
|
||||
ret.decoupledoors = world.decoupledoors.copy()
|
||||
ret.door_self_loops = world.door_self_loops.copy()
|
||||
ret.experimental = world.experimental.copy()
|
||||
ret.shopsanity = world.shopsanity.copy()
|
||||
ret.dropshuffle = world.dropshuffle.copy()
|
||||
|
||||
@@ -1022,7 +1022,7 @@ key_drop_data = {
|
||||
'Ice Palace - Jelly Key Drop': ['Drop', (0x09DA21, 0xE, 3), 'dropped in Ice Palace', 'Small Key (Ice Palace)'],
|
||||
'Ice Palace - Conveyor Key Drop': ['Drop', (0x09DE08, 0x3E, 8), 'dropped in Ice Palace', 'Small Key (Ice Palace)'],
|
||||
'Ice Palace - Hammer Block Key Drop': ['Pot', 0x3F, 'under a block in Ice Palace', 'Small Key (Ice Palace)'],
|
||||
'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'int a pot in Ice Palace', 'Small Key (Ice Palace)'],
|
||||
'Ice Palace - Many Pots Pot Key': ['Pot', 0x9F, 'in a pot in Ice Palace', 'Small Key (Ice Palace)'],
|
||||
'Misery Mire - Spikes Pot Key': ['Pot', 0xB3, 'in a pot in Misery Mire', 'Small Key (Misery Mire)'],
|
||||
'Misery Mire - Fishbone Pot Key': ['Pot', 0xA1, 'in a pot in forgotten Mire', 'Small Key (Misery Mire)'],
|
||||
'Misery Mire - Conveyor Crystal Key Drop': ['Drop', (0x09E7FB, 0xC1, 9), 'dropped in Misery Mire', 'Small Key (Misery Mire)'],
|
||||
|
||||
@@ -109,6 +109,15 @@ These are now independent of retro mode and have three options: None, Random, an
|
||||
|
||||
# Bug Fixes and Notes
|
||||
|
||||
* 1.2.0.20u
|
||||
* New generation feature that allows Spiral Stair to link to themselves (thank Catobat)
|
||||
* Added logic for trap doors that could be opened using existing room triggers
|
||||
* Fixed a problem with inverted generation and the experimental flag
|
||||
* Added a notes field for user added notes either via CLI or Customizer (thanks Hiimcody and Codemann)
|
||||
* Fixed a typo for a specific pot hint
|
||||
* Fix for Hera Boss music (thanks Codemann)
|
||||
* 1.1.6 (from Stable)
|
||||
* Minor issue with dungeon counter hud interfering with timer
|
||||
* 1.2.0.19u
|
||||
* Added min/max for triforce pool, goal, and difference for CLI and Customizer. (Thanks Catobat)
|
||||
* Fixed a bug with dungeon generation
|
||||
|
||||
2
Rom.py
2
Rom.py
@@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127
|
||||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '32f6a9f479f6ccb7e66e9906ff0d0e4c'
|
||||
RANDOMIZERBASEHASH = 'da111397d4118054e5ab4b9375cfb9e4'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
|
||||
99
Rules.py
99
Rules.py
@@ -383,11 +383,18 @@ def global_rules(world, player):
|
||||
# Start of door rando rules
|
||||
# TODO: Do these need to flag off when door rando is off? - some of them, yes
|
||||
|
||||
def is_trapped(entrance):
|
||||
return world.get_entrance(entrance, player).door.trapped
|
||||
|
||||
# Eastern Palace
|
||||
# Eyegore room needs a bow
|
||||
set_rule(world.get_entrance('Eastern Duo Eyegores NE', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('Eastern Single Eyegore NE', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('Eastern Map Balcony Hook Path', player), lambda state: state.has('Hookshot', player))
|
||||
if is_trapped('Eastern Single Eyegore ES'):
|
||||
set_rule(world.get_entrance('Eastern Single Eyegore ES', player), lambda state: state.can_shoot_arrows(player))
|
||||
if is_trapped('Eastern Duo Eyegores SE'):
|
||||
set_rule(world.get_entrance('Eastern Duo Eyegores SE', player), lambda state: state.can_shoot_arrows(player))
|
||||
|
||||
# Boss rules. Same as below but no BK or arrow requirement.
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Eastern Palace - Prize', player))
|
||||
@@ -412,13 +419,18 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Tower Red Spears WN', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('Tower Red Guards EN', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('Tower Red Guards SW', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('Tower Circle of Pots NW', player), lambda state: state.can_kill_most_things(player))
|
||||
if is_trapped('Tower Circle of Pots ES'):
|
||||
set_rule(world.get_entrance('Tower Circle of Pots ES', player),
|
||||
lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player))
|
||||
|
||||
|
||||
set_rule(world.get_entrance('PoD Arena Landing Bonk Path', player), lambda state: state.has_Boots(player))
|
||||
set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('PoD Mimics 2 NW', player), lambda state: state.can_shoot_arrows(player))
|
||||
if is_trapped('PoD Mimics 2 SW'):
|
||||
set_rule(world.get_entrance('PoD Mimics 2 SW', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('PoD Bow Statue Down Ladder', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('PoD Map Balcony Drop Down', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('PoD Dark Pegs Landing to Right', player), lambda state: state.has('Hammer', player))
|
||||
@@ -467,6 +479,8 @@ def global_rules(world, player):
|
||||
|
||||
set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player))
|
||||
if is_trapped('Skull Torch Room WS'):
|
||||
set_rule(world.get_entrance('Skull Torch Room WS', player), lambda state: state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
|
||||
|
||||
hidden_pits_door = world.get_door('Skull Small Hall WS', player)
|
||||
@@ -504,6 +518,8 @@ def global_rules(world, player):
|
||||
set_rule(world.get_location('Thieves\' Town - Prize', player), lambda state: state.has('Maiden Unmasked', player) and world.get_location('Thieves\' Town - Prize', player).parent_region.dungeon.boss.can_defeat(state))
|
||||
|
||||
set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.can_melt_things(player))
|
||||
if is_trapped('Ice Lobby SE'):
|
||||
set_rule(world.get_entrance('Ice Lobby SE', player), lambda state: state.can_melt_things(player))
|
||||
set_rule(world.get_entrance('Ice Hammer Block ES', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Ice Palace - Hammer Block Key Drop', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_location('Ice Palace - Map Chest', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
|
||||
@@ -518,6 +534,12 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('Ice Hookshot Balcony Path', player), lambda state: state.has('Hookshot', player))
|
||||
if not world.get_door('Ice Switch Room SE', player).entranceFlag:
|
||||
set_rule(world.get_entrance('Ice Switch Room SE', player), lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
|
||||
if is_trapped('Ice Switch Room ES'):
|
||||
set_rule(world.get_entrance('Ice Switch Room ES', player),
|
||||
lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
|
||||
if is_trapped('Ice Switch Room NE'):
|
||||
set_rule(world.get_entrance('Ice Switch Room NE', player),
|
||||
lambda state: state.has('Cane of Somaria', player) or state.has('Convenient Block', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Boss', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Ice Palace - Prize', player))
|
||||
|
||||
@@ -538,8 +560,15 @@ def global_rules(world, player):
|
||||
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 Tile Room NW', player), lambda state: state.has_fire_source(player))
|
||||
if is_trapped('Mire Tile Room SW'):
|
||||
set_rule(world.get_entrance('Mire Tile Room SW', player), lambda state: state.has_fire_source(player))
|
||||
if is_trapped('Mire Tile Room ES'):
|
||||
set_rule(world.get_entrance('Mire Tile Room ES', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_entrance('Mire Attic Hint Hole', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_entrance('Mire Dark Shooters SW', player), lambda state: state.has('Cane of Somaria', player))
|
||||
if is_trapped('Mire Dark Shooters SE'):
|
||||
set_rule(world.get_entrance('Mire Dark Shooters SE', player),
|
||||
lambda state: state.has('Cane of Somaria', player))
|
||||
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Boss', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Misery Mire - Prize', player))
|
||||
@@ -555,6 +584,9 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('TR Hub Path', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('TR Hub Ledges Path', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('TR Torches NW', player), lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
if is_trapped('TR Torches WN'):
|
||||
set_rule(world.get_entrance('TR Torches WN', player),
|
||||
lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player))
|
||||
set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player))
|
||||
@@ -574,10 +606,20 @@ def global_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has_Boots(player))
|
||||
set_rule(world.get_entrance('GT Hope Room EN', player), lambda state: state.has('Cane of Somaria', player))
|
||||
if is_trapped('GT Hope Room WN'):
|
||||
set_rule(world.get_entrance('GT Hope Room WN', player), lambda state: state.has('Cane of Somaria', player))
|
||||
set_rule(world.get_entrance('GT Conveyor Cross Hammer Path', player), lambda state: state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('GT Conveyor Cross Hookshot Path', player), lambda state: state.has('Hookshot', player))
|
||||
if is_trapped('GT Conveyor Cross EN'):
|
||||
set_rule(world.get_entrance('GT Conveyor Cross EN', player), lambda state: state.has('Hammer', player))
|
||||
if not world.get_door('GT Speed Torch SE', player).entranceFlag:
|
||||
set_rule(world.get_entrance('GT Speed Torch SE', player), lambda state: state.has('Fire Rod', player))
|
||||
if is_trapped('GT Speed Torch NE'):
|
||||
set_rule(world.get_entrance('GT Speed Torch NE', player), lambda state: state.has('Fire Rod', player))
|
||||
if is_trapped('GT Speed Torch WS'):
|
||||
set_rule(world.get_entrance('GT Speed Torch WS', player), lambda state: state.has('Fire Rod', player))
|
||||
if is_trapped('GT Speed Torch WN'):
|
||||
set_rule(world.get_entrance('GT Speed Torch WN', player), lambda state: state.has('Fire Rod', player))
|
||||
set_rule(world.get_entrance('GT Hookshot South-Mid Path', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('GT Hookshot Mid-North Path', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('GT Hookshot East-Mid Path', player), lambda state: state.has('Hookshot', player) or state.has_Boots(player))
|
||||
@@ -612,6 +654,8 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
|
||||
set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player))
|
||||
if is_trapped('GT Torch Cross WN'):
|
||||
set_rule(world.get_entrance('GT Torch Cross WN', player), lambda state: state.has_fire_source(player))
|
||||
set_rule(world.get_entrance('GT Falling Torches NE', player), lambda state: state.has_fire_source(player))
|
||||
# todo: the following only applies to crystal state propagation from this supertile
|
||||
# you can also reset the supertile, but I'm not sure how to model that
|
||||
@@ -871,13 +915,29 @@ def bomb_rules(world, player):
|
||||
('GT Petting Zoo SE', False), # Dont make anyone do this room with bombs and/or pots.
|
||||
('GT DMs Room SW', False) # Four red stalfos
|
||||
]
|
||||
conditional_kill_traps = [
|
||||
('Hyrule Dungeon Armory Interior Key Door N', True),
|
||||
('Desert Compass Key Door WN', True),
|
||||
('Thieves Blocked Entry SW', True),
|
||||
('TR Tongue Pull WS', True),
|
||||
('TR Twin Pokeys NW', False),
|
||||
]
|
||||
for killdoor,bombable in easy_kill_rooms:
|
||||
if bombable:
|
||||
add_rule(world.get_entrance(killdoor, player), lambda state: (state.can_use_bombs(player) or state.can_kill_most_things(player)))
|
||||
else:
|
||||
add_rule(world.get_entrance(killdoor, player), lambda state: state.can_kill_most_things(player))
|
||||
for kill_door, bombable in conditional_kill_traps:
|
||||
if world.get_entrance(kill_door, player).door.trapped:
|
||||
if bombable:
|
||||
add_rule(world.get_entrance(kill_door, player),
|
||||
lambda state: (state.can_use_bombs(player) or state.can_kill_most_things(player)))
|
||||
else:
|
||||
add_rule(world.get_entrance(kill_door, player), lambda state: state.can_kill_most_things(player))
|
||||
add_rule(world.get_entrance('Ice Stalfos Hint SE', player), lambda state: state.can_use_bombs(player)) # Need bombs for big stalfos knights
|
||||
add_rule(world.get_entrance('Mire Cross ES', player), lambda state: state.can_kill_most_things(player)) # 4 Sluggulas. Bombs don't work // or (state.can_use_bombs(player) and state.has('Magic Powder'), player)
|
||||
if world.get_entrance('Mire Cross SW', player).door.trapped:
|
||||
add_rule(world.get_entrance('Mire Cross SW', player), lambda state: state.can_kill_most_things(player))
|
||||
|
||||
enemy_kill_drops = [ # Location, bool-bombable
|
||||
('Hyrule Castle - Map Guard Key Drop', True),
|
||||
@@ -1400,6 +1460,9 @@ def swordless_rules(world, player):
|
||||
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: True)
|
||||
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: True)
|
||||
set_rule(world.get_entrance('Ice Lobby WS', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
|
||||
if world.get_entrance('Ice Lobby SE', player).door.trapped:
|
||||
set_rule(world.get_entrance('Ice Lobby SE', player),
|
||||
lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
|
||||
set_rule(world.get_location('Ice Palace - Freezor Chest', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player))
|
||||
|
||||
set_rule(world.get_location('Ether Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
|
||||
@@ -1413,7 +1476,7 @@ def swordless_rules(world, player):
|
||||
if not world.is_atgt_swapped(player):
|
||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player)) # barrier gets removed after killing agahnim, rule for that added later
|
||||
|
||||
|
||||
# todo: new traps
|
||||
std_kill_rooms = {
|
||||
'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], # One green guard
|
||||
'Hyrule Dungeon Armory Boomerang': ['Hyrule Dungeon Armory Boomerang WS'], # One blue guard
|
||||
@@ -1444,6 +1507,18 @@ std_kill_rooms = {
|
||||
'GT Wizzrobes 2': ['GT Wizzrobes 2 SE', 'GT Wizzrobes 2 NE'] # Wizzrobes. Bombs don't work
|
||||
} # all trap rooms?
|
||||
|
||||
std_kill_doors_if_trapped = {
|
||||
'Hyrule Dungeon Armory Main': 'Hyrule Dungeon Armory Interior Key Door N',
|
||||
# 'Eastern Single Eyegore ES', # arrow rule is sufficient
|
||||
# 'Eastern Duo Eyegores S', # arrow rule is sufficient
|
||||
'TR Twin Pokeys': 'TR Twin Pokeys NW',
|
||||
'Thieves Basement Block': 'Thieves Blocked Entry SW',
|
||||
'Desert Compass Room': 'Desert Compass Key Door WN',
|
||||
'Mire Cross': 'Mire Cross SW',
|
||||
'Tower Circle of Pots': 'Tower Circle of Pots ES',
|
||||
# 'Ice Lobby S' # can melt rule is sufficient
|
||||
}
|
||||
|
||||
def add_connection(parent_name, target_name, entrance_name, world, player):
|
||||
parent = world.get_region(parent_name, player)
|
||||
target = world.get_region(target_name, player)
|
||||
@@ -1496,6 +1571,10 @@ def standard_rules(world, player):
|
||||
if region.name in std_kill_rooms:
|
||||
for ent in std_kill_rooms[region.name]:
|
||||
add_rule(world.get_entrance(ent, player), lambda state: standard_escape_rule(state))
|
||||
if region.name in std_kill_doors_if_trapped:
|
||||
ent = world.get_entrance(std_kill_doors_if_trapped[region.name], player)
|
||||
if ent.door.trapped:
|
||||
add_rule(ent, lambda state: standard_escape_rule(state))
|
||||
|
||||
set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player))
|
||||
set_rule(world.get_entrance('Hyrule Castle Throne Room Tapestry', player), lambda state: state.has('Zelda Herself', player))
|
||||
@@ -1686,6 +1765,11 @@ def set_bunny_rules(world, player, inverted):
|
||||
if bunny_exit.connected_region and is_bunny(bunny_exit.parent_region):
|
||||
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
|
||||
|
||||
for ent_name in bunny_impassible_if_trapped:
|
||||
bunny_exit = world.get_entrance(ent_name, player)
|
||||
if bunny_exit.door.trapped and is_bunny(bunny_exit.parent_region):
|
||||
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
|
||||
|
||||
doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors]
|
||||
doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked]
|
||||
for door in doors_to_check:
|
||||
@@ -1817,6 +1901,17 @@ bunny_impassible_doors = {
|
||||
'GT Validation Block Path'
|
||||
}
|
||||
|
||||
bunny_impassible_if_trapped = {
|
||||
'Hyrule Dungeon Armory Interior Key Door N', 'Eastern Pot Switch WN', 'Eastern Lobby NW',
|
||||
'Eastern Lobby NE', 'Desert Compass Key Door WN', 'Tower Circle of Pots ES', 'PoD Mimics 2 SW',
|
||||
'PoD Middle Cage S', 'Swamp Push Statue S', 'Skull 2 East Lobby WS', 'Skull Torch Room WS',
|
||||
'Thieves Conveyor Maze WN', 'Thieves Conveyor Maze SW', 'Thieves Blocked Entry SW', 'Ice Bomb Jump NW',
|
||||
'Ice Tall Hint EN', 'Ice Switch Room ES', 'Ice Switch Room NE', 'Mire Cross SW',
|
||||
'Mire Tile Room SW', 'Mire Tile Room ES', 'TR Twin Pokeys NW', 'TR Torches WN', 'GT Hope Room WN',
|
||||
'GT Speed Torch NE', 'GT Speed Torch WS', 'GT Torch Cross WN', 'GT Hidden Spikes SE', 'GT Conveyor Cross EN',
|
||||
'GT Speed Torch WN', 'Ice Lobby SE'
|
||||
}
|
||||
|
||||
|
||||
def add_key_logic_rules(world, player):
|
||||
key_logic = world.key_logic[player]
|
||||
|
||||
Binary file not shown.
@@ -52,6 +52,9 @@
|
||||
partial: 0
|
||||
strict: 0
|
||||
decoupledoors: off
|
||||
door_self_loops:
|
||||
on: 1
|
||||
off: 1
|
||||
dropshuffle:
|
||||
on: 1
|
||||
off: 1
|
||||
|
||||
@@ -31,6 +31,9 @@ key_logic_algorithm:
|
||||
decoupledoors:
|
||||
off: 9 # more strict
|
||||
on: 1
|
||||
door_self_loops:
|
||||
on: 1
|
||||
off: 1
|
||||
dropshuffle:
|
||||
on: 1
|
||||
off: 1
|
||||
|
||||
@@ -259,6 +259,10 @@
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"door_self_loops": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"experimental": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
|
||||
@@ -266,6 +266,7 @@
|
||||
"door_shuffle": [
|
||||
"Select Door Shuffling Algorithm. (default: %(default)s)",
|
||||
"Basic: Doors are mixed within a single dungeon.",
|
||||
"Paired Dungeon are paired (with one trio) and only mixed in those groups",
|
||||
"Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8",
|
||||
"Crossed: Doors are mixed between all dungeons.",
|
||||
"Vanilla: All doors are connected the same way they were in the",
|
||||
@@ -299,6 +300,7 @@
|
||||
"strict: Ensure small keys are available"
|
||||
],
|
||||
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
|
||||
"door_self_loops" : [ "Spiral stairs are allowed to self-loop" ],
|
||||
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
||||
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
||||
"crystals_ganon": [
|
||||
|
||||
@@ -58,10 +58,12 @@
|
||||
"randomizer.dungeon.smallkeyshuffle.universal": "Universal",
|
||||
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
|
||||
"randomizer.dungeon.decoupledoors": "Decouple Doors",
|
||||
"randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs",
|
||||
|
||||
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
|
||||
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
|
||||
"randomizer.dungeon.dungeondoorshuffle.basic": "Basic",
|
||||
"randomizer.dungeon.dungeondoorshuffle.paired": "Paired",
|
||||
"randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned",
|
||||
"randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed",
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
}
|
||||
},
|
||||
"decoupledoors": {
|
||||
"type": "checkbox",
|
||||
"config": {
|
||||
"padx": [20,0]
|
||||
}
|
||||
},
|
||||
"door_self_loops": {
|
||||
"type": "checkbox",
|
||||
"config": {
|
||||
"padx": [20,0],
|
||||
|
||||
@@ -123,6 +123,7 @@ class CustomSettings(object):
|
||||
args.trap_door_mode[p] = get_setting(settings['trap_door_mode'], args.trap_door_mode[p])
|
||||
args.key_logic_algorithm[p] = get_setting(settings['key_logic_algorithm'], args.key_logic_algorithm[p])
|
||||
args.decoupledoors[p] = get_setting(settings['decoupledoors'], args.decoupledoors[p])
|
||||
args.door_self_loops[p] = get_setting(settings['door_self_loops'], args.door_self_loops[p])
|
||||
args.dungeon_counters[p] = get_setting(settings['dungeon_counters'], args.dungeon_counters[p])
|
||||
args.crystals_gt[p] = get_setting(settings['crystals_gt'], args.crystals_gt[p])
|
||||
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
|
||||
@@ -256,6 +257,7 @@ class CustomSettings(object):
|
||||
settings_dict[p]['trap_door_mode'] = world.trap_door_mode[p]
|
||||
settings_dict[p]['key_logic_algorithm'] = world.key_logic_algorithm[p]
|
||||
settings_dict[p]['decoupledoors'] = world.decoupledoors[p]
|
||||
settings_dict[p]['door_self_loops'] = world.door_self_loops[p]
|
||||
settings_dict[p]['logic'] = world.logic[p]
|
||||
settings_dict[p]['mode'] = world.mode[p]
|
||||
settings_dict[p]['swords'] = world.swords[p]
|
||||
|
||||
@@ -116,6 +116,7 @@ SETTINGSTOPROCESS = {
|
||||
"door_type_mode": "door_type_mode",
|
||||
"trap_door_mode": "trap_door_mode",
|
||||
"decoupledoors": "decoupledoors",
|
||||
"door_self_loops": "door_self_loops",
|
||||
"experimental": "experimental",
|
||||
"dungeon_counters": "dungeon_counters",
|
||||
"mixed_travel": "mixed_travel",
|
||||
|
||||
@@ -22,7 +22,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
|
||||
queue = collections.deque(proposed_map.items())
|
||||
while len(queue) > 0:
|
||||
a, b = queue.popleft()
|
||||
if world.decoupledoors[player]:
|
||||
if a == b or world.decoupledoors[player]:
|
||||
connect_doors_one_way(a, b)
|
||||
else:
|
||||
connect_doors(a, b)
|
||||
@@ -128,14 +128,14 @@ def create_random_proposal(doors_to_connect, world, player):
|
||||
next_hook = random.choice(hooks_left)
|
||||
primary_door = random.choice(primary_bucket[next_hook])
|
||||
opp_hook, secondary_door = type_map[next_hook], None
|
||||
while (secondary_door is None or secondary_door == primary_door
|
||||
while (secondary_door is None or (secondary_door == primary_door and not world.door_self_loops[player])
|
||||
or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
|
||||
primary_door, secondary_door, world, player)):
|
||||
secondary_door = random.choice(secondary_bucket[opp_hook])
|
||||
proposal[primary_door] = secondary_door
|
||||
primary_bucket[next_hook].remove(primary_door)
|
||||
secondary_bucket[opp_hook].remove(secondary_door)
|
||||
if not world.decoupledoors[player]:
|
||||
if primary_door != secondary_door and not world.decoupledoors[player]:
|
||||
proposal[secondary_door] = primary_door
|
||||
primary_bucket[opp_hook].remove(secondary_door)
|
||||
secondary_bucket[next_hook].remove(primary_door)
|
||||
@@ -200,11 +200,19 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
|
||||
unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
|
||||
new_door = random.choice(unvisted_bucket[opp_hook])
|
||||
old_target = proposed_map[attempt]
|
||||
proposed_map[attempt] = new_door
|
||||
if not world.decoupledoors[player]:
|
||||
old_attempt = proposed_map[new_door]
|
||||
else:
|
||||
old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
|
||||
# ensure nothing gets messed up when something loops with itself
|
||||
if attempt == old_target and old_attempt == new_door:
|
||||
old_attempt = new_door
|
||||
old_target = attempt
|
||||
elif attempt == old_target:
|
||||
old_target = old_attempt
|
||||
elif old_attempt == new_door:
|
||||
old_attempt = old_target
|
||||
proposed_map[attempt] = new_door
|
||||
proposed_map[old_attempt] = old_target
|
||||
if not world.decoupledoors[player]:
|
||||
proposed_map[old_target] = old_attempt
|
||||
|
||||
@@ -98,6 +98,7 @@ def roll_settings(weights):
|
||||
ret.trap_door_mode = get_choice('trap_door_mode')
|
||||
ret.key_logic_algorithm = get_choice('key_logic_algorithm')
|
||||
ret.decoupledoors = get_choice('decoupledoors') == 'on'
|
||||
ret.door_self_loops = get_choice('door_self_loops') == 'on'
|
||||
ret.experimental = get_choice('experimental') == 'on'
|
||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||
|
||||
|
||||
125
test/dungeons/trap_test.yaml
Normal file
125
test/dungeons/trap_test.yaml
Normal file
@@ -0,0 +1,125 @@
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
door_shuffle: basic
|
||||
intensity: 3
|
||||
door_type_mode: all
|
||||
doors:
|
||||
1:
|
||||
doors:
|
||||
PoD Mimics 2 SW:
|
||||
type: Trap Door
|
||||
# TR Twin Pokeys NW: # not possible due to trap flags
|
||||
# type: Trap Door
|
||||
Thieves Blocked Entry SW:
|
||||
type: Trap Door
|
||||
Hyrule Dungeon Armory Interior Key Door N:
|
||||
type: Trap Door
|
||||
Desert Compass Key Door WN:
|
||||
type: Trap Door
|
||||
TR Tile Room SE:
|
||||
type: Trap Door
|
||||
# Mire Cross SW: # not possible due to trap flags
|
||||
# type: Trap Door
|
||||
Tower Circle of Pots ES:
|
||||
type: Trap Door
|
||||
Eastern Single Eyegore ES:
|
||||
type: Trap Door
|
||||
Eastern Duo Eyegores SE:
|
||||
type: Trap Door
|
||||
Swamp Push Statue S:
|
||||
type: Trap Door
|
||||
# Skull 2 East Lobby WS: # currently not possible due to trap flags
|
||||
# type: Trap Door
|
||||
GT Hope Room WN :
|
||||
type: Trap Door
|
||||
|
||||
# Eastern Courtyard Ledge S: # currently not possible due to trap flags
|
||||
# type: Trap Door
|
||||
Ice Switch Room ES :
|
||||
type: Trap Door
|
||||
Ice Switch Room NE :
|
||||
type: Trap Door
|
||||
Skull Torch Room WS :
|
||||
type: Trap Door
|
||||
GT Speed Torch NE :
|
||||
type: Trap Door
|
||||
GT Speed Torch WS :
|
||||
type: Trap Door
|
||||
GT Torch Cross WN :
|
||||
type: Trap Door
|
||||
Mire Tile Room SW :
|
||||
type: Trap Door
|
||||
Mire Tile Room ES :
|
||||
type: Trap Door
|
||||
TR Torches WN :
|
||||
type: Trap Door
|
||||
PoD Lobby N:
|
||||
type: Trap Door
|
||||
PoD Middle Cage S:
|
||||
type: Trap Door
|
||||
Ice Bomb Jump NW:
|
||||
type: Trap Door
|
||||
GT Hidden Spikes SE:
|
||||
type: Trap Door
|
||||
Ice Tall Hint EN:
|
||||
type: Trap Door
|
||||
GT Conveyor Cross EN:
|
||||
type: Trap Door
|
||||
Eastern Pot Switch WN:
|
||||
type: Trap Door
|
||||
Thieves Conveyor Maze WN:
|
||||
type: Trap Door
|
||||
# Thieves Conveyor Maze SW: #not possible due to 4 door limit
|
||||
# type: Trap Door
|
||||
Eastern Dark Square Key Door WN:
|
||||
type: Trap Door
|
||||
Eastern Lobby NW:
|
||||
type: Trap Door
|
||||
Eastern Lobby NE:
|
||||
type: Trap Door
|
||||
# Ice Cross Bottom SE: # not possible due to trap flags
|
||||
# type: Trap Door
|
||||
Desert Back Lobby S:
|
||||
type: Trap Door
|
||||
# Desert West S: need enough lobbies for basic, should otherwise work
|
||||
# type: Trap Door
|
||||
Desert West Lobby ES:
|
||||
type: Trap Door
|
||||
# Mire Hidden Shooters SE: # not possible due to trap flags
|
||||
# type: Trap Door
|
||||
# Mire Hidden Shooters ES: # not possible due to trap flags
|
||||
# type: Trap Door
|
||||
Mire Hidden Shooters WS:
|
||||
type: Trap Door
|
||||
Tower Dark Pits EN:
|
||||
type: Trap Door
|
||||
Tower Dark Maze ES:
|
||||
type: Trap Door
|
||||
TR Tongue Pull WS:
|
||||
type: Trap Door
|
||||
|
||||
# Lower layer: not valid
|
||||
# Sewers Pull Switch N:
|
||||
# type: Trap Door
|
||||
# PoD Sexy Statue W: # not possible due to trap flags and low layer too, so likely not an exception
|
||||
# type: Trap Door
|
||||
|
||||
# Not valid due to disappearing somaria block
|
||||
# Mire Dark Shooters SE:
|
||||
# type: Trap Door
|
||||
|
||||
# These triggers don't open doors
|
||||
# Ice Compass Room NE:
|
||||
# type: Trap Door
|
||||
# Hera Torches NE:
|
||||
# type: Trap Door
|
||||
# Mire Spikes WS:
|
||||
# type: Trap Door
|
||||
# Mire Spikes SW:
|
||||
# type: Trap Door
|
||||
# Mire Spikes NW:
|
||||
# type: Trap Door
|
||||
# Tower Room 03 WN:
|
||||
# type: Trap Door
|
||||
Reference in New Issue
Block a user