Merge branch 'Ambrosia' into DoorDevVolatile
# Conflicts: # BaseClasses.py # CLI.py # Main.py
This commit is contained in:
@@ -115,6 +115,7 @@ class World(object):
|
||||
set_player_attr('compassshuffle', False)
|
||||
set_player_attr('keyshuffle', False)
|
||||
set_player_attr('bigkeyshuffle', False)
|
||||
set_player_attr('restrict_boss_items', 'none')
|
||||
set_player_attr('bombbag', False)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
@@ -326,7 +327,7 @@ class World(object):
|
||||
elif item.name.startswith('Bottle'):
|
||||
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
|
||||
ret.prog_items[item.name, item.player] += 1
|
||||
elif item.advancement or item.smallkey or item.bigkey:
|
||||
elif item.advancement or item.smallkey or item.bigkey or item.compass or item.map:
|
||||
ret.prog_items[item.name, item.player] += 1
|
||||
|
||||
for item in self.itempool:
|
||||
@@ -341,6 +342,8 @@ class World(object):
|
||||
key_list += [dungeon.big_key.name]
|
||||
if len(dungeon.small_keys) > 0:
|
||||
key_list += [x.name for x in dungeon.small_keys]
|
||||
# map/compass may be required now
|
||||
key_list += [x.name for x in dungeon.dungeon_items]
|
||||
from Items import ItemFactory
|
||||
for item in ItemFactory(key_list, p):
|
||||
soft_collect(item)
|
||||
@@ -851,7 +854,7 @@ class CollectionState(object):
|
||||
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_player == player and self.check_if_progressive(item_name, player):
|
||||
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']:
|
||||
@@ -867,8 +870,7 @@ class CollectionState(object):
|
||||
reduced[('Heart Container', player)] = 1
|
||||
return frozenset(reduced.items())
|
||||
|
||||
@staticmethod
|
||||
def check_if_progressive(item_name):
|
||||
def check_if_progressive(self, item_name, player):
|
||||
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',
|
||||
@@ -880,7 +882,8 @@ class CollectionState(object):
|
||||
'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')))
|
||||
or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))
|
||||
or (self.world.restrict_boss_items[player] != 'none' and item_name.startswith(('Map', 'Compass'))))
|
||||
|
||||
def can_reach(self, spot, resolution_hint=None, player=None):
|
||||
try:
|
||||
@@ -2192,6 +2195,12 @@ class Item(object):
|
||||
item_dungeon = 'Hyrule Castle'
|
||||
return item_dungeon
|
||||
|
||||
def is_inside_dungeon_item(self, world):
|
||||
return ((self.smallkey and not world.keyshuffle[self.player])
|
||||
or (self.bigkey and not world.bigkeyshuffle[self.player])
|
||||
or (self.compass and not world.compassshuffle[self.player])
|
||||
or (self.map and not world.mapshuffle[self.player]))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
@@ -2411,6 +2420,7 @@ class Spoiler(object):
|
||||
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
||||
'open_pyramid': self.world.open_pyramid,
|
||||
'accessibility': self.world.accessibility,
|
||||
'restricted_boss_items': self.world.restrict_boss_items,
|
||||
'hints': self.world.hints,
|
||||
'mapshuffle': self.world.mapshuffle,
|
||||
'compassshuffle': self.world.compassshuffle,
|
||||
@@ -2426,6 +2436,7 @@ class Spoiler(object):
|
||||
'experimental': self.world.experimental,
|
||||
'keydropshuffle': self.world.keydropshuffle,
|
||||
'shopsanity': self.world.shopsanity,
|
||||
'pseudoboots': self.world.pseudoboots,
|
||||
'triforcegoal': self.world.treasure_hunt_count,
|
||||
'triforcepool': self.world.treasure_hunt_total,
|
||||
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}
|
||||
@@ -2453,6 +2464,9 @@ class Spoiler(object):
|
||||
return json.dumps(out)
|
||||
|
||||
def to_file(self, filename):
|
||||
def yn(flag):
|
||||
return 'Yes' if flag else 'No'
|
||||
|
||||
self.parse_data()
|
||||
with open(filename, 'w') as outfile:
|
||||
outfile.write('ALttP Entrance Randomizer Version %s - Seed: %s\n\n' % (self.metadata['version'], self.world.seed))
|
||||
@@ -2477,7 +2491,7 @@ class Spoiler(object):
|
||||
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
|
||||
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][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(f"Link's House Shuffled: {yn(self.metadata['shufflelinks'])}\n")
|
||||
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
||||
outfile.write('Intensity: %s\n' % self.metadata['intensity'][player])
|
||||
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
|
||||
@@ -2486,6 +2500,7 @@ class Spoiler(object):
|
||||
outfile.write('Crystals required for Ganon: %s\n' % (str(self.metadata['ganon_crystals'][player]) + addition))
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
|
||||
outfile.write(f"Restricted Boss Items: {self.metadata['restricted_boss_items'][player]}\n")
|
||||
outfile.write('Map shuffle: %s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
||||
outfile.write('Compass shuffle: %s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
||||
outfile.write('Small Key shuffle: %s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
|
||||
@@ -2494,12 +2509,13 @@ class Spoiler(object):
|
||||
outfile.write('Enemy shuffle: %s\n' % self.metadata['enemy_shuffle'][player])
|
||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||
outfile.write('Pot shuffle: %s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No'))
|
||||
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][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(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n")
|
||||
outfile.write('Bombbag: %s\n' % ('Yes' if self.metadata['bombbag'][player] else 'No'))
|
||||
outfile.write(f"Pot shuffle: {yn(self.metadata['potshuffle'][player])}\n")
|
||||
outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n")
|
||||
outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n")
|
||||
outfile.write(f"Key Drops shuffled: {yn(self.metadata['keydropshuffle'][player])}\n")
|
||||
outfile.write(f"Shopsanity: {yn(self.metadata['shopsanity'][player])}\n")
|
||||
outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n")
|
||||
outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n")
|
||||
if self.doors:
|
||||
outfile.write('\n\nDoors:\n\n')
|
||||
outfile.write('\n'.join(
|
||||
@@ -2681,6 +2697,16 @@ enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2, "legacy": 3}
|
||||
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
|
||||
e_dmg = {"default": 0, "shuffled": 1, "random": 2}
|
||||
|
||||
# byte 8: RRAA A??? (restrict boss mode, algorithm, ? = unused)
|
||||
rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2}
|
||||
# algorithm: todo with "biased shuffles"
|
||||
algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4}
|
||||
|
||||
# additions
|
||||
# psuedoboots does not effect code
|
||||
# sfx_shuffle and other adjust items does not effect settings code
|
||||
|
||||
|
||||
class Settings(object):
|
||||
|
||||
@staticmethod
|
||||
@@ -2709,7 +2735,9 @@ class Settings(object):
|
||||
| (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)
|
||||
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0)])
|
||||
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0),
|
||||
|
||||
(rb_mode[w.restrict_boss_items[p]] << 6)])
|
||||
return base64.b64encode(code, "+-".encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
@@ -2755,6 +2783,8 @@ class Settings(object):
|
||||
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
|
||||
if len(settings) > 8:
|
||||
args.restrict_boss_items[p] = True if r(rb_mode)[(settings[8] & 0x80) >> 6] else False
|
||||
|
||||
|
||||
class KeyRuleType(FastEnum):
|
||||
|
||||
3
CLI.py
3
CLI.py
@@ -96,7 +96,7 @@ def parse_cli(argv, no_defaults=False):
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
||||
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'bombbag', 'overworld_map',
|
||||
'bombbag', 'overworld_map', 'restrict_boss_items',
|
||||
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
|
||||
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
|
||||
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
|
||||
@@ -140,6 +140,7 @@ def parse_settings():
|
||||
"progressive": "on",
|
||||
"accessibility": "items",
|
||||
"algorithm": "balanced",
|
||||
"restrict_boss_items": "none",
|
||||
|
||||
# Shuffle Ganon defaults to TRUE
|
||||
"openpyramid": False,
|
||||
|
||||
@@ -218,7 +218,8 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro
|
||||
return name == 'Skull Woods 2' and d.name == 'Skull Pinball WS'
|
||||
original_state = extend_reachable_state_improved(entrance_regions, start, proposed_map, all_regions,
|
||||
valid_doors, bk_flag, world, player, exception)
|
||||
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception)
|
||||
dungeon['Origin'] = create_graph_piece_from_state(None, original_state, original_state, proposed_map, exception,
|
||||
world, player)
|
||||
either_crystal = True # if all hooks from the origin are either, explore all bits with either
|
||||
for hook, crystal in dungeon['Origin'].hooks.items():
|
||||
if crystal != CrystalBarrier.Either:
|
||||
@@ -239,7 +240,7 @@ def gen_dungeon_info(name, available_sectors, entrance_regions, all_regions, pro
|
||||
o_state = extend_reachable_state_improved([parent], init_state, proposed_map, all_regions,
|
||||
valid_doors, bk_flag, world, player, exception)
|
||||
o_state_cache[door.name] = o_state
|
||||
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception)
|
||||
piece = create_graph_piece_from_state(door, o_state, o_state, proposed_map, exception, world, player)
|
||||
dungeon[door.name] = piece
|
||||
check_blue_states(hanger_set, dungeon, o_state_cache, proposed_map, all_regions, valid_doors,
|
||||
group_flags, door_map, world, player, exception)
|
||||
@@ -339,7 +340,7 @@ def explore_blue_state(door, dungeon, o_state, proposed_map, all_regions, valid_
|
||||
blue_start.big_key_special = o_state.big_key_special
|
||||
b_state = extend_reachable_state_improved([parent], blue_start, proposed_map, all_regions, valid_doors, bk_flag,
|
||||
world, player, exception)
|
||||
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception)
|
||||
dungeon[door.name] = create_graph_piece_from_state(door, o_state, b_state, proposed_map, exception, world, player)
|
||||
|
||||
|
||||
def make_a_choice(dungeon, hangers, avail_hooks, prev_choices, name):
|
||||
@@ -603,7 +604,7 @@ def winnow_hangers(hangers, hooks):
|
||||
hangers[hanger].remove(door)
|
||||
|
||||
|
||||
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, world, player):
|
||||
# todo: info about dungeon events - not sure about that
|
||||
graph_piece = GraphPiece()
|
||||
all_unattached = {}
|
||||
@@ -635,16 +636,15 @@ def create_graph_piece_from_state(door, o_state, b_state, proposed_map, exceptio
|
||||
graph_piece.visited_regions.update(o_state.visited_orange)
|
||||
graph_piece.visited_regions.update(b_state.visited_blue)
|
||||
graph_piece.visited_regions.update(b_state.visited_orange)
|
||||
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found))
|
||||
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found))
|
||||
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(o_state.bk_found, world, player))
|
||||
graph_piece.possible_bk_locations.update(filter_for_potential_bk_locations(b_state.bk_found, world, player))
|
||||
graph_piece.pinball_used = o_state.pinball_used or b_state.pinball_used
|
||||
return graph_piece
|
||||
|
||||
|
||||
def filter_for_potential_bk_locations(locations):
|
||||
return [x for x in locations if
|
||||
'- Big Chest' not in x.name and '- Prize' not in x.name and x.name not in dungeon_events
|
||||
and not x.forced_item and x.name not in ['Agahnim 1', 'Agahnim 2']]
|
||||
def filter_for_potential_bk_locations(locations, world, player):
|
||||
return [x for x in locations if '- Big Chest' not in x.name and not reserved_location(x, world, player) and
|
||||
not x.forced_item and not prize_or_event(x) and not blind_boss_unavail(x, locations, world, player)]
|
||||
|
||||
|
||||
type_map = {
|
||||
@@ -987,12 +987,8 @@ class ExplorationState(object):
|
||||
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
|
||||
return True
|
||||
|
||||
def count_locations_exclude_specials(self):
|
||||
cnt = 0
|
||||
for loc in self.found_locations:
|
||||
if '- Big Chest' not in loc.name and '- Prize' not in loc.name and loc.name not in dungeon_events and not loc.forced_item:
|
||||
cnt += 1
|
||||
return cnt
|
||||
def count_locations_exclude_specials(self, world, player):
|
||||
return count_locations_exclude_big_chest(self.found_locations, world, player)
|
||||
|
||||
def validate(self, door, region, world, player):
|
||||
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
|
||||
@@ -1033,6 +1029,32 @@ class ExplorationState(object):
|
||||
return 2
|
||||
|
||||
|
||||
def count_locations_exclude_big_chest(locations, world, player):
|
||||
cnt = 0
|
||||
for loc in locations:
|
||||
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
|
||||
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
|
||||
def prize_or_event(loc):
|
||||
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||
|
||||
|
||||
def reserved_location(loc, world, player):
|
||||
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
|
||||
|
||||
|
||||
def blind_boss_unavail(loc, locations, world, player):
|
||||
if loc.name == "Thieves' Town - Boss":
|
||||
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
||||
(not any(x for x in 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 locations if x.name == 'Attic Cracked Floor'))))
|
||||
return False
|
||||
|
||||
|
||||
class ExplorableDoor(object):
|
||||
|
||||
def __init__(self, door, crystal, flag):
|
||||
@@ -1056,7 +1078,8 @@ def extend_reachable_state_improved(search_regions, state, proposed_map, all_reg
|
||||
explorable_door = local_state.next_avail_door()
|
||||
if explorable_door.door.bigKey:
|
||||
if bk_flag:
|
||||
big_not_found = not special_big_key_found(local_state) if local_state.big_key_special else local_state.count_locations_exclude_specials() == 0
|
||||
big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
|
||||
else local_state.count_locations_exclude_specials(world, player) == 0)
|
||||
if big_not_found:
|
||||
continue # we can't open this door
|
||||
if explorable_door.door in proposed_map:
|
||||
|
||||
@@ -117,6 +117,7 @@ def fill_dungeons(world):
|
||||
def get_dungeon_item_pool(world):
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
all_state_base = world.get_all_state()
|
||||
|
||||
@@ -137,10 +138,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
|
||||
item.priority = True
|
||||
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if ((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player]))]
|
||||
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)]
|
||||
|
||||
# sort in the order Big Key, Small Key, Other before placing dungeon items
|
||||
sort_order = {"BigKey": 3, "SmallKey": 2}
|
||||
@@ -441,7 +439,6 @@ dungeon_bigs = {
|
||||
'Ganons Tower': 'Big Key (Ganons Tower)'
|
||||
}
|
||||
|
||||
|
||||
dungeon_hints = {
|
||||
'Hyrule Castle': 'in Hyrule Castle',
|
||||
'Eastern Palace': 'in Eastern Palace',
|
||||
|
||||
15
Fill.py
15
Fill.py
@@ -237,7 +237,10 @@ def fill_restrictive(world, base_state, locations, itempool, keys_in_itempool =
|
||||
|
||||
|
||||
def valid_key_placement(item, location, itempool, world):
|
||||
if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player] or world.logic[item.player] == 'nologic':
|
||||
if not valid_reserved_placement(item, location, world):
|
||||
return False
|
||||
if ((not item.smallkey and not item.bigkey) or item.player != location.player
|
||||
or world.retro[item.player] or world.logic[item.player] == 'nologic'):
|
||||
return True
|
||||
dungeon = location.parent_region.dungeon
|
||||
if dungeon:
|
||||
@@ -251,9 +254,13 @@ def valid_key_placement(item, location, itempool, world):
|
||||
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:
|
||||
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player]))
|
||||
return not inside_dungeon_item
|
||||
return not item.is_inside_dungeon_item(world) # todo: big deal for ambrosia to fix this
|
||||
|
||||
|
||||
def valid_reserved_placement(item, location, world):
|
||||
if item.player == location.player and item.is_inside_dungeon_item(world):
|
||||
return location.name not in world.item_pool_config.reserved_locations[location.player]
|
||||
return True
|
||||
|
||||
|
||||
def track_outside_keys(item, location, world):
|
||||
|
||||
@@ -5,7 +5,8 @@ from collections import defaultdict, deque
|
||||
from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType
|
||||
from Regions import dungeon_events
|
||||
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, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import reserved_location, blind_boss_unavail
|
||||
|
||||
|
||||
class KeyLayout(object):
|
||||
@@ -1100,40 +1101,30 @@ def location_is_bk_locked(loc, key_logic):
|
||||
return loc in key_logic.bk_chests or loc in key_logic.bk_locked
|
||||
|
||||
|
||||
def prize_or_event(loc):
|
||||
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||
|
||||
|
||||
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
|
||||
# todo: verfiy this code is defunct
|
||||
# def prize_or_event(loc):
|
||||
# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||
#
|
||||
#
|
||||
# def reserved_location(loc, world, player):
|
||||
# return loc in world.item_pool.config.reserved_locations[player]
|
||||
#
|
||||
#
|
||||
# def blind_boss_unavail(loc, state, world, player):
|
||||
# if loc.name == "Thieves' Town - Boss":
|
||||
# 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
|
||||
|
||||
|
||||
# counts free locations for keys - hence why reserved locations don't count
|
||||
def count_free_locations(state, world, player):
|
||||
cnt = 0
|
||||
for loc in state.found_locations:
|
||||
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
|
||||
return cnt
|
||||
|
||||
|
||||
def count_locations_exclude_big_chest(state, world, player):
|
||||
cnt = 0
|
||||
for loc in state.found_locations:
|
||||
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)):
|
||||
if (not prize_or_event(loc) and not loc.forced_item and not reserved_location(loc, world, player)
|
||||
and not blind_boss_unavail(loc, state.found_locations, world, player)):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
@@ -1437,7 +1428,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
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_locations = count_locations_exclude_big_chest(state.found_locations, world, player)
|
||||
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_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
@@ -1663,7 +1654,7 @@ 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)
|
||||
ttl_locations = count_locations_exclude_big_chest(state.found_locations, 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)
|
||||
|
||||
4
Main.py
4
Main.py
@@ -29,6 +29,8 @@ from Fill import sell_potions, sell_keys, balance_multiworld_progression, balanc
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
from source.item.FillUtil import create_item_pool_config
|
||||
|
||||
__version__ = '1.0.1.0-v'
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
@@ -105,6 +107,7 @@ def main(args, seed=None, fish=None):
|
||||
world.shufflelinks = args.shufflelinks.copy()
|
||||
world.pseudoboots = args.pseudoboots.copy()
|
||||
world.overworld_map = args.overworld_map.copy()
|
||||
world.restrict_boss_items = args.restrict_boss_items.copy()
|
||||
|
||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||
|
||||
@@ -149,6 +152,7 @@ def main(args, seed=None, fish=None):
|
||||
create_dungeons(world, player)
|
||||
adjust_locations(world, player)
|
||||
place_bosses(world, player)
|
||||
create_item_pool_config(world)
|
||||
|
||||
if any(world.potshuffle.values()):
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
||||
|
||||
@@ -134,6 +134,7 @@ def roll_settings(weights):
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
|
||||
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
ret.restrict_boss_items = get_choice('restrict_boss_items')
|
||||
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
|
||||
19
Rules.py
19
Rules.py
@@ -4,6 +4,7 @@ from collections import deque
|
||||
|
||||
import OverworldGlitchRules
|
||||
from BaseClasses import CollectionState, RegionType, DoorType, Entrance, CrystalBarrier, KeyRuleType
|
||||
from Dungeons import dungeon_table
|
||||
from RoomData import DoorKind
|
||||
from OverworldGlitchRules import overworld_glitches_rules
|
||||
|
||||
@@ -557,11 +558,29 @@ def global_rules(world, player):
|
||||
add_key_logic_rules(world, player)
|
||||
# End of door rando rules.
|
||||
|
||||
if world.restrict_boss_items[player] != 'none':
|
||||
def add_mc_rule(l):
|
||||
boss_location = world.get_location(l, player)
|
||||
d_name = boss_location.parent_region.dungeon.name
|
||||
compass_name = f'Compass ({d_name})'
|
||||
map_name = f'Map ({d_name})'
|
||||
add_rule(boss_location, lambda state: state.has(compass_name, player) and state.has(map_name, player))
|
||||
|
||||
for dungeon, info in dungeon_table.items():
|
||||
if info.prize:
|
||||
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
|
||||
for loc in [info.prize, f'{d_name} - Boss']:
|
||||
add_mc_rule(loc)
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
add_mc_rule('Agahnim 1')
|
||||
add_mc_rule('Agahnim 2')
|
||||
|
||||
add_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player))
|
||||
set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) and state.has_crystals(world.crystals_needed_for_ganon[player], player)
|
||||
and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times
|
||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop
|
||||
|
||||
|
||||
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.
|
||||
bombable_doors = ['Ice Rod Cave', 'Light World Bomb Hut', 'Light World Death Mountain Shop', 'Mini Moldorm Cave',
|
||||
|
||||
@@ -254,6 +254,13 @@
|
||||
"none"
|
||||
]
|
||||
},
|
||||
"restrict_boss_items": {
|
||||
"choices": [
|
||||
"none",
|
||||
"mapcompass",
|
||||
"dungeon"
|
||||
]
|
||||
},
|
||||
"hints": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
|
||||
@@ -276,6 +276,13 @@
|
||||
"Locations: You will be able to reach every location in the game.",
|
||||
"None: You will be able to reach enough locations to beat the game."
|
||||
],
|
||||
"restrict_boss_items": [
|
||||
"Select which dungeon are not allowed on bosses (default: %(default)s)",
|
||||
"None: All items allowed",
|
||||
"Mapcompass: Map and Compass are required before you defeat the boss.",
|
||||
"Dungeon: Same as above and keys too cannot be on the boss. Small key shuffle",
|
||||
" and big key shuffle override this behavior"
|
||||
],
|
||||
"hints": [ "Make telepathic tiles and storytellers give helpful hints. (default: %(default)s)" ],
|
||||
"shuffleganon": [
|
||||
"Include the Ganon's Tower and Pyramid Hole in the",
|
||||
|
||||
@@ -291,6 +291,10 @@
|
||||
"randomizer.item.sortingalgo.vt26": "VT8.26",
|
||||
"randomizer.item.sortingalgo.balanced": "Balanced",
|
||||
|
||||
"randomizer.item.restrict_boss_items": "Forbidden Boss Items",
|
||||
"randomizer.item.restrict_boss_items.none": "None",
|
||||
"randomizer.item.restrict_boss_items.mapcompass": "Map & Compass",
|
||||
"randomizer.item.restrict_boss_items.dungeon": "Map & Compass & Keys",
|
||||
|
||||
"bottom.content.worlds": "Worlds",
|
||||
"bottom.content.names": "Player names",
|
||||
|
||||
@@ -124,6 +124,15 @@
|
||||
"vt26",
|
||||
"balanced"
|
||||
]
|
||||
},
|
||||
"restrict_boss_items": {
|
||||
"type": "selectbox",
|
||||
"default": "none",
|
||||
"options": [
|
||||
"none",
|
||||
"mapcompass",
|
||||
"dungeon"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ SETTINGSTOPROCESS = {
|
||||
"progressives": "progressive",
|
||||
"accessibility": "accessibility",
|
||||
"sortingalgo": "algorithm",
|
||||
"beemizer": "beemizer"
|
||||
"beemizer": "beemizer",
|
||||
"restrict_boss_items": "restrict_boss_items"
|
||||
},
|
||||
"entrance": {
|
||||
"openpyramid": "openpyramid",
|
||||
|
||||
26
source/item/FillUtil.py
Normal file
26
source/item/FillUtil.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from Dungeons import dungeon_table
|
||||
|
||||
class ItemPoolConfig(object):
|
||||
|
||||
def __init__(self):
|
||||
self.reserved_locations = defaultdict(set)
|
||||
|
||||
|
||||
def create_item_pool_config(world):
|
||||
world.item_pool_config = config = ItemPoolConfig()
|
||||
player_set = set()
|
||||
for player in range(1, world.players+1):
|
||||
if world.restrict_boss_items[player] != 'none':
|
||||
player_set.add(player)
|
||||
if world.restrict_boss_items[player] == 'dungeon':
|
||||
for dungeon, info in dungeon_table.items():
|
||||
if info.prize:
|
||||
d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon
|
||||
config.reserved_locations[player].add(f'{d_name} - Boss')
|
||||
for dungeon in world.dungeons:
|
||||
if world.restrict_boss_items[dungeon.player] != 'none':
|
||||
for item in dungeon.all_items:
|
||||
if item.map or item.compass:
|
||||
item.advancement = True
|
||||
Reference in New Issue
Block a user