Merge branch 'DoorDevUnstable' into Sandbox
# Conflicts: # ItemList.py # Items.py # Main.py # Rom.py # data/base2current.bps
This commit is contained in:
+22
-22
@@ -32,10 +32,10 @@ jobs:
|
||||
steps:
|
||||
# checkout commit
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
# install python
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: "x64"
|
||||
@@ -50,21 +50,21 @@ jobs:
|
||||
pip install pyinstaller
|
||||
# get parent directory
|
||||
- name: Get Repo Name
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: repoName
|
||||
with:
|
||||
source: ${{ github.repository }}
|
||||
find: '${{ github.repository_owner }}/'
|
||||
replace: ''
|
||||
- name: Get Parent Directory Path (!Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDirNotWin
|
||||
with:
|
||||
source: ${{ github.workspace }}
|
||||
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
|
||||
replace: ${{ steps.repoName.outputs.value }}
|
||||
- name: Get Parent Directory Path (Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDir
|
||||
with:
|
||||
source: ${{ steps.parentDirNotWin.outputs.value }}
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
python ./resources/ci/common/prepare_binary.py
|
||||
# upload binary artifacts for later step
|
||||
- name: Upload Binary Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: binaries-${{ matrix.os-name }}
|
||||
path: ${{ steps.parentDir.outputs.value }}/artifact
|
||||
@@ -117,10 +117,10 @@ jobs:
|
||||
steps:
|
||||
# checkout commit
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
# install python
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: "x64"
|
||||
@@ -134,21 +134,21 @@ jobs:
|
||||
python ./resources/ci/common/install.py
|
||||
# get parent directory
|
||||
- name: Get Repo Name
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: repoName
|
||||
with:
|
||||
source: ${{ github.repository }}
|
||||
find: '${{ github.repository_owner }}/'
|
||||
replace: ''
|
||||
- name: Get Parent Directory Path (!Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDirNotWin
|
||||
with:
|
||||
source: ${{ github.workspace }}
|
||||
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
|
||||
replace: ${{ steps.repoName.outputs.value }}
|
||||
- name: Get Parent Directory Path (Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDir
|
||||
with:
|
||||
source: ${{ steps.parentDirNotWin.outputs.value }}
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
replace: ${{ steps.repoName.outputs.value }}
|
||||
# download binary artifact
|
||||
- name: Download Binary Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: binaries-${{ matrix.os-name }}
|
||||
path: ./
|
||||
@@ -170,13 +170,13 @@ jobs:
|
||||
python ./resources/ci/common/prepare_release.py
|
||||
# upload appversion artifact for later step
|
||||
- name: Upload AppVersion Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: appversion-${{ matrix.os-name }}
|
||||
path: ./resources/app/meta/manifests/app_version.txt
|
||||
# upload archive artifact for later step
|
||||
- name: Upload Archive Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: archive-${{ matrix.os-name }}
|
||||
path: ${{ steps.parentDir.outputs.value }}/deploy
|
||||
@@ -202,24 +202,24 @@ jobs:
|
||||
steps:
|
||||
# checkout commit
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
# get parent directory
|
||||
- name: Get Repo Name
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: repoName
|
||||
with:
|
||||
source: ${{ github.repository }}
|
||||
find: '${{ github.repository_owner }}/'
|
||||
replace: ''
|
||||
- name: Get Parent Directory Path (!Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDirNotWin
|
||||
with:
|
||||
source: ${{ github.workspace }}
|
||||
find: '${{ steps.repoName.outputs.value }}/${{ steps.repoName.outputs.value }}'
|
||||
replace: ${{ steps.repoName.outputs.value }}
|
||||
- name: Get Parent Directory Path (Windows)
|
||||
uses: mad9000/actions-find-and-replace-string@1
|
||||
uses: mad9000/actions-find-and-replace-string@3
|
||||
id: parentDir
|
||||
with:
|
||||
source: ${{ steps.parentDirNotWin.outputs.value }}
|
||||
@@ -230,25 +230,25 @@ jobs:
|
||||
python -m pip install pytz requests
|
||||
# download appversion artifact
|
||||
- name: Download AppVersion Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: appversion-${{ matrix.os-name }}
|
||||
path: ${{ steps.parentDir.outputs.value }}/build
|
||||
# download ubuntu archive artifact
|
||||
- name: Download Ubuntu Archive Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: archive-ubuntu-latest
|
||||
path: ${{ steps.parentDir.outputs.value }}/deploy/linux
|
||||
# download macos archive artifact
|
||||
- name: Download MacOS Archive Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: archive-macOS-latest
|
||||
path: ${{ steps.parentDir.outputs.value }}/deploy/macos
|
||||
# download windows archive artifact
|
||||
- name: Download Windows Archive Artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: archive-windows-latest
|
||||
path: ${{ steps.parentDir.outputs.value }}/deploy/windows
|
||||
|
||||
+109
-26
@@ -29,6 +29,8 @@ class World(object):
|
||||
self.doorShuffle = doorShuffle.copy()
|
||||
self.intensity = {}
|
||||
self.door_type_mode = {}
|
||||
self.trap_door_mode = {}
|
||||
self.key_logic_algorithm = {}
|
||||
self.logic = logic.copy()
|
||||
self.mode = mode.copy()
|
||||
self.swords = swords.copy()
|
||||
@@ -110,7 +112,8 @@ class World(object):
|
||||
set_player_attr('can_access_trock_front', None)
|
||||
set_player_attr('can_access_trock_big_chest', None)
|
||||
set_player_attr('can_access_trock_middle', None)
|
||||
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity'])
|
||||
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic']
|
||||
or shuffle[player] in ['lean', 'crossed', 'insanity'])
|
||||
set_player_attr('mapshuffle', False)
|
||||
set_player_attr('compassshuffle', False)
|
||||
set_player_attr('keyshuffle', 'none')
|
||||
@@ -139,10 +142,12 @@ class World(object):
|
||||
set_player_attr('pot_contents', None)
|
||||
set_player_attr('pseudoboots', False)
|
||||
set_player_attr('collection_rate', False)
|
||||
set_player_attr('colorizepots', False)
|
||||
set_player_attr('colorizepots', True)
|
||||
set_player_attr('pot_pool', {})
|
||||
set_player_attr('decoupledoors', False)
|
||||
set_player_attr('door_type_mode', 'original')
|
||||
set_player_attr('trap_door_mode', 'optional')
|
||||
set_player_attr('key_logic_algorithm', 'default')
|
||||
|
||||
set_player_attr('shopsanity', False)
|
||||
set_player_attr('mixed_travel', 'prevent')
|
||||
@@ -156,7 +161,9 @@ class World(object):
|
||||
def finish_init(self):
|
||||
for player in range(1, self.players + 1):
|
||||
if self.mode[player] == 'retro':
|
||||
self.mode[player] == 'open'
|
||||
self.mode[player] = 'open'
|
||||
if self.goal[player] == 'completionist':
|
||||
self.accessibility[player] = 'locations'
|
||||
|
||||
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)})'
|
||||
@@ -472,7 +479,10 @@ class World(object):
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
|
||||
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
|
||||
prog_locations = [location for location in self.get_locations() if location.item is not None
|
||||
and (location.item.advancement or location.event
|
||||
or self.goal[location.player] == 'completionist')
|
||||
and location not in state.locations_checked]
|
||||
|
||||
while prog_locations:
|
||||
sphere = []
|
||||
@@ -504,6 +514,7 @@ class CollectionState(object):
|
||||
self.world = parent
|
||||
if not skip_init:
|
||||
self.prog_items = Counter()
|
||||
self.forced_keys = Counter()
|
||||
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
||||
self.events = []
|
||||
@@ -518,6 +529,7 @@ class CollectionState(object):
|
||||
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.placing_item = None
|
||||
# self.trace = None
|
||||
|
||||
def update_reachable_regions(self, player):
|
||||
@@ -535,12 +547,13 @@ class CollectionState(object):
|
||||
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)
|
||||
if self.world.key_logic_algorithm[player] == 'default':
|
||||
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
|
||||
@@ -632,6 +645,7 @@ class CollectionState(object):
|
||||
|
||||
def check_key_doors_in_dungeons(self, rrp, player):
|
||||
for dungeon_name, checklist in self.dungeons_to_check[player].items():
|
||||
# todo: optimization idea - abort exploration if there are unresolved events now
|
||||
if self.apply_dungeon_exploration(rrp, player, dungeon_name, checklist):
|
||||
continue
|
||||
init_door_candidates = self.should_explore_child_state(self, dungeon_name, player)
|
||||
@@ -740,6 +754,15 @@ class CollectionState(object):
|
||||
rrp[k] = missing_regions[k]
|
||||
possible_path = terminal_states[0].path[k]
|
||||
self.path[k] = paths[k] = possible_path
|
||||
for conn in k.exits:
|
||||
if self.is_small_door(conn):
|
||||
door = conn.door if conn.door.smallKey else conn.door.controller
|
||||
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)
|
||||
missing_bc = {}
|
||||
for blocked, crystal in common_bc.items():
|
||||
if (blocked not in bc and blocked.parent_region in rrp
|
||||
@@ -790,7 +813,23 @@ class CollectionState(object):
|
||||
else:
|
||||
door_candidates.append(door.name)
|
||||
return door_candidates
|
||||
return None
|
||||
door_candidates, skip = [], set()
|
||||
if (state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name != 'Universal'
|
||||
and state.placing_item and state.placing_item.name == small_key_name):
|
||||
key_logic = state.world.key_logic[player][dungeon_name]
|
||||
for door, paired in key_logic.sm_doors.items():
|
||||
if door.name in key_logic.door_rules:
|
||||
rule = key_logic.door_rules[door.name]
|
||||
key = KeyRuleType.AllowSmall
|
||||
if (key in rule.new_rules and key_total >= rule.new_rules[key] and door.name not in skip
|
||||
and door.name in state.reached_doors[player] and door.name not in state.opened_doors[player]
|
||||
and rule.small_location.item is None):
|
||||
if paired:
|
||||
door_candidates.append((door.name, paired.name))
|
||||
skip.add(paired.name)
|
||||
else:
|
||||
door_candidates.append(door.name)
|
||||
return door_candidates if door_candidates else None
|
||||
|
||||
@staticmethod
|
||||
def print_rrp(rrp):
|
||||
@@ -806,6 +845,7 @@ class CollectionState(object):
|
||||
def copy(self):
|
||||
ret = CollectionState(self.world, skip_init=True)
|
||||
ret.prog_items = self.prog_items.copy()
|
||||
ret.forced_keys = self.forced_keys.copy()
|
||||
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.events = copy.copy(self.events)
|
||||
@@ -820,6 +860,7 @@ class CollectionState(object):
|
||||
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)}
|
||||
ret.placing_item = self.placing_item
|
||||
return ret
|
||||
|
||||
def apply_dungeon_exploration(self, rrp, player, dungeon_name, checklist):
|
||||
@@ -929,29 +970,30 @@ class CollectionState(object):
|
||||
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)
|
||||
found_new = False
|
||||
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)
|
||||
found_new = True
|
||||
return found_new
|
||||
|
||||
def sweep_for_events(self, key_only=False, locations=None):
|
||||
# this may need improvement
|
||||
if locations is None:
|
||||
locations = self.world.get_filled_locations()
|
||||
new_locations = True
|
||||
checked_locations = 0
|
||||
while new_locations:
|
||||
reachable_events = [location for location in locations if location.event and
|
||||
(not key_only or (self.world.keyshuffle[location.item.player] == 'none' and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
and location.can_reach(self)]
|
||||
reachable_events = self._do_not_flood_the_keys(reachable_events)
|
||||
new_locations = False
|
||||
for event in reachable_events:
|
||||
if (event.name, event.player) not in self.events:
|
||||
self.events.append((event.name, event.player))
|
||||
self.collect(event.item, True, event)
|
||||
new_locations = len(reachable_events) > checked_locations
|
||||
checked_locations = len(reachable_events)
|
||||
new_locations = True
|
||||
|
||||
|
||||
def can_reach_blue(self, region, player):
|
||||
@@ -1011,6 +1053,14 @@ class CollectionState(object):
|
||||
return (item, player) in self.prog_items
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def has_sm_key_strict(self, item, player, count=1):
|
||||
if self.world.keyshuffle[player] == 'universal':
|
||||
if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)':
|
||||
return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool
|
||||
return self.can_buy_unlimited('Small Key (Universal)', player)
|
||||
obtained = self.prog_items[item, player] - self.forced_keys[item, player]
|
||||
return obtained >= count
|
||||
|
||||
def can_buy_unlimited(self, item, player):
|
||||
for shop in self.world.shops[player]:
|
||||
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
|
||||
@@ -1020,6 +1070,12 @@ class CollectionState(object):
|
||||
def item_count(self, item, player):
|
||||
return self.prog_items[item, player]
|
||||
|
||||
def everything(self, player):
|
||||
all_locations = self.world.get_filled_locations(player)
|
||||
all_locations.remove(self.world.get_location('Ganon', player))
|
||||
return (len([x for x in self.locations_checked if x.player == player])
|
||||
>= len(all_locations))
|
||||
|
||||
def has_crystals(self, count, player):
|
||||
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
|
||||
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
|
||||
@@ -1137,6 +1193,8 @@ class CollectionState(object):
|
||||
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
||||
|
||||
def can_flute(self, player):
|
||||
if self.world.mode[player] == 'standard' and not self.has('Zelda Delivered', player):
|
||||
return False # can't flute in rain state
|
||||
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
|
||||
return True
|
||||
lw = self.world.get_region('Light World', player)
|
||||
@@ -1199,6 +1257,8 @@ class CollectionState(object):
|
||||
def collect(self, item, event=False, location=None):
|
||||
if location:
|
||||
self.locations_checked.add(location)
|
||||
if item and item.smallkey and location.forced_item is not None:
|
||||
self.forced_keys[item.name, item.player] += 1
|
||||
if not item:
|
||||
return
|
||||
changed = False
|
||||
@@ -1385,10 +1445,10 @@ class Region(object):
|
||||
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
||||
or (item.map and not self.world.mapshuffle[item.player])
|
||||
or (item.compass and not self.world.compassshuffle[item.player]))
|
||||
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if sewer_hack or inside_dungeon_item:
|
||||
# not all small keys to escape must be in escape
|
||||
# sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if inside_dungeon_item:
|
||||
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
||||
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
@@ -1849,6 +1909,9 @@ class Door(object):
|
||||
return world.get_room(self.roomIndex, self.player).kind(self)
|
||||
return None
|
||||
|
||||
def dungeon_name(self):
|
||||
return self.entrance.parent_region.dungeon.name if self.entrance.parent_region.dungeon else 'Cave'
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and self.name == other.name
|
||||
|
||||
@@ -2260,6 +2323,9 @@ class Item(object):
|
||||
def __unicode__(self):
|
||||
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name and self.player == other.player
|
||||
|
||||
|
||||
# have 6 address that need to be filled
|
||||
class Crystal(Item):
|
||||
@@ -2395,6 +2461,8 @@ class Spoiler(object):
|
||||
'door_shuffle': self.world.doorShuffle,
|
||||
'intensity': self.world.intensity,
|
||||
'door_type_mode': self.world.door_type_mode,
|
||||
'trap_door_mode': self.world.trap_door_mode,
|
||||
'key_logic': self.world.key_logic_algorithm,
|
||||
'decoupledoors': self.world.decoupledoors,
|
||||
'dungeon_counters': self.world.dungeon_counters,
|
||||
'item_pool': self.world.difficulty,
|
||||
@@ -2574,7 +2642,7 @@ class Spoiler(object):
|
||||
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Swords: %s\n' % self.metadata['weapons'][player])
|
||||
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
|
||||
if self.metadata['goal'][player] in ['triforcehunt', 'trinity']:
|
||||
if self.metadata['goal'][player] in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
outfile.write('Triforce Pieces Required: %s\n' % self.metadata['triforcegoal'][player])
|
||||
outfile.write('Triforce Pieces Total: %s\n' % self.metadata['triforcepool'][player])
|
||||
outfile.write('Crystals required for GT: %s\n' % (str(self.world.crystals_gt_orig[player])))
|
||||
@@ -2595,13 +2663,14 @@ class Spoiler(object):
|
||||
outfile.write(f"GT/Ganon Shuffled: {yn(self.metadata['shuffleganon'])}\n")
|
||||
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
|
||||
outfile.write(f"Take Any Caves: {self.metadata['take_any'][player]}\n")
|
||||
outfile.write(f"Overworld Map: {self.metadata['overworld_map'][player]}\n")
|
||||
if self.metadata['goal'][player] != 'trinity':
|
||||
outfile.write('Pyramid hole pre-opened: %s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
||||
outfile.write('Door Shuffle: %s\n' % self.metadata['door_shuffle'][player])
|
||||
if self.metadata['door_shuffle'][player] != 'vanilla':
|
||||
outfile.write(f"Intensity: {self.metadata['intensity'][player]}\n")
|
||||
outfile.write(f"Door Type Mode: {self.metadata['door_type_mode'][player]}\n")
|
||||
outfile.write(f"Trap Door Mode: {self.metadata['trap_door_mode'][player]}\n")
|
||||
outfile.write(f"Key Logic Algorithm: {self.metadata['key_logic'][player]}\n")
|
||||
outfile.write(f"Decouple Doors: {yn(self.metadata['decoupledoors'][player])}\n")
|
||||
outfile.write(f"Experimental: {yn(self.metadata['experimental'][player])}\n")
|
||||
outfile.write(f"Dungeon Counters: {self.metadata['dungeon_counters'][player]}\n")
|
||||
@@ -2617,6 +2686,7 @@ class Spoiler(object):
|
||||
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
|
||||
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
|
||||
outfile.write(f"Hints: {yn(self.metadata['hints'][player])}\n")
|
||||
outfile.write('Race: %s\n' % ('Yes' if self.world.settings.world_rep['meta']['race'] else 'No'))
|
||||
|
||||
if self.startinventory:
|
||||
outfile.write('Starting Inventory:'.ljust(line_width))
|
||||
@@ -2854,7 +2924,8 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2}
|
||||
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
|
||||
|
||||
# byte 2: GGGD DFFH (goal, diff, item_func, hints)
|
||||
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5}
|
||||
goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5,
|
||||
'ganonhunt': 6, 'completionist': 7}
|
||||
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
|
||||
@@ -2892,15 +2963,19 @@ boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique
|
||||
|
||||
|
||||
# byte 10: settings_version
|
||||
# byte 11: FBBB, TTSS (flute_mode, bow_mode, take_any, small_key_mode)
|
||||
# byte 11: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode)
|
||||
flute_mode = {'normal': 0, 'active': 1}
|
||||
keyshuffle_mode = {'none': 0, 'wild': 1, 'universal': 2} # reserved 8 modes?
|
||||
take_any_mode = {'none': 0, 'random': 1, 'fixed': 2}
|
||||
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silver': 3}
|
||||
bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3}
|
||||
|
||||
# additions
|
||||
# psuedoboots does not effect code
|
||||
# sfx_shuffle and other adjust items does not effect settings code
|
||||
# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo)
|
||||
overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2}
|
||||
trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3}
|
||||
key_logic_algo = {'default': 0, 'partial': 1, 'strict': 2}
|
||||
|
||||
# sfx_shuffle and other adjust items does not affect settings code
|
||||
|
||||
# Bump this when making changes that are not backwards compatible (nearly all of them)
|
||||
settings_version = 1
|
||||
@@ -2944,7 +3019,10 @@ class Settings(object):
|
||||
settings_version,
|
||||
|
||||
(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]])
|
||||
| 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]]),
|
||||
])
|
||||
return base64.b64encode(code, "+-".encode()).decode()
|
||||
|
||||
@@ -3012,6 +3090,11 @@ class Settings(object):
|
||||
args.bow_mode[p] = r(bow_mode)[(settings[11] & 0x70) >> 4]
|
||||
args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2]
|
||||
args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3]
|
||||
if len(settings) > 12:
|
||||
args.pseudoboots[p] = True if settings[12] & 0x80 else False
|
||||
args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 6]
|
||||
args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x14) >> 4]
|
||||
args.key_logic_algorithm[p] = r(key_logic_algo)[settings[12] & 0x07]
|
||||
|
||||
|
||||
class KeyRuleType(FastEnum):
|
||||
|
||||
@@ -122,9 +122,14 @@ def parse_cli(argv, no_defaults=False):
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, player_num + 1):
|
||||
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
if playerargs.filename:
|
||||
playersettings = apply_settings_file({}, playerargs.filename)
|
||||
for k, v in playersettings.items():
|
||||
setattr(playerargs, k, v)
|
||||
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
|
||||
'flute_mode', 'bow_mode', 'take_any',
|
||||
'flute_mode', 'bow_mode', 'take_any', 'boots_hint',
|
||||
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'usestartinventory', 'bombbag', 'overworld_map', 'restrict_boss_items',
|
||||
@@ -135,7 +140,8 @@ def parse_cli(argv, no_defaults=False):
|
||||
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor',
|
||||
'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']:
|
||||
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode',
|
||||
'trap_door_mode', 'key_logic_algorithm']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
@@ -161,6 +167,7 @@ def parse_settings():
|
||||
"retro": False,
|
||||
"bombbag": False,
|
||||
"mode": "open",
|
||||
"boots_hint": False,
|
||||
"logic": "noglitches",
|
||||
"goal": "ganon",
|
||||
"crystals_gt": "7",
|
||||
@@ -198,7 +205,7 @@ def parse_settings():
|
||||
'keydropshuffle': False,
|
||||
'dropshuffle': 'none',
|
||||
'pottery': 'none',
|
||||
'colorizepots': False,
|
||||
'colorizepots': True,
|
||||
'shufflepots': False,
|
||||
'mapshuffle': False,
|
||||
'compassshuffle': False,
|
||||
@@ -208,6 +215,8 @@ def parse_settings():
|
||||
'door_shuffle': 'vanilla',
|
||||
'intensity': 2,
|
||||
'door_type_mode': 'original',
|
||||
'trap_door_mode': 'optional',
|
||||
'key_logic_algorithm': 'default',
|
||||
'decoupledoors': False,
|
||||
'experimental': False,
|
||||
'dungeon_counters': 'default',
|
||||
|
||||
+299
-191
@@ -6,7 +6,7 @@ from enum import unique, Flag
|
||||
from typing import DefaultDict, Dict, List
|
||||
from itertools import chain
|
||||
|
||||
from BaseClasses import RegionType, Region, Door, DoorType, Direction, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
|
||||
from BaseClasses import RegionType, Region, Door, DoorType, Sector, CrystalBarrier, DungeonInfo, dungeon_keys
|
||||
from BaseClasses import PotFlags, LocationType, Direction
|
||||
from Doors import reset_portals
|
||||
from Dungeons import dungeon_regions, region_starts, standard_starts, split_region_starts
|
||||
@@ -15,13 +15,12 @@ from Items import ItemFactory
|
||||
from RoomData import DoorKind, PairedDoor, reset_rooms
|
||||
from source.dungeon.DungeonStitcher import GenerationException, generate_dungeon
|
||||
from source.dungeon.DungeonStitcher import ExplorationState as ExplorationState2
|
||||
# from DungeonGenerator import generate_dungeon
|
||||
from DungeonGenerator import ExplorationState, convert_regions, pre_validate, determine_required_paths, drop_entrances
|
||||
from DungeonGenerator import ExplorationState, convert_regions, determine_required_paths, drop_entrances
|
||||
from DungeonGenerator import create_dungeon_builders, split_dungeon_builder, simple_dungeon_builder, default_dungeon_entrances
|
||||
from DungeonGenerator import dungeon_portals, dungeon_drops, connect_doors, count_reserved_locations
|
||||
from DungeonGenerator import valid_region_to_explore
|
||||
from KeyDoorShuffle import analyze_dungeon, build_key_layout, validate_key_layout, determine_prize_lock
|
||||
from KeyDoorShuffle import validate_bk_layout, check_bk_special
|
||||
from KeyDoorShuffle import validate_bk_layout
|
||||
from Utils import ncr, kth_combination
|
||||
|
||||
|
||||
@@ -129,7 +128,7 @@ def link_doors_prep(world, player):
|
||||
vanilla_key_logic(world, player)
|
||||
|
||||
|
||||
def link_doors_main(world, player):
|
||||
def create_dungeon_pool(world, player):
|
||||
pool = None
|
||||
if world.doorShuffle[player] == 'basic':
|
||||
pool = [([name], regions) for name, regions in dungeon_regions.items()]
|
||||
@@ -143,6 +142,11 @@ def link_doors_main(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
|
||||
|
||||
|
||||
def link_doors_main(world, player):
|
||||
pool = create_dungeon_pool(world, player)
|
||||
if pool:
|
||||
main_dungeon_pool(pool, world, player)
|
||||
if world.doorShuffle[player] != 'vanilla':
|
||||
@@ -226,21 +230,37 @@ def vanilla_key_logic(world, player):
|
||||
|
||||
add_inaccessible_doors(world, player)
|
||||
entrances_map, potentials, connections = determine_entrance_list(world, player)
|
||||
for builder in builders:
|
||||
enabled_entrances = world.enabled_entrances[player] = {}
|
||||
builder_queue = deque(builders)
|
||||
last_key, loops = None, 0
|
||||
while len(builder_queue) > 0:
|
||||
builder = builder_queue.popleft()
|
||||
origin_list = entrances_map[builder.name]
|
||||
start_regions = convert_regions(origin_list, world, player)
|
||||
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
||||
key_layout = build_key_layout(builder, start_regions, doors, world, player)
|
||||
valid = validate_key_layout(key_layout, world, player)
|
||||
if not valid:
|
||||
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
|
||||
builder.key_door_proposal = doors
|
||||
if player not in world.key_logic.keys():
|
||||
world.key_logic[player] = {}
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
world.key_layout[player][builder.name] = key_layout
|
||||
log_key_logic(builder.name, key_layout.key_logic)
|
||||
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, builder.name)
|
||||
if len(origin_list) <= 0:
|
||||
if last_key == builder.name or loops > 1000:
|
||||
origin_name = (world.get_region(origin_list[0], player).entrances[0].parent_region.name
|
||||
if len(origin_list) > 0 else 'no origin')
|
||||
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
|
||||
builder_queue.append(builder)
|
||||
last_key = builder.name
|
||||
loops += 1
|
||||
else:
|
||||
find_new_entrances(builder.master_sector, entrances_map, connections, potentials,
|
||||
enabled_entrances, world, player)
|
||||
start_regions = convert_regions(origin_list, world, player)
|
||||
doors = convert_key_doors(default_small_key_doors[builder.name], world, player)
|
||||
key_layout = build_key_layout(builder, start_regions, doors, {}, world, player)
|
||||
valid = validate_key_layout(key_layout, world, player)
|
||||
if not valid:
|
||||
logging.getLogger('').info('Vanilla key layout not valid %s', builder.name)
|
||||
builder.key_door_proposal = doors
|
||||
if player not in world.key_logic.keys():
|
||||
world.key_logic[player] = {}
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
world.key_layout[player][builder.name] = key_layout
|
||||
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]:
|
||||
# validate_vanilla_key_logic(world, player)
|
||||
|
||||
@@ -477,14 +497,14 @@ def choose_portals(world, player):
|
||||
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():
|
||||
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, need_passage=True,
|
||||
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
|
||||
bk_shuffle=bk_shuffle, standard=std_flag, rupee_bow=rupee_bow_flag)
|
||||
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
|
||||
portal.destination = True
|
||||
clean_up_portal_assignment(portal_assignment, dungeon, portal, master_door_list, outstanding_portals)
|
||||
dead_end_choices = info.total - 1 - len(portal_assignment[dungeon])
|
||||
for i in range(0, dead_end_choices):
|
||||
candidates = find_portal_candidates(master_door_list, dungeon, custom, allowed, dead_end_allowed=True,
|
||||
bk_shuffle=bk_shuffle, rupee_bow=rupee_bow_flag)
|
||||
bk_shuffle=bk_shuffle, standard=std_flag, 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]
|
||||
choice, portal = assign_portal(candidates, possible_portals, custom, world, player)
|
||||
if choice.deadEnd:
|
||||
@@ -542,7 +562,9 @@ def customizer_portals(master_door_list, world, player):
|
||||
custom_doors = world.customizer.get_doors()[player]
|
||||
if custom_doors and 'lobbies' in custom_doors:
|
||||
for portal, assigned_door in custom_doors['lobbies'].items():
|
||||
door = next(x for x in master_door_list if x.name == assigned_door)
|
||||
door = next((x for x in master_door_list if x.name == assigned_door), None)
|
||||
if door is None:
|
||||
raise Exception(f'{assigned_door} not found. Check for typos')
|
||||
custom_portals[portal] = door
|
||||
assigned_doors.add(door)
|
||||
if custom_doors and 'doors' in custom_doors:
|
||||
@@ -552,9 +574,27 @@ def customizer_portals(master_door_list, world, player):
|
||||
if isinstance(dest, str):
|
||||
door = world.get_door(dest, player)
|
||||
assigned_doors.add(door)
|
||||
else:
|
||||
elif 'dest' in dest:
|
||||
door = world.get_door(dest['dest'], player)
|
||||
assigned_doors.add(door)
|
||||
# restricts connected doors to the customized portals
|
||||
if assigned_doors:
|
||||
pool = create_dungeon_pool(world, player)
|
||||
if pool:
|
||||
pool_map = {}
|
||||
for pool, region_list in pool:
|
||||
sector_pool = convert_to_sectors(region_list, world, player)
|
||||
merge_sectors(sector_pool, world, player)
|
||||
for p in pool:
|
||||
pool_map[p] = sector_pool
|
||||
for portal, assigned_door in custom_portals.items():
|
||||
portal_region = world.get_door(assigned_door, player).entrance.parent_region
|
||||
portal_dungeon = world.get_region(f'{portal} Portal', player).dungeon.name
|
||||
sector_pool = pool_map[portal_dungeon]
|
||||
sector = next((s for s in sector_pool if portal_region in s.regions), None)
|
||||
for door in sector.outstanding_doors:
|
||||
if door.portalAble:
|
||||
door.dungeonLink = portal_dungeon
|
||||
return custom_portals, assigned_doors
|
||||
|
||||
|
||||
@@ -798,15 +838,18 @@ def main_dungeon_pool(dungeon_pool, world, player):
|
||||
hc = world.get_dungeon('Hyrule Castle', player)
|
||||
hc_compass = ItemFactory('Compass (Escape)', player)
|
||||
hc_compass.advancement = world.restrict_boss_items[player] != 'none'
|
||||
hc.dungeon_items.append(hc_compass)
|
||||
if hc.dungeon_items.count(hc_compass) < 1:
|
||||
hc.dungeon_items.append(hc_compass)
|
||||
if 'Agahnims Tower' in pool:
|
||||
at = world.get_dungeon('Agahnims Tower', player)
|
||||
at_compass = ItemFactory('Compass (Agahnims Tower)', player)
|
||||
at_compass.advancement = world.restrict_boss_items[player] != 'none'
|
||||
at.dungeon_items.append(at_compass)
|
||||
if at.dungeon_items.count(at_compass) < 1:
|
||||
at.dungeon_items.append(at_compass)
|
||||
at_map = ItemFactory('Map (Agahnims Tower)', player)
|
||||
at_map.advancement = world.restrict_boss_items[player] != 'none'
|
||||
at.dungeon_items.append(at_map)
|
||||
if at.dungeon_items.count(at_map) < 1:
|
||||
at.dungeon_items.append(at_map)
|
||||
sector_pool = convert_to_sectors(region_list, world, player)
|
||||
merge_sectors(sector_pool, world, player)
|
||||
# todo: which dungeon to create
|
||||
@@ -831,7 +874,8 @@ def main_dungeon_pool(dungeon_pool, world, player):
|
||||
for name in pool:
|
||||
builder = world.dungeon_layouts[player][name]
|
||||
region_set = builder.master_sector.region_set()
|
||||
builder.bk_required = builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
|
||||
builder.bk_required = (builder.bk_door_proposal or any(x in region_set for x in special_bk_regions)
|
||||
or len(world.key_logic[player][name].bk_chests) > 0)
|
||||
dungeon = world.get_dungeon(name, player)
|
||||
if not builder.bk_required or builder.bk_provided:
|
||||
dungeon.big_key = None
|
||||
@@ -1018,7 +1062,8 @@ def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_
|
||||
find_standard_origins(builder, recombinant_builders, origin_list)
|
||||
find_enabled_origins(builder.sectors, enabled_entrances, origin_list, entrances_map, name)
|
||||
split_dungeon = treat_split_as_whole_dungeon(split_dungeon, name, origin_list, world, player)
|
||||
if len(origin_list) <= 0 or not pre_validate(builder, origin_list, split_dungeon, world, player):
|
||||
# todo: figure out pre-validate, ensure all needed origins are enabled?
|
||||
if len(origin_list) <= 0: # or not pre_validate(builder, origin_list, split_dungeon, world, player):
|
||||
if last_key == builder.name or loops > 1000:
|
||||
origin_name = world.get_region(origin_list[0], player).entrances[0].parent_region.name if len(origin_list) > 0 else 'no origin'
|
||||
raise GenerationException(f'Infinite loop detected for "{builder.name}" located at {origin_name}')
|
||||
@@ -1068,14 +1113,14 @@ def determine_entrance_list(world, player):
|
||||
connections = {}
|
||||
for key, portal_list in dungeon_portals.items():
|
||||
entrance_map[key] = []
|
||||
r_names = {}
|
||||
r_names = []
|
||||
if key in dungeon_drops.keys():
|
||||
for drop in dungeon_drops[key]:
|
||||
r_names[drop] = None
|
||||
r_names.append((drop, None))
|
||||
for portal_name in portal_list:
|
||||
portal = world.get_portal(portal_name, player)
|
||||
r_names[portal.door.entrance.parent_region.name] = portal
|
||||
for region_name, portal in r_names.items():
|
||||
r_names.append((portal.door.entrance.parent_region.name, portal))
|
||||
for region_name, portal in r_names:
|
||||
if portal:
|
||||
region = world.get_region(portal.name + ' Portal', player)
|
||||
else:
|
||||
@@ -1236,10 +1281,13 @@ def cross_dungeon(world, player):
|
||||
if world.restrict_boss_items[player] != 'none':
|
||||
hc_compass.advancement = at_compass.advancement = at_map.advancement = True
|
||||
hc = world.get_dungeon('Hyrule Castle', player)
|
||||
hc.dungeon_items.append(hc_compass)
|
||||
if hc.dungeon_items.count(hc_compass) < 1:
|
||||
hc.dungeon_items.append(hc_compass)
|
||||
at = world.get_dungeon('Agahnims Tower', player)
|
||||
at.dungeon_items.append(at_compass)
|
||||
at.dungeon_items.append(at_map)
|
||||
if at.dungeon_items.count(at_compass) < 1:
|
||||
at.dungeon_items.append(at_compass)
|
||||
if at.dungeon_items.count(at_map) < 1:
|
||||
at.dungeon_items.append(at_map)
|
||||
|
||||
setup_custom_door_types(world, player)
|
||||
assign_cross_keys(dungeon_builders, world, player)
|
||||
@@ -1695,7 +1743,6 @@ def setup_custom_door_types(world, player):
|
||||
custom_doors = custom_doors[player]
|
||||
if 'doors' not in custom_doors:
|
||||
return
|
||||
# todo: dash/bomb door pool specific
|
||||
customizeable_types = ['Key Door', 'Dash Door', 'Bomb Door', 'Trap Door', 'Big Key Door']
|
||||
world.custom_door_types[player] = type_map = {x: defaultdict(list) for x in customizeable_types}
|
||||
for door, dest in custom_doors['doors'].items():
|
||||
@@ -1707,8 +1754,8 @@ def setup_custom_door_types(world, player):
|
||||
if d.type == DoorType.SpiralStairs:
|
||||
type_map[door_kind][dungeon.name].append(d)
|
||||
else:
|
||||
# check if the
|
||||
if d.dest.type in [DoorType.Interior, DoorType.Normal]:
|
||||
# check if the dest is paired
|
||||
if d.dest and d.dest.type in [DoorType.Interior, DoorType.Normal] and door_kind != 'Trap Door':
|
||||
type_map[door_kind][dungeon.name].append((d, d.dest))
|
||||
else:
|
||||
type_map[door_kind][dungeon.name].append(d)
|
||||
@@ -1737,12 +1784,12 @@ class DoorTypePool:
|
||||
self.tricky += counts[6]
|
||||
|
||||
def chaos_shuffle(self, counts):
|
||||
weights = [1, 2, 4, 3, 2, 1]
|
||||
weights = [1, 2, 4, 3, 2]
|
||||
return [random.choices(self.get_choices(counts[i]), weights=weights)[0] for i, c in enumerate(counts)]
|
||||
|
||||
@staticmethod
|
||||
def get_choices(number):
|
||||
return [max(number-i, 0) for i in range(-1, 5)]
|
||||
return [max(number+i, 0) for i in range(-1, 4)]
|
||||
|
||||
|
||||
class BuilderDoorCandidates:
|
||||
@@ -1756,88 +1803,103 @@ class BuilderDoorCandidates:
|
||||
def shuffle_door_types(door_type_pools, paths, world, player):
|
||||
start_regions_map = {}
|
||||
for name, builder in world.dungeon_layouts[player].items():
|
||||
start_regions = convert_regions(builder.path_entrances, world, player)
|
||||
start_regions = convert_regions(find_possible_entrances(world, player, builder), world, player)
|
||||
start_regions_map[name] = start_regions
|
||||
builder.candidates = BuilderDoorCandidates()
|
||||
|
||||
all_custom = defaultdict(list)
|
||||
if player in world.custom_door_types:
|
||||
for custom_dict in world.custom_door_types[player].values():
|
||||
for dungeon, doors in custom_dict.items():
|
||||
all_custom[dungeon].extend(doors)
|
||||
|
||||
world.paired_doors[player].clear()
|
||||
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player)
|
||||
used_doors = shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player)
|
||||
# big keys
|
||||
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
|
||||
used_doors = shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
|
||||
# small keys
|
||||
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player)
|
||||
used_doors = shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
|
||||
# bombable / dashable
|
||||
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player)
|
||||
used_doors = shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player)
|
||||
# handle paired list
|
||||
|
||||
|
||||
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, world, player):
|
||||
def shuffle_trap_doors(door_type_pools, paths, start_regions_map, all_custom, world, player):
|
||||
used_doors = set()
|
||||
for pool, door_type_pool in door_type_pools:
|
||||
ttl = 0
|
||||
suggestion_map, trap_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.traps
|
||||
if player in world.custom_door_types:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door']
|
||||
else:
|
||||
custom_trap_doors = defaultdict(list)
|
||||
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
find_trappable_candidates(builder, world, player)
|
||||
if custom_trap_doors[dungeon]:
|
||||
builder.candidates.trap = filter_key_door_pool(builder.candidates.trap, custom_trap_doors[dungeon])
|
||||
remaining -= len(custom_trap_doors[dungeon])
|
||||
ttl += len(builder.candidates.trap)
|
||||
if ttl == 0:
|
||||
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))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
|
||||
start_regions_map[dungeon], paths, world, player,
|
||||
drop=True)
|
||||
trap_map[dungeon] = valid_traps
|
||||
if trap_number < suggestion_map[dungeon]:
|
||||
flex_map[dungeon] = 0
|
||||
remaining += suggestion_map[dungeon] - trap_number
|
||||
suggestion_map[dungeon] = trap_number
|
||||
builder_order = [x for x in pool if flex_map[x] > 0]
|
||||
random.shuffle(builder_order)
|
||||
queue = deque(builder_order)
|
||||
while len(queue) > 0 and remaining > 0:
|
||||
dungeon = queue.popleft()
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
increased = suggestion_map[dungeon] + 1
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
|
||||
paths, world, player)
|
||||
if valid_traps:
|
||||
if world.trap_door_mode[player] != 'oneway':
|
||||
ttl = 0
|
||||
suggestion_map, trap_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.traps
|
||||
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door']
|
||||
else:
|
||||
custom_trap_doors = defaultdict(list)
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if 'Mire Warping Pool' in builder.master_sector.region_set():
|
||||
custom_trap_doors[dungeon].append(world.get_door('Mire Warping Pool ES', player))
|
||||
world.custom_door_types[player]['Trap Door'] = custom_trap_doors
|
||||
find_trappable_candidates(builder, world, player)
|
||||
if all_custom[dungeon]:
|
||||
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:
|
||||
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))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
flex_map[dungeon] = (proportion - suggested) if suggested < proportion else 0
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, suggestion_map[dungeon],
|
||||
start_regions_map[dungeon], paths, world, player,
|
||||
drop=True)
|
||||
trap_map[dungeon] = valid_traps
|
||||
remaining -= 1
|
||||
suggestion_map[dungeon] = increased
|
||||
flex_map[dungeon] -= 1
|
||||
if flex_map[dungeon] > 0:
|
||||
queue.append(dungeon)
|
||||
# time to re-assign
|
||||
if trap_number < suggestion_map[dungeon]:
|
||||
flex_map[dungeon] = 0
|
||||
remaining += suggestion_map[dungeon] - trap_number
|
||||
suggestion_map[dungeon] = trap_number
|
||||
builder_order = [x for x in pool if flex_map[x] > 0]
|
||||
random.shuffle(builder_order)
|
||||
queue = deque(builder_order)
|
||||
while len(queue) > 0 and remaining > 0:
|
||||
dungeon = queue.popleft()
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
increased = suggestion_map[dungeon] + 1
|
||||
valid_traps, trap_number = find_valid_trap_combination(builder, increased, start_regions_map[dungeon],
|
||||
paths, world, player)
|
||||
if valid_traps:
|
||||
trap_map[dungeon] = valid_traps
|
||||
remaining -= 1
|
||||
suggestion_map[dungeon] = increased
|
||||
flex_map[dungeon] -= 1
|
||||
if flex_map[dungeon] > 0:
|
||||
queue.append(dungeon)
|
||||
# time to re-assign
|
||||
else:
|
||||
trap_map = {dungeon: [] for dungeon in pool}
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if 'Mire Warping Pool' in builder.master_sector.region_set():
|
||||
trap_map[dungeon].append(world.get_door('Mire Warping Pool ES', player))
|
||||
reassign_trap_doors(trap_map, world, player)
|
||||
for name, traps in trap_map.items():
|
||||
used_doors.update(traps)
|
||||
return used_doors
|
||||
|
||||
|
||||
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
|
||||
def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
|
||||
for pool, door_type_pool in door_type_pools:
|
||||
ttl = 0
|
||||
suggestion_map, bk_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.bigs
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
|
||||
custom_bk_doors = world.custom_door_types[player]['Big Key Door']
|
||||
else:
|
||||
custom_bk_doors = defaultdict(list)
|
||||
@@ -1845,16 +1907,17 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
find_big_key_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
|
||||
if custom_bk_doors[dungeon]:
|
||||
builder.candidates.big = filter_key_door_pool(builder.candidates.big, custom_bk_doors[dungeon])
|
||||
if all_custom[dungeon]:
|
||||
builder.candidates.big = filter_key_door_pool(builder.candidates.big, all_custom[dungeon])
|
||||
remaining -= len(custom_bk_doors[dungeon])
|
||||
ttl += len(builder.candidates.big)
|
||||
if ttl == 0:
|
||||
continue
|
||||
remaining = max(0, remaining)
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
proportion = len(builder.candidates.big)
|
||||
calc = int(round(proportion * door_type_pool.bigs/ttl))
|
||||
calc = int(round(proportion * remaining/ttl))
|
||||
suggested = min(proportion, calc)
|
||||
remaining -= suggested
|
||||
suggestion_map[dungeon] = suggested
|
||||
@@ -1891,48 +1954,56 @@ def shuffle_big_key_doors(door_type_pools, used_doors, start_regions_map, world,
|
||||
return used_doors
|
||||
|
||||
|
||||
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, world, player):
|
||||
def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
|
||||
max_computation = 11 # this is around 6 billion worse case factorial don't want to exceed this much
|
||||
for pool, door_type_pool in door_type_pools:
|
||||
ttl = 0
|
||||
suggestion_map, small_map, flex_map = {}, {}, {}
|
||||
remaining = door_type_pool.smalls
|
||||
total_keys = remaining
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
|
||||
custom_key_doors = world.custom_door_types[player]['Key Door']
|
||||
else:
|
||||
custom_key_doors = defaultdict(list)
|
||||
|
||||
total_adjustable = len(pool) > 1
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if not total_adjustable:
|
||||
builder.total_keys = total_keys
|
||||
find_small_key_door_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
|
||||
if custom_key_doors[dungeon]:
|
||||
builder.candidates.small = filter_key_door_pool(builder.candidates.small, custom_key_doors[dungeon])
|
||||
remaining -= len(custom_key_doors[dungeon])
|
||||
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt)
|
||||
custom_doors = 0
|
||||
if all_custom[dungeon]:
|
||||
builder.candidates.small = filter_key_door_pool(builder.candidates.small, all_custom[dungeon])
|
||||
custom_doors = len(custom_key_doors[dungeon])
|
||||
remaining -= custom_doors
|
||||
builder.key_doors_num = max(0, len(builder.candidates.small) - builder.key_drop_cnt) + custom_doors
|
||||
total_keys -= builder.key_drop_cnt
|
||||
ttl += builder.key_doors_num
|
||||
remaining = max(0, remaining)
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
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)
|
||||
limit = min(max_keys, cand_len)
|
||||
limit = min(max_keys, cand_len, max_computation)
|
||||
suggested = min(calculated, limit)
|
||||
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
|
||||
while combo_size > 500000 and suggested > 0:
|
||||
suggested -= 1
|
||||
combo_size = ncr(len(builder.candidates.small), suggested + builder.key_drop_cnt)
|
||||
suggestion_map[dungeon] = builder.key_doors_num = suggested + builder.key_drop_cnt
|
||||
remaining -= suggested + builder.key_drop_cnt
|
||||
key_door_num = min(suggested + builder.key_drop_cnt, max_computation)
|
||||
combo_size = ncr(len(builder.candidates.small), key_door_num)
|
||||
suggestion_map[dungeon] = builder.key_doors_num = key_door_num
|
||||
remaining -= key_door_num + builder.key_drop_cnt
|
||||
builder.combo_size = combo_size
|
||||
flex_map[dungeon] = (limit - suggested) if suggested < limit else 0
|
||||
flex_map[dungeon] = (limit - key_door_num) if key_door_num < limit else 0
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
if total_adjustable:
|
||||
builder.total_keys = max(suggestion_map[dungeon], builder.key_drop_cnt)
|
||||
valid_doors, small_number = find_valid_combination(builder, suggestion_map[dungeon],
|
||||
start_regions_map[dungeon], world, player)
|
||||
small_map[dungeon] = valid_doors
|
||||
actual_chest_keys = small_number - builder.key_drop_cnt
|
||||
if actual_chest_keys < suggestion_map[dungeon]:
|
||||
if total_adjustable:
|
||||
builder.total_keys = actual_chest_keys + builder.key_drop_cnt
|
||||
flex_map[dungeon] = 0
|
||||
remaining += suggestion_map[dungeon] - actual_chest_keys
|
||||
suggestion_map[dungeon] = small_number
|
||||
@@ -1943,6 +2014,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
builder = queue.popleft()
|
||||
dungeon = builder.name
|
||||
increased = suggestion_map[dungeon] + 1
|
||||
if increased > max_computation:
|
||||
continue
|
||||
builder.key_doors_num = increased
|
||||
valid_doors, small_number = find_valid_combination(builder, increased, start_regions_map[dungeon],
|
||||
world, player)
|
||||
@@ -1951,6 +2024,8 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
remaining -= 1
|
||||
suggestion_map[dungeon] = increased
|
||||
flex_map[dungeon] -= 1
|
||||
if total_adjustable:
|
||||
builder.total_keys = max(increased, builder.key_drop_cnt)
|
||||
if flex_map[dungeon] > 0:
|
||||
builder.combo_size = ncr(len(builder.candidates.small), builder.key_doors_num)
|
||||
queue.append(builder)
|
||||
@@ -1976,14 +2051,14 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
return used_doors
|
||||
|
||||
|
||||
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, world, player):
|
||||
def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, all_custom, world, player):
|
||||
for pool, door_type_pool in door_type_pools:
|
||||
ttl = 0
|
||||
suggestion_map, bd_map = {}, {}
|
||||
remaining_bomb = door_type_pool.bombable
|
||||
remaining_dash = door_type_pool.dashable
|
||||
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
|
||||
custom_bomb_doors = world.custom_door_types[player]['Bomb Door']
|
||||
custom_dash_doors = world.custom_door_types[player]['Dash Door']
|
||||
else:
|
||||
@@ -1993,11 +2068,9 @@ def shuffle_bomb_dash_doors(door_type_pools, used_doors, start_regions_map, worl
|
||||
for dungeon in pool:
|
||||
builder = world.dungeon_layouts[player][dungeon]
|
||||
find_bd_candidates(builder, start_regions_map[dungeon], used_doors, world, player)
|
||||
if custom_bomb_doors[dungeon]:
|
||||
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_bomb_doors[dungeon])
|
||||
if all_custom[dungeon]:
|
||||
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, all_custom[dungeon])
|
||||
remaining_bomb -= len(custom_bomb_doors[dungeon])
|
||||
if custom_dash_doors[dungeon]:
|
||||
builder.candidates.bomb_dash = filter_key_door_pool(builder.candidates.bomb_dash, custom_dash_doors[dungeon])
|
||||
remaining_dash -= len(custom_dash_doors[dungeon])
|
||||
ttl += len(builder.candidates.bomb_dash)
|
||||
if ttl == 0:
|
||||
@@ -2094,7 +2167,7 @@ def find_trappable_candidates(builder, world, player):
|
||||
if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]]
|
||||
for d in filtered_doors:
|
||||
# I only support the first 3 due to the trapFlag right now
|
||||
if 0 <= d.doorListPos < 3 and not d.entranceFlag:
|
||||
if 0 <= d.doorListPos < 3 and not d.entranceFlag and d.name != 'Skull Small Hall WS':
|
||||
room = world.get_room(d.roomIndex, player)
|
||||
kind = room.kind(d)
|
||||
if d.type == DoorType.Interior:
|
||||
@@ -2115,20 +2188,21 @@ def find_trappable_candidates(builder, world, player):
|
||||
for ext in world.get_region(r, player).exits:
|
||||
if ext.door:
|
||||
d = ext.door
|
||||
if d.blocked and d.trapFlag != 0 and 'Boss' not in d.name and 'Agahnim' not in d.name:
|
||||
if d.blocked and d.trapFlag != 0 and exclude_boss_traps(d):
|
||||
builder.candidates.trap.append(d)
|
||||
|
||||
|
||||
def find_valid_trap_combination(builder, suggested, start_regions, paths, world, player, drop=True):
|
||||
trap_door_pool = builder.candidates.trap
|
||||
trap_doors_needed = suggested
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Trap Door' in world.custom_door_types[player]:
|
||||
custom_trap_doors = world.custom_door_types[player]['Trap Door'][builder.name]
|
||||
else:
|
||||
custom_trap_doors = []
|
||||
if custom_trap_doors:
|
||||
trap_door_pool = filter_key_door_pool(trap_door_pool, custom_trap_doors)
|
||||
trap_doors_needed -= len(custom_trap_doors)
|
||||
trap_doors_needed = max(0, trap_doors_needed)
|
||||
if len(trap_door_pool) < trap_doors_needed:
|
||||
if not drop:
|
||||
return None, 0
|
||||
@@ -2139,7 +2213,7 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
|
||||
proposal = kth_combination(sample_list[itr], trap_door_pool, trap_doors_needed)
|
||||
proposal.extend(custom_trap_doors)
|
||||
|
||||
start_regions = filter_start_regions(builder, start_regions, world, player)
|
||||
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
|
||||
while not validate_trap_layout(proposal, builder, start_regions, paths, world, player):
|
||||
itr += 1
|
||||
if itr >= len(sample_list):
|
||||
@@ -2160,14 +2234,32 @@ def find_valid_trap_combination(builder, suggested, start_regions, paths, world,
|
||||
# eliminate start region if portal marked as destination
|
||||
def filter_start_regions(builder, start_regions, world, player):
|
||||
std_flag = world.mode[player] == 'standard' and builder.name == 'Hyrule Castle'
|
||||
excluded = {}
|
||||
excluded = {} # todo: drop lobbies, might be better to white list instead (two entrances per region)
|
||||
event_doors = {}
|
||||
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
|
||||
# make sure that a drop is not accessible for this "destination"
|
||||
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 not drop_region:
|
||||
excluded[region] = None
|
||||
if portal and not portal.destination:
|
||||
portal_entrance_region = portal.door.entrance.parent_region.name
|
||||
if portal_entrance_region not in builder.path_entrances:
|
||||
excluded[region] = None
|
||||
if std_flag and (not portal or portal.find_portal_entrance().parent_region.name != 'Hyrule Castle Courtyard'):
|
||||
excluded[region] = None
|
||||
return [x for x in start_regions if x not in excluded.keys()]
|
||||
if portal is None:
|
||||
entrance = next((x for x in region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||
or x.parent_region.name == 'Sewer Drop'), None)
|
||||
event_doors[entrance] = None
|
||||
else:
|
||||
event_doors[portal.find_portal_entrance()] = None
|
||||
|
||||
return [x for x in start_regions if x not in excluded.keys()], event_doors
|
||||
|
||||
|
||||
def validate_trap_layout(proposal, builder, start_regions, paths, world, player):
|
||||
@@ -2193,6 +2285,7 @@ def find_bk_special_location(builder, world, player):
|
||||
return loc
|
||||
return None
|
||||
|
||||
|
||||
def check_required_paths_with_traps(paths, proposal, dungeon_name, start_regions, world, player):
|
||||
cached_initial_state = None
|
||||
if len(paths[dungeon_name]) > 0:
|
||||
@@ -2238,7 +2331,7 @@ def reassign_trap_doors(trap_map, world, player):
|
||||
logger = logging.getLogger('')
|
||||
for name, traps in trap_map.items():
|
||||
builder = world.dungeon_layouts[player][name]
|
||||
queue = deque(find_current_trap_doors(builder))
|
||||
queue = deque(find_current_trap_doors(builder, world, player))
|
||||
while len(queue) > 0:
|
||||
d = queue.pop()
|
||||
if d.type is DoorType.Interior and d not in traps:
|
||||
@@ -2257,16 +2350,21 @@ def reassign_trap_doors(trap_map, world, player):
|
||||
d.blocked = False
|
||||
for d in traps:
|
||||
change_door_to_trap(d, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Trap Door', player)
|
||||
logger.debug('Trap Door: %s', d.name)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Trap Door', player)
|
||||
logger.debug(f'Trap Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def find_current_trap_doors(builder):
|
||||
def exclude_boss_traps(d):
|
||||
return ' Boss ' not in d.name and ' Agahnim ' not in d.name and d.name not in ['Skull Spike Corner SW']
|
||||
|
||||
|
||||
def find_current_trap_doors(builder, world, player):
|
||||
checker = exclude_boss_traps if world.trap_door_mode[player] in ['vanilla', 'optional'] else (lambda x: True)
|
||||
current_doors = []
|
||||
for region in builder.master_sector.regions:
|
||||
for ext in region.exits:
|
||||
d = ext.door
|
||||
if d and d.blocked and d.trapFlag != 0:
|
||||
if d and d.blocked and d.trapFlag != 0 and checker(d):
|
||||
current_doors.append(d)
|
||||
return current_doors
|
||||
|
||||
@@ -2276,7 +2374,9 @@ def change_door_to_trap(d, world, player):
|
||||
if d.type is DoorType.Interior:
|
||||
kind = room.kind(d)
|
||||
new_kind = None
|
||||
if kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
|
||||
if kind == DoorKind.Trap:
|
||||
new_kind = DoorKind.Trap
|
||||
elif kind == DoorKind.TrapTriggerable and d.direction in [Direction.South, Direction.East]:
|
||||
new_kind = DoorKind.Trap
|
||||
elif kind == DoorKind.Trap2 and d.direction in [Direction.North, Direction.West]:
|
||||
new_kind = DoorKind.Trap
|
||||
@@ -2356,21 +2456,16 @@ def find_big_key_door_candidates(region, checked, used, world, player):
|
||||
if valid and d.dest not in candidates: # interior doors are not separable yet
|
||||
candidates.append(d.dest)
|
||||
elif d.type == DoorType.Normal:
|
||||
if decoupled:
|
||||
valid = kind in okay_normals
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
if valid and not decoupled:
|
||||
d2 = d.dest
|
||||
if d2 not in candidates and d2 not in used:
|
||||
if d2.type == DoorType.Normal:
|
||||
room_b = world.get_room(d2.roomIndex, player)
|
||||
pos_b, kind_b = room_b.doorList[d2.doorListPos]
|
||||
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
if valid and 0 <= d2.doorListPos < 4:
|
||||
candidates.append(d2)
|
||||
else:
|
||||
valid = True
|
||||
if valid and d not in candidates:
|
||||
candidates.append(d)
|
||||
connected = ext.connected_region
|
||||
@@ -2384,13 +2479,14 @@ def find_big_key_door_candidates(region, checked, used, world, player):
|
||||
def find_valid_bk_combination(builder, suggested, start_regions, world, player, drop=True):
|
||||
bk_door_pool = builder.candidates.big
|
||||
bk_doors_needed = suggested
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Big Key Door' in world.custom_door_types[player]:
|
||||
custom_bk_doors = world.custom_door_types[player]['Big Key Door'][builder.name]
|
||||
else:
|
||||
custom_bk_doors = []
|
||||
if custom_bk_doors:
|
||||
bk_door_pool = filter_key_door_pool(bk_door_pool, custom_bk_doors)
|
||||
bk_doors_needed -= len(custom_bk_doors)
|
||||
bk_doors_needed = max(0, bk_doors_needed)
|
||||
if len(bk_door_pool) < bk_doors_needed:
|
||||
if not drop:
|
||||
return None, 0
|
||||
@@ -2401,7 +2497,7 @@ def find_valid_bk_combination(builder, suggested, start_regions, world, player,
|
||||
proposal = kth_combination(sample_list[itr], bk_door_pool, bk_doors_needed)
|
||||
proposal.extend(custom_bk_doors)
|
||||
|
||||
start_regions = filter_start_regions(builder, start_regions, world, player)
|
||||
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
|
||||
while not validate_bk_layout(proposal, builder, start_regions, world, player):
|
||||
itr += 1
|
||||
if itr >= len(sample_list):
|
||||
@@ -2452,17 +2548,20 @@ def reassign_big_key_doors(bk_map, world, player):
|
||||
if d1.type is DoorType.Interior:
|
||||
change_door_to_big_key(d1, world, player)
|
||||
d2.bigKey = True # ensure flag is set
|
||||
if d2.smallKey:
|
||||
d2.smallKey = False
|
||||
else:
|
||||
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
|
||||
change_door_to_big_key(d1, world, player)
|
||||
change_door_to_big_key(d2, world, player)
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name}')
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
change_door_to_big_key(d, world, player)
|
||||
d.dest.bigKey = True # ensure flag is set
|
||||
if world.door_type_mode[player] != 'original':
|
||||
d.dest.bigKey = True # ensure flag is set when bk doors are double sided
|
||||
elif d.type is DoorType.SpiralStairs:
|
||||
pass # we don't have spiral stairs candidates yet that aren't already key doors
|
||||
elif d.type is DoorType.Normal:
|
||||
@@ -2473,12 +2572,14 @@ def reassign_big_key_doors(bk_map, world, player):
|
||||
if stateful_door(d.dest, dest_room.kind(d.dest)):
|
||||
change_door_to_big_key(d.dest, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d.name}')
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Big Key Door', player)
|
||||
logger.debug(f'Big Key Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def change_door_to_big_key(d, world, player):
|
||||
d.bigKey = True
|
||||
if d.smallKey:
|
||||
d.smallKey = False
|
||||
room = world.get_room(d.roomIndex, player)
|
||||
if room.doorList[d.doorListPos][1] != DoorKind.BigKey:
|
||||
verify_door_list_pos(d, room, world, player)
|
||||
@@ -2522,14 +2623,14 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
|
||||
logger = logging.getLogger('')
|
||||
key_door_pool = list(builder.candidates.small)
|
||||
key_doors_needed = target
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Key Door' in world.custom_door_types[player]:
|
||||
custom_key_doors = world.custom_door_types[player]['Key Door'][builder.name]
|
||||
else:
|
||||
custom_key_doors = []
|
||||
if custom_key_doors: # could validate that each custom item is in the candidates
|
||||
key_door_pool = filter_key_door_pool(key_door_pool, custom_key_doors)
|
||||
key_doors_needed -= len(custom_key_doors)
|
||||
|
||||
key_doors_needed = max(0, key_doors_needed)
|
||||
# find valid combination of candidates
|
||||
if len(key_door_pool) < key_doors_needed:
|
||||
if not drop_keys:
|
||||
@@ -2544,9 +2645,10 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
|
||||
sample_list = build_sample_list(combinations)
|
||||
proposal = kth_combination(sample_list[itr], key_door_pool, key_doors_needed)
|
||||
proposal.extend(custom_key_doors)
|
||||
start_regions = filter_start_regions(builder, start_regions, world, player)
|
||||
builder.key_doors_num = len(proposal)
|
||||
start_regions, event_starts = filter_start_regions(builder, start_regions, world, player)
|
||||
|
||||
key_layout = build_key_layout(builder, start_regions, proposal, world, player)
|
||||
key_layout = build_key_layout(builder, start_regions, proposal, event_starts, world, player)
|
||||
determine_prize_lock(key_layout, world, player)
|
||||
while not validate_key_layout(key_layout, world, player):
|
||||
itr += 1
|
||||
@@ -2572,12 +2674,11 @@ def find_valid_combination(builder, target, start_regions, world, player, drop_k
|
||||
# make changes
|
||||
if player not in world.key_logic.keys():
|
||||
world.key_logic[player] = {}
|
||||
builder.total_keys = builder.key_doors_num
|
||||
analyze_dungeon(key_layout, world, player)
|
||||
builder.key_door_proposal = proposal
|
||||
world.key_logic[player][builder.name] = key_layout.key_logic
|
||||
world.key_layout[player][builder.name] = key_layout
|
||||
return builder.key_door_proposal, key_doors_needed
|
||||
return builder.key_door_proposal, key_doors_needed + len(custom_key_doors)
|
||||
|
||||
|
||||
def find_bd_candidates(builder, start_regions, used, world, player):
|
||||
@@ -2618,25 +2719,21 @@ def find_bd_door_candidates(region, checked, used, world, player):
|
||||
room = world.get_room(d.roomIndex, player)
|
||||
position, kind = room.doorList[d.doorListPos]
|
||||
if d.type == DoorType.Interior:
|
||||
valid = kind in okay_interiors
|
||||
if valid and d.dest not in candidates: # interior doors are not separable yet
|
||||
# interior doors are not separable yet
|
||||
valid = kind in okay_interiors and d.dest not in used
|
||||
if valid and d.dest not in candidates:
|
||||
candidates.append(d.dest)
|
||||
elif d.type == DoorType.Normal:
|
||||
if decoupled:
|
||||
valid = kind in okay_normals
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
if valid and not decoupled:
|
||||
d2 = d.dest
|
||||
if d2 not in candidates and d2 not in used:
|
||||
if d2.type == DoorType.Normal:
|
||||
room_b = world.get_room(d2.roomIndex, player)
|
||||
pos_b, kind_b = room_b.doorList[d2.doorListPos]
|
||||
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
if valid and 0 <= d2.doorListPos < 4:
|
||||
candidates.append(d2)
|
||||
else:
|
||||
valid = True
|
||||
if valid and d not in candidates:
|
||||
candidates.append(d)
|
||||
connected = ext.connected_region
|
||||
@@ -2654,7 +2751,7 @@ def find_valid_bd_combination(builder, suggested, world, player):
|
||||
bd_door_pool = builder.candidates.bomb_dash
|
||||
bomb_doors_needed, dash_doors_needed = suggested
|
||||
ttl_needed = bomb_doors_needed + dash_doors_needed
|
||||
if player in world.custom_door_types:
|
||||
if player in world.custom_door_types and 'Bomb Door' in world.custom_door_types[player]:
|
||||
custom_bomb_doors = world.custom_door_types[player]['Bomb Door'][builder.name]
|
||||
custom_dash_doors = world.custom_door_types[player]['Dash Door'][builder.name]
|
||||
else:
|
||||
@@ -2667,8 +2764,15 @@ def find_valid_bd_combination(builder, suggested, world, player):
|
||||
bd_door_pool = filter_key_door_pool(bd_door_pool, custom_dash_doors)
|
||||
dash_doors_needed -= len(custom_dash_doors)
|
||||
while len(bd_door_pool) < bomb_doors_needed + dash_doors_needed:
|
||||
bomb_doors_needed = round(len(bd_door_pool) * bomb_doors_needed/ttl_needed)
|
||||
dash_doors_needed = round(len(bd_door_pool) * dash_doors_needed/ttl_needed)
|
||||
test = random.choice([True, False])
|
||||
if test:
|
||||
bomb_doors_needed -= 1
|
||||
if bomb_doors_needed < 0:
|
||||
bomb_doors_needed = 0
|
||||
else:
|
||||
dash_doors_needed -= 1
|
||||
if dash_doors_needed < 0:
|
||||
dash_doors_needed = 0
|
||||
bomb_proposal = random.sample(bd_door_pool, k=bomb_doors_needed)
|
||||
bomb_proposal.extend(custom_bomb_doors)
|
||||
dash_pool = [x for x in bd_door_pool if x not in bomb_proposal]
|
||||
@@ -2696,8 +2800,8 @@ def reassign_bd_doors(bd_map, world, player):
|
||||
elif d.type is DoorType.Normal and not_in_proposal(d):
|
||||
if not d.entranceFlag:
|
||||
world.get_room(d.roomIndex, player).change(d.doorListPos, DoorKind.Normal)
|
||||
do_bombable_dashable(flat_bomb_proposal, DoorKind.Bombable, world, player)
|
||||
do_bombable_dashable(flat_dash_proposal, DoorKind.Dashable, world, player)
|
||||
do_bombable_dashable(pair[0], DoorKind.Bombable, world, player)
|
||||
do_bombable_dashable(pair[1], DoorKind.Dashable, world, player)
|
||||
|
||||
|
||||
def do_bombable_dashable(proposal, kind, world, player):
|
||||
@@ -2723,7 +2827,7 @@ def do_bombable_dashable(proposal, kind, world, player):
|
||||
change_door_to_kind(d1, kind, world, player)
|
||||
change_door_to_kind(d2, kind, world, player)
|
||||
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', spoiler_type, player)
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
@@ -2737,7 +2841,7 @@ def do_bombable_dashable(proposal, kind, world, player):
|
||||
change_door_to_kind(d.dest, kind, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
spoiler_type = 'Bomb Door' if kind == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(d.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', spoiler_type, player)
|
||||
|
||||
|
||||
def find_current_bd_doors(builder, world):
|
||||
@@ -2852,27 +2956,23 @@ def find_key_door_candidates(region, checked, used, world, player):
|
||||
room = world.get_room(d.roomIndex, player)
|
||||
position, kind = room.doorList[d.doorListPos]
|
||||
if d.type == DoorType.Interior:
|
||||
valid = kind in okay_interiors
|
||||
if valid and d.dest not in candidates: # interior doors are not separable yet
|
||||
valid = kind in okay_interiors and d.dest not in used
|
||||
# interior doors are not separable yet
|
||||
if valid and d.dest not in candidates:
|
||||
candidates.append(d.dest)
|
||||
elif d.type == DoorType.SpiralStairs:
|
||||
valid = kind in [DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow]
|
||||
elif d.type == DoorType.Normal:
|
||||
if decoupled:
|
||||
valid = kind in okay_normals
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
if valid and not decoupled:
|
||||
d2 = d.dest
|
||||
if d2 not in candidates and d2 not in used:
|
||||
if d2.type == DoorType.Normal:
|
||||
room_b = world.get_room(d2.roomIndex, player)
|
||||
pos_b, kind_b = room_b.doorList[d2.doorListPos]
|
||||
valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
else:
|
||||
valid = kind in okay_normals
|
||||
valid &= kind_b in okay_normals and valid_key_door_pair(d, d2)
|
||||
if valid and 0 <= d2.doorListPos < 4:
|
||||
candidates.append(d2)
|
||||
else:
|
||||
valid = True
|
||||
if valid and d not in candidates:
|
||||
candidates.append(d)
|
||||
connected = ext.connected_region
|
||||
@@ -2944,8 +3044,8 @@ def reassign_key_doors(small_map, world, player):
|
||||
world.paired_doors[player].append(PairedDoor(d1.name, d2.name))
|
||||
change_door_to_small_key(d1, world, player)
|
||||
change_door_to_small_key(d2, world, player)
|
||||
world.spoiler.set_door_type(d1.name+' <-> '+d2.name, 'Key Door', player)
|
||||
logger.debug('Key Door: %s', d1.name+' <-> '+d2.name)
|
||||
world.spoiler.set_door_type(f'{d1.name} <-> {d2.name} ({d1.dungeon_name()})', 'Key Door', player)
|
||||
logger.debug(f'Key Door: {d1.name} <-> {d2.name} ({d1.dungeon_name()})')
|
||||
else:
|
||||
d = obj
|
||||
if d.type is DoorType.Interior:
|
||||
@@ -2961,8 +3061,8 @@ def reassign_key_doors(small_map, world, player):
|
||||
if stateful_door(d.dest, dest_room.kind(d.dest)):
|
||||
change_door_to_small_key(d.dest, world, player)
|
||||
add_pair(d, d.dest, world, player)
|
||||
world.spoiler.set_door_type(d.name, 'Key Door', player)
|
||||
logger.debug('Key Door: %s', d.name)
|
||||
world.spoiler.set_door_type(f'{d.name} ({d.dungeon_name()})', 'Key Door', player)
|
||||
logger.debug(f'Key Door: {d.name} ({d.dungeon_name()})')
|
||||
|
||||
|
||||
def change_door_to_small_key(d, world, player):
|
||||
@@ -3152,7 +3252,7 @@ def change_pair_type(door, new_type, world, player):
|
||||
room_b.change(door.dest.doorListPos, new_type)
|
||||
add_pair(door, door.dest, world, player)
|
||||
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'
|
||||
world.spoiler.set_door_type(door.name + ' <-> ' + door.dest.name, spoiler_type, player)
|
||||
world.spoiler.set_door_type(f'{door.name} <-> {door.dest.name} ({door.dungeon_name()})', spoiler_type, player)
|
||||
|
||||
|
||||
def remove_pair_type_if_present(door, world, player):
|
||||
@@ -3245,6 +3345,14 @@ def find_accessible_entrances(world, player, builder):
|
||||
return visited_entrances
|
||||
|
||||
|
||||
def find_possible_entrances(world, player, builder):
|
||||
entrances = [region.name for region in
|
||||
(portal.door.entrance.parent_region for portal in world.dungeon_portals[player])
|
||||
if region.dungeon.name == builder.name]
|
||||
entrances.extend(drop_entrances[builder.name])
|
||||
return entrances
|
||||
|
||||
|
||||
def valid_inaccessible_region(r):
|
||||
return r.type is not RegionType.Cave or (len(r.exits) > 0 and r.name not in ['Links House', 'Chris Houlihan Room'])
|
||||
|
||||
@@ -4216,7 +4324,7 @@ default_door_connections = [
|
||||
('Eastern Map Valley SW', 'Eastern Dark Square NW'),
|
||||
('Eastern Attic Start WS', 'Eastern False Switches ES'),
|
||||
('Eastern Cannonball Hell WS', 'Eastern Single Eyegore ES'),
|
||||
('Desert Compass NW', 'Desert Cannonball S'),
|
||||
('Desert Compass NE', 'Desert Cannonball S'),
|
||||
('Desert Beamos Hall NE', 'Desert Tiles 2 SE'),
|
||||
('PoD Middle Cage N', 'PoD Pit Room S'),
|
||||
('PoD Pit Room NW', 'PoD Arena Main SW'),
|
||||
@@ -4503,7 +4611,7 @@ door_type_counts = {
|
||||
'Swamp Palace': (6, 0, 0, 2, 0, 0, 0),
|
||||
'Palace of Darkness': (6, 1, 1, 3, 2, 0, 0),
|
||||
'Misery Mire': (6, 3, 5, 2, 0, 0, 0),
|
||||
'Skull Woods': (5, 0, 2, 2, 0, 1, 0),
|
||||
'Skull Woods': (5, 0, 1, 2, 0, 1, 0),
|
||||
'Ice Palace': (6, 1, 3, 0, 0, 0, 0),
|
||||
'Tower of Hera': (1, 1, 0, 0, 0, 0, 0),
|
||||
'Thieves Town': (3, 1, 2, 1, 1, 0, 0),
|
||||
|
||||
@@ -66,7 +66,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Hyrule Castle Back Hall Down Stairs', Sprl).dir(Dn, 0x01, 0, HTL).ss(A, 0x2a, 0x00),
|
||||
create_door(player, 'Hyrule Castle Throne Room Tapestry', Lgcl),
|
||||
create_door(player, 'Hyrule Castle Tapestry Backwards', Lgcl),
|
||||
create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(1),
|
||||
create_door(player, 'Hyrule Castle Throne Room N', Nrml).dir(No, 0x51, Mid, High).pos(0),
|
||||
create_door(player, 'Hyrule Castle Throne Room South Stairs', StrS).dir(So, 0x51, Mid, Low),
|
||||
|
||||
# hyrule dungeon level
|
||||
@@ -207,7 +207,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Desert East Wing ES', Intr).dir(Ea, 0x85, Bot, High).pos(3),
|
||||
create_door(player, 'Desert East Wing Key Door EN', Intr).dir(Ea, 0x85, Top, High).small_key().pos(1),
|
||||
create_door(player, 'Desert Compass Key Door WN', Intr).dir(We, 0x85, Top, High).small_key().pos(1),
|
||||
create_door(player, 'Desert Compass NW', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0),
|
||||
create_door(player, 'Desert Compass NE', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0),
|
||||
create_door(player, 'Desert Cannonball S', Nrml).dir(So, 0x75, Right, High).pos(1).portal(X, 0x02),
|
||||
create_door(player, 'Desert Arrow Pot Corner S Edge', Open).dir(So, 0x75, None, High).edge(6, Z, 0x20),
|
||||
create_door(player, 'Desert Arrow Pot Corner W Edge', Open).dir(We, 0x75, None, High).edge(2, Z, 0x20),
|
||||
@@ -780,7 +780,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Ice Freezors Hole', Hole),
|
||||
create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot
|
||||
create_door(player, 'Ice Freezors Ledge Hole', Hole),
|
||||
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2),
|
||||
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(1),
|
||||
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1),
|
||||
create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(2),
|
||||
create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0).portal(X, 0x02),
|
||||
@@ -1478,6 +1478,8 @@ def create_doors(world, player):
|
||||
world.get_door('GT Spike Crystal Right to Left Barrier - Orange', player).barrier(CrystalBarrier.Orange)
|
||||
world.get_door('GT Spike Crystal Left to Right Bypass', player).barrier(CrystalBarrier.Blue)
|
||||
|
||||
world.get_door('Sanctuary Mirror Route', player).barrier(CrystalBarrier.Orange)
|
||||
|
||||
# kill certain doors
|
||||
if world.intensity[player] == 1: # due to ladder & warp being fixed
|
||||
world.get_door('PoD Mimics 2 SW', player).kill()
|
||||
@@ -1524,6 +1526,7 @@ def create_doors(world, player):
|
||||
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: allows this to be a portal
|
||||
world.get_door('Swamp Lobby S', player).standard_restricted = True
|
||||
world.get_door('Sanctuary 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
|
||||
|
||||
+53
-19
@@ -10,9 +10,9 @@ import time
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector
|
||||
from BaseClasses import Hook, hook_from_door
|
||||
from BaseClasses import Hook, hook_from_door, Door
|
||||
from Regions import dungeon_events, flooded_keys_reverse
|
||||
from Dungeons import dungeon_regions, split_region_starts
|
||||
from Dungeons import split_region_starts
|
||||
from RoomData import DoorKind
|
||||
|
||||
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
|
||||
@@ -772,6 +772,13 @@ def connect_simple_door(exit_door, region):
|
||||
|
||||
|
||||
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
|
||||
|
||||
|
||||
def get_special_big_key_doors(world, player):
|
||||
if world.mode[player] == 'standard':
|
||||
return std_special_big_key_doors
|
||||
return special_big_key_doors
|
||||
|
||||
|
||||
class ExplorationState(object):
|
||||
@@ -846,13 +853,21 @@ class ExplorationState(object):
|
||||
ret.prize_received = self.prize_received
|
||||
return ret
|
||||
|
||||
def init_zelda_event_doors(self, event_starts, player):
|
||||
for entrance in event_starts:
|
||||
event_door = Door(player, entrance.name, DoorType.Logical)
|
||||
event_door.req_event = 'Zelda Drop Off'
|
||||
event_door.entrance = entrance
|
||||
event_door.crystal = CrystalBarrier.Orange # always start in orange
|
||||
self.append_door_to_list(event_door, self.event_doors)
|
||||
|
||||
def next_avail_door(self):
|
||||
self.avail_doors.sort(key=lambda x: 0 if x.flag else 1 if x.door.bigKey else 2)
|
||||
exp_door = self.avail_doors.pop()
|
||||
self.crystal = exp_door.crystal
|
||||
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:
|
||||
@@ -873,7 +888,7 @@ class ExplorationState(object):
|
||||
self.ttl_locations += 1
|
||||
if location not in self.found_locations:
|
||||
self.found_locations.append(location)
|
||||
if not bk_Flag and (not location.forced_item or 'Big Key' in location.item.name):
|
||||
if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name):
|
||||
self.bk_found.add(location)
|
||||
if location.name in dungeon_events and location.name not in self.events:
|
||||
if self.flooded_key_check(location):
|
||||
@@ -994,7 +1009,8 @@ class ExplorationState(object):
|
||||
if self.can_traverse(door):
|
||||
if door.controller:
|
||||
door = door.controller
|
||||
if (door in big_key_door_proposal or door.name in special_big_key_doors) and not self.big_key_opened:
|
||||
if (door in big_key_door_proposal
|
||||
or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
|
||||
if not self.in_door_list(door, self.big_doors):
|
||||
self.append_door_to_list(door, self.big_doors)
|
||||
elif door.req_event is not None and door.req_event not in self.events:
|
||||
@@ -1327,8 +1343,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
sector = find_sector(r_name, all_sectors)
|
||||
reverse_d_map[sector] = key
|
||||
if world.mode[player] == 'standard':
|
||||
current_dungeon = dungeon_map['Hyrule Castle']
|
||||
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
|
||||
if 'Hyrule Castle' in dungeon_map:
|
||||
current_dungeon = dungeon_map['Hyrule Castle']
|
||||
standard_stair_check(dungeon_map, current_dungeon, candidate_sectors, global_pole)
|
||||
|
||||
complete_dungeons = {x: y for x, y in dungeon_map.items() if sum(len(sector.outstanding_doors) for sector in y.sectors) <= 0}
|
||||
[dungeon_map.pop(key) for key in complete_dungeons.keys()]
|
||||
@@ -1356,7 +1373,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
sanc_builder = random.choice(lw_builders)
|
||||
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
||||
|
||||
bow_sectors, retro_std_flag = {}, world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard'
|
||||
retro_std_flag = world.bow_mode[player].startswith('retro') and world.mode[player] == 'standard'
|
||||
non_hc_sectors = {}
|
||||
free_location_sectors = {}
|
||||
crystal_switches = {}
|
||||
crystal_barriers = {}
|
||||
@@ -1364,7 +1382,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
neutral_sectors = {}
|
||||
for sector in candidate_sectors:
|
||||
if retro_std_flag and 'Bow' in sector.item_logic: # these need to be distributed outside of HC
|
||||
bow_sectors[sector] = None
|
||||
non_hc_sectors[sector] = None
|
||||
elif world.mode[player] == 'standard' and 'Open Floodgate' in sector.item_logic:
|
||||
non_hc_sectors[sector] = None
|
||||
elif sector.chest_locations > 0:
|
||||
free_location_sectors[sector] = None
|
||||
elif sector.c_switch:
|
||||
@@ -1375,8 +1395,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
neutral_sectors[sector] = None
|
||||
else:
|
||||
polarized_sectors[sector] = None
|
||||
if bow_sectors:
|
||||
assign_bow_sectors(dungeon_map, bow_sectors, global_pole)
|
||||
if non_hc_sectors:
|
||||
assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole)
|
||||
leftover = assign_location_sectors_minimal(dungeon_map, free_location_sectors, global_pole, world, player)
|
||||
free_location_sectors = scatter_extra_location_sectors(dungeon_map, leftover, global_pole)
|
||||
for sector in free_location_sectors:
|
||||
@@ -1420,7 +1440,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player, dunge
|
||||
def standard_stair_check(dungeon_map, dungeon, candidate_sectors, global_pole):
|
||||
# this is because there must be at least one non-dead stairway in hc to get out
|
||||
# this check may not be necessary
|
||||
filtered_sectors = [x for x in candidate_sectors if any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)]
|
||||
filtered_sectors = [x for x in candidate_sectors if 'Open Floodgate' not in x.item_logic and
|
||||
any(y for y in x.outstanding_doors if not y.dead and y.type == DoorType.SpiralStairs)]
|
||||
valid = False
|
||||
while not valid:
|
||||
chosen_sector = random.choice(filtered_sectors)
|
||||
@@ -1550,7 +1571,7 @@ def define_sector_features(sectors):
|
||||
sector.bk_required = True
|
||||
for ext in region.exits:
|
||||
door = ext.door
|
||||
if door is not None:
|
||||
if door is not None and not door.blocked:
|
||||
if door.crystal == CrystalBarrier.Either:
|
||||
sector.c_switch = True
|
||||
elif door.crystal == CrystalBarrier.Orange:
|
||||
@@ -1562,6 +1583,8 @@ def define_sector_features(sectors):
|
||||
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')
|
||||
if region.name in ['Swamp Lobby', 'Swamp Entrance']:
|
||||
sector.item_logic.add('Open Floodgate')
|
||||
|
||||
|
||||
def assign_sector(sector, dungeon, candidate_sectors, global_pole):
|
||||
@@ -1613,8 +1636,8 @@ def find_sector(r_name, sectors):
|
||||
return None
|
||||
|
||||
|
||||
def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
|
||||
sector_list = list(bow_sectors)
|
||||
def assign_non_hc_sectors(dungeon_map, non_hc_sectors, global_pole):
|
||||
sector_list = list(non_hc_sectors)
|
||||
random.shuffle(sector_list)
|
||||
population = []
|
||||
for name in dungeon_map:
|
||||
@@ -1623,7 +1646,7 @@ def assign_bow_sectors(dungeon_map, bow_sectors, global_pole):
|
||||
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)
|
||||
assign_sector(sector_list[i], builder, non_hc_sectors, global_pole)
|
||||
|
||||
|
||||
def scatter_extra_location_sectors(dungeon_map, free_location_sectors, global_pole):
|
||||
@@ -1801,6 +1824,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
|
||||
for name, builder in dungeon_map.items():
|
||||
if builder.c_switch_present and builder.c_switch_required and not builder.c_locked:
|
||||
invalid_builders.append(builder)
|
||||
random.shuffle(invalid_builders)
|
||||
while len(invalid_builders) > 0:
|
||||
valid_builders = []
|
||||
for builder in invalid_builders:
|
||||
@@ -1826,6 +1850,7 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
|
||||
if eq.c_switch:
|
||||
reachable_crystals[hook_from_door(eq.door)] = True
|
||||
valid_ent_sectors = []
|
||||
random.shuffle(entrance_sectors)
|
||||
for entrance_sector in entrance_sectors:
|
||||
other_sectors = [x for x in builder.sectors if x != entrance_sector]
|
||||
reachable, access = is_c_switch_reachable(entrance_sector, reachable_crystals, other_sectors)
|
||||
@@ -1843,7 +1868,12 @@ def ensure_crystal_switches_reachable(dungeon_map, crystal_switches, polarized_s
|
||||
while not valid:
|
||||
if len(candidates) <= 0:
|
||||
raise GenerationException(f'need to provide more sophisticated crystal connection for {entrance_sector}')
|
||||
sector, which_list = random.choice(list(candidates.items()))
|
||||
# prioritize candidates
|
||||
if any(x == 'Crystals' for x in candidates.values()):
|
||||
cand_list = [x for x in candidates.items() if x[1] == 'Crystals']
|
||||
else:
|
||||
cand_list = list(candidates.items())
|
||||
sector, which_list = random.choice(cand_list)
|
||||
del candidates[sector]
|
||||
valid = global_pole.is_valid_choice(dungeon_map, builder, [sector])
|
||||
if which_list == 'Polarized':
|
||||
@@ -3494,6 +3524,8 @@ def identify_branching_issues(dungeon_map, builder_info):
|
||||
unconnected_builders[name] = builder
|
||||
for hook, door_list in unreached_doors.items():
|
||||
builder.unfulfilled[hook] += len(door_list)
|
||||
elif package:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
|
||||
return unconnected_builders
|
||||
|
||||
|
||||
@@ -3511,7 +3543,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
for portal in world.dungeon_portals[player]:
|
||||
if not portal.destination and portal.name in dungeon_portals[builder.name]:
|
||||
possible_regions.add(portal.door.entrance.parent_region.name)
|
||||
if builder.name in dungeon_drops.keys():
|
||||
if builder.name in dungeon_drops.keys() and (builder.name != 'Hyrule Castle'
|
||||
or world.mode[player] != 'standard'):
|
||||
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():
|
||||
@@ -3538,7 +3571,8 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
|
||||
builder.exception_list = list(sector_list)
|
||||
return True, {}, package
|
||||
except (GenerationException, NeutralizingException, OtherGenException):
|
||||
except (GenerationException, NeutralizingException, OtherGenException) as e:
|
||||
logging.getLogger('').debug(f'Bailing on this layout for {builder.name}', exc_info=1)
|
||||
builder.split_dungeon_map = None
|
||||
builder.valid_proposal = None
|
||||
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
|
||||
|
||||
+8
-4
@@ -2626,6 +2626,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
||||
('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'),
|
||||
('Bumper Cave Ledge Drop', 'West Dark World'),
|
||||
('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'),
|
||||
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
|
||||
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
|
||||
('Skull Woods Forest', 'Skull Woods Forest'),
|
||||
('Desert Ledge Mirror Spot', 'Desert Ledge'),
|
||||
('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'),
|
||||
@@ -2754,6 +2756,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
||||
('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'),
|
||||
('Bumper Cave Entrance Drop', 'West Dark World'),
|
||||
('Bumper Cave Ledge Drop', 'West Dark World'),
|
||||
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
|
||||
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
|
||||
('Skull Woods Forest', 'Skull Woods Forest'),
|
||||
('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'),
|
||||
('Paradox Cave Push Block', 'Paradox Cave Front'),
|
||||
@@ -2977,8 +2981,8 @@ default_connections = [('Links House', 'Links House'),
|
||||
('C-Shaped House', 'C-Shaped House'),
|
||||
('Chest Game', 'Chest Game'),
|
||||
('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'),
|
||||
('Bumper Cave (Bottom)', 'Bumper Cave'),
|
||||
('Bumper Cave (Top)', 'Bumper Cave'),
|
||||
('Bumper Cave (Bottom)', 'Bumper Cave (bottom)'),
|
||||
('Bumper Cave (Top)', 'Bumper Cave (top)'),
|
||||
('Red Shield Shop', 'Red Shield Shop'),
|
||||
('Dark Sanctuary Hint', 'Dark Sanctuary Hint'),
|
||||
('Fortune Teller (Dark)', 'Fortune Teller (Dark)'),
|
||||
@@ -3140,7 +3144,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
|
||||
('Inverted Big Bomb Shop', 'Inverted Big Bomb Shop'),
|
||||
('Inverted Dark Sanctuary', 'Inverted Dark Sanctuary'),
|
||||
('Inverted Dark Sanctuary Exit', 'West Dark World'),
|
||||
('Old Man Cave (West)', 'Bumper Cave'),
|
||||
('Old Man Cave (West)', 'Bumper Cave (bottom)'),
|
||||
('Old Man Cave (East)', 'Death Mountain Return Cave (left)'),
|
||||
('Old Man Cave Exit (West)', 'West Dark World'),
|
||||
('Old Man Cave Exit (East)', 'Dark Death Mountain'),
|
||||
@@ -3149,7 +3153,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'
|
||||
('Bumper Cave (Top)', 'Dark Death Mountain Healer Fairy'),
|
||||
('Bumper Cave Exit (Top)', 'Death Mountain Return Ledge'),
|
||||
('Bumper Cave Exit (Bottom)', 'Light World'),
|
||||
('Death Mountain Return Cave (West)', 'Bumper Cave'),
|
||||
('Death Mountain Return Cave (West)', 'Bumper Cave (top)'),
|
||||
('Death Mountain Return Cave (East)', 'Death Mountain Return Cave (right)'),
|
||||
('Death Mountain Return Cave Exit (West)', 'Death Mountain'),
|
||||
('Death Mountain Return Cave Exit (East)', 'Death Mountain'),
|
||||
|
||||
@@ -24,7 +24,6 @@ def promote_dungeon_items(world):
|
||||
item.advancement = True
|
||||
elif item.map or item.compass:
|
||||
item.priority = True
|
||||
dungeon_tracking(world)
|
||||
|
||||
|
||||
def dungeon_tracking(world):
|
||||
@@ -35,7 +34,6 @@ def dungeon_tracking(world):
|
||||
|
||||
|
||||
def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
dungeon_tracking(world)
|
||||
|
||||
# with shuffled dungeon items they are distributed as part of the normal item pool
|
||||
for item in world.get_items():
|
||||
@@ -73,11 +71,13 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
|
||||
def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False,
|
||||
vanilla=False):
|
||||
def sweep_from_pool():
|
||||
def sweep_from_pool(placing_item=None):
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.placing_item = placing_item
|
||||
new_state.sweep_for_events()
|
||||
new_state.placing_item = None
|
||||
return new_state
|
||||
|
||||
unplaced_items = []
|
||||
@@ -94,7 +94,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
||||
while any(player_items.values()) and locations:
|
||||
items_to_place = [[itempool.remove(items[-1]), items.pop()][-1] for items in player_items.values() if items]
|
||||
|
||||
maximum_exploration_state = sweep_from_pool()
|
||||
maximum_exploration_state = sweep_from_pool(placing_item=items_to_place[0])
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
for item_to_place in items_to_place:
|
||||
@@ -105,6 +105,8 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
||||
spot_to_fill = None
|
||||
|
||||
item_locations = filter_locations(item_to_place, locations, world, vanilla)
|
||||
verify(item_to_place, item_locations, maximum_exploration_state, single_player_placement,
|
||||
perform_access_check, key_pool, world)
|
||||
for location in item_locations:
|
||||
spot_to_fill = verify_spot_to_fill(location, item_to_place, maximum_exploration_state,
|
||||
single_player_placement, perform_access_check, key_pool, world)
|
||||
@@ -128,9 +130,6 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing
|
||||
raise FillError('No more spots to place %s' % item_to_place)
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
if item_to_place.smallkey:
|
||||
with suppress(ValueError):
|
||||
key_pool.remove(item_to_place)
|
||||
track_outside_keys(item_to_place, spot_to_fill, world)
|
||||
track_dungeon_items(item_to_place, spot_to_fill, world)
|
||||
locations.remove(spot_to_fill)
|
||||
@@ -143,21 +142,29 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
|
||||
key_pool, world):
|
||||
if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there
|
||||
location.item = item_to_place
|
||||
location.event = True
|
||||
if item_to_place.smallkey:
|
||||
with suppress(ValueError):
|
||||
key_pool.remove(item_to_place)
|
||||
test_state = max_exp_state.copy()
|
||||
test_state.stale[item_to_place.player] = True
|
||||
else:
|
||||
test_state = max_exp_state
|
||||
if not single_player_placement or location.player == item_to_place.player:
|
||||
test_state.sweep_for_events()
|
||||
if location.can_fill(test_state, item_to_place, perform_access_check):
|
||||
if valid_key_placement(item_to_place, location, key_pool, world):
|
||||
if valid_key_placement(item_to_place, location, key_pool, test_state, world):
|
||||
if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = None
|
||||
location.event = False
|
||||
if item_to_place.smallkey:
|
||||
key_pool.append(item_to_place)
|
||||
return None
|
||||
|
||||
|
||||
def valid_key_placement(item, location, key_pool, world):
|
||||
def valid_key_placement(item, location, key_pool, collection_state, world):
|
||||
if not valid_reserved_placement(item, location, world):
|
||||
return False
|
||||
if ((not item.smallkey and not item.bigkey) or item.player != location.player
|
||||
@@ -174,7 +181,15 @@ def valid_key_placement(item, location, key_pool, world):
|
||||
prize_loc = world.get_location(key_logic.prize_location, location.player)
|
||||
cr_count = world.crystals_needed_for_gt[location.player]
|
||||
wild_keys = world.keyshuffle[item.player] != 'none'
|
||||
return key_logic.check_placement(unplaced_keys, wild_keys, location if item.bigkey else None, prize_loc, cr_count)
|
||||
if wild_keys:
|
||||
reached_keys = {x for x in collection_state.locations_checked
|
||||
if x.item and x.item.name == key_logic.small_key_name and x.item.player == item.player}
|
||||
else:
|
||||
reached_keys = set() # will be calculated using key logic in a moment
|
||||
self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small
|
||||
and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name)
|
||||
return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys,
|
||||
location if item.bigkey else None, prize_loc, cr_count)
|
||||
else:
|
||||
return not item.is_inside_dungeon_item(world)
|
||||
|
||||
@@ -205,6 +220,7 @@ def track_outside_keys(item, location, world):
|
||||
if loc_dungeon and loc_dungeon.name == item_dungeon:
|
||||
return # this is an inside key
|
||||
world.key_logic[item.player][item_dungeon].outside_keys += 1
|
||||
world.key_logic[item.player][item_dungeon].outside_keys_locations.add(location)
|
||||
|
||||
|
||||
def track_dungeon_items(item, location, world):
|
||||
@@ -289,7 +305,7 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite
|
||||
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]
|
||||
if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked]
|
||||
swap_locations = sorted(possible_swaps, key=location_preference)
|
||||
return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool,
|
||||
key_pool, single_player_placement)
|
||||
@@ -345,7 +361,9 @@ def find_spot_for_item(item_to_place, locations, world, base_state, pool,
|
||||
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):
|
||||
and valid_key_placement(item_to_place, location,
|
||||
pool if (keys_in_itempool and keys_in_itempool[item_to_place.player]) else world.itempool,
|
||||
test_state, world):
|
||||
return location
|
||||
if item_to_place.smallkey or item_to_place.bigkey:
|
||||
location.item = old_item
|
||||
@@ -363,6 +381,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
# handle pot/drop shuffle
|
||||
|
||||
random.shuffle(world.itempool)
|
||||
config_sort(world)
|
||||
progitempool = [item for item in world.itempool if item.advancement]
|
||||
prioitempool = [item for item in world.itempool if not item.advancement and item.priority]
|
||||
restitempool = [item for item in world.itempool if not item.advancement and not item.priority]
|
||||
@@ -383,7 +402,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
else:
|
||||
max_trash = gt_count
|
||||
scaled_trash = math.floor(max_trash * scale_factor)
|
||||
if world.goal[player] in ['triforcehunt', 'trinity']:
|
||||
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
gftower_trash_count = random.randint(scaled_trash, max_trash)
|
||||
else:
|
||||
gftower_trash_count = random.randint(0, scaled_trash)
|
||||
@@ -402,10 +421,16 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
random.shuffle(fill_locations)
|
||||
fill_locations.reverse()
|
||||
|
||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||
# todo: crossed
|
||||
progitempool.sort(key=lambda item: 1 if item.name == 'Small Key (Escape)'
|
||||
and world.keyshuffle[item.player] != 'none' and world.mode[item.player] == 'standard' else 0)
|
||||
# Make sure the escape keys ire placed first in standard to prevent running out of spots
|
||||
def std_item_sort(item):
|
||||
if world.mode[item.player] == 'standard':
|
||||
if item.name == 'Small Key (Escape)':
|
||||
return 1
|
||||
if item.name == 'Big Key (Escape)':
|
||||
return 2
|
||||
return 0
|
||||
|
||||
progitempool.sort(key=std_item_sort)
|
||||
key_pool = [x for x in progitempool if x.smallkey]
|
||||
|
||||
# sort maps and compasses to the back -- this may not be viable in equitable & ambrosia
|
||||
@@ -455,6 +480,20 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
ensure_good_items(world)
|
||||
|
||||
|
||||
def config_sort(world):
|
||||
if world.item_pool_config.verify:
|
||||
config_sort_helper(world, world.item_pool_config.verify)
|
||||
elif world.item_pool_config.preferred:
|
||||
config_sort_helper(world, world.item_pool_config.preferred)
|
||||
|
||||
|
||||
def config_sort_helper(world, sort_dict):
|
||||
pref = list(sort_dict.keys())
|
||||
pref_len = len(pref)
|
||||
world.itempool.sort(key=lambda i: pref_len - pref.index((i.name, i.player))
|
||||
if (i.name, i.player) in sort_dict else 0)
|
||||
|
||||
|
||||
def calc_trash_locations(world, player):
|
||||
total_count, gt_count = 0, 0
|
||||
for loc in world.get_locations():
|
||||
@@ -499,10 +538,17 @@ def fast_fill_helper(world, item_pool, fill_locations):
|
||||
|
||||
|
||||
def fast_fill(world, item_pool, fill_locations):
|
||||
while item_pool and fill_locations:
|
||||
config = world.item_pool_config
|
||||
fast_pool = [x for x in item_pool if (x.name, x.player) not in config.restricted]
|
||||
filtered_pool = [x for x in item_pool if (x.name, x.player) in config.restricted]
|
||||
filtered_fill(world, filtered_pool, fill_locations)
|
||||
while fast_pool and fill_locations:
|
||||
spot_to_fill = fill_locations.pop()
|
||||
item_to_place = item_pool.pop()
|
||||
item_to_place = fast_pool.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
item_pool.clear()
|
||||
item_pool.extend(filtered_pool)
|
||||
item_pool.extend(fast_pool)
|
||||
|
||||
|
||||
def fast_fill_pot_for_multiworld(world, item_pool, fill_locations):
|
||||
@@ -612,6 +658,40 @@ def sell_keys(world, player):
|
||||
world.itempool.remove(universal_key)
|
||||
|
||||
|
||||
def verify(item_to_place, item_locations, state, spp, pac, key_pool, world):
|
||||
if world.item_pool_config.verify:
|
||||
logger = logging.getLogger('')
|
||||
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||
item_player = item_to_place.player
|
||||
config = world.item_pool_config
|
||||
if (item_name, item_player) in config.verify:
|
||||
tests = config.verify[(item_name, item_player)]
|
||||
issues = []
|
||||
for location in item_locations:
|
||||
if location.name in tests:
|
||||
expected = tests[location.name]
|
||||
spot = verify_spot_to_fill(location, item_to_place, state, spp, pac, key_pool, world)
|
||||
if spot and (item_to_place.smallkey or item_to_place.bigkey):
|
||||
location.item = None
|
||||
location.event = False
|
||||
if item_to_place.smallkey:
|
||||
key_pool.append(item_to_place)
|
||||
if (expected and spot) or (not expected and spot is None):
|
||||
logger.debug(f'Placing {item_name} ({item_player}) at {location.name} was {expected}')
|
||||
config.verify_count += 1
|
||||
if config.verify_count >= config.verify_target:
|
||||
exit()
|
||||
else:
|
||||
issues.append((item_name, item_player, location.name, expected))
|
||||
if len(issues) > 0:
|
||||
for name, player, loc, expected in issues:
|
||||
if expected:
|
||||
logger.error(f'Could not place {name} ({player}) at {loc}')
|
||||
else:
|
||||
logger.error(f'{name} ({player}) should not be allowed at {loc}')
|
||||
raise Exception(f'Test failed placing {name}')
|
||||
|
||||
|
||||
def balance_multiworld_progression(world):
|
||||
state = CollectionState(world)
|
||||
checked_locations = set()
|
||||
|
||||
+2
-2
@@ -142,8 +142,8 @@ class InitialSram:
|
||||
'Big Key (Misery Mire)': (0x367, 0x01), 'Compass (Misery Mire)': (0x365, 0x01), 'Map (Misery Mire)': (0x369, 0x01),
|
||||
'Big Key (Turtle Rock)': (0x366, 0x08), 'Compass (Turtle Rock)': (0x364, 0x08), 'Map (Turtle Rock)': (0x368, 0x08),
|
||||
'Big Key (Ganons Tower)': (0x366, 0x04), 'Compass (Ganons Tower)': (0x364, 0x04), 'Map (Ganons Tower)': (0x368, 0x04)}
|
||||
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02),'Pegasus Boots': (0x355, 1, 0x379, 0x04),
|
||||
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 3, 0x38C, 0x01),
|
||||
set_or_table = {'Flippers': (0x356, 1, 0x379, 0x02), 'Pegasus Boots': (0x355, 1, 0x379, 0x04),
|
||||
'Shovel': (0x34C, 1, 0x38C, 0x04), 'Ocarina': (0x34C, 2, 0x38C, 0x02),
|
||||
'Ocarina (Activated)': (0x34C, 3, 0x38C, 0x01),
|
||||
'Mushroom': (0x344, 1, 0x38C, 0x20 | 0x08), 'Magic Powder': (0x344, 2, 0x38C, 0x10),
|
||||
'Blue Boomerang': (0x341, 1, 0x38C, 0x80), 'Red Boomerang': (0x341, 2, 0x38C, 0x40)}
|
||||
|
||||
+2
-1
@@ -184,7 +184,8 @@ def create_inverted_regions(world, player):
|
||||
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']),
|
||||
create_cave_region(player, 'Inverted Dark Sanctuary', 'a storyteller', None, ['Inverted Dark Sanctuary Exit']),
|
||||
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_cave_region(player, 'Bumper Cave (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']),
|
||||
create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']),
|
||||
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
create_dw_region(player, 'Skull Woods Forest (West)', None, ['Skull Woods Second Section Hole', 'Skull Woods Second Section Door (West)', 'Skull Woods Final Section']),
|
||||
|
||||
+261
-71
@@ -6,7 +6,7 @@ import RaceRandom as random
|
||||
from BaseClasses import Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem
|
||||
from EntranceShuffle import connect_entrance
|
||||
from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location
|
||||
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, is_dungeon_item
|
||||
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys
|
||||
from PotShuffle import vanilla_pots
|
||||
from Items import ItemFactory
|
||||
|
||||
@@ -182,8 +182,12 @@ def get_custom_array_key(item):
|
||||
|
||||
|
||||
def generate_itempool(world, player):
|
||||
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals']
|
||||
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
|
||||
if (world.difficulty[player] not in ['normal', 'hard', 'expert']
|
||||
or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals',
|
||||
'ganonhunt', 'completionist']
|
||||
or world.mode[player] not in ['open', 'standard', 'inverted']
|
||||
or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']
|
||||
or world.progressive not in ['on', 'off', 'random']):
|
||||
raise NotImplementedError('Not supported yet')
|
||||
|
||||
if world.timer in ['ohko', 'timed-ohko']:
|
||||
@@ -283,6 +287,7 @@ def generate_itempool(world, player):
|
||||
for _ in range(0, amt):
|
||||
pool.append('Rupees (20)')
|
||||
|
||||
start_inventory = list(world.precollected_items)
|
||||
for item in precollected_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
@@ -345,7 +350,7 @@ def generate_itempool(world, player):
|
||||
world.clock_mode = clock_mode
|
||||
|
||||
goal = world.goal[player]
|
||||
if goal in ['triforcehunt', 'trinity']:
|
||||
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
g, t = set_default_triforce(goal, world.treasure_hunt_count[player], world.treasure_hunt_total[player])
|
||||
world.treasure_hunt_count[player], world.treasure_hunt_total[player] = g, t
|
||||
world.treasure_hunt_icon[player] = 'Triforce Piece'
|
||||
@@ -368,21 +373,7 @@ def generate_itempool(world, player):
|
||||
for i in range(4):
|
||||
next(adv_heart_pieces).advancement = True
|
||||
|
||||
beeweights = {'0': {None: 100},
|
||||
'1': {None: 75, 'trap': 25},
|
||||
'2': {None: 40, 'trap': 40, 'bee': 20},
|
||||
'3': {'trap': 50, 'bee': 50},
|
||||
'4': {'trap': 100}}
|
||||
def beemizer(item):
|
||||
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
|
||||
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
|
||||
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
|
||||
return item
|
||||
|
||||
if not skip_pool_adjustments:
|
||||
world.itempool += [beemizer(item) for item in items]
|
||||
else:
|
||||
world.itempool += items
|
||||
world.itempool += items
|
||||
|
||||
# shuffle medallions
|
||||
mm_medallion, tr_medallion = None, None
|
||||
@@ -392,8 +383,16 @@ def generate_itempool(world, player):
|
||||
custom_medallions = medal_map[player]
|
||||
if 'Misery Mire' in custom_medallions:
|
||||
mm_medallion = custom_medallions['Misery Mire']
|
||||
if isinstance(mm_medallion, dict):
|
||||
mm_medallion = random.choices(list(mm_medallion.keys()), list(mm_medallion.values()), k=1)[0]
|
||||
if mm_medallion == 'Random':
|
||||
mm_medallion = None
|
||||
if 'Turtle Rock' in custom_medallions:
|
||||
tr_medallion = custom_medallions['Turtle Rock']
|
||||
if isinstance(tr_medallion, dict):
|
||||
tr_medallion = random.choices(list(tr_medallion.keys()), list(tr_medallion.values()), k=1)[0]
|
||||
if tr_medallion == 'Random':
|
||||
tr_medallion = None
|
||||
if not mm_medallion:
|
||||
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
if not tr_medallion:
|
||||
@@ -427,6 +426,38 @@ def generate_itempool(world, player):
|
||||
if world.dropshuffle[player] == 'underworld' and not skip_pool_adjustments:
|
||||
add_drop_contents(world, player)
|
||||
|
||||
# modfiy based on start inventory, if any
|
||||
modify_pool_for_start_inventory(start_inventory, world, player)
|
||||
|
||||
beeweights = {'0': {None: 100},
|
||||
'1': {None: 75, 'trap': 25},
|
||||
'2': {None: 40, 'trap': 40, 'bee': 20},
|
||||
'3': {'trap': 50, 'bee': 50},
|
||||
'4': {'trap': 100}}
|
||||
def beemizer(item):
|
||||
if world.beemizer[item.player] and not item.advancement and not item.priority and not item.type:
|
||||
choice = random.choices(list(beeweights[world.beemizer[item.player]].keys()), weights=list(beeweights[world.beemizer[item.player]].values()))[0]
|
||||
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
|
||||
return item
|
||||
|
||||
if not skip_pool_adjustments:
|
||||
world.itempool = [beemizer(item) for item in world.itempool]
|
||||
|
||||
# increase pool if not enough items
|
||||
ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name)
|
||||
pool_size = count_player_dungeon_item_pool(world, player)
|
||||
pool_size += sum(1 for x in world.itempool if x.player == player)
|
||||
|
||||
if pool_size < ttl_locations:
|
||||
retro_bow = world.bow_mode[player].startswith('retro')
|
||||
amount_to_add = ttl_locations - pool_size
|
||||
filler_additions = random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add)
|
||||
for item in filler_additions:
|
||||
item_name = 'Rupees (5)' if retro_bow and item == 'Arrows (10)' else item
|
||||
world.itempool.append(ItemFactory(item_name, player))
|
||||
|
||||
|
||||
|
||||
|
||||
take_any_locations = [
|
||||
'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut',
|
||||
@@ -813,15 +844,15 @@ def add_pot_contents(world, player):
|
||||
world.itempool.append(ItemFactory(item, player))
|
||||
|
||||
|
||||
def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, bombbag,
|
||||
door_shuffle, logic, flute_activated):
|
||||
def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords,
|
||||
bombbag, door_shuffle, logic, flute_activated):
|
||||
pool = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
clock_mode = None
|
||||
if goal in ['triforcehunt', 'trinity']:
|
||||
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
if treasure_hunt_total == 0:
|
||||
treasure_hunt_total = 30 if goal == 'triforcehunt' else 10
|
||||
treasure_hunt_total = 30 if goal in ['triforcehunt', 'ganonhunt'] else 10
|
||||
# triforce pieces max out
|
||||
triforcepool = ['Triforce Piece'] * min(treasure_hunt_total, max_goal)
|
||||
|
||||
@@ -924,7 +955,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
|
||||
elif timer == 'timed-ohko':
|
||||
pool.extend(diff.timedohko)
|
||||
clock_mode = 'countdown-ohko'
|
||||
if goal in ['triforcehunt', 'trinity']:
|
||||
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
pool.extend(triforcepool)
|
||||
|
||||
for extra in diff.extras:
|
||||
@@ -957,6 +988,80 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt
|
||||
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
|
||||
|
||||
|
||||
item_alternates = {
|
||||
# Bows
|
||||
'Progressive Bow (Alt)': ('Progressive Bow', 1),
|
||||
'Bow': ('Progressive Bow', 1),
|
||||
'Silver Arrows': ('Progressive Bow', 2),
|
||||
# Gloves
|
||||
'Power Glove': ('Progressive Glove', 1),
|
||||
'Titans Mitts': ('Progressive Glove', 2),
|
||||
# Swords
|
||||
'Sword and Shield': ('Progressive Sword', 1), # could find a way to also remove a shield, but mostly not impactful
|
||||
'Fighter Sword': ('Progressive Sword', 1),
|
||||
'Master Sword': ('Progressive Sword', 2),
|
||||
'Tempered Sword': ('Progressive Sword', 3),
|
||||
'Golden Sword': ('Progressive Sword', 4),
|
||||
# Shields
|
||||
'Blue Shield': ('Progressive Shield', 1),
|
||||
'Red Shield': ('Progressive Shield', 2),
|
||||
'Mirror Shield': ('Progressive Shield', 3),
|
||||
# Armors
|
||||
'Blue Mail': ('Progressive Armor', 1),
|
||||
'Red Mail': ('Progressive Armor', 2),
|
||||
|
||||
'Magic Upgrade (1/4)': ('Magic Upgrade (1/2)', 2),
|
||||
'Ocarina': ('Ocarina (Activated)', 1),
|
||||
'Ocarina (Activated)': ('Ocarina', 1),
|
||||
'Boss Heart Container': ('Sanctuary Heart Container', 1),
|
||||
'Sanctuary Heart Container': ('Boss Heart Container', 1),
|
||||
'Power Star': ('Triforce Piece', 1)
|
||||
}
|
||||
|
||||
|
||||
def modify_pool_for_start_inventory(start_inventory, world, player):
|
||||
if (world.customizer and world.customizer.get_item_pool()) or world.custom:
|
||||
# custom item pools only adjust in dungeon items
|
||||
for item in start_inventory:
|
||||
if item.dungeon:
|
||||
d = world.get_dungeon(item.dungeon, item.player)
|
||||
match = next((i for i in d.all_items if i.name == item.name), None)
|
||||
if match:
|
||||
if match.map or match.compass:
|
||||
d.dungeon_items.remove(match)
|
||||
elif match.smallkey:
|
||||
d.small_keys.remove(match)
|
||||
elif match.bigkey and d.big_key == match:
|
||||
d.big_key = None
|
||||
return
|
||||
for item in start_inventory:
|
||||
if item.player == player:
|
||||
if item in world.itempool:
|
||||
world.itempool.remove(item)
|
||||
elif item.name in item_alternates:
|
||||
alt = item_alternates[item.name]
|
||||
i = alt[1]
|
||||
while i > 0:
|
||||
alt_item = ItemFactory([alt[0]], player)[0]
|
||||
if alt_item in world.itempool:
|
||||
world.itempool.remove(alt_item)
|
||||
i = i-1
|
||||
elif 'Bottle' in item.name:
|
||||
bottle_item = next((x for x in world.itempool if 'Bottle' in item.name and x.player == player), None)
|
||||
if bottle_item is not None:
|
||||
world.itempool.remove(bottle_item)
|
||||
if item.dungeon:
|
||||
d = world.get_dungeon(item.dungeon, item.player)
|
||||
match = next((i for i in d.all_items if i.name == item.name), None)
|
||||
if match:
|
||||
if match.map or match.compass:
|
||||
d.dungeon_items.remove(match)
|
||||
elif match.smallkey:
|
||||
d.small_keys.remove(match)
|
||||
elif match.bigkey and d.big_key == match:
|
||||
d.big_key = None
|
||||
|
||||
|
||||
def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray):
|
||||
if isinstance(customitemarray,dict) and 1 in customitemarray:
|
||||
customitemarray = customitemarray[1]
|
||||
@@ -983,7 +1088,7 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
|
||||
customitemarray["triforce"] = total_items_to_place
|
||||
|
||||
# Triforce Pieces
|
||||
if goal in ['triforcehunt', 'trinity']:
|
||||
if goal in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"])
|
||||
customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t
|
||||
|
||||
@@ -1021,8 +1126,8 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
|
||||
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], max_goal), 1)
|
||||
treasure_hunt_icon = 'Triforce Piece'
|
||||
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
|
||||
if ((customitemarray["triforcepieces"] < treasure_hunt_count) and (goal in ['triforcehunt', 'trinity'])
|
||||
and (customitemarray["triforce"] == 0)):
|
||||
if ((customitemarray["triforcepieces"] < treasure_hunt_count)
|
||||
and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)):
|
||||
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
|
||||
pool.extend(['Triforce Piece'] * extrapieces)
|
||||
itemtotal = itemtotal + extrapieces
|
||||
@@ -1060,6 +1165,22 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer
|
||||
# print("Placing " + str(nothings) + " Nothings")
|
||||
pool.extend(['Nothing'] * nothings)
|
||||
|
||||
start_inventory = [x for x in world.precollected_items if x.player == player]
|
||||
if not start_inventory:
|
||||
if world.logic[player] in ['owglitches', 'nologic']:
|
||||
precollected_items.append('Pegasus Boots')
|
||||
if 'Pegasus Boots' in pool:
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.append('Rupees (20)')
|
||||
if world.swords[player] == 'assured':
|
||||
precollected_items.append('Progressive Sword')
|
||||
if 'Progressive Sword' in pool:
|
||||
pool.remove('Progressive Sword')
|
||||
pool.append('Rupees (50)')
|
||||
elif 'Fighter Sword' in pool:
|
||||
pool.remove('Fighter Sword')
|
||||
pool.append('Rupees (50)')
|
||||
|
||||
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
|
||||
|
||||
|
||||
@@ -1144,37 +1265,35 @@ def make_customizer_pool(world, player):
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
|
||||
guaranteed_items = alwaysitems + ['Magic Mirror', 'Moon Pearl']
|
||||
missing_items = []
|
||||
if world.shopsanity[player]:
|
||||
guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion'])
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
guaranteed_items.append('Small Key (Universal)')
|
||||
for item in guaranteed_items:
|
||||
if item not in pool:
|
||||
pool.append(item)
|
||||
missing_items.append(item)
|
||||
|
||||
glove_count = sum(1 for i in pool if i == 'Progressive Glove')
|
||||
glove_count = 2 if next((i for i in pool if i == 'Titans Glove'), None) is not None else glove_count
|
||||
for i in range(glove_count, 2):
|
||||
pool.append('Progressive Glove')
|
||||
missing_items.append('Progressive Glove')
|
||||
|
||||
if world.bombbag[player]:
|
||||
if 'Bomb Upgrade (+10)' not in pool:
|
||||
pool.append('Bomb Upgrade (+10)')
|
||||
missing_items.append('Bomb Upgrade (+10)')
|
||||
|
||||
if world.swords[player] != 'swordless':
|
||||
beam_swords = {'Master Sword', 'Tempered Sword', 'Golden Sword'}
|
||||
sword_count = sum(1 for i in pool if i in 'Progressive Sword')
|
||||
sword_count = 2 if next((i for i in pool if i in beam_swords), None) is not None else sword_count
|
||||
for i in range(sword_count, 2):
|
||||
pool.append('Progressive Sword')
|
||||
missing_items.append('Progressive Sword')
|
||||
|
||||
bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None)
|
||||
if not bow_found:
|
||||
pool.append('Progressive Bow')
|
||||
|
||||
heart_found = next((i for i in pool if i in {'Boss Heart Container', 'Sanctuary Heart Container'}), None)
|
||||
if heart_found is None:
|
||||
pool.append('Boss Heart Container')
|
||||
missing_items.append('Progressive Bow')
|
||||
logging.getLogger('').warning(f'The following items are not in the custom item pool {", ".join(missing_items)}')
|
||||
|
||||
g, t = set_default_triforce(world.goal[player], world.treasure_hunt_count[player],
|
||||
world.treasure_hunt_total[player])
|
||||
@@ -1183,12 +1302,20 @@ def make_customizer_pool(world, player):
|
||||
if pieces < t:
|
||||
pool.extend(['Triforce Piece'] * (t - pieces))
|
||||
|
||||
ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name)
|
||||
pool_size = len(get_player_dungeon_item_pool(world, player)) + len(pool)
|
||||
|
||||
if pool_size < ttl_locations:
|
||||
amount_to_add = ttl_locations - pool_size
|
||||
pool.extend(random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add))
|
||||
if not world.customizer.get_start_inventory():
|
||||
if world.logic[player] in ['owglitches', 'nologic']:
|
||||
precollected_items.append('Pegasus Boots')
|
||||
if 'Pegasus Boots' in pool:
|
||||
pool.remove('Pegasus Boots')
|
||||
pool.append('Rupees (20)')
|
||||
if world.swords[player] == 'assured':
|
||||
precollected_items.append('Progressive Sword')
|
||||
if 'Progressive Sword' in pool:
|
||||
pool.remove('Progressive Sword')
|
||||
pool.append('Rupees (50)')
|
||||
elif 'Fighter Sword' in pool:
|
||||
pool.remove('Fighter Sword')
|
||||
pool.append('Rupees (50)')
|
||||
|
||||
return pool, placed_items, precollected_items, clock_mode, 1
|
||||
|
||||
@@ -1204,15 +1331,15 @@ filler_items = {
|
||||
}
|
||||
|
||||
|
||||
def get_player_dungeon_item_pool(world, player):
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items
|
||||
if dungeon.player == player and item.location is None]
|
||||
def count_player_dungeon_item_pool(world, player):
|
||||
return sum(1 for dungeon in world.dungeons for item in dungeon.all_items
|
||||
if dungeon.player == player and item.location is None and is_dungeon_item(item.name, world, player))
|
||||
|
||||
|
||||
# location pool doesn't support larger values at this time
|
||||
def set_default_triforce(goal, custom_goal, custom_total):
|
||||
triforce_goal, triforce_total = 0, 0
|
||||
if goal == 'triforcehunt':
|
||||
if goal in ['triforcehunt', 'ganonhunt']:
|
||||
triforce_goal, triforce_total = 20, 30
|
||||
elif goal == 'trinity':
|
||||
triforce_goal, triforce_total = 8, 10
|
||||
@@ -1267,31 +1394,94 @@ def fill_specific_items(world):
|
||||
for player, placement_list in placements.items():
|
||||
for location, item in placement_list.items():
|
||||
loc = world.get_location(location, player)
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
event_flag = False
|
||||
if is_dungeon_item(item_name, world, item_player):
|
||||
item_to_place = next(x for x in dungeon_pool
|
||||
if x.name == item_name and x.player == item_player)
|
||||
dungeon_pool.remove(item_to_place)
|
||||
event_flag = True
|
||||
elif item_name in prize_set:
|
||||
item_player = player # prizes must be for that player
|
||||
item_to_place = ItemFactory(item_name, item_player)
|
||||
prize_pool.remove(item_name)
|
||||
event_flag = True
|
||||
else:
|
||||
item_to_place = next((x for x in world.itempool
|
||||
if x.name == item_name and x.player == item_player), None)
|
||||
if item_to_place is None:
|
||||
item_to_place = ItemFactory(item_name, player)
|
||||
else:
|
||||
world.itempool.remove(item_to_place)
|
||||
world.push_item(loc, item_to_place, False)
|
||||
# track_outside_keys(item_to_place, spot_to_fill, world)
|
||||
# track_dungeon_items(item_to_place, spot_to_fill, world)
|
||||
loc.event = event_flag or item_to_place.advancement
|
||||
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
|
||||
dungeon_pool, prize_set, prize_pool)
|
||||
if item_to_place:
|
||||
world.push_item(loc, item_to_place, False)
|
||||
track_outside_keys(item_to_place, loc, world)
|
||||
track_dungeon_items(item_to_place, loc, world)
|
||||
loc.event = (event_flag or item_to_place.advancement
|
||||
or item_to_place.bigkey or item_to_place.smallkey)
|
||||
advanced_placements = world.customizer.get_advanced_placements()
|
||||
if advanced_placements:
|
||||
for player, placement_list in advanced_placements.items():
|
||||
for placement in placement_list:
|
||||
if placement['type'] == 'LocationGroup':
|
||||
item = placement['item']
|
||||
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
|
||||
dungeon_pool, prize_set, prize_pool)
|
||||
if not item_to_place:
|
||||
continue
|
||||
locations = placement['locations']
|
||||
handled = False
|
||||
while not handled:
|
||||
if isinstance(locations, dict):
|
||||
chosen_loc = random.choices(list(locations.keys()), list(locations.values()), k=1)[0]
|
||||
else: # if isinstance(locations, list):
|
||||
chosen_loc = random.choice(locations)
|
||||
if chosen_loc == 'Random':
|
||||
if is_dungeon_item(item_to_place.name, world, item_to_place.player):
|
||||
dungeon_pool.append(item_to_place)
|
||||
elif item_to_place.name in prize_set:
|
||||
prize_pool.append(item_to_place.name)
|
||||
else:
|
||||
world.itempool.append(item_to_place)
|
||||
else:
|
||||
loc = world.get_location(chosen_loc, player)
|
||||
if loc.item:
|
||||
continue
|
||||
world.push_item(loc, item_to_place, False)
|
||||
track_outside_keys(item_to_place, loc, world)
|
||||
track_dungeon_items(item_to_place, loc, world)
|
||||
loc.event = (event_flag or item_to_place.advancement
|
||||
or item_to_place.bigkey or item_to_place.smallkey)
|
||||
handled = True
|
||||
elif placement['type'] == 'NotLocationGroup':
|
||||
item = placement['item']
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
world.item_pool_config.restricted[(item_name, item_player)] = placement['locations']
|
||||
elif placement['type'] == 'PreferredLocationGroup':
|
||||
item = placement['item']
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
world.item_pool_config.preferred[(item_name, item_player)] = placement['locations']
|
||||
elif placement['type'] == 'Verification':
|
||||
item = placement['item']
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
world.item_pool_config.verify[(item_name, item_player)] = placement['locations']
|
||||
world.item_pool_config.verify_target += len(placement['locations'])
|
||||
|
||||
|
||||
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):
|
||||
item_parts = item.split('#')
|
||||
item_player = player if len(item_parts) < 2 else int(item_parts[1])
|
||||
item_name = item_parts[0]
|
||||
event_flag = False
|
||||
if is_dungeon_item(item_name, world, item_player):
|
||||
item_to_place = next(x for x in dungeon_pool
|
||||
if x.name == item_name and x.player == item_player)
|
||||
dungeon_pool.remove(item_to_place)
|
||||
event_flag = True
|
||||
elif item_name in prize_set:
|
||||
item_player = player # prizes must be for that player
|
||||
item_to_place = ItemFactory(item_name, item_player)
|
||||
prize_pool.remove(item_name)
|
||||
event_flag = True
|
||||
else:
|
||||
matcher = lambda x: x.name == item_name and x.player == item_player
|
||||
if item_name == 'Bottle':
|
||||
matcher = lambda x: x.name.startswith(item_name) and x.player == item_player
|
||||
item_to_place = next((x for x in world.itempool if matcher(x)), None)
|
||||
if item_to_place is None:
|
||||
return None, event_flag
|
||||
else:
|
||||
world.itempool.remove(item_to_place)
|
||||
return item_to_place, event_flag
|
||||
|
||||
|
||||
def is_dungeon_item(item, world, player):
|
||||
|
||||
@@ -81,7 +81,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche
|
||||
'Arrows (5)': (False, False, None, 0x5A, 15, 'This will give\nyou five shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'five arrows'),
|
||||
'Small Magic': (False, False, None, 0x45, 5, 'A bit of magic', 'and the bit of magic', 'bit-o-magic kid', 'magic bit for sale', 'fungus for magic', 'magic boy conjures again', 'a bit of magic'),
|
||||
'Big Magic': (False, False, None, 0xB4, 40, 'A lot of magic', 'and lots of magic', 'lot-o-magic kid', 'magic refill for sale', 'fungus for magic', 'magic boy conjures again', 'a magic refill'),
|
||||
'Chicken': (False, False, None, 0xB3, 999, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'),
|
||||
'Chicken': (False, False, None, 0xB3, 5, 'Cucco of Legend', 'and the legendary cucco', 'chicken kid', 'fried chicken for sale', 'fungus for chicken', 'cucco boy clucks again', 'a cucco'),
|
||||
'Bombs (3)': (False, False, None, 0x28, 15, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
|
||||
'Bombs (10)': (False, False, None, 0x31, 50, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),
|
||||
'Bomb Upgrade (+10)': (False, False, None, 0x52, 100, 'Increase bomb\nstorage, low\nlow price', 'and the bomb bag', 'boom-enlarging kid', 'bomb boost for sale', 'the shroom goes boom', 'upgrade boy explodes more again', 'bomb capacity'),
|
||||
|
||||
+49
-24
@@ -5,7 +5,7 @@ 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, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event
|
||||
from DungeonGenerator import reserved_location, blind_boss_unavail
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class KeyLayout(object):
|
||||
def __init__(self, sector, starts, proposal):
|
||||
self.sector = sector
|
||||
self.start_regions = starts
|
||||
self.event_starts = []
|
||||
self.proposal = proposal
|
||||
self.key_logic = KeyLogic(sector.name)
|
||||
|
||||
@@ -58,13 +59,16 @@ class KeyLogic(object):
|
||||
self.placement_rules = []
|
||||
self.location_rules = {}
|
||||
self.outside_keys = 0
|
||||
self.outside_keys_locations = set()
|
||||
self.dungeon = dungeon_name
|
||||
self.sm_doors = {}
|
||||
self.prize_location = None
|
||||
|
||||
def check_placement(self, unplaced_keys, wild_keys, big_key_loc=None, prize_loc=None, cr_count=7):
|
||||
def check_placement(self, unplaced_keys, wild_keys, reached_keys, self_locking_keys,
|
||||
big_key_loc=None, prize_loc=None, cr_count=7):
|
||||
for rule in self.placement_rules:
|
||||
if not rule.is_satisfiable(self.outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_loc, cr_count):
|
||||
if not rule.is_satisfiable(self.outside_keys_locations, wild_keys, reached_keys, self_locking_keys,
|
||||
unplaced_keys, big_key_loc, prize_loc, cr_count):
|
||||
return False
|
||||
if big_key_loc:
|
||||
for rule_a, rule_b in itertools.combinations(self.placement_rules, 2):
|
||||
@@ -158,7 +162,8 @@ class PlacementRule(object):
|
||||
left -= rule_needed
|
||||
return False
|
||||
|
||||
def is_satisfiable(self, outside_keys, wild_keys, unplaced_keys, big_key_loc, prize_location, cr_count):
|
||||
def is_satisfiable(self, outside_keys_locations, wild_keys, reached_keys, self_locking_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']:
|
||||
@@ -185,10 +190,11 @@ class PlacementRule(object):
|
||||
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:
|
||||
return True
|
||||
available_keys = outside_keys
|
||||
available_keys = len(outside_keys_locations)
|
||||
# todo: sometimes we need an extra empty chest to accomodate the big key too
|
||||
# dungeon bias seed 563518200 for example
|
||||
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
|
||||
threshold -= self_locking_keys
|
||||
if not wild_keys:
|
||||
empty_chests = 0
|
||||
for loc in check_locations:
|
||||
@@ -199,7 +205,8 @@ class PlacementRule(object):
|
||||
place_able_keys = min(empty_chests, unplaced_keys)
|
||||
available_keys += place_able_keys
|
||||
else:
|
||||
available_keys += unplaced_keys
|
||||
available_keys += len(reached_keys.difference(outside_keys_locations)) # already placed small keys
|
||||
available_keys += unplaced_keys # small keys not yet placed
|
||||
return available_keys >= threshold
|
||||
|
||||
|
||||
@@ -223,13 +230,14 @@ class KeyCounter(object):
|
||||
return max(self.used_keys + reserve - len(self.key_only_locations), 0)
|
||||
|
||||
|
||||
def build_key_layout(builder, start_regions, proposal, world, player):
|
||||
def build_key_layout(builder, start_regions, proposal, event_starts, world, player):
|
||||
key_layout = KeyLayout(builder.master_sector, start_regions, proposal)
|
||||
key_layout.flat_prop = flatten_pair_list(key_layout.proposal)
|
||||
key_layout.max_drops = count_key_drops(key_layout.sector)
|
||||
key_layout.max_chests = calc_max_chests(builder, key_layout, world, player)
|
||||
key_layout.big_key_special = check_bk_special(key_layout.sector.region_set(), world, player)
|
||||
key_layout.all_locations = find_all_locations(key_layout.sector)
|
||||
key_layout.event_starts = list(event_starts.keys())
|
||||
return key_layout
|
||||
|
||||
|
||||
@@ -301,10 +309,10 @@ def analyze_dungeon(key_layout, world, player):
|
||||
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))
|
||||
# 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
|
||||
child_queue = deque()
|
||||
for child in key_counter.child_doors.keys():
|
||||
@@ -1000,7 +1008,7 @@ def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_c
|
||||
|
||||
|
||||
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 get_special_big_key_doors(world, player):
|
||||
child_state.big_key_opened = True
|
||||
child_state.avail_doors.extend(child_state.big_doors)
|
||||
child_state.opened_doors.extend(set([d.door for d in child_state.big_doors]))
|
||||
@@ -1443,7 +1451,7 @@ def validate_bk_layout(proposal, builder, start_regions, world, player):
|
||||
if loc.forced_big_key():
|
||||
return True
|
||||
else:
|
||||
return len(state.bk_found) > 0
|
||||
return state.count_locations_exclude_specials(world, player) > 0
|
||||
return False
|
||||
|
||||
|
||||
@@ -1455,6 +1463,7 @@ def validate_key_layout(key_layout, world, player):
|
||||
return True
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
state.init_zelda_event_doors(key_layout.event_starts, player)
|
||||
state.key_locations = key_layout.max_chests
|
||||
state.big_key_special = check_bk_special(key_layout.sector.regions, world, player)
|
||||
for region in key_layout.start_regions:
|
||||
@@ -1482,14 +1491,16 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
else:
|
||||
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_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
|
||||
key_layout, world, player)
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
if invalid_self_locking_key(key_layout, state, prev_state, prev_avail, world, player):
|
||||
return False
|
||||
# todo: allow more key shuffles - refine placement rules
|
||||
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
|
||||
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 available_small_locations == 0
|
||||
# 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)
|
||||
# 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
|
||||
@@ -1611,18 +1622,24 @@ def determine_prize_lock(key_layout, world, player):
|
||||
key_layout.prize_can_lock = True
|
||||
|
||||
|
||||
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||
if world.keyshuffle[player] == 'none':
|
||||
def cnt_avail_small_locations(free_locations, key_only, state, key_layout, world, player):
|
||||
std_flag = world.mode[player] == 'standard' and key_layout.sector.name == 'Hyrule Castle'
|
||||
if world.keyshuffle[player] == 'none' or std_flag:
|
||||
bk_adj = 1 if state.big_key_opened and not state.big_key_special else 0
|
||||
avail_chest_keys = min(free_locations - bk_adj, state.key_locations - key_only)
|
||||
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
|
||||
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
|
||||
avail_chest_keys = min(free_locations + std_adj - bk_adj, state.key_locations - key_only)
|
||||
return max(0, avail_chest_keys + key_only - state.used_smalls)
|
||||
return state.key_locations - state.used_smalls
|
||||
|
||||
|
||||
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
|
||||
if world.keyshuffle[player] == 'none':
|
||||
std_flag = world.mode[player] == 'standard' and layout.sector.name == 'Hyrule Castle'
|
||||
if world.keyshuffle[player] == 'none' or std_flag:
|
||||
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)
|
||||
# this is the secret passage, could expand to Uncle/Links House with appropriate logic
|
||||
std_adj = 1 if std_flag and world.keyshuffle[player] != 'none' else 0
|
||||
avail_chest_keys = min(free_locations + std_adj - 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
|
||||
|
||||
@@ -1646,6 +1663,7 @@ def create_key_counters(key_layout, world, player):
|
||||
key_layout.found_doors.clear()
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
state.init_zelda_event_doors(key_layout.event_starts, player)
|
||||
if world.doorShuffle[player] == 'vanilla':
|
||||
builder = world.dungeon_layouts[player][key_layout.sector.name]
|
||||
state.key_locations = len(builder.key_door_proposal) - builder.key_drop_cnt
|
||||
@@ -1678,10 +1696,10 @@ def create_key_counters(key_layout, world, player):
|
||||
if door.dest in flat_proposal and door.type != DoorType.SpiralStairs:
|
||||
key_layout.found_doors.add(door.dest)
|
||||
child_state = parent_state.copy()
|
||||
if door.bigKey or door.name in special_big_key_doors:
|
||||
if door.bigKey or door.name in get_special_big_key_doors(world, player):
|
||||
key_layout.key_logic.bk_doors.add(door)
|
||||
# open the door, if possible
|
||||
if can_open_door(door, child_state, world, player):
|
||||
if can_open_door(door, child_state, key_layout, world, player):
|
||||
open_a_door(door, child_state, flat_proposal, world, player)
|
||||
expand_key_state(child_state, flat_proposal, world, player)
|
||||
code = state_id(child_state, key_layout.flat_prop)
|
||||
@@ -1702,14 +1720,15 @@ def find_outside_connection(region):
|
||||
return None, None
|
||||
|
||||
|
||||
def can_open_door(door, state, world, player):
|
||||
def can_open_door(door, state, key_layout, world, player):
|
||||
if state.big_key_opened:
|
||||
ttl_locations = count_free_locations(state, world, player)
|
||||
else:
|
||||
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)
|
||||
available_small_locations = cnt_avail_small_locations(ttl_locations, ttl_small_key_only, state,
|
||||
key_layout, world, player)
|
||||
return available_small_locations > 0
|
||||
elif door.bigKey:
|
||||
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
|
||||
@@ -2075,6 +2094,12 @@ def validate_key_placement(key_layout, world, player):
|
||||
if world.bigkeyshuffle[player]:
|
||||
max_counter = find_max_counter(key_layout)
|
||||
big_key_outside = bigkey_name not in (l.item.name for l in max_counter.free_locations if l.item)
|
||||
for i in world.precollected_items:
|
||||
if i.player == player and i.name == bigkey_name:
|
||||
big_key_outside = True
|
||||
break
|
||||
if i.player == player and i.name == smallkey_name:
|
||||
keys_outside += 1
|
||||
|
||||
for code, counter in key_layout.key_counters.items():
|
||||
if len(counter.child_doors) == 0:
|
||||
|
||||
@@ -24,6 +24,7 @@ from RoomData import create_rooms
|
||||
from Rules import set_rules
|
||||
from Dungeons import create_dungeons
|
||||
from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dungeons_restrictive, ensure_good_items
|
||||
from Fill import dungeon_tracking
|
||||
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items
|
||||
from Utils import output_path, parse_player_names
|
||||
@@ -35,7 +36,7 @@ from source.classes.CustomSettings import CustomSettings
|
||||
from source.rom.DataTables import init_data_tables
|
||||
|
||||
|
||||
__version__ = '1.0.1.3-x'
|
||||
__version__ = '1.3.0.0-x'
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
|
||||
@@ -71,9 +72,8 @@ def main(args, seed=None, fish=None):
|
||||
if args.customizer:
|
||||
customized = CustomSettings()
|
||||
customized.load_yaml(args.customizer)
|
||||
seed = customized.determine_seed()
|
||||
if seed:
|
||||
seeded = True
|
||||
seed = customized.determine_seed(seed)
|
||||
seeded = True
|
||||
customized.adjust_args(args)
|
||||
world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
|
||||
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
|
||||
@@ -91,6 +91,7 @@ def main(args, seed=None, fish=None):
|
||||
if args.securerandom:
|
||||
world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9))
|
||||
|
||||
world.boots_hint = args.boots_hint.copy()
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
@@ -111,6 +112,8 @@ def main(args, seed=None, fish=None):
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
|
||||
world.door_type_mode = args.door_type_mode.copy()
|
||||
world.trap_door_mode = args.trap_door_mode.copy()
|
||||
world.key_logic_algorithm = args.key_logic_algorithm.copy()
|
||||
world.decoupledoors = args.decoupledoors.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
@@ -151,7 +154,7 @@ def main(args, seed=None, fish=None):
|
||||
world.player_names[player].append(name)
|
||||
logger.info('')
|
||||
world.settings = CustomSettings()
|
||||
world.settings.create_from_world(world)
|
||||
world.settings.create_from_world(world, args.race)
|
||||
|
||||
outfilebase = f'DR_{args.outputname if args.outputname else world.seed}'
|
||||
|
||||
@@ -261,6 +264,7 @@ def main(args, seed=None, fish=None):
|
||||
massage_item_pool(world)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_item_pool(world)
|
||||
dungeon_tracking(world)
|
||||
fill_specific_items(world)
|
||||
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
|
||||
|
||||
@@ -299,6 +303,7 @@ def main(args, seed=None, fish=None):
|
||||
balance_multiworld_progression(world)
|
||||
|
||||
# if we only check for beatable, we can do this sanity check first before creating the rom
|
||||
world.clear_exp_cache()
|
||||
if not world.can_beat_game(log_error=True):
|
||||
raise RuntimeError(world.fish.translate("cli", "cli", "cannot.beat.game"))
|
||||
|
||||
@@ -518,10 +523,8 @@ def copy_world(world):
|
||||
new_location.item = item
|
||||
item.location = new_location
|
||||
item.world = ret
|
||||
if location.event:
|
||||
new_location.event = True
|
||||
if location.locked:
|
||||
new_location.locked = True
|
||||
new_location.event = location.event
|
||||
new_location.locked = location.locked
|
||||
# these need to be modified properly by set_rules
|
||||
new_location.access_rule = lambda state: True
|
||||
new_location.item_rule = lambda state: True
|
||||
@@ -590,7 +593,8 @@ def create_playthrough(world):
|
||||
world = copy_world(world)
|
||||
|
||||
# get locations containing progress items
|
||||
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement]
|
||||
prog_locations = [location for location in world.get_filled_locations() if location.item.advancement
|
||||
or world.goal[location.player] == 'completionist']
|
||||
optional_locations = ['Trench 1 Switch', 'Trench 2 Switch', 'Ice Block Drop', 'Skull Star Tile']
|
||||
state_cache = [None]
|
||||
collection_spheres = []
|
||||
@@ -627,13 +631,15 @@ def create_playthrough(world):
|
||||
for num, sphere in reversed(list(enumerate(collection_spheres))):
|
||||
to_delete = set()
|
||||
for location in sphere:
|
||||
if world.goal[location.player] == 'completionist':
|
||||
continue # every location for that player is required
|
||||
# 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)
|
||||
old_item = location.item
|
||||
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]):
|
||||
world.clear_exp_cache()
|
||||
if world.can_beat_game(state_cache[max(num-1, 0)]):
|
||||
logging.getLogger('').debug(f'{old_item.name} (Player {old_item.player}) is not required')
|
||||
to_delete.add(location)
|
||||
else:
|
||||
|
||||
+7
-1
@@ -46,6 +46,9 @@ class Context:
|
||||
self.lookup_name_to_id = {}
|
||||
self.lookup_id_to_name = {}
|
||||
|
||||
self.disable_client_forfeit = False
|
||||
|
||||
|
||||
async def send_msgs(websocket, msgs):
|
||||
if not websocket or not websocket.open or websocket.closed:
|
||||
return
|
||||
@@ -281,7 +284,10 @@ async def process_client_cmd(ctx : Context, client : Client, cmd, args):
|
||||
if args.startswith('!players'):
|
||||
notify_all(ctx, get_connected_players_string(ctx))
|
||||
if args.startswith('!forfeit'):
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
if ctx.disable_client_forfeit:
|
||||
notify_client(client, 'Client-initiated forfeits are disabled. Please ask the host of this game to forfeit on your behalf.')
|
||||
else:
|
||||
forfeit_player(ctx, client.team, client.slot)
|
||||
if args.startswith('!countdown'):
|
||||
try:
|
||||
timer = int(args.split()[1])
|
||||
|
||||
+9
-9
@@ -745,11 +745,11 @@ vanilla_pots = {
|
||||
0xE7: [Pot(68, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB389, [0x8B, 0x2B, 0xFA])),
|
||||
Pot(72, 5, PotItem.OneRupee, 'Death Mountain Return Cave (right)', obj=RoomObject(0x0AB38C, [0x93, 0x2B, 0xFA]))],
|
||||
0xE8: [Pot(96, 4, PotItem.Heart, 'Superbunny Cave (Bottom)', obj=RoomObject(0x0AA98E, [0xC3, 0x23, 0xFA]))],
|
||||
0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])),
|
||||
Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])),
|
||||
Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])),
|
||||
Pot(92, 14, PotItem.Heart, 'Bumper Cave', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])),
|
||||
Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))],
|
||||
0xEB: [Pot(206, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADE7, [0x9F, 0x47, 0xFA])),
|
||||
Pot(210, 8, PotItem.FiveRupees, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADEA, [0xA7, 0x47, 0xFA])),
|
||||
Pot(88, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADED, [0xB3, 0x73, 0xFA])),
|
||||
Pot(92, 14, PotItem.Heart, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF0, [0xBB, 0x73, 0xFA])),
|
||||
Pot(96, 14, PotItem.SmallMagic, 'Bumper Cave (bottom)', obj=RoomObject(0x0AADF3, [0xC3, 0x73, 0xFA]))],
|
||||
0xF1: [Pot(64, 5, PotItem.Heart, 'Old Man Cave', obj=RoomObject(0x0AA6B2, [0x83, 0x2B, 0xFA]))],
|
||||
0xF3: [Pot(0x28, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA76F, [0x53, 0xA3, 0xFA])),
|
||||
Pot(0x2C, 0x14, PotItem.Nothing, 'Elder House', obj=RoomObject(0x0AA772, [0x5B, 0xA3, 0xFA])),
|
||||
@@ -851,10 +851,10 @@ vanilla_pots = {
|
||||
Pot(100, 22, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB62A, [0xCB, 0xB3, 0xFA])),
|
||||
Pot(88, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB633, [0xB3, 0xE3, 0xFA])),
|
||||
Pot(100, 28, PotItem.Heart, 'Dark Lake Hylia Ledge Spike Cave', obj=RoomObject(0x0AB636, [0xCB, 0xE3, 0xFA]))],
|
||||
0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801A, [0x33, 0xCB, 0xFA])),
|
||||
Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A801D, [0x3B, 0xCB, 0xFA])),
|
||||
Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8020, [0x43, 0xCB, 0xFA])),
|
||||
Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2A8023, [0x4B, 0xCB, 0xFA]))],
|
||||
0x127: [Pot(24, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801A, [0x33, 0xCB, 0xFA])),
|
||||
Pot(28, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B801D, [0x3B, 0xCB, 0xFA])),
|
||||
Pot(32, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8020, [0x43, 0xCB, 0xFA])),
|
||||
Pot(36, 25, PotItem.Nothing, 'Dark World Hammer Peg Cave', obj=RoomObject(0x2B8023, [0x4B, 0xCB, 0xFA]))],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,15 +4,47 @@ This is a door randomizer for _The Legend of Zelda: A Link to the Past_ for the
|
||||
based on the Entrance Randomizer found at [KevinCathcart's Github Project.](https://github.com/KevinCathcart/ALttPEntranceRandomizer)
|
||||
See https://alttpr.com/ for more details on the normal randomizer.
|
||||
|
||||
# Known Issues
|
||||
# Documentation
|
||||
1. [Setup and Installation](#setup-and-installation)
|
||||
2. [Commonly Missed Things](#commonly-missed-things) (** **Read This If New** **)
|
||||
3. [Settings](#settings)
|
||||
1. [Dungeon Randomization](#dungeon-settings)
|
||||
1. [Dungeon Door Shuffle](#door-shuffle)
|
||||
2. [Intensity Level](#intensity---intensity-number)
|
||||
3. [Key Drop Shuffle (Legacy)](#key-drop-shuffle-legacy---keydropshuffle)
|
||||
4. [Door Type Shuffle](#door-type-shuffle)
|
||||
5. [Trap Door Removal](#trap-door-removal)
|
||||
6. [Key Logic Algorithm](#key-logic-algorithm)
|
||||
7. [Decouple Doors](#decouple-doors)
|
||||
8. [Pottery](#pottery)
|
||||
9. [Small Key Shuffle](#small-key-shuffle)
|
||||
10. [Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
|
||||
11. [Experimental Features](#experimental-features)
|
||||
12. [Crossed Dungeon Specific Settings](#crossed-dungeon-specific-settings)
|
||||
2. [Item Randomization Changes](#item-randomization)
|
||||
1. [New "Items"](#new-items)
|
||||
2. [Shopsanity](#shopsanity)
|
||||
3. [Logic Level](#logic-level)
|
||||
4. [Goal](#goal)
|
||||
5. [Item Sorting](#item-sorting)
|
||||
6. [Forbidden Boss Items](#forbidden-boss-items)
|
||||
3. [Customizer](#customizer)
|
||||
4. [Entrance Randomization](#entrance-randomization)
|
||||
1. [Shuffle Links House](#shuffle-links-house)
|
||||
2. [Overworld Map](#overworld-map)
|
||||
5. [Enemizer](#enemizer)
|
||||
6. [Retro Changes](#retro-changes)
|
||||
7. [Standard Changes](#standard-changes)
|
||||
8. [Game Options](#game-options)
|
||||
9. [Generation Setup & Miscellaneous](#generation-setup--miscellaneous)
|
||||
|
||||
[List of Known Issues and Their Status](https://docs.google.com/document/d/1Bk-m-QRvH5iF60ndptKYgyaV7P93D3TiG8xmdxp_bdQ/edit?usp=sharing)
|
||||
## Setup and Installation
|
||||
|
||||
# Feedback and Bug Reports
|
||||
### Feedback and Bug Reports
|
||||
|
||||
Please just DM me on discord for now. I (Aerinon) can be found at the [ALTTP Randomizer discord](https://discordapp.com/invite/alttprandomizer).
|
||||
You can use the #bug-reports or #door-rando channel at the [ALTTP Randomizer discord](https://discordapp.com/invite/alttprandomizer) to provide feedback or bug reports.
|
||||
|
||||
# Installation
|
||||
### Installation
|
||||
|
||||
Click on
|
||||
|
||||
@@ -22,7 +54,7 @@ Go down to Assets and find a build for your system (Windows, Mac, or Linux)
|
||||
|
||||
Download and unzip. Find the DungeonRandomizer.exe or equivalent
|
||||
|
||||
# Installation from source
|
||||
### Installation from source
|
||||
|
||||
See these instructions.
|
||||
|
||||
@@ -36,21 +68,21 @@ To use the CLI, run ```DungeonRandomizer.py```.
|
||||
|
||||
Alternatively, run ```Gui.py``` for a simple graphical user interface.
|
||||
|
||||
# Commonly Missed Things and Differences from other Randomizers
|
||||
# Commonly Missed Things
|
||||
#### and Differences from other Randomizers
|
||||
|
||||
Most of these apply only when the door shuffle is not vanilla.
|
||||
|
||||
### Starting Item
|
||||
|
||||
You start with a “Mirror Scroll”, a dumbed-down mirror that only works in dungeons, not the overworld and can’t erase blocks like the Mirror.
|
||||
You start with a “Mirror Scroll” (it looks like a map), a dumbed-down mirror that only works in dungeons, not the overworld, and can’t erase blocks like the Mirror.
|
||||
|
||||
### Navigation
|
||||
|
||||
* The Pinball Room’s trap door can be removed in the case where it is required to go through to get to the back of Skull Woods.
|
||||
* Holes in Mire Torches Top and Mire Torches Bottom fall through to rooms below (you only need fire to get the chest)
|
||||
* You can Hookshot from the left Mire wooden Bridge to the right one.
|
||||
* In the PoD Arena, you can bonk with Boots between the two blue crystal barriers against the ladder to reach the Arena Bridge chest and door. (Bomb Jump also possible but not in logic - Boots are required)
|
||||
* Flooded Rooms in Swamp can be traversed backward and may be required.
|
||||
* Flooded Rooms in Swamp can be traversed backward and may be required. The flippers are needed to get out of the water.
|
||||
|
||||
### Other Logic
|
||||
|
||||
@@ -76,69 +108,189 @@ You start with a “Mirror Scroll”, a dumbed-down mirror that only works in du
|
||||
|
||||
# Settings
|
||||
|
||||
## Dungeon Settings
|
||||
|
||||
Only extra settings are found here. All entrance randomizer settings are supported. See their [readme](https://github.com/KevinCathcart/ALttPEntranceRandomizer/blob/master/README.md)
|
||||
|
||||
## Door Shuffle (--doorShuffle)
|
||||
### Door Shuffle
|
||||
|
||||
### Basic
|
||||
* Vanilla - Doors are not shuffled
|
||||
* Basic - Doors are shuffled only within a single dungeon.
|
||||
* Paritioned - Dungeons are shuffled in 3 pools: Light World, Early Dark World, Late Dark World. (Late Dark are the four dungeons that require Mitts in vanilla, including Ganons Tower)
|
||||
* Crossed - Doors are shuffled between dungeons as well.
|
||||
|
||||
Doors are shuffled only within a single dungeon.
|
||||
CLI: `--doorShuffle [vanilla|basic|partitioned|crossed]`
|
||||
|
||||
### Crossed
|
||||
### Intensity (--intensity number)
|
||||
|
||||
Doors are shuffled between dungeons as well.
|
||||
* Level 1 - Normal door and spiral staircases are shuffled
|
||||
* Level 2 - Same as Level 1 plus open edges and both types of straight staircases are shuffled.
|
||||
* Level 3 - Same as Level 2 plus Dungeon Lobbies are shuffled
|
||||
|
||||
### Vanilla
|
||||
|
||||
Doors are not shuffled.
|
||||
|
||||
## Intensity (--intensity number)
|
||||
|
||||
#### Level 1
|
||||
Normal door and spiral staircases are shuffled
|
||||
#### Level 2
|
||||
Same as Level 1 plus open edges and both types of straight staircases are shuffled.
|
||||
#### Level 3
|
||||
Same as Level 2 plus Dungeon Lobbies are shuffled
|
||||
|
||||
## KeyDropShuffle (--keydropshuffle)
|
||||
### Key Drop Shuffle (Legacy) (--keydropshuffle)
|
||||
|
||||
Adds 33 new locations to the randomization pool. The 32 small keys found under pots and dropped by enemies and the Big
|
||||
Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds
|
||||
32 generic keys to the pool instead.
|
||||
Key drop location are added to the pool. The keys normally found there are added to the item pool. Retro adds
|
||||
32 generic keys to the pool instead. This has been can be controlled more granularly with the [Pottery](#pottery) and
|
||||
[Shuffle Enemy Key Drops](#shuffle-enemy-key-drops)
|
||||
|
||||
## Crossed Dungeon Specific Settings
|
||||
### Door Type Shuffle
|
||||
|
||||
### Mixed Travel (--mixed_travel value)
|
||||
Four options here, and all of them only take effect if Dungeon Door Shuffle is not vanilla:
|
||||
|
||||
Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump two sections of a supertile that are
|
||||
* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously
|
||||
* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change
|
||||
* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled, excluding those by bosses.
|
||||
* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more.
|
||||
|
||||
CLI: `--door_type_mode [original|big|all|chaos]`
|
||||
|
||||
### Trap Door Removal
|
||||
|
||||
Options here for making dungeon traversal nicer. Only applies if door shuffle is not vanilla.
|
||||
|
||||
* No Removal: This does not remove any trap doors.
|
||||
* Removed If Blocking Path: Dungeon generation is relaxed to allow annoying trap doors to be removed if necessary. Note that boss trap doors are never shuffled in this mode.
|
||||
* Remove Boss Traps: Boss traps are removed, this includes the one near Mothula.
|
||||
* Remove All Annoying Traps: This removes all trap doors that are annoying, including boss traps.
|
||||
|
||||
If trap doors are shuffled the first two option behave the same. The last option overrides the shuffle because there is nothing left to shuffle. Boss traps are never shuffled.
|
||||
|
||||
In all cases, that the trap door near the mire cutscene chest (Mire Warping Pool ES) is left alone because it enforces the use of fire to get to the chest.
|
||||
|
||||
CLI: `--trap_door_mode [vanilla|optional|boss|oneway]`
|
||||
|
||||
### Key Logic Algorithm
|
||||
|
||||
Determines how small key door logic works.
|
||||
|
||||
* Default: Current key logic. Assumes worse case usage, placement checks, but assumes you can't get to a chest until you have sufficient keys. (May assume items are unreachable)
|
||||
* Partial Protection: Assumes you always have full inventory and worse case usage. This should account for dark room and bunny revival glitches.
|
||||
* Strict: For those would like to glitch and be protected from yourselves. Small keys door require all small keys to be available to be in logic.
|
||||
|
||||
CLI: `--key_logic [default|partial|strict]`
|
||||
|
||||
### Decouple Doors
|
||||
|
||||
This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
|
||||
|
||||
CLI `--decoupledoors`
|
||||
|
||||
### Pottery
|
||||
|
||||
New pottery option that control which pots (and large blocks) are in the locations pool:
|
||||
|
||||
* None: No pots are in the pool, like normal randomizer
|
||||
* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option
|
||||
* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does
|
||||
not include key pots.
|
||||
* Cave + Keys Pots: Both non-dungeon pots and pots that used to have keys are in the pool.
|
||||
* Reduced Dungeon Pots: Same as Cave+Keys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents.
|
||||
* Clustered Dungeon Pots: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those groups. This is a dynamic mode like the above.
|
||||
* Excludes Empty Pots: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons.
|
||||
* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks)
|
||||
* Lottery: All pots and large blocks are in the pool.
|
||||
|
||||
By default, switches remain in their vanilla location (unless you turn on the legacy option below)
|
||||
|
||||
CLI `--pottery <option>` from `none, keys, cave, cavekeys, reduced, clustered, nonempty, dungeon, lottery`
|
||||
|
||||
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
|
||||
|
||||
#### Colorize Pots
|
||||
|
||||
If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly.
|
||||
|
||||
CLI `--colorizepots`
|
||||
|
||||
#### Pot Shuffle (Legacy)
|
||||
|
||||
This continues to works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
|
||||
|
||||
### Small Key Shuffle
|
||||
|
||||
There are three options now available:
|
||||
|
||||
* In Dungeon: The small key will be in their own dungeon
|
||||
* Randomized: Small keys can be shuffled outside their own dungeon
|
||||
* Universal: Retro keys without the other options
|
||||
|
||||
CLI: `--keyshuffle [none|wild|universal]`
|
||||
|
||||
### Shuffle Enemy Key Drops
|
||||
|
||||
Enemies that drop keys can have their drop shuffled into the pool. This is the one part of the keydropshuffle option.
|
||||
See the pottery option for more options involving pots.
|
||||
|
||||
CLI: `--dropshuffle`
|
||||
|
||||
### Experimental Features
|
||||
|
||||
You will start as a bunny if your spawn point is in the dark world. CLI: `--experimental`
|
||||
|
||||
### Crossed Dungeon Specific Settings
|
||||
|
||||
#### Dungeon Chest Counters
|
||||
|
||||
* Auto - picks an appropriate setting based on other settings. Generally will be pickup if the number of item in a dungeon changes.
|
||||
* On - Dungeon counters on hud always displayed
|
||||
* Off - Dungeon counters on hud never displayer
|
||||
* On Compass Pickup - Dungeons with a compass item will display the counter once the compass is found. Dungeons without a compass item will display always unless the number of items in the dungeon is completely vanilla.
|
||||
|
||||
CLI: `--dungeon_counters` from `auto, on, off, pickup`
|
||||
|
||||
#### Mixed Travel (--mixed_travel value)
|
||||
|
||||
Due to Hammerjump, Hovering in PoD Arena, and the Mire Big Key Chest bomb jump, two sections of a supertile that are
|
||||
otherwise unconnected logically can be reached using these glitches. To prevent the player from unintentionally changing
|
||||
dungeons while doing these tricks, you may use one of the following options.
|
||||
dungeons while doing these tricks, you may use one of the following options:
|
||||
|
||||
#### Prevent (default)
|
||||
* Prevent (default):
|
||||
Rails are added the 3 spots to prevent these tricks. This setting is recommend for those learning crossed dungeon mode to learn what is dangerous and what is not. No logic seeds ignore this setting.
|
||||
* Allow: The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not.
|
||||
* Force: The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.
|
||||
|
||||
Rails are added the 3 spots to prevent this tricks. This setting is recommend for those learning crossed dungeon mode to
|
||||
learn what is dangerous and what is not. No logic seeds ignore this setting.
|
||||
|
||||
#### Allow
|
||||
|
||||
The rooms are left alone and it is up to the discretion of the player whether to use these tricks or not.
|
||||
|
||||
#### Force
|
||||
|
||||
The two disjointed sections are forced to be in the same dungeon but the glitches are never logically required to complete that game.cause then you would need time to check the map in a d
|
||||
|
||||
### Standardize Palettes (--standardize_palettes)
|
||||
#### Standardize Palettes (--standardize_palettes)
|
||||
No effect if door shuffle is not on crossed
|
||||
|
||||
#### Standardize (default)
|
||||
Rooms in the same dungeon have their palettes changed to match. Hyrule Castle is split between Sewer and HC palette.
|
||||
* Standardize (default): Rooms in the same dungeon have their palettes changed to match. Hyrule Castle is split between Sewer and HC palette.
|
||||
Rooms adjacent to sanctuary get their coloring to match the Sanctuary's original palette.
|
||||
* Original: Rooms/supertiles keep their original palettes.
|
||||
|
||||
#### Original
|
||||
Rooms/supertiles keep their original palettes.
|
||||
## Item Randomization
|
||||
|
||||
## Shopsanity
|
||||
### New "Items"
|
||||
|
||||
#### Bombbag
|
||||
|
||||
Two bomb bags are added to the item pool (They look like +10 Capacity upgrades). Bombs are unable to be used until one is found. Bomb capacity upgrades are otherwise unavailable.
|
||||
|
||||
CLI `--bombbag`
|
||||
|
||||
#### Pseudo Boots
|
||||
|
||||
Dashing is allowed without the boots item however doors and certain rocks remain un-openable until boots are found. Items that require boots are still unattainable. Specific sequence breaks like hovering and water-walking are not allowed until boots are found. Bonk distance is shortened to prevent certain pits from being crossed. Finding boots restores all normal behavior.
|
||||
|
||||
CLI `--pseudoboots`
|
||||
|
||||
#### Flute Mode
|
||||
|
||||
Normal mode for flute means you need to activate it at the village statue after finding it like usual. Activated flute mode mean you can use it immediately upon finding it. The flute SFX plays to let you know this is the case.
|
||||
|
||||
CLI:`--flute_mode`
|
||||
|
||||
#### Bow Mode
|
||||
|
||||
Four options here :
|
||||
|
||||
* Progressive. Standard progressive bows.
|
||||
* Silvers separate. One bow in the pool and silvers are a separate item.
|
||||
* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places.
|
||||
* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found).
|
||||
|
||||
CLI: `--bow_mode [progressive|silvers|retro|retro_silvers]`
|
||||
|
||||
### Shopsanity
|
||||
|
||||
This adds 32 shop locations (9 more in retro) to the general location pool.
|
||||
|
||||
@@ -192,34 +344,34 @@ Detailed explanation: It is calculated how much money is available in the item p
|
||||
|
||||
All prices range approx. from half the base price to twice the base price (as a max) in increments of 5, the exact price is chosen randomly within the range subject to adjustments by the sphere effects above.
|
||||
|
||||
| Category | Items | Base Price | Typical Range |
|
||||
| ----------------- | ------- |:----------:|:-------------:|
|
||||
| Major Progression | Hammer, Hookshot, Mirror, Ocarina, Boots, Somaria, Fire Rod, Ice Rod | 250 | 125-500
|
||||
| | Moon Pearl | 200 | 100-400
|
||||
| | Lamp, Progressive Bows, Gloves, & Swords | 150 | 75-300
|
||||
| | Triforce Piece | 100 | 50-200
|
||||
| Medallions | Bombos, Ether, Quake | 100 | 50-200
|
||||
| Safety/Fetch | Cape, Mushroom, Shovel, Powder, Bug Net, Byrna, Progressive Armor & Shields, Half Magic | 50 | 25-100
|
||||
| Bottles | Empty Bottle or Bee Bottle | 50 | 25-100
|
||||
| | Green Goo or Good Bee | 60 | 30-120
|
||||
| | Red Goo or Fairy | 70 | 35-140
|
||||
| | Blue Goo | 80 | 40-160
|
||||
| Health | Heart Container | 40 | 20-80
|
||||
| | Sanctuary Heart | 50 | 25-100
|
||||
| | Piece of Heart | 10 | 5-20
|
||||
| Dungeon | Big Keys | 60 | 30-120
|
||||
| | Small Keys | 40 | 20-80
|
||||
| | Info Maps | 20 | 10-40
|
||||
| | Other Maps & Compasses | 10 | 5-20
|
||||
| Rupees | Green | Free | Free
|
||||
| | Blue | 2 | 2-4
|
||||
| | Red | 10 | 5-20
|
||||
| | Fifty | 25 | 15-50
|
||||
| | One Hundred | 50 | 25-100
|
||||
| | Three Hundred | 150 | 75-300
|
||||
| Ammo | Three Bombs | 15 | 10-30
|
||||
| | Single Arrow | 3 | 3-6
|
||||
| Original Shop Items | Other Ammo, Refills, Non-Progressive Shields, Capacity Upgrades, Small Hearts, Retro Quiver, Universal Key | Original | .5 - 2 * Original
|
||||
| Category | Items | Base Price | Typical Range |
|
||||
|---------------------|------------------------------------------------------------------------------------------------------------|:----------:|:-----------------:|
|
||||
| Major Progression | Hammer, Hookshot, Mirror, Ocarina, Boots, Somaria, Fire Rod, Ice Rod | 250 | 125-500 |
|
||||
| | Moon Pearl | 200 | 100-400 |
|
||||
| | Lamp, Progressive Bows, Gloves, & Swords | 150 | 75-300 |
|
||||
| | Triforce Piece | 100 | 50-200 |
|
||||
| Medallions | Bombos, Ether, Quake | 100 | 50-200 |
|
||||
| Safety/Fetch | Cape, Mushroom, Shovel, Powder, Bug Net, Byrna, Progressive Armor & Shields, Half Magic | 50 | 25-100 |
|
||||
| Bottles | Empty Bottle or Bee Bottle | 50 | 25-100 |
|
||||
| | Green Goo or Good Bee | 60 | 30-120 |
|
||||
| | Red Goo or Fairy | 70 | 35-140 |
|
||||
| | Blue Goo | 80 | 40-160 |
|
||||
| Health | Heart Container | 40 | 20-80 |
|
||||
| | Sanctuary Heart | 50 | 25-100 |
|
||||
| | Piece of Heart | 10 | 5-20 |
|
||||
| Dungeon | Big Keys | 60 | 30-120 |
|
||||
| | Small Keys | 40 | 20-80 |
|
||||
| | Info Maps | 20 | 10-40 |
|
||||
| | Other Maps & Compasses | 10 | 5-20 |
|
||||
| Rupees | Green | Free | Free |
|
||||
| | Blue | 2 | 2-4 |
|
||||
| | Red | 10 | 5-20 |
|
||||
| | Fifty | 25 | 15-50 |
|
||||
| | One Hundred | 50 | 25-100 |
|
||||
| | Three Hundred | 150 | 75-300 |
|
||||
| Ammo | Three Bombs | 15 | 10-30 |
|
||||
| | Single Arrow | 3 | 3-6 |
|
||||
| Original Shop Items | Other Ammo, Refills, Non-Progressive Shields, Capacity Upgrades, Small Hearts, Retro Quiver, Universal Key | Original | .5 - 2 * Original |
|
||||
|
||||
#### Rupee Balancing Algorithm
|
||||
|
||||
@@ -241,39 +393,187 @@ Arrow Capacity upgrades are now replaced by Rupees wherever it might end up.
|
||||
|
||||
The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item (represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys retain their original base price, but may be discounted.
|
||||
|
||||
## Logic Level
|
||||
### Logic Level
|
||||
|
||||
### Overworld Glitches
|
||||
Overworld Glitches is now supported.
|
||||
|
||||
Set `--logic` to `owglitches` to make overworld glitches required in the logic.
|
||||
CLI: `--logic owglitches`
|
||||
|
||||
## Shuffle Links House
|
||||
### Goal
|
||||
|
||||
New supported goals:
|
||||
|
||||
* Trinity: Find one of 3 triforces to win. One is at pedestal. One is with Ganon. One is with Murahdahla who wants you to find 8 of 10 triforce pieces to complete.
|
||||
* Triforce Hunt + Ganon: Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
|
||||
* Completionist: All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
|
||||
|
||||
|
||||
### Item Sorting
|
||||
|
||||
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
|
||||
|
||||
When referenced below, Major Items include all Y items, all A items, all equipment (swords, shields, & armor) and Heart Containers. Dungeon items are considered major if shuffled outside of dungeons. Bomb and arrows upgrades are Major if shopsanity is turned on. The arrow quiver and universal small keys are Major if retro is turned on. Triforce Pieces are Major if that is the goal, and the Bomb Bag is Major if that is enabled.
|
||||
|
||||
Here are the current fill options:
|
||||
|
||||
#### Balanced
|
||||
|
||||
This one stays the same as before and is recommended for the most random distribution of items.
|
||||
|
||||
#### Vanilla Fill
|
||||
|
||||
This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation.
|
||||
|
||||
#### Major Location Restriction
|
||||
|
||||
This fill attempts to place major items in major locations. Major locations are where the major items are found in the vanilla game. This includes the spot next to Uncle in the Sewers, and the Boomerang chest in Hyrule Castle.
|
||||
|
||||
This location pool is expanded to where dungeon items are locations if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the Old Man Sword Cave and one location in each retro cave is included. Key drop locations can be included if small or big key shuffle is on. This gives a very good balance between overworld and underworld locations though the dungeons ones will be on bosses and in big chests generally. Seeds do become more linear but usually easier to figure out.
|
||||
|
||||
#### Dungeon Restriction
|
||||
|
||||
The fill attempts to place all major items in dungeons. It will overflow to the overworld if there are more items than locations (e.g. Triforce hunt.) This fill does attempt to run the GT trash fill when possible. Seeds are typically very linear but tend to be more difficult.
|
||||
|
||||
#### District Restriction
|
||||
|
||||
The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts:
|
||||
|
||||
Light world:
|
||||
|
||||
* Kakariko (The main screen, blacksmith screen, and library/maze race screens)
|
||||
* Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop)
|
||||
* Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove)
|
||||
* Desert (From the thief to the main desert screen)
|
||||
* Lake Hylia (Around the lake)
|
||||
* Eastern Hyrule (The eastern wild, the potion shop, and Zora's Domain)
|
||||
* Death Mountain
|
||||
|
||||
Dark world:
|
||||
|
||||
* East Dark World (The pyramid, Palace of darkness, and Catfish)
|
||||
* South Dark World (The dark lake, swamp area, to the dig game)
|
||||
* Northwest Dark World (Village of Outcasts, to the Dark Sanctuary and screens in between)
|
||||
* The Mire
|
||||
* Dark Death Mountain
|
||||
|
||||
These districts are chosen at random and then filled with major items. If a location is part of a chosen district, but there are no more major items to place, a single green rupee is placed in the extra to indicate that as a placeholder. All other single green rupees are changed to be a blue rupee in order to not give false positives.
|
||||
|
||||
In entrance shuffle, what is shuffled to the entrances is considered instead of where the interior was originally. For example, if Blind's Hut is shuffled to the Dam, then the 5 chests in Blind's Hut are part of Central Hyrule instead of Kakariko.
|
||||
|
||||
Note: Bombos Tablet, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the light world region rather than the dark world region you mirror from.
|
||||
|
||||
In multiworld, the districts chosen apply to all players.
|
||||
|
||||
#### CLI values:
|
||||
|
||||
```balanced, vanilla_fill, major_only, dungeon_only, district```
|
||||
|
||||
#### New Hints
|
||||
|
||||
Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish.
|
||||
|
||||
### Forbidden Boss Items
|
||||
|
||||
You may now restrict the items that can appear on the boss, like the popular ambrosia preset does.
|
||||
|
||||
CLI: ```--restrict_boss_items <option>```
|
||||
|
||||
#### Options
|
||||
|
||||
* none: As before, the boss may have any item including any dungeon item that could occur there.
|
||||
* mapcompass: ~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~ Currently bugged, not recommended for use.
|
||||
* dungeon: Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific)
|
||||
|
||||
## Customizer
|
||||
|
||||
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
|
||||
|
||||
## Entrance Randomization
|
||||
|
||||
### New Modes
|
||||
|
||||
Lite and Lean ER is now available when Experimental Features are turned on. (todo: bring over documenation of these modes)
|
||||
|
||||
### Shuffle Links House
|
||||
|
||||
In certain ER shuffles, (not dungeonssimple or dungeonsfulls), you can now control whether Links House is shuffled or remains vanilla. Previously, inverted seeds had this behavior and would shuffle links house, but now if will only do so if this is specified. Now, also works for open modes, but links house is never shuffled in standard mode.
|
||||
|
||||
## Bomb Logic (--bombbag)
|
||||
### Shuffle Back of Tavern
|
||||
|
||||
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.
|
||||
You may shuffle the back of tavern entrance in ER modes when Experimental Features are turned on.
|
||||
|
||||
## Reduce Flashing
|
||||
### Overworld Map
|
||||
|
||||
Accessibility option to reducing some flashing animations in the game.
|
||||
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
|
||||
|
||||
## Pseudo-boots
|
||||
* Default: Status quo. Showing only the prize markers on the vanilla dungeon locations.
|
||||
* Compass:
|
||||
The compass item controls whether the marker is moved to the dungeons locations. If you possess the compass but not the map, only a glowing X will be present regardless of dungeon prize type, if you only possess the map, the prizes will be shown in predicable locations at the bottom of the overworld map instead of the vanilla location. Light world dungeons on the light world map and dark world dungeons on the dark world map. If you posses both map and compass, then the prize of the dungeon and the location will be on the map.
|
||||
* Map: The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items.
|
||||
|
||||
Option to start with ability to dash, but not able to make any boots required logical checks or traversal.
|
||||
If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information.
|
||||
|
||||
## SFX Shuffle (--shuffle_sfx)
|
||||
CLI ```--overworld_map [default|compass|map]```
|
||||
|
||||
|
||||
|
||||
## Enemizer
|
||||
|
||||
### Boss Shuffle: Unique
|
||||
|
||||
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
|
||||
|
||||
### Blind Note
|
||||
|
||||
If bosses are shuffled and Blind is chosen to be the boss of Thieves Town, then bombing the attic and delivering the maiden is still required.
|
||||
|
||||
## Standard Changes
|
||||
|
||||
When dungeon door shuffle is on, the Sanctuary is guaranteed to be behind the Throne Room and the Mirror Scroll works like death warping instead of the mirror during the escape sequence.
|
||||
|
||||
## Retro Changes
|
||||
|
||||
Retro can be partially enabled: see Small Key Shuffle and Bow Mode. Retro checkbox or Retro mode still enable all 3 options.
|
||||
|
||||
New supported option:
|
||||
|
||||
### Take Any Caves
|
||||
|
||||
These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are:
|
||||
|
||||
* Desert Healer Fairy
|
||||
* Swamp Healer Fairy (aka Light Hype Cave)
|
||||
* Dark Death Mountain Healer Fairy
|
||||
* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
|
||||
* Bonk Fairy (Dark)
|
||||
|
||||
## Game Options
|
||||
|
||||
### MSU Resume
|
||||
|
||||
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
|
||||
|
||||
### Collection Rate
|
||||
|
||||
Display the collection rate unless the triforce piece counter is needed. If the game was generated as a mystery, then the total count is not displayed. `--collection_rate`
|
||||
|
||||
### Reduce Flashing
|
||||
|
||||
Accessibility option to reducing some flashing animations in the game. `--reduce_flashing`
|
||||
|
||||
### Shuffle Sound Effects (--shuffle_sfx)
|
||||
|
||||
Shuffles a large portion of the sounds effects. Can be used with the adjuster.
|
||||
|
||||
## Experimental Features
|
||||
## Generation Setup & Miscellaneous
|
||||
|
||||
You will start as a bunny if your spawn point is in the dark world.
|
||||
### Create BPS Patches
|
||||
|
||||
## Triforce Hunt Settings
|
||||
Create bps patch(es) instead of generating rom(s) for distribution. `--bps`
|
||||
|
||||
A collection of settings to control the triforce piece pool.
|
||||
### Triforce Hunt Settings
|
||||
|
||||
A collection of settings to control the triforce piece pool for the CLI/Mystery
|
||||
|
||||
* --triforce_goal_min: Minimum number of pieces to collect to win
|
||||
* --triforce_goal_max: Maximum number of pieces to collect to win
|
||||
@@ -281,110 +581,13 @@ A collection of settings to control the triforce piece pool.
|
||||
* --triforce_pool_max: Maximum number of pieces in item pool
|
||||
* --triforce_min_difference: Minimum difference between pool and goal to win
|
||||
|
||||
## Seed
|
||||
### Seed
|
||||
|
||||
Can be used to set a seed number to generate. Using the same seed with same settings on the same version of the entrance randomizer will always yield an identical output.
|
||||
|
||||
## Count
|
||||
### Count
|
||||
|
||||
Use to batch generate multiple seeds with same settings. If a seed number is provided, it will be used for the first seed, then used to derive the next seed (i.e. generating 10 seeds with the same seed number given will produce the same 10 (different) roms each time).
|
||||
|
||||
# Command Line Options
|
||||
|
||||
```
|
||||
-h, --help
|
||||
```
|
||||
|
||||
Show the help message and exit.
|
||||
|
||||
```
|
||||
--door_shuffle <mode>
|
||||
```
|
||||
|
||||
For specifying the door shuffle you want as above. (default: basic)
|
||||
|
||||
```
|
||||
--intensity <number>
|
||||
```
|
||||
|
||||
For specifying the door shuffle intensity level you want as above. (default: 2)
|
||||
|
||||
```
|
||||
--keydropshuffle
|
||||
```
|
||||
|
||||
Include mobs and pots drop in the item pool. (default: not enabled)
|
||||
|
||||
```
|
||||
--shopsanity
|
||||
```
|
||||
|
||||
Includes shop locations in the item pool.
|
||||
|
||||
```
|
||||
--pseudoboots
|
||||
```
|
||||
|
||||
Start with dash ability, but no way to use boots to accomplish checks
|
||||
|
||||
```
|
||||
--shufflelinks
|
||||
```
|
||||
|
||||
Whether to shuffle links house in most ER modes.
|
||||
|
||||
```
|
||||
--shuffletavern
|
||||
```
|
||||
|
||||
Whether to shuffle the back of the tavern in most ER modes.
|
||||
|
||||
```
|
||||
--bombbag
|
||||
```
|
||||
Need to find the bombbag upgrade to used bombs
|
||||
|
||||
```
|
||||
--experimental
|
||||
```
|
||||
|
||||
Enables experimental features
|
||||
|
||||
```
|
||||
--mixed_travel <mode>
|
||||
```
|
||||
|
||||
How to handle certain glitches in crossed dungeon mode. (default: prevent)
|
||||
|
||||
```
|
||||
--standardize_palettes (mode)
|
||||
```
|
||||
|
||||
Whether to standardize dungeon palettes in crossed dungeon mode. (default: standardize)
|
||||
|
||||
```
|
||||
--reduce_flashing
|
||||
```
|
||||
|
||||
Reduces amount of flashing in some animations
|
||||
|
||||
```
|
||||
--shuffle_sfx
|
||||
```
|
||||
|
||||
Shuffles a bunch of the sounds effects
|
||||
|
||||
```
|
||||
--msu_resume
|
||||
```
|
||||
|
||||
Enable MSU resume feature
|
||||
|
||||
```
|
||||
--collection_rate
|
||||
```
|
||||
|
||||
Total collection rate is displayed unless Triforce Hunt is on. If the game was designed as a mystery, then the total count is not displayed.
|
||||
|
||||
|
||||
|
||||
|
||||
+145
-252
@@ -1,297 +1,190 @@
|
||||
## New Features
|
||||
# New Features
|
||||
|
||||
## Pottery Lottery and Key Drop Shuffle Changes
|
||||
One major change with this update is that big key doors and certain trap doors are no longer guaranteed to be vanilla in Dungeon Door Shuffle modes even if you choose not to shuffle those types. A newer algorithm for putting dungeons together has been written and it will remove big key doors and trap doors when necessary to ensure progress can be made.
|
||||
|
||||
### Pottery
|
||||
Please note that retro features are now independently customizable as referenced below. Selecting Retro mode or World State: Retro will change Bow Mode to Retro (Progressive). Take Anys to Random, and Small Keys to Universal.
|
||||
|
||||
New pottery option that control which pots (and large blocks) are in the locations pool:
|
||||
## Flute Mode
|
||||
|
||||
* None: No pots are in the pool, like normal randomizer
|
||||
* Key Pots: The pots that have keys are in the pool. This is about half of the old keydropshuffle option
|
||||
* Cave Pots: The pots that are not found in dungeons are in the pool. (Includes the large block in Spike Cave). Does
|
||||
not include key pots.
|
||||
* CaveKeys: Both non-dungeon pots and pots that used to have keys are in the pool.
|
||||
* Reduced: Same as CaveKeys but also roughly a quarter of dungeon pots are added to the location pool picked at random. This is a dynamic mode so pots in the pool will be colored. Pots out of the pool will have vanilla contents.
|
||||
* Clustered: Like reduced but pots are grouped by logical sets and roughly 50% of pots are chosen from those group. This is a dynamic mode like the above.
|
||||
* Nonempty: All pots that had some sort of objects under them are chosen to be in the location pool. This excludes most large blocks and some pots out of dungeons.
|
||||
* Dungeon Pots: The pots that are in dungeons are in the pool. (Includes serveral large blocks)
|
||||
* Lottery: All pots and large blocks are in the pool
|
||||
Normal mode for flute means you need to activate it at the village statue after finding it like usual.
|
||||
Activated flute mode mean you can use it immediately upon finding it. the flute SFX plays to let you know this is the case.
|
||||
|
||||
By default, switches remain in their vanilla location (unless you turn on the legacy option below)
|
||||
## Bow Mode
|
||||
|
||||
CLI `--pottery <option>` from `none, keys, cave, cavekeys, reduced, clustered, nonempty, dungeon, lottery`
|
||||
Four options here:
|
||||
|
||||
Note for multiworld: due to the design of the pottery lottery, only 256 items for other players can be under pots in your world.
|
||||
* Progressive. Standard progressive bows.
|
||||
* Silvers separate. One bow in the pool and silvers are a separate item.
|
||||
* Retro (progressive). Arrows cost rupees. You need to purchase the single arrow item at a shop and there are two progressive bows places.
|
||||
* Retro + Silvers. Arrows cost rupees. You need to purchase the single arrow item or find the silvers, there is only one bow, and silvers are a separate item (but count for the quiver if found).
|
||||
|
||||
### Colorize Pots
|
||||
## Dungeon Shuffle Features
|
||||
|
||||
If the pottery mode is dynamic, this option is forced to be on (clustered and reduced). It is allowed to be on in all other pottery modes. Exceptions include "none" where no pots would be colored, and "lottery" where all pots would be. This option colors the pots differently that have been chosen to be part of the location pool. If not specified, you are expected to remember the pottery setting you chose. Note that Mystery will colorize all pots if lottery is chosen randomly.
|
||||
### Small Keys
|
||||
|
||||
CLI `--colorizepots`
|
||||
There are three options now available:
|
||||
|
||||
### Shuffle key drops
|
||||
* In Dungeon: The small key will be in their own dungeon
|
||||
* Randomized: Small keys can be shuffled outside their own dungeon
|
||||
* Universal: Retro keys without the other options
|
||||
|
||||
Enemies that drop keys can have their drop shuffled into the pool. This is the other half of the keydropshuffle option.
|
||||
### Dungeon Door Shuffle
|
||||
|
||||
CLI `--dropshuffle`
|
||||
New mode: Partitioned. Partly between basic and crossed, dungeons are shuffled in 3 pools:
|
||||
|
||||
#### Legacy options
|
||||
* Light World dungeons, Hyrule Castle and Aga Tower are mixed together
|
||||
* Palace of Darkness, Swamp Palace, Skull Woods, and Thieves Town are mixed together
|
||||
* The other dark world dungeons including Ganons Tower are mixed together
|
||||
|
||||
"Drop and Pot Keys" or `--keydropshuffle` is still availabe for use. This simply sets the pottery to keys and turns dropshuffle on as well to have the same behavior as the old setting.
|
||||
### Door Types to Shuffle
|
||||
|
||||
The old "Pot Shuffle" option is still available under "Pot Shuffle (Legacy)" or `--shufflepots` and works the same by shuffling all pots on a supertile. It works with the lottery option as well to move the switches to any valid pot on the supertile regardless of the pots chosen in the pottery mode. This may increase the number of pot locations slightly depending on the mode.
|
||||
Four options here, and all of them only take effect if Dungeon Door Shuffle is not Vanilla:
|
||||
|
||||
#### Tracking Notes
|
||||
* Small Key Doors, Bomb Doors, Dash Doors: This is what was normally shuffled previously
|
||||
* Adds Big Keys Doors: Big key doors are now shuffled in addition to those above, and Big Key doors are enabled to be on in both vertical directions thanks to a graphic that ended up on the cutting room floor. This does change
|
||||
* Adds Trap Doors: All trap doors that are permanently shut in vanilla are shuffled.
|
||||
* Increases all Door Types: This is a chaos mode where each door type per dungeon is randomized between 1 less and 4 more.
|
||||
|
||||
The sram locations for pots and sprite drops are now final, please reach out for assistance or investigate the rom changes if needed.
|
||||
Note: Boss Trap doors are removed currently and not added into the trap door pool as extra trap doors. This may not be a permanent change
|
||||
|
||||
## New Options
|
||||
### Decouple Doors
|
||||
|
||||
### Collection Rate
|
||||
|
||||
You can set the collection rate counter on using the "Display Collection Rate" on the Game Options tab are using the CLI option `--collection_rate`. Mystery seeds will not display the total.
|
||||
|
||||
### Goal: Trinity
|
||||
|
||||
Triforces are placed behind Ganon, on the pedestal, and on Murahdahla with 8/10 triforce pieces required. Recommend to run with 4-5 Crystal requirement for Ganon. Automatically pre-opens the pyramid.
|
||||
|
||||
### Boss Shuffle: Unique
|
||||
|
||||
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
|
||||
|
||||
### MSU Resume
|
||||
|
||||
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
|
||||
|
||||
### BPS Patch
|
||||
|
||||
Creates a bps patch for the seed. Found on the "Generation Setup" tab called "Create BPS Patches" or `--bps`. Can turn off generating a rom using the existing "Create Patched ROM" option or `--suppress_rom`. There is an option on the Adjust/Patch tab to select a bps file to apply to the Base Rom selected on the Generation Setup tab using the Patch Rom button. Selected adjustments will be applied during patching.
|
||||
|
||||
## New Font
|
||||
|
||||
Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name.
|
||||
This is similar to insanity mode in ER where door entrances and exits are not paired anymore. Tends to remove more logic from dungeons as many rooms will not be required to traverse to explore. Hope you like transitions.
|
||||
|
||||
## Customizer
|
||||
|
||||
Please refer to [the documentation](docs/Customizer.md) and examples of customizer [here](docs/customizer_example.yaml) and [here](docs/multi_mystery_example.yaml)
|
||||
note that entrance customization is only available with experimental features turned on.
|
||||
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
|
||||
|
||||
## Experimental Entrance Shuffle
|
||||
## New Goals
|
||||
|
||||
To support customizer and future entrance shuffle modes (perhaps even customizable ones), the entrance shuffle algorithm has been re-written. It is currently in an unstable state, and will use the old method unless you turn experimental features on. I'm currently in the process of evaluating most modes with different combinations of settings and checking the distribution of entrances. Entrance customization is only supported with this new experimental entrance shuffle. The experimental entrance shuffle includes prototypes of Lean and Lite entrance shuffles from the OWR branch.
|
||||
### Triforce Hunt + Ganon
|
||||
Collect the requisite triforce pieces, then defeat Ganon. (Aga2 not required). Use `ganonhunt` on CLI
|
||||
|
||||
## Restricted Item Placement Algorithm
|
||||
### Completionist
|
||||
All dungeons not enough for you? You have to obtain every item in the game too. This option turns on the collection rate counter and forces accessibility to be 100% locations. Finish by defeating Ganon.
|
||||
|
||||
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
|
||||
|
||||
When referenced below, Major Items include all Y items, all A items, all equipment (swords, shields, & armor) and Heart Containers. Dungeon items are considered major if shuffled outside of dungeons. Bomb and arrows upgrades are Major if shopsanity is turned on. The arrow quiver and universal small keys are Major if retro is turned on. Triforce Pieces are Major if that is the goal, and the Bomb Bag is Major if that is enabled.
|
||||
|
||||
Here are the current fill options:
|
||||
|
||||
### Balanced
|
||||
## Standard Generation Change
|
||||
|
||||
This one stays the same as before and is recommended for the most random distribution of items.
|
||||
Hyrule Castle in standard mode is generated a little differently now. The throne room is guaranteed to be in Hyrule Castle and the Sanctuary is guaranteed to be beyond that. Additionally, the Mirror Scroll will bring you back to Zelda's Cell or the Throne Room depending on what save point you last obtained, this is to make it consistent with where you end up if you die. If you are lucky enough to find the Mirror, it behaves differently and brings you the last entrance used - giving you more options for exploration in Hyrule Castle.
|
||||
|
||||
### Vanilla Fill
|
||||
## ER Features
|
||||
|
||||
This fill attempts to place all items in their vanilla locations when possible. Obviously shuffling entrances or the dungeon interiors will often prevent items from being placed in their vanilla location. If the vanilla fill is not possible, then other locations are tried in sequence preferring "major" locations (see below), then heart piece locations, then the rest except for GT locations which are preferred last. Note the PoD small key that is normally found in the dark maze in vanilla is move to Harmless Hellway due to the placement algorithm limitation.
|
||||
### New Experimental Algorithm
|
||||
|
||||
### Major Location Restriction
|
||||
To accommodate future flexibility the ER algorithm was rewritten for easy of use. This allows future modes to be added more easily. This new algorithm is only used when the experimental flag is turned on.
|
||||
|
||||
This fill attempts to place major items in major locations. Major locations are where the major items are found in the vanilla game. This includes the spot next to Uncle in the Sewers, and the Boomerang chest in Hyrule Castle.
|
||||
### Lite/Lean ER (Experimental required)
|
||||
|
||||
This location pool is expanded to where dungeon items are locations if those dungeon items are shuffled. The Capacity Fairy locations are included if Shopsanity is on. If retro is enabled in addition to shopsanity, then the Old Man Sword Cave and one location in each retro cave is included. Key drop locations can be included if small or big key shuffle is on. This gives a very good balance between overworld and underworld locations though the dungeons ones will be on bosses and in big chests generally. Seeds do become more linear but usually easier to figure out.
|
||||
Designed by Codemann, these are available now (only with experimental turned on - they otherwise fail)
|
||||
|
||||
### Dungeon Restriction
|
||||
#### Lite
|
||||
- Dungeon and multi-entrance caves can only lead to dungeon and multi-entrance caves
|
||||
- Dropdowns can only lead to dropdowns, with them staying coupled to their appropriate exits
|
||||
- Cave entrances that normally lead to items can only lead to caves that have items (this includes Potion Shop and Big Bomb Shop)
|
||||
- All remaining entrances remain vanilla
|
||||
- Multi-entrance caves are connected same-world only
|
||||
- LW is guaranteed to have HC/EP/DP/ToH/AT and DW: IP/MM/TR/GT
|
||||
- Shop locations are included in the Item Cave pool if Shopsanity is enabled
|
||||
- Houses with pots are included in the Item Cave pool if Pottery is enabled
|
||||
|
||||
The fill attempts to place all major items in dungeons. It will overflow to the overworld if there are more items than locations (e.g. Triforce hunt.) This fill does attempt to run the GT trash fill when possible. Seeds are typically very linear but tend to be more difficult.
|
||||
#### Lean
|
||||
- Same grouping/pooling mechanism as in Lite ER
|
||||
- Both dungeons and connectors can be cross-world connections
|
||||
- No dungeon guarantees like in Lite ER
|
||||
|
||||
### District Restriction
|
||||
|
||||
The world is divided up into different regions or districts. Each dungeon is its own district. The overworld consists of the following districts:
|
||||
### Back of Tavern Shuffle (Experimental required)
|
||||
|
||||
Light world:
|
||||
Thanks goes to Catobat which now allows the back of tavern to be shuffled anywhere and any valid cave can be at the back of tavern with this option checked. Available in experimental only for now as it requires the new algorithm to be shuffled properly.
|
||||
|
||||
* Kakariko (The main screen, blacksmith screen, and library/maze race screens)
|
||||
* Northwest Hyrule (The lost woods and fortune teller screens all the way to the river west of the potion shop)
|
||||
* Central Hyrule (Hyrule castle, Link's House, the marsh, and the haunted grove)
|
||||
* Desert (From the thief to the main desert screen)
|
||||
* Lake Hylia (Around the lake)
|
||||
* Eastern Hyrule (The eastern wild, the potion shop, and Zora's Domain)
|
||||
* Death Mountain
|
||||
#### Take Any Caves
|
||||
|
||||
Dark world:
|
||||
These are now independent of retro mode and have three options: None, Random, and Fixed. None disables the caves. Random works as take-any caves did before. Fixed means that the take any caves replace specific fairy caves in the pool and will be at those entrances unless ER is turned on (then they can be shuffled wherever). The fixed entrances are:
|
||||
|
||||
* East Dark World (The pyramid, Palace of darkness, and Catfish)
|
||||
* South Dark World (The dark lake, swamp area, to the dig game)
|
||||
* Northwest Dark World (Village of Outcasts, to the Dark Sanctuary and screens in between)
|
||||
* The Mire
|
||||
* Dark Death Mountain
|
||||
* Desert Healer Fairy
|
||||
* Swamp Healer Fairy (aka Light Hype Cave)
|
||||
* Dark Death Mountain Healer Fairy
|
||||
* Dark Lake Hylia Ledge Healer Fairy (aka Shopping Mall Bomb)
|
||||
* Bonk Fairy (Dark)
|
||||
|
||||
These districts are chosen at random and then filled with major items. If a location is part of a chosen district, but there are no more major items to place, a single green rupee is placed in the extra to indicate that as a placeholder. All other single green rupees are changed to be a blue rupee in order to not give false positives.
|
||||
# Bug Fixes and Notes
|
||||
|
||||
In entrance shuffle, what is shuffled to the entrances is considered instead of where the interior was originally. For example, if Blind's Hut is shuffled to the Dam, then the 5 chests in Blind's Hut are part of Central Hyrule instead of Kakariko.
|
||||
* Unreleased Version
|
||||
* MultiServer can not disable forfeits if desired
|
||||
* 1.2.0.12u
|
||||
* Fix for mirror portal in inverted
|
||||
* Yet another fix for blocked door in Standard ER
|
||||
* 1.2.0.11u
|
||||
* Fixed an issue with lower layer doors in Standard
|
||||
* Fix for doors in cave state (will no longer be vanilla)
|
||||
* Added a logic rule for th murderdactyl near bumper ledge for OHKO purposes
|
||||
* Enemizer alteration for Hovers and normal enemies in shallow water
|
||||
* Fix for beemizer including modes with an increased item pool
|
||||
* Fix for district algorithm
|
||||
* 1.2.0.10u
|
||||
* Fixed overrun issues with edge transitions
|
||||
* Better support for customized start_inventory with dungeon items
|
||||
* Colorized pots now available with lottery. Default is on.
|
||||
* Dungeon_only support pottery
|
||||
* Fix AllowAccidentalGlitches flag in OWG
|
||||
* Potential fix for mirror portal and entering cave on same frame
|
||||
* A few other minor issues, generation and graphical
|
||||
* 1.2.0.9-u
|
||||
* Disallowed standard exits (due to ER) are now graphically half blocked instead of missing
|
||||
* Graphical issues with Sanctuary and Swamp Hub lobbies are fixed
|
||||
* Fixes an issue surrounding door state and decoupled doors leading to blocked doors
|
||||
* Customizer improvements:
|
||||
* Better logic around customized lobbies
|
||||
* Better logic around customized door types
|
||||
* Fix to key doors that was causing extra key doors
|
||||
* Generation improvement around crystal switches
|
||||
* Fix bug in dungeon_only that wasn't using pot key locations (known issue still exists in pottery modes)
|
||||
* Fixes for multiworld:
|
||||
* Fixes an issue when keys are found in own dungeon for another player when using the bizhawk plugin.
|
||||
* Fixes an issue with absorbables for another player also being received by the player picking it up.
|
||||
* 1.2.0.8-u
|
||||
* New Features: trap_door_mode and key_logic_algorithm
|
||||
* Change S&Q in door shuffle + standard during escape to spawn as Uncle
|
||||
* Fix for vanilla doors + certain ER modes
|
||||
* Fix for unintentional decoupled door in standard
|
||||
* Fix a problem with BK doors being one-sided
|
||||
* Change to how wilds keys are placed in standard, better randomization
|
||||
* Removed a Triforce text
|
||||
* Fix for Desert Tiles 1 key door
|
||||
* 1.2.0.7-u
|
||||
* Fix for some misery mire key logic
|
||||
* Minor standard generation fix
|
||||
* Fix for inactive flute start
|
||||
* Settingsfile for multiworld generation support
|
||||
* Fix for duped HC/AT Maps/Compasses
|
||||
* 1.2.0.6-u
|
||||
* Fix for light cone in Escape when entering from Dark World post-zelda
|
||||
* Fix for light cone in Escape when lighting a torch with fire rod
|
||||
* 1.2.0.5.u
|
||||
* Logic fix for Sanctuary mirror (it wasn't resetting the crystal state)
|
||||
* Minor bugfixes for customizer
|
||||
* 1.2.0.4-u
|
||||
* Starting inventory fixes if item not present in the item pool.
|
||||
* Support for Assured sword setting and OWG Boots when using a custom item pool. (Customizer or GUI)
|
||||
* Logic fix for the skull woods star tile that lets you into the X pot room. Now accounts for small key or big key door there blocking the way from the star tile. A trap door is not allowed there.
|
||||
* Standard logic improvement that requires a path from Zelda to the start so that you cannot get softlocked by rescuing Zelda. Standard mirror scroll change may need to be reverted if impossible seed are still generated.
|
||||
* 1.2.0.3-u
|
||||
* Starting inventory taken into account with default item pool. (Custom pools must do this themselves)
|
||||
* Fast ROM update
|
||||
* Fix for restricted boss item counting maps & compasses as vital
|
||||
* Bug fix for vanilla ER + inverted + experimental
|
||||
* 1.2.0.2-u
|
||||
* Fixed a bug with certain trap doors missing
|
||||
* Added a hint reference for district hints
|
||||
* 1.2.0.1-u
|
||||
* Added new ganonhunt and completionist goals
|
||||
* Fixed the issue when defeating Agahnim and standing in the doorway can cause door state to linger.
|
||||
* Fix for Inverted Lean/Lite ER
|
||||
* Fix for vanilla Doors + Standard + ER
|
||||
* Added a limit per dungeon on small key doors to ensure reasonable generation
|
||||
* Fixed many small bugs
|
||||
|
||||
Note: Bombos Tablet, Lake Hylia Island, Bumper Cave Ledge, the Floating Island, Cave 45, the Graveyard Cave, Checkerboard Cave and Mimic Cave are considered part of the light world region rather than the dark world region you mirror from.
|
||||
# Known Issues
|
||||
|
||||
In multiworld, the districts chosen apply to all players.
|
||||
|
||||
### CLI values:
|
||||
|
||||
```balanced, vanilla_fill, major_only, dungeon_only, district```
|
||||
|
||||
## New Hints
|
||||
|
||||
Based on the district algorithm above (whether it is enabled or not,) new hints can appear about that district or dungeon. For each district and dungeon, it is evaluated whether it contains vital items and how many. If it has not any vital item, items then it moves onto useful items. Useful items are generally safeties or convenience items: shields, mails, half magic, bottles, medallions that aren't required, etc. If it contains none of those and is an overworld district, then it checks for a couple more things. First, if dungeons are shuffled, it looks to see if any are in the district, if so, one of those dungeons is picked for the hint. Then, if connectors are shuffled, it checks to see if you can get to unique region through a connector in that district. If none of the above apply, the district or dungeon is considered completely foolish.
|
||||
|
||||
## Overworld Map shows Dungeon Entrances
|
||||
|
||||
Option to move indicators on overworld map to reference dungeon location. The non-default options include indicators for Hyrule Castle, Agahnim's Tower, and Ganon's Tower.
|
||||
|
||||
CLI ```--overworld_map```
|
||||
|
||||
#### Options
|
||||
|
||||
##### default
|
||||
|
||||
Status quo. Showing only the prize markers on the vanilla dungeon locations.
|
||||
|
||||
##### compass
|
||||
|
||||
The compass item controls whether the marker is moved to the dungeons locations. If you possess the compass but not the map, only a glowing X will be present regardless of dungeon prize type, if you only possess the map, the prizes will be shown in predicable locations at the bottom of the overworld map instead of the vanilla location. Light world dungeons on the light world map and dark world dungeons on the dark world map. If you posses both map and compass, then the prize of the dungeon and the location will be on the map.
|
||||
|
||||
If you do not shuffle the compass or map outside of the dungeon, the non-shuffled items are not needed to display the information. If a dungeon does not have a map or compass, it is not needed for the information. Talking to the bomb shop or Sahasrahla furnishes you with complete information as well as map information.
|
||||
|
||||
##### map
|
||||
|
||||
The map item plays double duty in this mode and only possession of the map will show both prize and location of the dungeon. If you do not shuffle maps or the dungeon does not have a map, the information will be displayed without needing to find any items.
|
||||
|
||||
## Restricted Dungeon Items on Bosses
|
||||
|
||||
You may now restrict the items that can appear on the boss, like the popular ambrosia preset does.
|
||||
|
||||
CLI: ```--restrict_boss_items <option>```
|
||||
|
||||
#### Options
|
||||
|
||||
##### none
|
||||
|
||||
As before, the boss may have any item including any dungeon item that could occur there.
|
||||
|
||||
##### mapcompass
|
||||
|
||||
~~The map and compass are logically required to defeat a boss. This prevents both of those from appearing on the dungeon boss. Note that this does affect item placement logic and the placement algorithm as maps and compasses are considered as required items to beat a boss.~~
|
||||
|
||||
Currently bugged, not recommended for use.
|
||||
|
||||
##### dungeon
|
||||
|
||||
Same as above but both small keys and bigs keys of the dungeon are not allowed on a boss. (Note: this does not affect universal keys as they are not dungeon-specific)
|
||||
|
||||
## Notes and Bug Fixes
|
||||
|
||||
#### StandardThrone
|
||||
|
||||
* Bug fixes
|
||||
* Fix HC Ledge entrances after rescuing Zelda, also includes the throne spawn point now.
|
||||
* Original Release
|
||||
* Changed standard dungeon generation to always have Throne Room in hyrule castle and always have sanctuary behind it
|
||||
* S&Q/death in standard after moving the tapestry but before delivering Zelda will result in spawning at the tapestry
|
||||
* Mirror scroll will return you to Zelda's cell instead of last entrance. This reverts to normal behavior once the tapestry open trigger is reached
|
||||
|
||||
#### Customizer
|
||||
|
||||
* Fixed an issue where Interior Key Doors were missing from custom yaml output
|
||||
* Updated lite/lean ER for pottery settings
|
||||
|
||||
* Fixed an issue with lite/lean ER not generating
|
||||
* Fixed up the GUI selection of the customizer file.
|
||||
* Fixed up the item_pool section to skip a lot of pool manipulations. Key items will be added (like the bow) if not detected. Extra dungeon items can be added to the pool and will be confined to the dungeon if possible (and not shuffled). If the pool isn't full, junk items are added to the pool to fill it out.
|
||||
|
||||
#### Unstable
|
||||
|
||||
* 1.0.1.3
|
||||
* Fix for rain prevented doors fouling up key doors
|
||||
* Couple minor issues
|
||||
* 1.0.1.2
|
||||
* Removed "good bee" as an in-logic way of killing Mothula
|
||||
* Fixed an issue with Mystery generation and Windows path
|
||||
* Fixed an issue with small key bias rework
|
||||
* Fixed an issue where trinity goal would open pyramid unexpectedly. (No longer does so if ER mdoe is shuffling holes). Crystals goal updated to match that behavior.
|
||||
* Fixed a playthrough issue that was not respecting pot rules
|
||||
* Fixed an issue that was conflicting with downstream OWR project
|
||||
* Fixed an issue with inverted and certain pottery settings
|
||||
* Fixed an issue with small keys being shuffled and big keys not (key distribution)
|
||||
* 1.0.1.1
|
||||
* Fixed the pots in Mire Storyteller/ Dark Desert Hint to be colorized when they should be
|
||||
* Certain pot items no longer reload when reloading the supertile (matches original pot behavior better)
|
||||
* Changed the key distribution that made small keys placement more random when keys are in their own dungeon
|
||||
* Unique boss shuffle no longer allows repeat bosses in GT (e.g. only one Trinexx in GT, so exactly 3 bosses are repeated in the seed. This is a difference process than full which does affect the probability distribution.)
|
||||
* Removed text color in hints due to vanilla bug
|
||||
* 1.0.1.0
|
||||
* Large features
|
||||
* New pottery modes - see notes above
|
||||
* Pot substitutions added for red rupees, 10 bomb packs, 3 bomb packs, and 10 arrows have been added. They use objects that can result from a tree pull or other drop. The 3 bomb pack becomes a 4 bomb pack and the 10 bomb pack becomes an 8 pack. These substitutions are repeatable like all other normal pot contents.
|
||||
* Updated TFH to support up to 850 pieces
|
||||
* New font support
|
||||
* Trinity goal added
|
||||
* Separated Collection Rate counter from experimental
|
||||
* Added MSU Resume option
|
||||
* Support for BPS patch creation and applying patches during adjustment
|
||||
* New option for Boss Shuffle: Unique (Prize bosses will be one of each, but GT bosses can be anything)
|
||||
* Logic Notes
|
||||
* Skull X Room requires Boots or access to Skull Back Drop
|
||||
* GT Falling Torches requires Boots to get over the falling tile gap (this is a stop-gap measure until more sophisticated crystal switch traversal is possible)
|
||||
* Waterfall of Wishing logic in open. You must have flippers to exit the Waterfall (flippers also required in glitched modes as well)
|
||||
* Fix for GT Crystal Conveyor not requiring Somaria/Bombs to get through
|
||||
* Pedestal goal + vanilla swords places a random sword in the pool
|
||||
* Added a few more places Links House shouldn't go when shuffled
|
||||
* Small features
|
||||
* Added a check for python package requirements before running code. GUI and console both for better error messages. Thanks to mtrethewey for the idea.
|
||||
* Refactored spoiler to generate in stages for better error collection. A meta file will be generated additionally for mystery seeds. Some random settings moved later in the spoiler to have the meta section at the top not spoil certain things. (GT/Ganon requirements.) Thanks to codemann and OWR for most of this work.
|
||||
* Updated tourney winners (included Doors Async League winners)
|
||||
* Some textual changes for hints (capitalization standardization)
|
||||
* Reworked GT Trash Fill. Base rate is 0-75% of locations fill with 7 crystals entrance requirements. Triforce hunt is 75%-100% of locations. The 75% number will decrease based on the crystal entrance requirement. Dungeon_only algorithm caps it based on how many items need to be placed in dungeons. Cross dungeon shuffle will now work with the trash fill.
|
||||
* Expanded Mystery logic options (e.g. owglitches)
|
||||
* Updated indicators on keysanity menu for overworld map option
|
||||
* Bug fixes:
|
||||
* Fix for Zelda (or any follower) going to the maiden cell supertile and the boss is not Blind. The follower will not despawn unless the boss is Blind, then the maiden will spawn as normal.
|
||||
* Bug with 0 GT crystals not opening GT
|
||||
* Fixed a couple issues with dungeon counters and the DungeonCompletion field for autotracking
|
||||
* Settings code fix
|
||||
* Fix for forbidding certain dashable doors (it actually does something this time)
|
||||
* Fix for major item algorithm and pottery
|
||||
* Updated map display on keysanity menu to work better with overworld_map option
|
||||
* Minor bug in crossed doors
|
||||
* Fix for Multiworld forfeits, shops and pot items now included
|
||||
* MultiServer fix for ssl certs and python
|
||||
* forbid certain doors from being dashable when you either can't dash them open (but bombs would work) or you'd fall into a pit from the bonk recoil in OHKO
|
||||
* Fixed a couple rain state issues
|
||||
* Add major_only algorithm to settings code
|
||||
* Fixes for Links House being at certain entrances (did not generate)
|
||||
* Fix for vanilla_fill, it now prioritizes heart container placements
|
||||
* Fix for dungeon counter showing up in AT/HC in crossed dungeon mode
|
||||
* Fixed usestartinventory with mystery
|
||||
* Added double click fix for install.py
|
||||
* Fix for SFX shuffle
|
||||
* Fix for districting + shopsanity
|
||||
* Fix for multiworld progression balancing would place Nothing or Arrow items
|
||||
* Fixed a bug with shopsanity + district algorithm where pre-placed potions messed up the placeholder count
|
||||
* Fixed usestartinventory flag (can be use on a per player basis)
|
||||
* Sprite selector fix for systems with SSL issues
|
||||
* Fix for Standard ER where locations in rain state could be in logic
|
||||
* 1.0.0.3
|
||||
* overworld_map=map mode fixed. Location of dungeons with maps are not shown until map is retrieved. (Dungeon that do not have map like Castle Tower are simply never shown)
|
||||
* Aga2 completion on overworld_map now tied to boss defeat flag instead of pyramid hole being opened (fast ganon fix)
|
||||
* Minor issue in dungeon_only algorithm fixed (minorly affected major_only keyshuffle and vanilla fallbacks)
|
||||
* 1.0.0.2
|
||||
* Include 1.0.1 fixes
|
||||
* District hint rework
|
||||
* 1.0.0.1
|
||||
* Add Light Hype Fairy to bombbag mode as needing bombs
|
||||
|
||||
### From stable DoorDev
|
||||
|
||||
* 1.0.1
|
||||
* Fixed a bug with key doors not detecting one side of an interior door
|
||||
* Sprite selector fix for systems with SSL issues
|
||||
* Decoupled doors can lead to situations where you aren't logically supposed to go back through a door without a big key or small key, but you can if you press the correct direction back through the door first. There are some transitions where you may get stuck without a bomb. These problems are planned to be fixed.
|
||||
* Logic getting to Skull X room may be wrong if a trap door, big key door, or bombable wall is shuffled there. A bomb jump to get to those pot may be required if you don't have boots to bonk across.
|
||||
+3
-2
@@ -183,7 +183,8 @@ def create_regions(world, player):
|
||||
create_cave_region(player, 'Chest Game', 'a game of 16 chests', ['Chest Game']),
|
||||
create_cave_region(player, 'Red Shield Shop', 'the rare shop', ['Red Shield Shop - Left', 'Red Shield Shop - Middle', 'Red Shield Shop - Right']),
|
||||
create_cave_region(player, 'Dark Sanctuary Hint', 'a storyteller'),
|
||||
create_cave_region(player, 'Bumper Cave', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Exit (Top)']),
|
||||
create_cave_region(player, 'Bumper Cave (bottom)', 'a connector', None, ['Bumper Cave Exit (Bottom)', 'Bumper Cave Bottom to Top']),
|
||||
create_cave_region(player, 'Bumper Cave (top)', 'a connector', None, ['Bumper Cave Exit (Top)', 'Bumper Cave Top To Bottom']),
|
||||
create_dw_region(player, 'Bumper Cave Ledge', ['Bumper Cave Ledge'], ['Bumper Cave Ledge Drop', 'Bumper Cave (Top)', 'Bumper Cave Ledge Mirror Spot'], 'a ledge with an item'),
|
||||
create_dw_region(player, 'Skull Woods Forest', None, ['Skull Woods First Section Hole (East)', 'Skull Woods First Section Hole (West)', 'Skull Woods First Section Hole (North)',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)']),
|
||||
@@ -339,7 +340,7 @@ def create_dungeon_regions(world, player):
|
||||
create_dungeon_region(player, 'Desert Dead End', 'Desert Palace', None, ['Desert Dead End Edge']),
|
||||
create_dungeon_region(player, 'Desert East Lobby', 'Desert Palace', None, ['Desert East Lobby WS', 'Desert East Lobby S']),
|
||||
create_dungeon_region(player, 'Desert East Wing', 'Desert Palace', None, ['Desert East Wing ES', 'Desert East Wing Key Door EN', 'Desert East Wing W Edge', 'Desert East Wing N Edge']),
|
||||
create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NW']),
|
||||
create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NE']),
|
||||
create_dungeon_region(player, 'Desert Cannonball', 'Desert Palace', ['Desert Palace - Big Key Chest'], ['Desert Cannonball S']),
|
||||
create_dungeon_region(player, 'Desert Arrow Pot Corner', 'Desert Palace', None, ['Desert Arrow Pot Corner S Edge', 'Desert Arrow Pot Corner W Edge', 'Desert Arrow Pot Corner NW']),
|
||||
create_dungeon_region(player, 'Desert Trap Room', 'Desert Palace', None, ['Desert Trap Room SW']),
|
||||
|
||||
@@ -15,7 +15,7 @@ try:
|
||||
except ImportError:
|
||||
raise Exception('Could not load BPS module')
|
||||
|
||||
from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType, Item
|
||||
from BaseClasses import ShopType, Region, Location, Door, DoorType, RegionType, LocationType
|
||||
from DoorShuffle import compass_data, DROptions, boss_indicator, dungeon_portals
|
||||
from Dungeons import dungeon_music_addresses, dungeon_table
|
||||
from Regions import location_table, shop_to_location_table, retro_shops
|
||||
@@ -561,7 +561,7 @@ class Sprite(object):
|
||||
|
||||
def handle_native_dungeon(location, itemid):
|
||||
# Keys in their native dungeon should use the original item code for keys
|
||||
if location.parent_region.dungeon:
|
||||
if location.parent_region.dungeon and location.player == location.item.player:
|
||||
if location.parent_region.dungeon.name == location.item.dungeon:
|
||||
if location.item.bigkey:
|
||||
return 0x32
|
||||
@@ -708,7 +708,8 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
dr_flags |= DROptions.Map_Info
|
||||
if world.collection_rate[player] and world.goal[player] not in ['triforcehunt', 'trinity']:
|
||||
if ((world.collection_rate[player] or world.goal[player] == 'completionist')
|
||||
and world.goal[player] not in ['triforcehunt', 'trinity', 'ganonhunt']):
|
||||
dr_flags |= DROptions.Debug
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
|
||||
and world.mixed_travel[player] == 'prevent':
|
||||
@@ -1062,9 +1063,14 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
rom.write_byte(0x178000 + i, random.randint(0, 255))
|
||||
|
||||
# shuffle prize packs
|
||||
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
|
||||
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC, 0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2, 0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
|
||||
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2, 0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
|
||||
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9,
|
||||
0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA,
|
||||
0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
|
||||
0xDC, 0xDC, 0xDC, 0xDD, 0xDC, 0xDC, 0xDE, 0xDC,
|
||||
0xE1, 0xD8, 0xE1, 0xE2, 0xE1, 0xD8, 0xE1, 0xE2,
|
||||
0xDF, 0xD9, 0xD8, 0xE1, 0xDF, 0xDC, 0xD9, 0xD8,
|
||||
0xD8, 0xE3, 0xE0, 0xDB, 0xDE, 0xD8, 0xDB, 0xE2,
|
||||
0xD9, 0xDA, 0xDB, 0xD9, 0xDB, 0xD9, 0xDB]
|
||||
dig_prizes = [0xB2, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8, 0xD8,
|
||||
0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xDA, 0xDA, 0xDA, 0xDA, 0xDA,
|
||||
0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0xDC, 0xDC, 0xDC, 0xDC, 0xDC,
|
||||
@@ -1077,13 +1083,41 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
return [l[i:i+n] for i in range(0, len(l), n)]
|
||||
|
||||
# randomize last 7 slots
|
||||
prizes [-7:] = random.sample(prizes, 7)
|
||||
possible_prizes = {
|
||||
'Small Heart': 0xD8, 'Fairy': 0xE3,
|
||||
'Rupee (1)': 0xD9, 'Rupees (5)': 0xDA, 'Rupees (20)': 0xDB,
|
||||
'Big Magic': 0xE0, 'Small Magic': 0xDF,
|
||||
'Single Bomb': 0xDC, 'Bombs (4)': 0xDD,
|
||||
'Bombs (8)': 0xDE, 'Arrows (5)': 0xE1, 'Arrows (10)': 0xE2
|
||||
} #weights, if desired 13, 1, 9, 7, 6, 3, 6, 7, 1, 2, 5, 3
|
||||
uniform_prizes = list(possible_prizes.values())
|
||||
prizes[-7:] = random.sample(prizes, 7)
|
||||
|
||||
#shuffle order of 7 main packs
|
||||
packs = chunk(prizes[:56], 8)
|
||||
random.shuffle(packs)
|
||||
prizes[:56] = [drop for pack in packs for drop in pack]
|
||||
|
||||
if world.customizer:
|
||||
drops = world.customizer.get_drops()
|
||||
if drops:
|
||||
for player, drop_config in drops.items():
|
||||
for pack_num in range(1, 8):
|
||||
if f'Pack {pack_num}' in drop_config:
|
||||
for prize, idx in enumerate(drop_config[f'Pack {pack_num}']):
|
||||
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
|
||||
prizes[(pack_num-1)*8 + idx] = chosen
|
||||
for tree_pull_tier in range(1, 4):
|
||||
if f'Tree Pull Tier {tree_pull_tier}' in drop_config:
|
||||
prize = drop_config[f'Tree Pull Tier {tree_pull_tier}']
|
||||
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
|
||||
prizes[63-tree_pull_tier] = chosen # (62 through 60 in reverse)
|
||||
for key, pos in {'Crab Normal': 59, 'Crab Special': 58, 'Stun Prize': 57, 'Fish': 56}.items():
|
||||
if key in drop_config:
|
||||
prize = drop_config[key]
|
||||
chosen = random.choice(uniform_prizes) if prize == 'Random' else possible_prizes[prize]
|
||||
prizes[pos] = chosen
|
||||
|
||||
if world.difficulty_adjustments[player] in ['hard', 'expert']:
|
||||
prize_replacements = {0xE0: 0xDF, # Fairy -> heart
|
||||
0xE3: 0xD8} # Big magic -> small magic
|
||||
@@ -1220,7 +1254,7 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
|
||||
# set up goals for treasure hunt
|
||||
rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28])
|
||||
if world.goal[player] in ['triforcehunt', 'trinity']:
|
||||
if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']:
|
||||
rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player]))
|
||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||
|
||||
@@ -1283,6 +1317,10 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
|
||||
elif world.goal[player] in ['crystals', 'trinity']:
|
||||
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
|
||||
elif world.goal[player] in ['ganonhunt']:
|
||||
rom.write_byte(0x18003E, 0x05) # make ganon invincible until all triforce pieces collected
|
||||
elif world.goal[player] in ['completionist']:
|
||||
rom.write_byte(0x18003E, 0x0a) # make ganon invincible until everything is collected
|
||||
else:
|
||||
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
|
||||
|
||||
@@ -1427,7 +1465,7 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
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(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)
|
||||
rom.write_byte(0x180358, 0x01 if (world.logic[player] in ['owglitches', 'nologic']) else 0x00)
|
||||
|
||||
# remove shield from uncle
|
||||
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
|
||||
@@ -1468,8 +1506,10 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
# Uncle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180185, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
|
||||
rom.write_bytes(0x180188, [0x20, 3, 10]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0x20, 3, 10]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
# Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x180188, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
|
||||
# Mantle respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [max(0x20, magic_max), max(3, bomb_max), max(10, bow_max)])
|
||||
elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest
|
||||
rom.write_bytes(0x180185, [max(0x00, magic_max), max(3, bomb_max), max(0, bow_max)])
|
||||
rom.write_bytes(0x180188, [magic_small, 3, bow_small]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
@@ -1524,11 +1564,11 @@ def patch_rom(world, rom, player, team, is_mystery=False):
|
||||
rom.write_bytes(room.address(), room.rom_data())
|
||||
|
||||
if world.data_tables[player]:
|
||||
colorize_pots = is_mystery or (world.pottery[player] not in ['vanilla', 'lottery']
|
||||
and (world.colorizepots[player]
|
||||
or world.pottery[player] in ['reduced', 'clustered']))
|
||||
colorize_pots = (world.pottery[player] != 'vanilla', 'lottery'
|
||||
and (world.colorizepots[player] or world.pottery[player] in ['reduced', 'clustered']))
|
||||
world.data_tables[player].write_to_rom(rom, colorize_pots)
|
||||
|
||||
write_enemizer_tweaks(rom, world, player)
|
||||
write_strings(rom, world, player, team)
|
||||
|
||||
# write initial sram
|
||||
@@ -1618,6 +1658,11 @@ def write_custom_shops(rom, world, player):
|
||||
rom.write_bytes(0x184900, items_data)
|
||||
|
||||
|
||||
def write_enemizer_tweaks(rom, world, player):
|
||||
if world.enemy_shuffle[player] != 'none':
|
||||
rom.write_byte(snes_to_pc(0x1DF6D8), 0) # lets enemies walk on water instead of clipping into infinity?
|
||||
rom.write_byte(snes_to_pc(0x0DB6B3), 0x82) # hovers don't need water necessarily?
|
||||
|
||||
def hud_format_text(text):
|
||||
output = bytes()
|
||||
for char in text.lower():
|
||||
@@ -2157,44 +2202,42 @@ def write_strings(rom, world, player, team):
|
||||
hint_candidates = []
|
||||
for name, district in world.districts[player].items():
|
||||
hint_type = 'foolish'
|
||||
choice_set = set()
|
||||
choices = []
|
||||
item_count, item_type = 0, 'useful'
|
||||
for loc_name in district.locations:
|
||||
location_item = world.get_location(loc_name, player).item
|
||||
if location_item.advancement:
|
||||
if 'Heart Container' in location_item.name:
|
||||
if 'Heart Container' in location_item.name or location_item.compass or location_item.map:
|
||||
continue
|
||||
itm_type = 'useful' if useful_item_for_hint(location_item, world) else 'vital'
|
||||
hint_type = 'path'
|
||||
if item_type == itm_type:
|
||||
choice_set.add(location_item)
|
||||
choices.append(location_item)
|
||||
item_count += 1
|
||||
elif itm_type == 'vital':
|
||||
item_type = 'vital'
|
||||
item_count = 1
|
||||
choice_set.clear()
|
||||
choice_set.add(location_item)
|
||||
choices.clear()
|
||||
choices.append(location_item)
|
||||
if hint_type == 'foolish':
|
||||
if district.dungeons and world.shuffle[player] != 'vanilla':
|
||||
choice_set.update(district.dungeons)
|
||||
choices.extend(district.dungeons)
|
||||
hint_type = 'dungeon_path'
|
||||
elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple',
|
||||
'dungeonsfull']:
|
||||
choice_set.update([x.hint_text for x in district.access_points])
|
||||
choices.extend([x.hint_text for x in district.access_points])
|
||||
hint_type = 'connector'
|
||||
if hint_type == 'foolish':
|
||||
hint_candidates.append((hint_type, f'{name} is a foolish choice'))
|
||||
elif hint_type == 'dungeon_path':
|
||||
choices = sorted(list(choice_set))
|
||||
dungeon_choice = random.choice(choices) # prefer required dungeons...
|
||||
hint_candidates.append((hint_type, f'{name} is on the path to {dungeon_choice}'))
|
||||
elif hint_type == 'connector':
|
||||
choices = sorted(list(choice_set))
|
||||
access_point = random.choice(choices) # prefer required access...
|
||||
hint_candidates.append((hint_type, f'{name} can reach {access_point}'))
|
||||
elif hint_type == 'path':
|
||||
if item_count == 1:
|
||||
the_item = text_for_item(next(iter(choice_set)), world, player, team)
|
||||
the_item = text_for_item(next(iter(choices)), world, player, team)
|
||||
hint_candidates.append((hint_type, f'{name} conceals only {the_item}'))
|
||||
else:
|
||||
hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items'))
|
||||
@@ -2264,7 +2307,24 @@ def write_strings(rom, world, player, team):
|
||||
if world.goal[player] in ['dungeons']:
|
||||
tt['sign_ganon'] = 'You need to complete all the dungeons.'
|
||||
|
||||
tt['uncle_leaving_text'] = Uncle_texts[random.randint(0, len(Uncle_texts) - 1)]
|
||||
if world.boots_hint[player]:
|
||||
starting_boots = next((i for i in world.precollected_items if i.player == player
|
||||
and i.name == 'Pegasus Boots'), None)
|
||||
if starting_boots:
|
||||
uncle_text = 'Lonk! Boots\nare on\nyour feet.'
|
||||
else:
|
||||
boots_location = next((l for l in world.get_locations()
|
||||
if l.player == player and l.item and l.item.name == 'Pegasus Boots'), None)
|
||||
if boots_location:
|
||||
district = next((d for k, d in world.districts[player].items()
|
||||
if boots_location.name in d.locations), 'Zebes')
|
||||
uncle_text = f'Lonk! Boots\nare in {district.name}'
|
||||
else:
|
||||
uncle_text = "I couldn't\nfind the Boots\ntoday.\nRIP me."
|
||||
|
||||
tt['uncle_leaving_text'] = uncle_text
|
||||
else:
|
||||
tt['uncle_leaving_text'] = Uncle_texts[random.randint(0, len(Uncle_texts) - 1)]
|
||||
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[random.randint(0, len(Triforce_texts) - 1)]
|
||||
tt['bomb_shop_big_bomb'] = BombShop2_texts[random.randint(0, len(BombShop2_texts) - 1)]
|
||||
|
||||
@@ -2286,6 +2346,10 @@ def write_strings(rom, world, player, team):
|
||||
trinity_crystal_text = ('%d crystal to beat Ganon.' if world.crystals_needed_for_ganon[player] == 1 else '%d crystals to beat Ganon.') % world.crystals_needed_for_ganon[player]
|
||||
tt['sign_ganon'] = 'Three ways to victory! %s Get to it!' % trinity_crystal_text
|
||||
tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player])
|
||||
elif world.goal[player] == 'ganonhunt':
|
||||
tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon'
|
||||
elif world.goal[player] == 'completionist':
|
||||
tt['sign_ganon'] = 'Ganon only respects those who have done everything'
|
||||
tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)]
|
||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||
|
||||
+4
-2
@@ -103,7 +103,7 @@ def create_rooms(world, player):
|
||||
Room(player, 0x60, 0x51309).door(Position.NorthE2, DoorKind.NormalLow2).door(Position.East2, DoorKind.NormalLow2).door(Position.East2, DoorKind.ToggleFlag).door(Position.EastN, DoorKind.Normal).door(Position.SouthE, DoorKind.Normal).door(Position.SouthE, DoorKind.IncognitoEntrance),
|
||||
Room(player, 0x61, 0x51454).door(Position.West2, DoorKind.NormalLow).door(Position.West2, DoorKind.ToggleFlag).door(Position.East2, DoorKind.NormalLow).door(Position.East2, DoorKind.ToggleFlag).door(Position.South2, DoorKind.NormalLow).door(Position.South2, DoorKind.IncognitoEntrance).door(Position.WestN, DoorKind.Normal),
|
||||
Room(player, 0x62, 0x51577).door(Position.West2, DoorKind.NormalLow2).door(Position.West2, DoorKind.ToggleFlag).door(Position.NorthW2, DoorKind.NormalLow2).door(Position.North, DoorKind.Normal).door(Position.SouthW, DoorKind.Normal).door(Position.SouthW, DoorKind.IncognitoEntrance),
|
||||
Room(player, 0x63, 0xf88ed).door(Position.NorthE, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
|
||||
Room(player, 0x63, 0xf88ed).door(Position.NorthW, DoorKind.StairKey).door(Position.InteriorW, DoorKind.TrapTriggerable).door(Position.SouthW, DoorKind.DungeonEntrance), # looked like a huge typo - I had to guess on StairKey
|
||||
Room(player, 0x64, 0xfda53).door(Position.InteriorS, DoorKind.Trap2),
|
||||
Room(player, 0x65, 0xfdac5).door(Position.InteriorS, DoorKind.Normal),
|
||||
Room(player, 0x66, 0xfa01b).door(Position.InteriorE2, DoorKind.Waterfall).door(Position.SouthW2, DoorKind.NormalLow2).door(Position.SouthW2, DoorKind.ToggleFlag).door(Position.InteriorW2, DoorKind.NormalLow2),
|
||||
@@ -244,7 +244,9 @@ def create_rooms(world, player):
|
||||
# Room(player, 0xff, 0x52c9a).door(Position.InteriorW, DoorKind.Bombable).door(Position.InteriorE, DoorKind.Bombable).door(Position.SouthE, DoorKind.CaveEntrance),
|
||||
]
|
||||
# fix some wonky things
|
||||
world.get_room(0x51, player).change(1, DoorKind.Normal) # fix the dungeon changer
|
||||
# should I put back the dungeon changer for certain logic - like no logic? maybe in basic
|
||||
if world.doorShuffle[player] != 'vanilla':
|
||||
world.get_room(0x51, player).delete(1) # remove the dungeon changer
|
||||
world.get_room(0x60, player).swap(2, 4) # puts the exit at pos 2 - enables pos 3
|
||||
world.get_room(0x61, player).swap(1, 6) # puts the WN door at pos 1 - enables it
|
||||
world.get_room(0x61, player).swap(5, 6) # puts the Incognito Entrance at the end, so it can be deleted
|
||||
|
||||
@@ -63,6 +63,10 @@ def set_rules(world, player):
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||
elif world.goal[player] in ['triforcehunt', 'trinity']:
|
||||
add_rule(world.get_location('Murahdahla', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
|
||||
elif world.goal[player] == 'ganonhunt':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player]))
|
||||
elif world.goal[player] == 'completionist':
|
||||
add_rule(world.get_location('Ganon', player), lambda state: state.everything(player))
|
||||
|
||||
if world.mode[player] != 'inverted':
|
||||
set_big_bomb_rules(world, player)
|
||||
@@ -123,6 +127,10 @@ def or_rule(rule1, rule2):
|
||||
return lambda state: rule1(state) or rule2(state)
|
||||
|
||||
|
||||
def and_rule(rule1, rule2):
|
||||
return lambda state: rule1(state) and rule2(state)
|
||||
|
||||
|
||||
def add_lamp_requirement(spot, player):
|
||||
add_rule(spot, lambda state: state.has('Lamp', player, state.world.lamps_needed_for_dark_rooms))
|
||||
|
||||
@@ -276,8 +284,22 @@ 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))
|
||||
set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player))
|
||||
set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) or state.has('Hidden Pits', player))
|
||||
set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), lambda state: state.has('Hidden Pits', player))
|
||||
|
||||
hidden_pits_door = world.get_door('Skull Small Hall WS', player)
|
||||
|
||||
def hidden_pits_rule(state):
|
||||
return state.has('Hidden Pits', player)
|
||||
|
||||
if hidden_pits_door.bigKey:
|
||||
key_logic = world.key_logic[player][hidden_pits_door.entrance.parent_region.dungeon.name]
|
||||
hidden_pits_rule = and_rule(hidden_pits_rule, create_rule(key_logic.bk_name, player))
|
||||
elif hidden_pits_door.smallKey:
|
||||
d_name = hidden_pits_door.entrance.parent_region.dungeon.name
|
||||
hidden_pits_rule = and_rule(hidden_pits_rule, eval_small_key_door('Skull Small Hall WS', d_name, player))
|
||||
|
||||
set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player)
|
||||
or hidden_pits_rule(state))
|
||||
set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), hidden_pits_rule)
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player))
|
||||
set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player))
|
||||
|
||||
@@ -389,7 +411,8 @@ def global_rules(world, player):
|
||||
set_rule(world.get_entrance('GT Mimics 1 ES', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('GT Mimics 2 WS', player), lambda state: state.can_shoot_arrows(player))
|
||||
set_rule(world.get_entrance('GT Mimics 2 NE', player), lambda state: state.can_shoot_arrows(player))
|
||||
# consider access to refill room
|
||||
# consider access to refill room - interior doors would need a change
|
||||
set_rule(world.get_entrance('GT Cannonball Bridge SE', player), lambda state: state.has_Boots(player))
|
||||
set_rule(world.get_entrance('GT Gauntlet 1 WN', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('GT Gauntlet 2 EN', player), lambda state: state.can_kill_most_things(player))
|
||||
set_rule(world.get_entrance('GT Gauntlet 2 SW', player), lambda state: state.can_kill_most_things(player))
|
||||
@@ -649,7 +672,7 @@ def bomb_rules(world, player):
|
||||
('Hyrule Dungeon Armory S', True), # One green guard
|
||||
('Hyrule Dungeon Armory ES', True), # One green guard
|
||||
('Hyrule Dungeon Armory Boomerang WS', True), # One blue guard
|
||||
('Desert Compass NW', True), # Three popos
|
||||
('Desert Compass NE', True), # Three popos
|
||||
('Desert Four Statues NW', True), # Four popos
|
||||
('Desert Four Statues ES', True), # Four popos
|
||||
('Hera Beetles WS', False), # Three blue beetles and only two pots, and bombs don't work.
|
||||
@@ -869,14 +892,17 @@ def default_rules(world, player):
|
||||
set_rule(world.get_entrance('Graveyard Ledge Mirror Spot', player), lambda state: state.has_Pearl(player) and state.has_Mirror(player))
|
||||
set_rule(world.get_entrance('Bumper Cave Entrance Rock', player), lambda state: state.has_Pearl(player) and state.can_lift_rocks(player))
|
||||
set_rule(world.get_entrance('Bumper Cave Ledge Mirror Spot', player), lambda state: state.has_Mirror(player))
|
||||
# this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off?
|
||||
set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: state.has_Pearl(player) and
|
||||
(state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player)))
|
||||
set_rule(world.get_entrance('Bat Cave Drop Ledge Mirror Spot', player), lambda state: state.has_Mirror(player))
|
||||
set_rule(world.get_entrance('Dark World Hammer Peg Cave', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Eastern Rocks', player), lambda state: state.has_Pearl(player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Peg Area Rocks', player), lambda state: state.has_Pearl(player) and state.can_lift_heavy_rocks(player))
|
||||
set_rule(world.get_entrance('Village of Outcasts Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Grassy Lawn Pegs', player), lambda state: state.has_Pearl(player) and state.has('Hammer', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Top)', player), lambda state: state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Exit (Bottom)', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Bottom to Top', player), lambda state: state.has('Cape', player))
|
||||
set_rule(world.get_entrance('Bumper Cave Top To Bottom', player), lambda state: state.has('Cape', player) or state.has('Hookshot', player))
|
||||
|
||||
set_rule(world.get_entrance('Skull Woods Final Section', player), lambda state: state.has('Fire Rod', player) and state.has_Pearl(player)) # bunny cannot use fire rod
|
||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has_Pearl(player) and state.has_sword(player) and state.has_misery_mire_medallion(player)) # sword required to cast magic (!)
|
||||
@@ -1233,7 +1259,7 @@ 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
|
||||
'Eastern Stalfos Spawn': ['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'], # Can use pots
|
||||
'Desert Compass Room': ['Desert Compass NW'], # Three popos
|
||||
'Desert Compass Room': ['Desert Compass NE'], # Three popos
|
||||
'Desert Four Statues': ['Desert Four Statues NW', 'Desert Four Statues ES'], # Four popos
|
||||
'Hera Beetles': ['Hera Beetles WS'], # Three blue beetles and only two pots, and bombs don't work.
|
||||
'Tower Gold Knights': ['Tower Gold Knights SW', 'Tower Gold Knights EN'], # Two ball and chain
|
||||
@@ -1284,7 +1310,7 @@ def standard_rules(world, player):
|
||||
# zelda should be saved before agahnim is in play
|
||||
add_rule(world.get_location('Agahnim 1', player), lambda state: state.has('Zelda Delivered', player))
|
||||
|
||||
# too restrictive for crossed?
|
||||
# uncle can't have keys generally because unplaced items aren't used here
|
||||
def uncle_item_rule(item):
|
||||
copy_state = CollectionState(world)
|
||||
copy_state.collect(item)
|
||||
@@ -1803,8 +1829,8 @@ def set_bunny_rules(world, player, inverted):
|
||||
|
||||
# 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.
|
||||
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave (Middle)',
|
||||
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
|
||||
bunny_impassable_caves = ['Bumper Cave (top)', 'Bumper Cave (bottom)', 'Two Brothers House',
|
||||
'Hookshot Cave (Middle)', 'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
|
||||
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
|
||||
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
|
||||
'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins',
|
||||
@@ -2008,7 +2034,7 @@ bunny_impassible_doors = {
|
||||
'Eastern Map Balcony Hook Path', 'Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW',
|
||||
'Eastern Darkness S', 'Eastern Darkness NE', 'Eastern Darkness Up Stairs',
|
||||
'Eastern Attic Start WS', 'Eastern Single Eyegore NE', 'Eastern Duo Eyegores NE', 'Desert Main Lobby Left Path',
|
||||
'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NW',
|
||||
'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NE',
|
||||
'Desert West Lobby NW', 'Desert Back Lobby NW', 'Desert Four Statues NW', 'Desert Four Statues ES',
|
||||
'Desert Beamos Hall WS', 'Desert Beamos Hall NE', 'Desert Wall Slide NW',
|
||||
'Hera Lobby to Front Barrier - Blue', 'Hera Front to Lobby Barrier - Blue', 'Hera Front to Down Stairs Barrier - Blue',
|
||||
@@ -2073,13 +2099,16 @@ bunny_impassible_doors = {
|
||||
|
||||
def add_key_logic_rules(world, player):
|
||||
key_logic = world.key_logic[player]
|
||||
eval_func = eval_small_key_door
|
||||
if world.key_logic_algorithm[player] == 'strict' and world.keyshuffle[player] == 'wild':
|
||||
eval_func = eval_small_key_door_strict
|
||||
for d_name, d_logic in key_logic.items():
|
||||
for door_name, rule in d_logic.door_rules.items():
|
||||
door_entrance = world.get_entrance(door_name, player)
|
||||
add_rule(door_entrance, eval_small_key_door(door_name, d_name, player))
|
||||
add_rule(door_entrance, eval_func(door_name, d_name, player))
|
||||
if door_entrance.door.dependents:
|
||||
for dep in door_entrance.door.dependents:
|
||||
add_rule(dep.entrance, eval_small_key_door(door_name, d_name, player))
|
||||
add_rule(dep.entrance, eval_func(door_name, d_name, player))
|
||||
for location in d_logic.bk_restricted:
|
||||
if not location.forced_item:
|
||||
forbid_item(location, d_logic.bk_name, player)
|
||||
@@ -2090,7 +2119,8 @@ def add_key_logic_rules(world, player):
|
||||
for chest in d_logic.bk_chests:
|
||||
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:
|
||||
if (len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1
|
||||
and world.accessibility[player] != 'locations'):
|
||||
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
for d_name, layout in world.key_layout[player].items():
|
||||
@@ -2103,6 +2133,8 @@ 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]
|
||||
if door_name not in key_logic.door_rules:
|
||||
return False
|
||||
door_rule = key_logic.door_rules[door_name]
|
||||
door_openable = False
|
||||
for ruleType, number in door_rule.new_rules.items():
|
||||
@@ -2111,9 +2143,9 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
|
||||
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
|
||||
small_loc_item = door_rule.small_location.item
|
||||
if small_loc_item and small_loc_item.name == key_logic.small_key_name and small_loc_item.player == player:
|
||||
door_openable |= state.has_sm_key(key_logic.small_key_name, player, number)
|
||||
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
|
||||
@@ -2125,10 +2157,24 @@ def eval_small_key_door_main(state, door_name, dungeon, player):
|
||||
return door_openable
|
||||
|
||||
|
||||
def eval_small_key_door_strict_main(state, door_name, dungeon, player):
|
||||
if state.is_door_open(door_name, player):
|
||||
return True
|
||||
key_layout = state.world.key_layout[player][dungeon]
|
||||
number = key_layout.max_chests
|
||||
if number <= 0:
|
||||
return True
|
||||
return state.has_sm_key_strict(key_layout.key_logic.small_key_name, player, number)
|
||||
|
||||
|
||||
def eval_small_key_door(door_name, dungeon, player):
|
||||
return lambda state: eval_small_key_door_main(state, door_name, dungeon, player)
|
||||
|
||||
|
||||
def eval_small_key_door_strict(door_name, dungeon, player):
|
||||
return lambda state: eval_small_key_door_strict_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
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ Triforce_texts = [
|
||||
'Who stole the fourth triangle?',
|
||||
'Trifource?\nMore Like Tritrice, am I right?'
|
||||
'\n Well Done!',
|
||||
'You just wasted 2 hours of your life.',
|
||||
'This was meant to be a trapezoid',
|
||||
# these ones are from web randomizer
|
||||
"\n G G",
|
||||
@@ -1716,7 +1715,7 @@ class TextTable(object):
|
||||
text['game_shooting_choice'] = CompressedTextMapper.convert("20 rupees.\n5 arrows.\nWin rupees!\nWant to play?\n ≥ Yes\n No\n{CHOICE}")
|
||||
text['game_shooting_yes'] = CompressedTextMapper.convert("Let's do this!")
|
||||
text['game_shooting_no'] = CompressedTextMapper.convert("Where are you going? Straight up!")
|
||||
text['game_shooting_continue'] = CompressedTextMapper.convert("Keep playing?\n ≥ yes\n no\n{CHOICE}")
|
||||
text['game_shooting_continue'] = CompressedTextMapper.convert("Keep playing?\n ≥ Yes\n No\n{CHOICE}")
|
||||
text['pond_of_wishing'] = CompressedTextMapper.convert("-Wishing Pond-\n\n On Vacation")
|
||||
text['pond_item_select'] = CompressedTextMapper.convert("Pick something\nto throw in.\n{ITEMSELECT}")
|
||||
text['pond_item_test'] = CompressedTextMapper.convert("You toss this?\n ≥ Yup\n Wrong\n{CHOICE}")
|
||||
|
||||
+40
-3
@@ -38,7 +38,7 @@ in each player section you can set up default settings for that player or you ca
|
||||
door_shuffle: basic
|
||||
```
|
||||
|
||||
Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each settings should be consistent with the CLI arguments.
|
||||
Player 1's settings will be determined by rolling the mystery weights and player 2's setting will be default except for those two specified in his section. Each setting should be consistent with the CLI arguments. Simple weighted settings are supported here. If you need sub-weights though, use a separate yaml file.
|
||||
|
||||
Start inventory is not supported here. It has a separate section.
|
||||
|
||||
@@ -71,7 +71,21 @@ You may list each location for a player and the item you wish to place there. A
|
||||
|
||||
Example:
|
||||
`Pegasus Boots#3` means the boots for player 3.
|
||||
|
||||
|
||||
|
||||
### advanced_placements
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate section. Each section is a list of placement rules. Each placement rule has a specific type.
|
||||
|
||||
Supported Types: PlacementGroup, NotPlacmentGroup
|
||||
|
||||
#### PlacementGroup
|
||||
|
||||
You may define an item, and a list of locations. The locations may be weighted if desired. The item will be placed at one of the listed locations - this currently ignores logic. The item will be placed there. The special location 'Random' indicates that the item should be placed randomly, without any other consideration. This may be repeated for placement of multiple items like multiple bows or swords.
|
||||
|
||||
#### NotPlacementGroup
|
||||
|
||||
You may define an item and a list of locations that an item should not be placed at. This will apply to all items of that type. The logic is considered for this. If it is otherwise impossible, the item will be considered for the listed locations. This is important for small key layouts mostly, but it will try other locations first.
|
||||
|
||||
### entrances
|
||||
|
||||
@@ -144,7 +158,7 @@ Misery Mire: Ether
|
||||
Turtle Rock: Quake
|
||||
```
|
||||
|
||||
Leave blank or omit if you wish it to be random.
|
||||
Leave blank or omit if you wish it to be random. Alternatively, a weighted dictionary is supported and a 'Random' option
|
||||
|
||||
### bosses
|
||||
|
||||
@@ -173,4 +187,27 @@ To start with multiple copies of progressive items, list them more than once.
|
||||
|
||||
This conflicts with the mystery yaml, if specified. These start inventory items will be added after those are added.
|
||||
|
||||
### drops
|
||||
|
||||
This must be defined by player. You may have prize packs, tree pulls, crab drops, stun prizes, and the fish prize defined using the following keys:
|
||||
```
|
||||
drops:
|
||||
1:
|
||||
Pack 1
|
||||
- Small Heart
|
||||
- Bombs (4)
|
||||
- Random
|
||||
- etc
|
||||
Pack 2: (list)
|
||||
...
|
||||
Pack 7: (list)
|
||||
Tree Pull Tier 1: Single Bomb
|
||||
Tree Pull Tier 2: Arrows (10)
|
||||
Tree Pull Tier 3: Fairy
|
||||
Crab Normal: Rupees (20)
|
||||
Crab Special: Small Magic
|
||||
Stun Prize: Bombs (8)
|
||||
Fish: Big Magic
|
||||
```
|
||||
|
||||
Prize packs expect a list of eight items each (anything not specified will be whatever randomization would have normally occurred). The special drops expect a single item. Packs 1 through 7 are supported. Prize pack 0 is not customizable.
|
||||
@@ -0,0 +1,96 @@
|
||||
Hint description:
|
||||
|
||||
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
|
||||
|
||||
Hint distribution for crossed entrance / crossed dungeon:
|
||||
|
||||
* 4 hints for inconvenient entrances.
|
||||
* 2 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
|
||||
* 1 hints for inconvenient item locations.
|
||||
* 7 hints for valuable items
|
||||
* 1 hint for the Thieves Town attic
|
||||
* 5 district hints
|
||||
|
||||
More vanilla shuffles of entrances reduces the number of inconvenient and random entrances hinted and generally follows adjustments in the ER_hint_reference.
|
||||
|
||||
Non-crossed door shuffles removes the attic hint and reduces the number of interesting items hinted at.
|
||||
|
||||
### Inconvenient entrance list
|
||||
|
||||
* Skull Woods Final
|
||||
* Ice Palace
|
||||
* Misery Mire
|
||||
* Turtle Rock
|
||||
* Ganon's Tower
|
||||
* Mimic Ledge
|
||||
* SW DM Foothills Cave (mirror from upper Bumper ledge)
|
||||
* Hammer Pegs (near purple chest)
|
||||
* Super Bomb cracked wall
|
||||
|
||||
### Inconvenient location list
|
||||
|
||||
* Purple Chest
|
||||
* Spike Cave
|
||||
* Magic Bat
|
||||
* Sahasrahla (Green Pendant)
|
||||
|
||||
The following are included only if door shuffle is vanilla:
|
||||
|
||||
* Swamp left (two chests)
|
||||
* Mire left (two chests)
|
||||
* Hera basement
|
||||
* Eastern Palace Big Key chest (protected by anti-fairies)
|
||||
* Thieves' Town Big Chest
|
||||
* Ice Palace Big Chest
|
||||
* Ganon's Tower Big Chest
|
||||
|
||||
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
|
||||
|
||||
* Graveyard Cave
|
||||
* Mimic Cave
|
||||
|
||||
### Valuable items
|
||||
|
||||
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
|
||||
|
||||
### District Hints
|
||||
|
||||
These replace all junk/joke hints.
|
||||
|
||||
Each dungeon is considered its own district. The overworld is divided into districts as according to this picture:
|
||||
|
||||

|
||||
|
||||
Each district is classified in one of five categories:
|
||||
|
||||
* Foolish
|
||||
* Connector Only
|
||||
* Dungeon Only
|
||||
* Useful
|
||||
* Vital
|
||||
|
||||
A vital district contains one or more vital items. This includes:
|
||||
* Y Items: Bows, Hookshot, Both Rods, Bombos, Lamp, Hammer, Ocarina, Book, Cane of Somaria, Mirror
|
||||
* A Items: Gloves, Moon Pearl, Boots, Flippers
|
||||
* All Swords, Triforce Pieces, and the Silver Arrows
|
||||
* Ether and Quake if used to open an entrance
|
||||
* Big Keys, Small Keys, Bomb Bags, Arrow Quivers in the appropriate mode (wild big keys, wild small keys, bomb bag, retro + shopsanity)
|
||||
|
||||
The hint will name the item if there is only one vital one, or give the number otherwise.
|
||||
|
||||
Useful district contain one or more useful items, but no vital items.
|
||||
|
||||
* Y Items: Boomerangs, Magic Powder, Mushroom, Shovel, Bug Net, Bottles, Cane of Byrna, Cape
|
||||
* Ether and Quake if not used to open an entrance
|
||||
* Shields, Mails, Magic Upgrades
|
||||
|
||||
Notably, the fetch items may technically be required depending on what is located at the fetch quest, but are considered useful still.
|
||||
|
||||
The hint will name the item if there is only one, or give the number of useful items otherwise.
|
||||
|
||||
A "Dungeon Only" district has no vital or useful items but contains one dungeon or more. The hint will pick one of the dungeon and state: "{district} is on the path to {dungeon}"
|
||||
|
||||
A "Connector Only" district has no vital or useful items but contains a connector that reaches a potentially inaccessible area like
|
||||
the Turtle Rock upper ledge or the Maze Race (you have no mirror yet). The hint will state "{district} can reach {region}"
|
||||
|
||||
Foolish districts have no dungeons, no potentially useful connector and no useful or vital items. These districts, if they exist, are preferred for hints. The others are chosen randomly.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
mode: inverted
|
||||
mapshuffle: True
|
||||
keyshuffle: wild
|
||||
placements:
|
||||
1:
|
||||
Link's Uncle: Progressive Sword
|
||||
Blacksmith: Progressive Sword
|
||||
Master Sword Pedestal: Progressive Sword
|
||||
Pyramid Fairy - Left: Progressive Sword
|
||||
Desert Palace - Boss: Compass (Desert Palace)
|
||||
Eastern Palace - Boss: Compass (Eastern Palace)
|
||||
Tower of Hera - Boss: Compass (Tower of Hera)
|
||||
Swamp Palace - Boss: Compass (Swamp Palace)
|
||||
Thieves' Town - Boss: Compass (Thieves Town)
|
||||
Skull Woods - Boss: Compass (Skull Woods)
|
||||
Ice Palace - Boss: Compass (Ice Palace)
|
||||
Misery Mire - Boss: Compass (Misery Mire)
|
||||
Turtle Rock - Boss: Compass (Turtle Rock)
|
||||
Palace of Darkness - Boss: Compass (Palace of Darkness)
|
||||
Eastern Palace - Big Key Chest: Big Key (Eastern Palace)
|
||||
Desert Palace - Big Key Chest: Big Key (Desert Palace)
|
||||
Tower of Hera - Big Key Chest: Big Key (Tower of Hera)
|
||||
Palace of Darkness - Big Key Chest: Big Key (Palace of Darkness)
|
||||
Thieves' Town - Big Key Chest: Big Key (Thieves Town)
|
||||
Skull Woods - Big Key Chest: Big Key (Skull Woods)
|
||||
Swamp Palace - Big Key Chest: Big Key (Swamp Palace)
|
||||
Ice Palace - Big Key Chest: Big Key (Ice Palace)
|
||||
Misery Mire - Big Key Chest: Big Key (Misery Mire)
|
||||
Turtle Rock - Big Key Chest: Big Key (Turtle Rock)
|
||||
Ganons Tower - Big Key Chest: Big Key (Ganons Tower)
|
||||
start_inventory:
|
||||
1:
|
||||
- Ocarina (Activated)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
mode: inverted
|
||||
goal: dungeons
|
||||
shuffleenemies: shuffled
|
||||
shufflebosses: full
|
||||
item_pool:
|
||||
1:
|
||||
Progressive Bow: 8
|
||||
Blue Boomerang: 4
|
||||
Red Boomerang: 4
|
||||
Hookshot: 4
|
||||
Magic Powder: 4
|
||||
Mushroom: 4
|
||||
Ice Rod: 4
|
||||
Fire Rod: 4
|
||||
Bombos: 4
|
||||
Ether: 4
|
||||
Quake: 4
|
||||
Lamp: 4
|
||||
Hammer: 4
|
||||
Ocarina: 4
|
||||
Shovel: 4
|
||||
Bug Catching Net: 4
|
||||
Book of Mudora: 4
|
||||
Bottle (Random): 16
|
||||
Cane of Byrna: 4
|
||||
Cane of Somaria: 4
|
||||
Cape: 4
|
||||
Magic Mirror: 4
|
||||
Flippers: 4
|
||||
Moon Pearl: 4
|
||||
Pegasus Boots: 4
|
||||
Progressive Glove: 8
|
||||
Progressive Armor: 8
|
||||
Progressive Shield: 12
|
||||
Progressive Sword: 16
|
||||
Magic Upgrade (1/2): 4
|
||||
start_inventory:
|
||||
1:
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Pegasus Boots
|
||||
- Rupees (5)
|
||||
- Rupees (5)
|
||||
- Rupees (5)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
- Compass (Desert Palace)
|
||||
- Compass (Eastern Palace)
|
||||
- Compass (Tower of Hera)
|
||||
- Compass (Swamp Palace)
|
||||
- Compass (Thieves Town)
|
||||
- Compass (Skull Woods)
|
||||
- Compass (Ice Palace)
|
||||
- Compass (Misery Mire)
|
||||
- Compass (Turtle Rock)
|
||||
- Compass (Palace of Darkness)
|
||||
- Compass (Ganons Tower)
|
||||
drops:
|
||||
1:
|
||||
Tree Pull Tier 1: Single Bomb
|
||||
Tree Pull Tier 2: Bombs (4)
|
||||
Tree Pull Tier 3: Bombs (8)
|
||||
Crab Normal: Rupees (20)
|
||||
Crab Special: Big Magic
|
||||
Stun Prize: Rupees (20)
|
||||
@@ -0,0 +1,443 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
mode:
|
||||
open: 1
|
||||
standard: 1
|
||||
mapshuffle: True
|
||||
compassshuffle: True
|
||||
boots_hint:
|
||||
True: 1
|
||||
False: 1
|
||||
advanced_placements:
|
||||
1:
|
||||
- item: Green Pendant
|
||||
locations:
|
||||
- Eastern Palace - Prize
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Crystal 5
|
||||
locations:
|
||||
- Ice Palace - Prize
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Crystal 6
|
||||
locations:
|
||||
- Misery Mire - Prize
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Bow
|
||||
locations:
|
||||
- Eastern Palace - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Bow
|
||||
locations:
|
||||
- Pyramid Fairy - Right
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Book of Mudora
|
||||
locations:
|
||||
- Library
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Hammer
|
||||
locations:
|
||||
- Palace of Darkness - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Hookshot
|
||||
locations:
|
||||
- Swamp Palace - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Magic Mirror
|
||||
locations:
|
||||
- Old Man
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Ocarina
|
||||
locations:
|
||||
- Flute Spot
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Pegasus Boots
|
||||
locations:
|
||||
- Sahasrahla
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Cape
|
||||
locations:
|
||||
- King's Tomb
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Mushroom
|
||||
locations:
|
||||
- Mushroom
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Shovel
|
||||
locations:
|
||||
- Stumpy
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Lamp
|
||||
locations:
|
||||
Link's House: 1
|
||||
Secret Passage: 1
|
||||
Hyrule Castle - Zelda's Chest: 1
|
||||
Random: 3
|
||||
type: LocationGroup
|
||||
- item: Magic Powder
|
||||
locations:
|
||||
- Potion Shop
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Moon Pearl
|
||||
locations:
|
||||
- Tower of Hera - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Cane of Somaria
|
||||
locations:
|
||||
- Misery Mire - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Fire Rod
|
||||
locations:
|
||||
- Skull Woods - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Flippers
|
||||
locations:
|
||||
- King Zora
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Ice Rod
|
||||
locations:
|
||||
- Ice Rod Cave
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bombos
|
||||
locations:
|
||||
- Bombos Tablet
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Ether
|
||||
locations:
|
||||
- Ether Tablet
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Quake
|
||||
locations:
|
||||
- Catfish
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bottle
|
||||
locations:
|
||||
- Bottle Merchant
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bottle
|
||||
locations:
|
||||
- Kakariko Tavern
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bottle
|
||||
locations:
|
||||
- Purple Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bottle
|
||||
locations:
|
||||
- Hobo
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Sword
|
||||
locations:
|
||||
- Link's Uncle
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Sword
|
||||
locations:
|
||||
- Blacksmith
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Sword
|
||||
locations:
|
||||
- Master Sword Pedestal
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Sword
|
||||
locations:
|
||||
- Pyramid Fairy - Left
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Glove
|
||||
locations:
|
||||
- Desert Palace - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Glove
|
||||
locations:
|
||||
- Thieves' Town - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Armor
|
||||
locations:
|
||||
- Ice Palace - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Armor
|
||||
locations:
|
||||
- Ganons Tower - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Blue Boomerang
|
||||
locations:
|
||||
- Hyrule Castle - Boomerang Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Red Boomerang
|
||||
locations:
|
||||
- Waterfall Fairy - Left
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Shield
|
||||
locations:
|
||||
- Secret Passage
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Shield
|
||||
locations:
|
||||
- Waterfall Fairy - Right
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Progressive Shield
|
||||
locations:
|
||||
- Turtle Rock - Big Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Bug Catching Net
|
||||
locations:
|
||||
- Sick Kid
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Cane of Byrna
|
||||
locations:
|
||||
- Spike Cave
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Desert Palace - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Eastern Palace - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Tower of Hera - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Swamp Palace - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Thieves' Town - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Skull Woods - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Ice Palace - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Misery Mire - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Turtle Rock - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Boss Heart Container
|
||||
locations:
|
||||
- Palace of Darkness - Boss
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Sanctuary Heart Container
|
||||
locations:
|
||||
- Sanctuary
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Rupees (300)
|
||||
locations:
|
||||
- Mini Moldorm Cave - Generous Guy
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Rupees (300)
|
||||
locations:
|
||||
- Sewers - Secret Room - Middle
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Rupees (300)
|
||||
locations:
|
||||
- Hype Cave - Generous Guy
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Rupees (300)
|
||||
locations:
|
||||
- Brewery
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Rupees (300)
|
||||
locations:
|
||||
- C-Shaped House
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Magic Upgrade (1/2)
|
||||
locations:
|
||||
- Magic Bat
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Eastern Palace)
|
||||
locations:
|
||||
- Eastern Palace - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Eastern Palace)
|
||||
locations:
|
||||
- Eastern Palace - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Desert Palace)
|
||||
locations:
|
||||
- Desert Palace - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Desert Palace)
|
||||
locations:
|
||||
- Desert Palace - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Tower of Hera)
|
||||
locations:
|
||||
- Tower of Hera - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Tower of Hera)
|
||||
locations:
|
||||
- Tower of Hera - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Escape)
|
||||
locations:
|
||||
- Hyrule Castle - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Palace of Darkness)
|
||||
locations:
|
||||
- Palace of Darkness - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Palace of Darkness)
|
||||
locations:
|
||||
- Palace of Darkness - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Thieves Town)
|
||||
locations:
|
||||
- Thieves' Town - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Thieves Town)
|
||||
locations:
|
||||
- Thieves' Town - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Skull Woods)
|
||||
locations:
|
||||
- Skull Woods - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Skull Woods)
|
||||
locations:
|
||||
- Skull Woods - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Swamp Palace)
|
||||
locations:
|
||||
- Swamp Palace - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Swamp Palace)
|
||||
locations:
|
||||
- Swamp Palace - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Ice Palace)
|
||||
locations:
|
||||
- Ice Palace - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Ice Palace)
|
||||
locations:
|
||||
- Ice Palace - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Misery Mire)
|
||||
locations:
|
||||
- Misery Mire - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Misery Mire)
|
||||
locations:
|
||||
- Misery Mire - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Turtle Rock)
|
||||
locations:
|
||||
- Turtle Rock - Compass Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Turtle Rock)
|
||||
locations:
|
||||
- Turtle Rock - Roller Room - Left
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Compass (Ganons Tower)
|
||||
locations:
|
||||
- Ganons Tower - Compass Room - Top Left
|
||||
- Random
|
||||
type: LocationGroup
|
||||
- item: Map (Ganons Tower)
|
||||
locations:
|
||||
- Ganons Tower - Map Chest
|
||||
- Random
|
||||
type: LocationGroup
|
||||
medallions:
|
||||
1:
|
||||
Misery Mire:
|
||||
Ether: 1
|
||||
Random: 1
|
||||
Turtle Rock:
|
||||
Quake: 1
|
||||
Random: 1
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
goal: triforcehunt
|
||||
shuffle: crossed
|
||||
keysanity: True
|
||||
shopsanity: True
|
||||
keydropshuffle: True
|
||||
pottery: lottery
|
||||
dungeon_counters: 'on'
|
||||
triforce_goal: 420
|
||||
triforce_pool: 420
|
||||
start_inventory:
|
||||
1:
|
||||
- Pegasus Boots
|
||||
- Lamp
|
||||
- Rupees (300)
|
||||
- Rupees (100)
|
||||
- Rupees (20)
|
||||
@@ -0,0 +1,21 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
goal: triforcehunt
|
||||
shuffle: crossed
|
||||
keysanity: True
|
||||
shopsanity: True
|
||||
keydropshuffle: True
|
||||
pottery: lottery
|
||||
dungeon_counters: 'on'
|
||||
triforce_goal: 213
|
||||
triforce_pool: 420
|
||||
start_inventory:
|
||||
1:
|
||||
- Pegasus Boots
|
||||
- Lamp
|
||||
- Rupees (300)
|
||||
- Rupees (100)
|
||||
- Rupees (20)
|
||||
@@ -0,0 +1,259 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
bigkeyshuffle: True
|
||||
placements:
|
||||
1:
|
||||
Potion Shop: Shovel
|
||||
Flute Spot: Magic Powder
|
||||
advanced_placements:
|
||||
1:
|
||||
- item: Progressive Bow
|
||||
locations:
|
||||
- Palace of Darkness - Boss
|
||||
- Palace of Darkness - Map Chest
|
||||
- Palace of Darkness - The Arena - Ledge
|
||||
- Eastern Palace - Boss
|
||||
- Ganons Tower - Pre-Moldorm Chest
|
||||
- Ganons Tower - Mini Helmasaur Room - Left
|
||||
- Ganons Tower - Mini Helmasaur Room - Right
|
||||
- Ganons Tower - Validation Chest
|
||||
type: LocationGroup
|
||||
- item: Fire Rod
|
||||
locations:
|
||||
- Ice Palace - Big Chest
|
||||
- Ice Palace - Boss
|
||||
- Ice Palace - Big Key Chest
|
||||
- Ice Palace - Compass Chest
|
||||
- Ice Palace - Map Chest
|
||||
- Ice Palace - Spike Room
|
||||
- Ice Palace - Iced T Room
|
||||
- Ice Palace - Freezor Chest
|
||||
- Desert Palace - Boss
|
||||
- Tower of Hera - Big Key Chest
|
||||
- Misery Mire - Big Key Chest
|
||||
- Misery Mire - Compass Chest
|
||||
type: LocationGroup
|
||||
- item: Pegasus Boots
|
||||
locations:
|
||||
- Hookshot Cave - Top Right
|
||||
- Hookshot Cave - Top Left
|
||||
- Hookshot Cave - Bottom Right
|
||||
- Hookshot Cave - Bottom Left
|
||||
- Mushroom Spot
|
||||
- Zora Ledge
|
||||
- Bombos Tablet
|
||||
- Ether Tablet
|
||||
- Floating Island
|
||||
- Lake Hylia Island
|
||||
- Maze Race
|
||||
- Spectacle Rock
|
||||
- Spectacle Rock Cave
|
||||
- Pyramid
|
||||
- Desert Ledge
|
||||
- Bumper Cave Ledge
|
||||
type: LocationGroup
|
||||
- item: Progressive Sword
|
||||
locations:
|
||||
- Swamp Palace - Big Chest
|
||||
- Swamp Palace - Boss
|
||||
- Swamp Palace - Big Key Chest
|
||||
- Swamp Palace - Compass Chest
|
||||
- Swamp Palace - Map Chest
|
||||
- Swamp Palace - Entrance
|
||||
- Swamp Palace - Flooded Room - Left
|
||||
- Swamp Palace - Flooded Room - Right
|
||||
- Swamp Palace - Waterfall Room
|
||||
- Swamp Palace - West Chest
|
||||
type: LocationGroup
|
||||
- item: Titans Mitts
|
||||
locations:
|
||||
- Swamp Palace - Big Chest
|
||||
- Swamp Palace - Boss
|
||||
- Swamp Palace - Big Key Chest
|
||||
- Swamp Palace - Compass Chest
|
||||
- Swamp Palace - Map Chest
|
||||
- Swamp Palace - Entrance
|
||||
- Swamp Palace - Flooded Room - Left
|
||||
- Swamp Palace - Flooded Room - Right
|
||||
- Swamp Palace - Waterfall Room
|
||||
- Swamp Palace - West Chest
|
||||
type: LocationGroup
|
||||
- item: Ice Rod
|
||||
type: LocationGroup
|
||||
locations:
|
||||
- Turtle Rock - Big Chest
|
||||
- Turtle Rock - Boss
|
||||
- Turtle Rock - Big Key Chest
|
||||
- Turtle Rock - Compass Chest
|
||||
- Turtle Rock - Roller Room - Left
|
||||
- Turtle Rock - Chain Chomps
|
||||
- Turtle Rock - Crystaroller Room
|
||||
- Turtle Rock - Roller Room - Right
|
||||
- Turtle Rock - Eye Bridge - Bottom Left
|
||||
- Turtle Rock - Eye Bridge - Bottom Right
|
||||
- Turtle Rock - Eye Bridge - Top Left
|
||||
- Turtle Rock - Eye Bridge - Top Right
|
||||
- Mimic Cave
|
||||
- Link's House
|
||||
- Link's Uncle
|
||||
- Secret Passage
|
||||
- Hyrule Castle - Map Chest
|
||||
- Sanctuary
|
||||
- Mushroom Spot
|
||||
- Lost Woods Hideout
|
||||
- Blind's Hideout - Left
|
||||
- Blind's Hideout - Right
|
||||
- Blind's Hideout - Far Left
|
||||
- Blind's Hideout - Far Right
|
||||
- Blind's Hideout - Top
|
||||
- Kakariko Well - Right
|
||||
- Kakariko Well - Left
|
||||
- Kakariko Well - Middle
|
||||
- Kakariko Well - Top
|
||||
- Kakariko Well - Bottom
|
||||
- Chicken House
|
||||
- Bottle Merchant
|
||||
- Kakariko Tavern
|
||||
- Maze Race
|
||||
- Floodgate Chest
|
||||
- Sunken Treasure
|
||||
- Mini Moldorm Cave - Left
|
||||
- Mini Moldorm Cave - Right
|
||||
- Mini Moldorm Cave - Generous Guy
|
||||
- Mini Moldorm Cave - Far Left
|
||||
- Mini Moldorm Cave - Far Right
|
||||
- Sahasrahla's Hut - Middle
|
||||
- Sahasrahla's Hut - Right
|
||||
- Sahasrahla's Hut - Left
|
||||
- Ice Rod Cave
|
||||
- Eastern Palace - Compass Chest
|
||||
- Eastern Palace - Map Chest
|
||||
- Eastern Palace - Cannonball Chest
|
||||
- Aginah's Cave
|
||||
- item: Big Key (Eastern Palace)
|
||||
locations:
|
||||
- Eastern Palace - Cannonball Chest
|
||||
- Eastern Palace - Big Key Chest
|
||||
- Eastern Palace - Compass Chest
|
||||
- Eastern Palace - Map Chest
|
||||
type: LocationGroup
|
||||
- item: Big Key (Desert Palace)
|
||||
locations:
|
||||
- Desert Palace - Big Key Chest
|
||||
- Desert Palace - Compass Chest
|
||||
- Desert Palace - Map Chest
|
||||
- Desert Palace - Torch
|
||||
type: LocationGroup
|
||||
- item: Big Key (Palace of Darkness)
|
||||
locations:
|
||||
- Palace of Darkness - Big Key Chest
|
||||
- Palace of Darkness - Compass Chest
|
||||
- Palace of Darkness - Map Chest
|
||||
- Palace of Darkness - Stalfos Basement
|
||||
- Palace of Darkness - Dark Basement - Right
|
||||
- Palace of Darkness - Harmless Hellway
|
||||
- Palace of Darkness - Shooter Room
|
||||
- Palace of Darkness - The Arena - Bridge
|
||||
- Palace of Darkness - The Arena - Ledge
|
||||
- Palace of Darkness - Dark Basement - Left
|
||||
- Palace of Darkness - Dark Maze - Bottom
|
||||
- Palace of Darkness - Dark Maze - Top
|
||||
type: LocationGroup
|
||||
- item: Big Key (Thieves Town)
|
||||
locations:
|
||||
- Thieves' Town - Big Key Chest
|
||||
- Thieves' Town - Compass Chest
|
||||
- Thieves' Town - Map Chest
|
||||
- Thieves' Town - Ambush Chest
|
||||
type: LocationGroup
|
||||
- item: Big Key (Misery Mire)
|
||||
locations:
|
||||
- Misery Mire - Big Key Chest
|
||||
- Misery Mire - Compass Chest
|
||||
- Misery Mire - Map Chest
|
||||
- Misery Mire - Spike Chest
|
||||
- Misery Mire - Main Lobby
|
||||
- Misery Mire - Bridge Chest
|
||||
type: LocationGroup
|
||||
- item: Big Key (Turtle Rock)
|
||||
locations:
|
||||
- Turtle Rock - Big Key Chest
|
||||
- Turtle Rock - Compass Chest
|
||||
- Turtle Rock - Roller Room - Left
|
||||
- Turtle Rock - Chain Chomps
|
||||
- Turtle Rock - Roller Room - Right
|
||||
type: LocationGroup
|
||||
- item: Big Key (Ganons Tower)
|
||||
locations:
|
||||
- Ganons Tower - Big Key Chest
|
||||
- Ganons Tower - Compass Room - Top Left
|
||||
- Ganons Tower - Map Chest
|
||||
- Ganons Tower - Bob's Torch
|
||||
- Ganons Tower - Tile Room
|
||||
- Ganons Tower - Firesnake Room
|
||||
- Ganons Tower - DMs Room - Top Right
|
||||
- Ganons Tower - DMs Room - Top Left
|
||||
- Ganons Tower - DMs Room - Bottom Left
|
||||
- Ganons Tower - DMs Room - Bottom Right
|
||||
- Ganons Tower - Compass Room - Top Right
|
||||
- Ganons Tower - Compass Room - Bottom Right
|
||||
- Ganons Tower - Compass Room - Bottom Left
|
||||
- Ganons Tower - Hope Room - Left
|
||||
- Ganons Tower - Hope Room - Right
|
||||
- Ganons Tower - Randomizer Room - Top Left
|
||||
- Ganons Tower - Randomizer Room - Top Right
|
||||
- Ganons Tower - Randomizer Room - Bottom Right
|
||||
- Ganons Tower - Randomizer Room - Bottom Left
|
||||
- Ganons Tower - Bob's Chest
|
||||
- Ganons Tower - Big Key Room - Left
|
||||
- Ganons Tower - Big Key Room - Right
|
||||
type: LocationGroup
|
||||
item_pool:
|
||||
1:
|
||||
Arrows (10): 12
|
||||
Blue Boomerang: 1
|
||||
Bombos: 1
|
||||
Bombs (10): 1
|
||||
Bombs (3): 16
|
||||
Book of Mudora: 1
|
||||
Boss Heart Container: 10
|
||||
Bottle (Random): 4
|
||||
Bug Catching Net: 1
|
||||
Cane of Byrna: 1
|
||||
Cane of Somaria: 1
|
||||
Cape: 1
|
||||
Ether: 1
|
||||
Fire Rod: 1
|
||||
Flippers: 1
|
||||
Hammer: 1
|
||||
Hookshot: 1
|
||||
Ice Rod: 1
|
||||
Lamp: 1
|
||||
Magic Mirror: 1
|
||||
Magic Powder: 1
|
||||
Magic Upgrade (1/2): 1
|
||||
Moon Pearl: 1
|
||||
Mushroom: 1
|
||||
Ocarina: 1
|
||||
Pegasus Boots: 1
|
||||
Piece of Heart: 24
|
||||
Progressive Armor: 2
|
||||
Progressive Bow: 2
|
||||
Progressive Glove: 2
|
||||
Progressive Shield: 3
|
||||
Progressive Sword: 4
|
||||
Titans Mitts: 1
|
||||
Quake: 1
|
||||
Red Boomerang: 1
|
||||
Rupee (1): 2
|
||||
Rupees (100): 1
|
||||
Rupees (20): 27
|
||||
Rupees (300): 5
|
||||
Rupees (5): 4
|
||||
Rupees (50): 7
|
||||
Sanctuary Heart Container: 1
|
||||
Shovel: 1
|
||||
Single Arrow: 1
|
||||
@@ -0,0 +1,24 @@
|
||||
meta:
|
||||
players: 1
|
||||
race: true
|
||||
settings:
|
||||
1:
|
||||
shopsanity: true
|
||||
pseudoboots: true
|
||||
goal: crystals
|
||||
crystals_gt: random
|
||||
bombbag: true
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
keysanity: true
|
||||
door_shuffle: crossed
|
||||
intensity: 3
|
||||
door_type_mode: big
|
||||
pottery: keys
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
dungeon_counters: 'on'
|
||||
hints: true
|
||||
msu_resume: true
|
||||
collection_rate: true
|
||||
quickswap: true
|
||||
@@ -0,0 +1,23 @@
|
||||
meta:
|
||||
players: 1
|
||||
race: true
|
||||
settings:
|
||||
1:
|
||||
shopsanity: true
|
||||
pseudoboots: true
|
||||
goal: crystals
|
||||
crystals_gt: random
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
keysanity: true
|
||||
door_shuffle: crossed
|
||||
intensity: 3
|
||||
door_type_mode: big
|
||||
pottery: keys
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
dungeon_counters: 'on'
|
||||
hints: true
|
||||
msu_resume: true
|
||||
collection_rate: true
|
||||
quickswap: true
|
||||
@@ -0,0 +1,23 @@
|
||||
meta:
|
||||
players: 1
|
||||
race: true
|
||||
settings:
|
||||
1:
|
||||
shopsanity: true
|
||||
pseudoboots: true
|
||||
goal: crystals
|
||||
crystals_gt: random
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
keysanity: true
|
||||
door_shuffle: crossed
|
||||
intensity: 3
|
||||
door_type_mode: big
|
||||
pottery: lottery
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
dungeon_counters: 'on'
|
||||
hints: true
|
||||
msu_resume: true
|
||||
collection_rate: true
|
||||
quickswap: true
|
||||
@@ -0,0 +1,25 @@
|
||||
meta:
|
||||
players: 1
|
||||
race: true
|
||||
settings:
|
||||
1:
|
||||
shopsanity: true
|
||||
pseudoboots: true
|
||||
mode: standard
|
||||
swords: assured
|
||||
goal: crystals
|
||||
crystals_gt: random
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
keysanity: true
|
||||
door_shuffle: crossed
|
||||
intensity: 3
|
||||
door_type_mode: big
|
||||
pottery: keys
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
dungeon_counters: 'on'
|
||||
hints: true
|
||||
msu_resume: true
|
||||
collection_rate: true
|
||||
quickswap: true
|
||||
+11
-1
@@ -22,6 +22,14 @@
|
||||
big: 2
|
||||
all: 1
|
||||
chaos: 1
|
||||
trap_door_mode:
|
||||
vanilla: 1
|
||||
boss: 0
|
||||
oneway: 0
|
||||
key_logic_algorithm:
|
||||
default: 1
|
||||
partial: 0
|
||||
strict: 0
|
||||
decoupledoors: off
|
||||
dropshuffle:
|
||||
none: 4
|
||||
@@ -83,6 +91,8 @@
|
||||
pedestal: 2
|
||||
triforce-hunt: 2
|
||||
trinity: 2
|
||||
ganonhunt: 2
|
||||
completionist: 1
|
||||
triforce_goal_min: 10
|
||||
triforce_goal_max: 30
|
||||
triforce_pool_min: 20
|
||||
@@ -96,7 +106,7 @@
|
||||
mcu: 1 # map, compass, universal smalls
|
||||
# for use when you aren't using the dungeon_items above
|
||||
# smallkey_shuffle:
|
||||
# standard: 5
|
||||
# none: 5
|
||||
# wild: 1
|
||||
# universal: 1
|
||||
dungeon_counters:
|
||||
|
||||
+19
-1
@@ -7,12 +7,30 @@ algorithm:
|
||||
district: 1
|
||||
door_shuffle:
|
||||
vanilla: 1
|
||||
basic: 2
|
||||
basic: 1
|
||||
partitioned: 1
|
||||
crossed: 3 # crossed yield more errors so is preferred
|
||||
intensity:
|
||||
1: 1
|
||||
2: 1
|
||||
3: 2 # intensity 3 usually yield more errors
|
||||
door_type_mode:
|
||||
original: 2
|
||||
big: 2
|
||||
all: 1
|
||||
chaos: 1
|
||||
trap_door_mode:
|
||||
vanilla: 3 # more errors
|
||||
optional: 1
|
||||
boss: 1
|
||||
oneway: 1
|
||||
key_logic_algorithm:
|
||||
default: 1
|
||||
partial: 0
|
||||
strict: 0
|
||||
decoupledoors:
|
||||
off: 9 # more strict
|
||||
on: 1
|
||||
dropshuffle:
|
||||
none: 10 # fewer locations
|
||||
keys: 1
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"retro"
|
||||
]
|
||||
},
|
||||
"boots_hint": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"swords": {
|
||||
"choices": [
|
||||
"random",
|
||||
@@ -63,7 +67,9 @@
|
||||
"dungeons",
|
||||
"triforcehunt",
|
||||
"trinity",
|
||||
"crystals"
|
||||
"crystals",
|
||||
"ganonhunt",
|
||||
"completionist"
|
||||
]
|
||||
},
|
||||
"difficulty": {
|
||||
@@ -190,6 +196,21 @@
|
||||
"chaos"
|
||||
]
|
||||
},
|
||||
"trap_door_mode": {
|
||||
"choices": [
|
||||
"vanilla",
|
||||
"optional",
|
||||
"boss",
|
||||
"oneway"
|
||||
]
|
||||
},
|
||||
"key_logic_algorithm": {
|
||||
"choices": [
|
||||
"default",
|
||||
"partial",
|
||||
"strict"
|
||||
]
|
||||
},
|
||||
"decoupledoors": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
|
||||
@@ -107,7 +107,10 @@
|
||||
"Triforce Hunt: Places 30 Triforce Pieces in the world, collect",
|
||||
" 20 of them to beat the game.",
|
||||
"Trinity: Can beat the game by defeating Ganon, pulling",
|
||||
" Pedestal, or delivering Triforce Pieces."
|
||||
" Pedestal, or delivering Triforce Pieces.",
|
||||
"Ganon Hunt: Places 30 Triforce Pieces in the world, collect",
|
||||
" 20 of them then defeat Ganon.",
|
||||
"Completionist: Find everything then defeat Ganon."
|
||||
],
|
||||
"difficulty": [
|
||||
"Select game difficulty. Affects available itempool. (default: %(default)s)",
|
||||
@@ -236,6 +239,19 @@
|
||||
"all: Adds traps doors (and any future supported door types)",
|
||||
"chaos: Increases the number of door types in all dungeon pools"
|
||||
],
|
||||
"trap_door_mode" : [
|
||||
"Trap Door Removal (default: %(default)s)",
|
||||
"vanilla: No trap door removal",
|
||||
"optional: Trap doors removed if blocking",
|
||||
"boss: Also remove boss traps",
|
||||
"oneway: Remove all annoying trap doors"
|
||||
],
|
||||
"key_logic_algorithm": [
|
||||
"Key Logic Algorithm (default: %(default)s)",
|
||||
"default: Balance between safety and randomization",
|
||||
"partial: Partial protection when using certain minor glitches",
|
||||
"strict: Ensure small keys are available"
|
||||
],
|
||||
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
|
||||
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
||||
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
||||
|
||||
@@ -94,6 +94,17 @@
|
||||
"randomizer.dungeon.door_type_mode.all": "Adds Trap Doors",
|
||||
"randomizer.dungeon.door_type_mode.chaos": "Increases all door types",
|
||||
|
||||
"randomizer.dungeon.trap_door_mode": "Trap Door Removal",
|
||||
"randomizer.dungeon.trap_door_mode.vanilla": "No Removal",
|
||||
"randomizer.dungeon.trap_door_mode.optional": "Removed If Blocking Path",
|
||||
"randomizer.dungeon.trap_door_mode.boss": "Also Remove Boss Traps",
|
||||
"randomizer.dungeon.trap_door_mode.oneway": "Remove All Annoying Traps",
|
||||
|
||||
"randomizer.dungeon.key_logic_algorithm": "Key Logic Algorithm",
|
||||
"randomizer.dungeon.key_logic_algorithm.default": "Default",
|
||||
"randomizer.dungeon.key_logic_algorithm.partial": "Partial Protection",
|
||||
"randomizer.dungeon.key_logic_algorithm.strict": "Strict",
|
||||
|
||||
"randomizer.dungeon.experimental": "Enable Experimental Features",
|
||||
|
||||
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
|
||||
@@ -216,6 +227,7 @@
|
||||
"randomizer.generation.print_custom_yaml": "Print Customizer File",
|
||||
"randomizer.generation.usestartinventory": "Use Starting Inventory",
|
||||
"randomizer.generation.usecustompool": "Use Custom Item Pool",
|
||||
"randomizer.generation.race": "Generate \"Race\" ROM",
|
||||
|
||||
"randomizer.generation.saveonexit": "Save Settings on Exit",
|
||||
"randomizer.generation.saveonexit.ask": "Ask Me",
|
||||
@@ -251,6 +263,8 @@
|
||||
"randomizer.item.goal.triforcehunt": "Triforce Hunt",
|
||||
"randomizer.item.goal.trinity": "Trinity",
|
||||
"randomizer.item.goal.crystals": "Crystals",
|
||||
"randomizer.item.goal.ganonhunt": "Triforce Hunt + Ganon",
|
||||
"randomizer.item.goal.completionist": "Completionist",
|
||||
|
||||
"randomizer.item.crystals_gt": "Crystals to open GT",
|
||||
"randomizer.item.crystals_gt.0": "0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"widgets": {
|
||||
"keyshuffle": {
|
||||
"smallkeyshuffle": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"none",
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"door_type_mode": {
|
||||
"type": "selectbox",
|
||||
"default": "basic",
|
||||
"default": "original",
|
||||
"options": [
|
||||
"original",
|
||||
"big",
|
||||
@@ -44,6 +44,28 @@
|
||||
"width": 45
|
||||
}
|
||||
},
|
||||
"trap_door_mode": {
|
||||
"type": "selectbox",
|
||||
"default": "optional",
|
||||
"options": [
|
||||
"vanilla",
|
||||
"optional",
|
||||
"boss",
|
||||
"oneway"
|
||||
],
|
||||
"config": {
|
||||
"width": 30
|
||||
}
|
||||
},
|
||||
"key_logic_algorithm": {
|
||||
"type": "selectbox",
|
||||
"default": "default",
|
||||
"options": [
|
||||
"default",
|
||||
"partial",
|
||||
"strict"
|
||||
]
|
||||
},
|
||||
"decoupledoors": { "type": "checkbox" },
|
||||
"keydropshuffle": { "type": "checkbox" },
|
||||
"pottery": {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"calcplaythrough": { "type": "checkbox" },
|
||||
"print_custom_yaml": { "type": "checkbox" },
|
||||
"usestartinventory": { "type": "checkbox" },
|
||||
"usecustompool": { "type": "checkbox" }
|
||||
"usecustompool": { "type": "checkbox" },
|
||||
"race": { "type": "checkbox" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
"dungeons",
|
||||
"triforcehunt",
|
||||
"trinity",
|
||||
"crystals"
|
||||
"crystals",
|
||||
"ganonhunt",
|
||||
"completionist"
|
||||
]
|
||||
},
|
||||
"crystals_gt": {
|
||||
|
||||
@@ -6,7 +6,7 @@ import subprocess # do stuff at the shell level
|
||||
|
||||
env = common.prepare_env()
|
||||
|
||||
pip_requirements = os.path.join("..","resources","app","meta","manifests","pip_requirements.txt")
|
||||
pip_requirements = os.path.join(".","resources","app","meta","manifests","pip_requirements.txt")
|
||||
if not os.path.isfile(pip_requirements):
|
||||
pip_requirements = os.path.join("..","..","..","resources","app","meta","manifests","pip_requirements.txt")
|
||||
|
||||
|
||||
@@ -24,15 +24,20 @@ class CustomSettings(object):
|
||||
head, filename = os.path.split(file)
|
||||
self.relative_dir = head
|
||||
|
||||
def determine_seed(self):
|
||||
if 'meta' not in self.file_source:
|
||||
return None
|
||||
meta = defaultdict(lambda: None, self.file_source['meta'])
|
||||
seed = meta['seed']
|
||||
if seed:
|
||||
random.seed(seed)
|
||||
return seed
|
||||
return None
|
||||
def determine_seed(self, default_seed):
|
||||
if 'meta' in self.file_source:
|
||||
meta = defaultdict(lambda: None, self.file_source['meta'])
|
||||
seed = meta['seed']
|
||||
if seed:
|
||||
random.seed(seed)
|
||||
return seed
|
||||
if default_seed is None:
|
||||
random.seed(None)
|
||||
seed = random.randint(0, 999999999)
|
||||
else:
|
||||
seed = default_seed
|
||||
random.seed(seed)
|
||||
return seed
|
||||
|
||||
def determine_players(self):
|
||||
if 'meta' not in self.file_source:
|
||||
@@ -43,7 +48,10 @@ class CustomSettings(object):
|
||||
def adjust_args(self, args):
|
||||
def get_setting(value, default):
|
||||
if value:
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return random.choices(list(value.keys()), list(value.values()), k=1)[0]
|
||||
else:
|
||||
return value
|
||||
return default
|
||||
if 'meta' in self.file_source:
|
||||
meta = defaultdict(lambda: None, self.file_source['meta'])
|
||||
@@ -53,6 +61,7 @@ class CustomSettings(object):
|
||||
args.bps = get_setting(meta['bps'], args.bps)
|
||||
args.suppress_rom = get_setting(meta['suppress_rom'], args.suppress_rom)
|
||||
args.names = get_setting(meta['names'], args.names)
|
||||
args.race = get_setting(meta['race'], args.race)
|
||||
self.player_range = range(1, args.multi + 1)
|
||||
if 'settings' in self.file_source:
|
||||
for p in self.player_range:
|
||||
@@ -67,6 +76,7 @@ class CustomSettings(object):
|
||||
args.door_shuffle[p] = get_setting(settings['door_shuffle'], args.door_shuffle[p])
|
||||
args.logic[p] = get_setting(settings['logic'], args.logic[p])
|
||||
args.mode[p] = get_setting(settings['mode'], args.mode[p])
|
||||
args.boots_hint[p] = get_setting(settings['boots_hint'], args.boots_hint[p])
|
||||
args.swords[p] = get_setting(settings['swords'], args.swords[p])
|
||||
args.flute_mode[p] = get_setting(settings['flute_mode'], args.flute_mode[p])
|
||||
args.bow_mode[p] = get_setting(settings['bow_mode'], args.bow_mode[p])
|
||||
@@ -87,11 +97,21 @@ class CustomSettings(object):
|
||||
if args.pottery[p] == 'none':
|
||||
args.pottery[p] = 'keys'
|
||||
|
||||
if args.retro[p] or args.mode[p] == 'retro':
|
||||
if args.bow_mode[p] == 'progressive':
|
||||
args.bow_mode[p] = 'retro'
|
||||
elif args.bow_mode[p] == 'silvers':
|
||||
args.bow_mode[p] = 'retro_silvers'
|
||||
args.take_any[p] = 'random' if args.take_any[p] == 'none' else args.take_any[p]
|
||||
args.keyshuffle[p] = 'universal'
|
||||
|
||||
args.mixed_travel[p] = get_setting(settings['mixed_travel'], args.mixed_travel[p])
|
||||
args.standardize_palettes[p] = get_setting(settings['standardize_palettes'],
|
||||
args.standardize_palettes[p])
|
||||
args.intensity[p] = get_setting(settings['intensity'], args.intensity[p])
|
||||
args.door_type_mode[p] = get_setting(settings['door_type_mode'], args.door_type_mode[p])
|
||||
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.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])
|
||||
@@ -105,12 +125,13 @@ class CustomSettings(object):
|
||||
|
||||
if get_setting(settings['keysanity'], args.keysanity):
|
||||
args.bigkeyshuffle[p] = True
|
||||
args.keyshuffle[p] = True
|
||||
if args.keyshuffle[p] == 'none':
|
||||
args.keyshuffle[p] = 'wild'
|
||||
args.mapshuffle[p] = True
|
||||
args.compassshuffle[p] = True
|
||||
|
||||
args.shufflebosses[p] = get_setting(settings['shufflebosses'], args.shufflebosses[p])
|
||||
args.shuffleenemies[p] = get_setting(settings['shuffleenemies'], args.shuffleenemies[p])
|
||||
args.shufflebosses[p] = get_setting(settings['boss_shuffle'], args.shufflebosses[p])
|
||||
args.shuffleenemies[p] = get_setting(settings['enemy_shuffle'], args.shuffleenemies[p])
|
||||
args.enemy_health[p] = get_setting(settings['enemy_health'], args.enemy_health[p])
|
||||
args.enemy_damage[p] = get_setting(settings['enemy_damage'], args.enemy_damage[p])
|
||||
args.shufflepots[p] = get_setting(settings['shufflepots'], args.shufflepots[p])
|
||||
@@ -139,6 +160,7 @@ class CustomSettings(object):
|
||||
args.ow_palettes[p] = get_setting(settings['ow_palettes'], args.ow_palettes[p])
|
||||
args.uw_palettes[p] = get_setting(settings['uw_palettes'], args.uw_palettes[p])
|
||||
args.shuffle_sfx[p] = get_setting(settings['shuffle_sfx'], args.shuffle_sfx[p])
|
||||
args.msu_resume[p] = get_setting(settings['msu_resume'], args.msu_resume[p])
|
||||
|
||||
def get_item_pool(self):
|
||||
if 'item_pool' in self.file_source:
|
||||
@@ -150,6 +172,11 @@ class CustomSettings(object):
|
||||
return self.file_source['placements']
|
||||
return None
|
||||
|
||||
def get_advanced_placements(self):
|
||||
if 'advanced_placements' in self.file_source:
|
||||
return self.file_source['advanced_placements']
|
||||
return None
|
||||
|
||||
def get_entrances(self):
|
||||
if 'entrances' in self.file_source:
|
||||
return self.file_source['entrances']
|
||||
@@ -175,13 +202,19 @@ class CustomSettings(object):
|
||||
return self.file_source['medallions']
|
||||
return None
|
||||
|
||||
def create_from_world(self, world):
|
||||
def get_drops(self):
|
||||
if 'drops' in self.file_source:
|
||||
return self.file_source['drops']
|
||||
return None
|
||||
|
||||
def create_from_world(self, world, race):
|
||||
self.player_range = range(1, world.players + 1)
|
||||
settings_dict, meta_dict = {}, {}
|
||||
self.world_rep['meta'] = meta_dict
|
||||
meta_dict['players'] = world.players
|
||||
meta_dict['algorithm'] = world.algorithm
|
||||
meta_dict['seed'] = world.seed
|
||||
meta_dict['race'] = race
|
||||
self.world_rep['settings'] = settings_dict
|
||||
for p in self.player_range:
|
||||
settings_dict[p] = {}
|
||||
@@ -189,6 +222,8 @@ class CustomSettings(object):
|
||||
settings_dict[p]['door_shuffle'] = world.doorShuffle[p]
|
||||
settings_dict[p]['intensity'] = world.intensity[p]
|
||||
settings_dict[p]['door_type_mode'] = world.door_type_mode[p]
|
||||
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]['logic'] = world.logic[p]
|
||||
settings_dict[p]['mode'] = world.mode[p]
|
||||
|
||||
@@ -98,6 +98,8 @@ SETTINGSTOPROCESS = {
|
||||
"dungeondoorshuffle": "door_shuffle",
|
||||
"dungeonintensity": "intensity",
|
||||
"door_type_mode": "door_type_mode",
|
||||
"trap_door_mode": "trap_door_mode",
|
||||
"key_logic_algorithm": "key_logic_algorithm",
|
||||
"decoupledoors": "decoupledoors",
|
||||
"keydropshuffle": "keydropshuffle",
|
||||
"dropshuffle": "dropshuffle",
|
||||
@@ -130,6 +132,7 @@ SETTINGSTOPROCESS = {
|
||||
"print_custom_yaml": "print_custom_yaml",
|
||||
"usestartinventory": "usestartinventory",
|
||||
"usecustompool": "custom",
|
||||
"race": "race",
|
||||
"saveonexit": "saveonexit"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,15 +58,17 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
||||
excluded[region] = None
|
||||
elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region:
|
||||
continue
|
||||
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]):
|
||||
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: # for holes
|
||||
if drop_region.name == 'Sewer Drop':
|
||||
drop_region = next(x.parent_region for x in drop_region.entrances)
|
||||
if (drop_region.name in world.inaccessible_regions[player] and
|
||||
region.name not in world.enabled_entrances[player]):
|
||||
excluded[region] = None
|
||||
elif region in excluded:
|
||||
del excluded[region]
|
||||
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
||||
doors_to_connect, idx = {}, 0
|
||||
all_regions = set()
|
||||
@@ -216,7 +218,6 @@ def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_se
|
||||
else:
|
||||
logger.debug(f' Re-Linking {attempt.name} -> {new_door.name}')
|
||||
logger.debug(f' Re-Linking {old_attempt.name} -> {old_target.name}')
|
||||
hash_code_set.add(hash_code)
|
||||
return proposed_map, hash_code
|
||||
|
||||
|
||||
@@ -315,9 +316,14 @@ def determine_paths_for_dungeon(world, player, all_regions, name):
|
||||
non_hole_portals.append(portal.door.entrance.parent_region.name)
|
||||
if portal.destination:
|
||||
paths.append(portal.door.entrance.parent_region.name)
|
||||
if world.mode[player] == 'standard' and name == 'Hyrule Castle':
|
||||
if world.mode[player] == 'standard' and name == 'Hyrule Castle Dungeon':
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
|
||||
entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South')
|
||||
# todo: in non-er, we can use the other portals too
|
||||
paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name))
|
||||
paths.append(('Hyrule Castle Throne Room', [entrance.door.entrance.parent_region.name,
|
||||
'Hyrule Dungeon Cellblock']))
|
||||
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
||||
paths.append('Thieves Attic Window')
|
||||
elif 'Thieves Attic Window' in all_r_names:
|
||||
@@ -431,6 +437,13 @@ def connect_simple_door(exit_door, region):
|
||||
|
||||
|
||||
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||
std_special_big_key_doors = ['Hyrule Castle Throne Room Tapestry'] + special_big_key_doors
|
||||
|
||||
|
||||
def get_special_big_key_doors(world, player):
|
||||
if world.mode[player] == 'standard':
|
||||
return std_special_big_key_doors
|
||||
return special_big_key_doors
|
||||
|
||||
|
||||
class ExplorationState(object):
|
||||
@@ -618,6 +631,7 @@ class ExplorationState(object):
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors, flag)
|
||||
|
||||
# same as above but traps are ignored, and flag is not used
|
||||
def add_all_doors_check_proposed_2(self, region, proposed_map, valid_doors, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if door in proposed_map and door.name in valid_doors:
|
||||
@@ -638,6 +652,27 @@ class ExplorationState(object):
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors)
|
||||
|
||||
# same as above but traps are checked for
|
||||
def add_all_doors_check_proposed_3(self, region, proposed_map, valid_doors, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if door in proposed_map and door.name in valid_doors:
|
||||
self.visited_doors.add(door)
|
||||
if self.can_traverse(door):
|
||||
if door.controller is not None:
|
||||
door = door.controller
|
||||
if door.dest is None and door not in proposed_map.keys() and door.name in valid_doors:
|
||||
if not self.in_door_list_ic(door, self.unattached_doors):
|
||||
self.append_door_to_list(door, self.unattached_doors)
|
||||
else:
|
||||
other = self.find_door_in_list(door, self.unattached_doors)
|
||||
if self.crystal != other.crystal:
|
||||
other.crystal = CrystalBarrier.Either
|
||||
elif door.req_event is not None and door.req_event not in self.events and not self.in_door_list(door,
|
||||
self.event_doors):
|
||||
self.append_door_to_list(door, self.event_doors)
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors)
|
||||
|
||||
def add_all_doors_check_proposed_traps(self, region, proposed_traps, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if self.can_traverse_ignore_traps(door) and door not in proposed_traps:
|
||||
@@ -671,7 +706,7 @@ class ExplorationState(object):
|
||||
if door in key_door_proposal and door not in self.opened_doors:
|
||||
if not self.in_door_list(door, self.small_doors):
|
||||
self.append_door_to_list(door, self.small_doors)
|
||||
elif (door.bigKey or door.name in special_big_key_doors) and not self.big_key_opened:
|
||||
elif (door.bigKey or door.name in get_special_big_key_doors(world, player)) and not self.big_key_opened:
|
||||
if not self.in_door_list(door, self.big_doors):
|
||||
self.append_door_to_list(door, self.big_doors)
|
||||
elif door.req_event is not None and door.req_event not in self.events:
|
||||
@@ -824,7 +859,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
|
||||
local_state = state.copy()
|
||||
for region in search_regions:
|
||||
local_state.visit_region(region)
|
||||
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
|
||||
if world.trap_door_mode[player] == 'vanilla':
|
||||
local_state.add_all_doors_check_proposed_3(region, proposed_map, valid_doors, world, player)
|
||||
else:
|
||||
local_state.add_all_doors_check_proposed_2(region, proposed_map, valid_doors, world, player)
|
||||
while len(local_state.avail_doors) > 0:
|
||||
explorable_door = local_state.next_avail_door()
|
||||
if explorable_door.door in proposed_map:
|
||||
@@ -835,7 +873,10 @@ def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regi
|
||||
if (valid_region_to_explore_in_regions(connect_region, all_regions, world, player)
|
||||
and not local_state.visited(connect_region)):
|
||||
local_state.visit_region(connect_region)
|
||||
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
|
||||
if world.trap_door_mode[player] == 'vanilla':
|
||||
local_state.add_all_doors_check_proposed_3(connect_region, proposed_map, valid_doors, world, player)
|
||||
else:
|
||||
local_state.add_all_doors_check_proposed_2(connect_region, proposed_map, valid_doors, world, player)
|
||||
return local_state
|
||||
|
||||
|
||||
|
||||
+67
-16
@@ -3,7 +3,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from source.item.District import resolve_districts
|
||||
from BaseClasses import PotItem, PotFlags
|
||||
from BaseClasses import PotItem, PotFlags, LocationType
|
||||
from DoorShuffle import validate_vanilla_reservation
|
||||
from Dungeons import dungeon_table
|
||||
from Items import item_table, ItemFactory
|
||||
@@ -18,6 +18,11 @@ class ItemPoolConfig(object):
|
||||
self.item_pool = None
|
||||
self.placeholders = None
|
||||
self.reserved_locations = defaultdict(set)
|
||||
self.restricted = {}
|
||||
self.preferred = {}
|
||||
self.verify = {}
|
||||
self.verify_count = 0
|
||||
self.verify_target = 0
|
||||
|
||||
self.recorded_choices = []
|
||||
|
||||
@@ -77,8 +82,8 @@ def create_item_pool_config(world):
|
||||
if pot.item not in [PotItem.Key, PotItem.Hole, PotItem.Switch]:
|
||||
item = pot_items[pot.item]
|
||||
descriptor = 'Large Block' if pot.flags & PotFlags.Block else f'Pot #{pot_index+1}'
|
||||
location = f'{pot.room} {descriptor}'
|
||||
config.static_placement[player][item].append(location)
|
||||
loc = f'{pot.room} {descriptor}'
|
||||
config.static_placement[player][item].append(loc)
|
||||
if world.shopsanity[player]:
|
||||
for item, locs in shop_vanilla_mapping.items():
|
||||
config.static_placement[player][item].extend(locs)
|
||||
@@ -158,7 +163,11 @@ def create_item_pool_config(world):
|
||||
dungeon_set = (mode_grouping['Big Chests'] + mode_grouping['Dungeon Trash'] + mode_grouping['Big Keys'] +
|
||||
mode_grouping['Heart Containers'] + mode_grouping['GT Trash'] + mode_grouping['Small Keys'] +
|
||||
mode_grouping['Compasses'] + mode_grouping['Maps'] + mode_grouping['Key Drops'] +
|
||||
mode_grouping['Big Key Drops'])
|
||||
mode_grouping['Pot Keys'] + mode_grouping['Big Key Drops'])
|
||||
dungeon_set = set(dungeon_set)
|
||||
for loc in world.get_locations():
|
||||
if loc.parent_region.dungeon and loc.type in [LocationType.Pot, LocationType.Drop]:
|
||||
dungeon_set.add(loc.name)
|
||||
for player in range(1, world.players + 1):
|
||||
config.item_pool[player] = determine_major_items(world, player)
|
||||
config.location_groups[0].locations = set(dungeon_set)
|
||||
@@ -187,26 +196,27 @@ def district_item_pool_config(world):
|
||||
if district.dungeon:
|
||||
adjustment = len([i for i in world.get_dungeon(name, p).all_items
|
||||
if i.is_inside_dungeon_item(world)])
|
||||
dist_len = len(district.locations) - adjustment
|
||||
dist_adj = adjustment
|
||||
if name not in district_choices:
|
||||
district_choices[name] = (district.sphere_one, dist_len)
|
||||
district_choices[name] = (district.sphere_one, dist_adj)
|
||||
else:
|
||||
so, amt = district_choices[name]
|
||||
district_choices[name] = (so or district.sphere_one, amt + dist_len)
|
||||
district_choices[name] = (so or district.sphere_one, amt + dist_adj)
|
||||
|
||||
chosen_locations = defaultdict(set)
|
||||
location_cnt = 0
|
||||
adjustment_cnt = 0
|
||||
|
||||
# choose a sphere one district
|
||||
sphere_one_choices = [d for d, info in district_choices.items() if info[0]]
|
||||
sphere_one = random.choice(sphere_one_choices)
|
||||
so, amt = district_choices[sphere_one]
|
||||
location_cnt += amt
|
||||
so, adj = district_choices[sphere_one]
|
||||
for player in range(1, world.players + 1):
|
||||
for location in world.districts[player][sphere_one].locations:
|
||||
chosen_locations[location].add(player)
|
||||
del district_choices[sphere_one]
|
||||
config.recorded_choices.append(sphere_one)
|
||||
adjustment_cnt += adj
|
||||
location_cnt = len(chosen_locations) - adjustment_cnt
|
||||
|
||||
scale_factors = defaultdict(int)
|
||||
scale_total = 0
|
||||
@@ -215,8 +225,9 @@ def district_item_pool_config(world):
|
||||
dungeon = world.get_entrance(ent, p).connected_region.dungeon
|
||||
if dungeon:
|
||||
scale = world.crystals_needed_for_gt[p]
|
||||
scale_total += scale
|
||||
scale_factors[dungeon.name] += scale
|
||||
if scale > 0:
|
||||
scale_total += scale
|
||||
scale_factors[dungeon.name] += scale
|
||||
scale_total = max(1, scale_total)
|
||||
scale_divisors = defaultdict(lambda: 1)
|
||||
scale_divisors.update(scale_factors)
|
||||
@@ -224,13 +235,15 @@ def district_item_pool_config(world):
|
||||
while location_cnt < item_cnt:
|
||||
weights = [scale_total / scale_divisors[d] for d in district_choices.keys()]
|
||||
choice = random.choices(list(district_choices.keys()), weights=weights, k=1)[0]
|
||||
so, amt = district_choices[choice]
|
||||
location_cnt += amt
|
||||
so, adj = district_choices[choice]
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
for location in world.districts[player][choice].locations:
|
||||
chosen_locations[location].add(player)
|
||||
del district_choices[choice]
|
||||
config.recorded_choices.append(choice)
|
||||
adjustment_cnt += adj
|
||||
location_cnt = len(chosen_locations) - adjustment_cnt
|
||||
config.placeholders = location_cnt - item_cnt
|
||||
config.location_groups[0].locations = chosen_locations
|
||||
|
||||
@@ -380,8 +393,13 @@ def vanilla_fallback(item_to_place, locations, world):
|
||||
|
||||
|
||||
def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion=False):
|
||||
config = world.item_pool_config
|
||||
if not isinstance(item_to_place, str):
|
||||
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||
else:
|
||||
item_name = item_to_place
|
||||
if world.algorithm == 'vanilla_fill':
|
||||
config, filtered = world.item_pool_config, []
|
||||
filtered = []
|
||||
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||
if item_name in config.static_placement[item_to_place.player]:
|
||||
restricted = config.static_placement[item_to_place.player][item_name]
|
||||
@@ -408,7 +426,8 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
|
||||
return filtered if len(filtered) > 0 else locations
|
||||
if world.algorithm == 'district':
|
||||
config = world.item_pool_config
|
||||
if item_to_place == 'Placeholder' or item_to_place.name in config.item_pool[item_to_place.player]:
|
||||
if ((isinstance(item_to_place,str) and item_to_place == 'Placeholder')
|
||||
or item_to_place.name in config.item_pool[item_to_place.player]):
|
||||
restricted = config.location_groups[0].locations
|
||||
filtered = [l for l in locations if l.name in restricted and l.player in restricted[l.name]]
|
||||
return filtered if len(filtered) > 0 else locations
|
||||
@@ -418,6 +437,15 @@ def filter_locations(item_to_place, locations, world, vanilla_skip=False, potion
|
||||
if len(filtered) == 0:
|
||||
raise RuntimeError('Can\'t sell potion of a certain type due to district restriction')
|
||||
return filtered
|
||||
if (item_name, item_to_place.player) in config.restricted:
|
||||
locs = config.restricted[(item_name, item_to_place.player)]
|
||||
return sorted(locations, key=lambda l: 1 if l.name in locs else 0)
|
||||
if (item_name, item_to_place.player) in config.preferred:
|
||||
locs = config.preferred[(item_name, item_to_place.player)]
|
||||
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||
if (item_name, item_to_place.player) in config.verify:
|
||||
locs = config.verify[(item_name, item_to_place.player)].keys()
|
||||
return sorted(locations, key=lambda l: 0 if l.name in locs else 1)
|
||||
return locations
|
||||
|
||||
|
||||
@@ -815,3 +843,26 @@ pot_items = {
|
||||
}
|
||||
|
||||
valid_pot_items = {y: x for x, y in pot_items.items()}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import yaml
|
||||
from yaml.representer import Representer
|
||||
advanced_placements = {'advanced_placements': {}}
|
||||
player_map = advanced_placements['advanced_placements']
|
||||
placement_list = []
|
||||
player_map[1] = placement_list
|
||||
for item, location_list in vanilla_mapping.items():
|
||||
for location in location_list:
|
||||
placement = {}
|
||||
placement['type'] = 'LocationGroup'
|
||||
placement['item'] = item
|
||||
locations = placement['locations'] = []
|
||||
locations.append(location)
|
||||
locations.append('Random')
|
||||
placement_list.append(placement)
|
||||
yaml.add_representer(defaultdict, Representer.represent_dict)
|
||||
with open('fillgen.yaml', 'w') as file:
|
||||
yaml.dump(advanced_placements, file)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import importlib.util
|
||||
import webbrowser
|
||||
from tkinter import Tk, Label, Button, Frame
|
||||
|
||||
|
||||
def check_requirements(console=False):
|
||||
@@ -26,6 +24,9 @@ def check_requirements(console=False):
|
||||
logger.error('See the step about "Installing Platform-specific dependencies":')
|
||||
logger.error('https://github.com/aerinon/ALttPDoorRandomizer/blob/DoorDev/docs/BUILDING.md')
|
||||
else:
|
||||
import webbrowser
|
||||
from tkinter import Tk, Label, Button, Frame
|
||||
|
||||
master = Tk()
|
||||
master.title('Error')
|
||||
frame = Frame(master)
|
||||
|
||||
@@ -66,6 +66,8 @@ def link_entrances_new(world, player):
|
||||
default_map['Old Man Cave (East)'] = 'Death Mountain Return Cave Exit (West)'
|
||||
one_way_map['Bumper Cave (Top)'] = 'Dark Death Mountain Healer Fairy'
|
||||
del default_map['Bumper Cave (Top)']
|
||||
del one_way_map['Big Bomb Shop']
|
||||
one_way_map['Inverted Big Bomb Shop'] = 'Inverted Big Bomb Shop'
|
||||
avail_pool.default_map = default_map
|
||||
avail_pool.one_way_map = one_way_map
|
||||
|
||||
@@ -273,7 +275,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
|
||||
# OM Cave entrance in lw/dw if cross_world off
|
||||
if 'Old Man Cave Exit (West)' in rem_exits:
|
||||
world_limiter = DW_Entrances if avail.inverted else LW_Entrances
|
||||
om_cave_options = [x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)]
|
||||
om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)])
|
||||
om_cave_choice = random.choice(om_cave_options)
|
||||
if not avail.coupled:
|
||||
connect_exit('Old Man Cave Exit (West)', om_cave_choice, avail)
|
||||
@@ -302,14 +304,15 @@ def do_main_shuffle(entrances, exits, avail, mode_def):
|
||||
unused_entrances = set()
|
||||
if not cross_world:
|
||||
lw_entrances, dw_entrances = [], []
|
||||
for x in rem_entrances:
|
||||
left = sorted(rem_entrances)
|
||||
for x in left:
|
||||
if bonk_fairy_exception(x):
|
||||
lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x)
|
||||
do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail)
|
||||
unused_entrances.update(lw_entrances)
|
||||
unused_entrances.update(dw_entrances)
|
||||
else:
|
||||
entrance_list = [x for x in rem_entrances if bonk_fairy_exception(x)]
|
||||
entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(x)])
|
||||
do_cross_world_connectors(entrance_list, multi_exit_caves, avail)
|
||||
unused_entrances.update(entrance_list)
|
||||
|
||||
@@ -630,7 +633,7 @@ def do_fixed_shuffle(avail, entrance_list):
|
||||
rules = Restrictions()
|
||||
rules.size = size
|
||||
if ('Hyrule Castle Entrance (South)' in entrances and
|
||||
avail.world.doorShuffle[avail.player] in ['basic', 'crossed']):
|
||||
avail.world.doorShuffle[avail.player] != 'vanilla'):
|
||||
rules.must_exit_to_lw = True
|
||||
if 'Inverted Ganons Tower' in entrances and not avail.world.shuffle_ganon:
|
||||
rules.fixed = True
|
||||
@@ -774,12 +777,19 @@ def do_vanilla_connect(pool_def, avail):
|
||||
if avail.world.pottery[avail.player] not in ['none', 'keys', 'dungeon']:
|
||||
return
|
||||
defaults = inverted_default_connections if avail.inverted else default_connections
|
||||
if avail.inverted:
|
||||
if 'Dark Death Mountain Fairy' in pool_def['entrances']:
|
||||
pool_def['entrances'].remove('Dark Death Mountain Fairy')
|
||||
pool_def['entrances'].append('Bumper Cave (top)')
|
||||
for entrance in pool_def['entrances']:
|
||||
if entrance in avail.entrances:
|
||||
target = defaults[entrance]
|
||||
connect_simple(avail.world, entrance, target, avail.player)
|
||||
avail.entrances.remove(entrance)
|
||||
avail.exits.remove(target)
|
||||
if entrance in avail.default_map:
|
||||
connect_vanilla_two_way(entrance, avail.default_map[entrance], avail)
|
||||
else:
|
||||
connect_simple(avail.world, entrance, target, avail.player)
|
||||
avail.entrances.remove(entrance)
|
||||
avail.exits.remove(target)
|
||||
|
||||
|
||||
def do_mandatory_connections(avail, entrances, cave_options, must_exit):
|
||||
@@ -1013,12 +1023,15 @@ def connect_custom(avail_pool, world, player):
|
||||
if world.customizer and world.customizer.get_entrances():
|
||||
custom_entrances = world.customizer.get_entrances()
|
||||
player_key = player
|
||||
for ent_name, exit_name in custom_entrances[player_key]['two-way'].items():
|
||||
connect_two_way(ent_name, exit_name, avail_pool)
|
||||
for ent_name, exit_name in custom_entrances[player_key]['entrances'].items():
|
||||
connect_entrance(ent_name, exit_name, avail_pool)
|
||||
for ent_name, exit_name in custom_entrances[player_key]['exits'].items():
|
||||
connect_exit(exit_name, ent_name, avail_pool)
|
||||
if 'two-way' in custom_entrances[player_key]:
|
||||
for ent_name, exit_name in custom_entrances[player_key]['two-way'].items():
|
||||
connect_two_way(ent_name, exit_name, avail_pool)
|
||||
if 'entrances' in custom_entrances[player_key]:
|
||||
for ent_name, exit_name in custom_entrances[player_key]['entrances'].items():
|
||||
connect_entrance(ent_name, exit_name, avail_pool)
|
||||
if 'exits' in custom_entrances[player_key]:
|
||||
for ent_name, exit_name in custom_entrances[player_key]['exits'].items():
|
||||
connect_exit(exit_name, ent_name, avail_pool)
|
||||
|
||||
|
||||
def connect_simple(world, exit_name, region_name, player):
|
||||
@@ -1228,7 +1241,7 @@ modes = {
|
||||
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
|
||||
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
|
||||
'Dark Desert Hint',
|
||||
'Links House', 'Inverted Links House', 'Tavern North']
|
||||
'Links House', 'Tavern North']
|
||||
},
|
||||
'old_man_cave': { # have to do old man cave first so lw dungeon don't use up everything
|
||||
'special': 'old_man_cave_east',
|
||||
@@ -1304,7 +1317,7 @@ modes = {
|
||||
'Light World Bomb Hut', '20 Rupee Cave', '50 Rupee Cave', 'Hookshot Fairy',
|
||||
'Palace of Darkness Hint', 'Dark Lake Hylia Ledge Spike Cave',
|
||||
'Dark Desert Hint',
|
||||
'Links House', 'Inverted Links House', 'Tavern North']
|
||||
'Links House', 'Tavern North'] # inverted links house gets substituted
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1572,6 +1585,7 @@ entrance_map = {
|
||||
'Paradox Cave (Bottom)': 'Paradox Cave Exit (Bottom)',
|
||||
'Paradox Cave (Middle)': 'Paradox Cave Exit (Middle)',
|
||||
'Paradox Cave (Top)': 'Paradox Cave Exit (Top)',
|
||||
'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary Exit',
|
||||
}
|
||||
|
||||
|
||||
@@ -1607,7 +1621,7 @@ single_entrance_map = {
|
||||
'Lake Hylia Fortune Teller': 'Lake Hylia Fortune Teller', 'Lake Hylia Fairy': 'Lake Hylia Healer Fairy',
|
||||
'Bonk Fairy (Light)': 'Bonk Fairy (Light)', 'Lumberjack House': 'Lumberjack House', 'Dam': 'Dam',
|
||||
'Blinds Hideout': 'Blinds Hideout', 'Waterfall of Wishing': 'Waterfall of Wishing',
|
||||
'Inverted Bomb Shop': 'Inverted Bomb Shop', 'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary',
|
||||
'Inverted Bomb Shop': 'Inverted Bomb Shop'
|
||||
}
|
||||
|
||||
default_dw = {
|
||||
@@ -1616,7 +1630,7 @@ default_dw = {
|
||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Turtle Rock Exit (Front)', 'Turtle Rock Ledge Exit (West)',
|
||||
'Turtle Rock Ledge Exit (East)', 'Turtle Rock Isolated Ledge Exit', 'Bumper Cave Exit (Top)',
|
||||
'Bumper Cave Exit (Bottom)', 'Superbunny Cave Exit (Top)', 'Superbunny Cave Exit (Bottom)',
|
||||
'Hookshot Cave Front Exit', 'Hookshot Cave Back Exit', 'Ganons Tower Exit', 'Pyramid Exit',
|
||||
'Hookshot Cave Front Exit', 'Hookshot Cave Back Exit', 'Ganons Tower Exit', 'Pyramid Exit', 'Bonk Fairy (Dark)',
|
||||
'Dark Lake Hylia Healer Fairy', 'Dark Lake Hylia Ledge Healer Fairy', 'Dark Desert Healer Fairy',
|
||||
'Dark Death Mountain Healer Fairy', 'Cave Shop (Dark Death Mountain)', 'Pyramid Fairy', 'East Dark World Hint',
|
||||
'Palace of Darkness Hint', 'Big Bomb Shop', 'Village of Outcasts Shop', 'Dark Lake Hylia Shop',
|
||||
@@ -1639,7 +1653,7 @@ default_lw = {
|
||||
'Spectacle Rock Cave Exit (Top)', 'Spectacle Rock Cave Exit (Peak)', 'Paradox Cave Exit (Bottom)',
|
||||
'Paradox Cave Exit (Middle)', 'Paradox Cave Exit (Top)', 'Fairy Ascension Cave Exit (Bottom)',
|
||||
'Fairy Ascension Cave Exit (Top)', 'Spiral Cave Exit', 'Spiral Cave Exit (Top)', 'Waterfall of Wishing', 'Dam',
|
||||
'Blinds Hideout', 'Lumberjack House', 'Bonk Fairy (Light)', 'Bonk Fairy (Dark)', 'Lake Hylia Healer Fairy',
|
||||
'Blinds Hideout', 'Lumberjack House', 'Bonk Fairy (Light)', 'Lake Hylia Healer Fairy',
|
||||
'Swamp Healer Fairy', 'Desert Healer Fairy', 'Fortune Teller (Light)', 'Lake Hylia Fortune Teller', 'Kings Grave', 'Tavern',
|
||||
'Chicken House', 'Aginahs Cave', 'Sahasrahlas Hut', 'Cave Shop (Lake Hylia)', 'Capacity Upgrade', 'Blacksmiths Hut',
|
||||
'Sick Kids House', 'Lost Woods Gamble', 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House',
|
||||
@@ -1684,7 +1698,7 @@ DW_Entrances = ['Bumper Cave (Bottom)', 'Superbunny Cave (Top)', 'Superbunny Ca
|
||||
'Dark Lake Hylia Ledge Hint', 'Chest Game', 'Dark Desert Fairy', 'Dark Lake Hylia Ledge Fairy',
|
||||
'Fortune Teller (Dark)', 'Dark World Hammer Peg Cave', 'Pyramid Entrance',
|
||||
'Skull Woods First Section Door', 'Skull Woods Second Section Door (East)',
|
||||
'Skull Woods Second Section Door (West)',
|
||||
'Skull Woods Second Section Door (West)', 'Ganons Tower',
|
||||
'Inverted Dark Sanctuary', 'Inverted Links House', 'Inverted Agahnims Tower']
|
||||
|
||||
LW_Must_Exit = ['Desert Palace Entrance (East)']
|
||||
@@ -1938,6 +1952,8 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
|
||||
('Bumper Cave Entrance Mirror Spot', 'Death Mountain Entrance'),
|
||||
('Bumper Cave Ledge Drop', 'West Dark World'),
|
||||
('Bumper Cave Ledge Mirror Spot', 'Death Mountain Return Ledge'),
|
||||
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
|
||||
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
|
||||
('Skull Woods Forest', 'Skull Woods Forest'),
|
||||
('Desert Ledge Mirror Spot', 'Desert Ledge'),
|
||||
('Desert Ledge (Northeast) Mirror Spot', 'Desert Ledge (Northeast)'),
|
||||
@@ -2066,6 +2082,8 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
|
||||
('Bumper Cave Entrance Rock', 'Bumper Cave Entrance'),
|
||||
('Bumper Cave Entrance Drop', 'West Dark World'),
|
||||
('Bumper Cave Ledge Drop', 'West Dark World'),
|
||||
('Bumper Cave Top To Bottom', 'Bumper Cave (bottom)'),
|
||||
('Bumper Cave Bottom to Top', 'Bumper Cave (top)'),
|
||||
('Skull Woods Forest', 'Skull Woods Forest'),
|
||||
('Paradox Cave Push Block Reverse', 'Paradox Cave Chest Area'),
|
||||
('Paradox Cave Push Block', 'Paradox Cave Front'),
|
||||
@@ -2290,8 +2308,8 @@ default_connections = {'Links House': 'Links House',
|
||||
'C-Shaped House': 'C-Shaped House',
|
||||
'Chest Game': 'Chest Game',
|
||||
'Dark World Hammer Peg Cave': 'Dark World Hammer Peg Cave',
|
||||
'Bumper Cave (Bottom)': 'Bumper Cave',
|
||||
'Bumper Cave (Top)': 'Bumper Cave',
|
||||
'Bumper Cave (Bottom)': 'Bumper Cave (bottom)',
|
||||
'Bumper Cave (Top)': 'Bumper Cave (top)',
|
||||
'Red Shield Shop': 'Red Shield Shop',
|
||||
'Dark Sanctuary Hint': 'Dark Sanctuary Hint',
|
||||
'Fortune Teller (Dark)': 'Fortune Teller (Dark)',
|
||||
@@ -2452,7 +2470,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing',
|
||||
'Inverted Big Bomb Shop': 'Inverted Big Bomb Shop',
|
||||
'Inverted Dark Sanctuary': 'Inverted Dark Sanctuary',
|
||||
'Inverted Dark Sanctuary Exit': 'West Dark World',
|
||||
'Old Man Cave (West)': 'Bumper Cave',
|
||||
'Old Man Cave (West)': 'Bumper Cave (bottom)',
|
||||
'Old Man Cave (East)': 'Death Mountain Return Cave (left)',
|
||||
'Old Man Cave Exit (West)': 'West Dark World',
|
||||
'Old Man Cave Exit (East)': 'Dark Death Mountain',
|
||||
@@ -2461,7 +2479,7 @@ inverted_default_connections = {'Waterfall of Wishing': 'Waterfall of Wishing',
|
||||
'Bumper Cave (Top)': 'Dark Death Mountain Healer Fairy',
|
||||
'Bumper Cave Exit (Top)': 'Death Mountain Return Ledge',
|
||||
'Bumper Cave Exit (Bottom)': 'Light World',
|
||||
'Death Mountain Return Cave (West)': 'Bumper Cave',
|
||||
'Death Mountain Return Cave (West)': 'Bumper Cave (top)',
|
||||
'Death Mountain Return Cave (East)': 'Death Mountain Return Cave (right)',
|
||||
'Death Mountain Return Cave Exit (West)': 'Death Mountain',
|
||||
'Death Mountain Return Cave Exit (East)': 'Death Mountain',
|
||||
|
||||
@@ -84,6 +84,8 @@ def roll_settings(weights):
|
||||
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
||||
ret.intensity = get_choice('intensity')
|
||||
ret.door_type_mode = get_choice('door_type_mode')
|
||||
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.experimental = get_choice('experimental') == 'on'
|
||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||
@@ -101,7 +103,7 @@ def roll_settings(weights):
|
||||
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' and keydropshuffle else ret.dropshuffle
|
||||
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
|
||||
ret.pottery = 'keys' if ret.pottery == 'none' and keydropshuffle else ret.pottery
|
||||
ret.colorizepots = get_choice('colorizepots') == 'on'
|
||||
ret.colorizepots = get_choice_default('colorizepots', default='on') == 'on'
|
||||
ret.shufflepots = get_choice('pot_shuffle') == 'on'
|
||||
ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent'
|
||||
ret.standardize_palettes = (get_choice('standardize_palettes') if 'standardize_palettes' in weights
|
||||
@@ -114,7 +116,9 @@ def roll_settings(weights):
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt',
|
||||
'trinity': 'trinity'
|
||||
'trinity': 'trinity',
|
||||
'ganonhunt': 'ganonhunt',
|
||||
'completionist': 'completionist'
|
||||
}[goal]
|
||||
ret.openpyramid = goal in ['fast_ganon', 'trinity'] if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
|
||||
|
||||
@@ -202,5 +206,6 @@ def roll_settings(weights):
|
||||
ret.ow_palettes = get_choice('ow_palettes', romweights)
|
||||
ret.uw_palettes = get_choice('uw_palettes', romweights)
|
||||
ret.shuffle_sfx = get_choice('shuffle_sfx', romweights) == 'on'
|
||||
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
|
||||
|
||||
return ret
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import multiprocessing
|
||||
import concurrent.futures
|
||||
import argparse
|
||||
from collections import OrderedDict
|
||||
|
||||
cpu_threads = multiprocessing.cpu_count()
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
|
||||
|
||||
def main(args=None):
|
||||
successes = []
|
||||
errors = []
|
||||
task_mapping = []
|
||||
tests = OrderedDict()
|
||||
|
||||
successes.append(f"Testing DR (NewTestSuite)")
|
||||
print(successes[0])
|
||||
|
||||
# max_attempts = args.count
|
||||
pool = concurrent.futures.ThreadPoolExecutor(max_workers=cpu_threads)
|
||||
dead_or_alive = 0
|
||||
alive = 0
|
||||
|
||||
def test(test_name: str, command: str, test_file: str):
|
||||
tests[test_name] = [command]
|
||||
|
||||
base_command = f"python3.8 DungeonRandomizer.py --suppress_rom --suppress_spoiler"
|
||||
|
||||
def gen_seed():
|
||||
task_command = base_command + " " + command
|
||||
return subprocess.run(task_command, capture_output=True, shell=True, text=True)
|
||||
|
||||
task = pool.submit(gen_seed)
|
||||
task.success = False
|
||||
task.name = test_name
|
||||
task.test_file = test_file
|
||||
task.cmd = base_command + " " + command
|
||||
task_mapping.append(task)
|
||||
|
||||
for test_suite, test_files in args.test_suite.items():
|
||||
for test_file in test_files:
|
||||
test(test_suite, f'--customizer {os.path.join(test_suite, test_file)}', test_file)
|
||||
|
||||
from tqdm import tqdm
|
||||
with tqdm(concurrent.futures.as_completed(task_mapping),
|
||||
total=len(task_mapping), unit="seed(s)",
|
||||
desc=f"Success rate: 0.00%") as progressbar:
|
||||
for task in progressbar:
|
||||
dead_or_alive += 1
|
||||
try:
|
||||
result = task.result()
|
||||
if result.returncode:
|
||||
errors.append([task.name + ' ' + task.test_file, task.cmd, result.stderr])
|
||||
else:
|
||||
alive += 1
|
||||
task.success = True
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
progressbar.set_description(f"Success rate: {(alive/dead_or_alive)*100:.2f}% - {task.name}")
|
||||
|
||||
def get_results(testname: str):
|
||||
result = ""
|
||||
dead_or_alive = [task.success for task in task_mapping if task.name == testname]
|
||||
alive = [x for x in dead_or_alive if x]
|
||||
success = f"{testname} Rate: {(len(alive) / len(dead_or_alive)) * 100:.2f}%"
|
||||
successes.append(success)
|
||||
print(success)
|
||||
result += f"{(len(alive)/len(dead_or_alive))*100:.2f}%\t"
|
||||
return result.strip()
|
||||
|
||||
results = []
|
||||
for t in tests.keys():
|
||||
results.append(get_results(t))
|
||||
|
||||
for result in results:
|
||||
print(result)
|
||||
successes.append(result)
|
||||
|
||||
return successes, errors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
successes = []
|
||||
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
# parser.add_argument('--count', default=0, type=lambda value: max(int(value), 0))
|
||||
parser.add_argument('--cpu_threads', default=cpu_threads, type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--help', default=False, action='store_true')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.help:
|
||||
parser.print_help()
|
||||
exit(0)
|
||||
|
||||
cpu_threads = args.cpu_threads
|
||||
|
||||
test_suites = {}
|
||||
# not sure if it supports subdirectories properly yet
|
||||
for root, dirnames, filenames in os.walk('test/suite'):
|
||||
test_suites[root] = fnmatch.filter(filenames, '*.yaml')
|
||||
|
||||
args = argparse.Namespace()
|
||||
args.test_suite = test_suites
|
||||
s, errors = main(args=args)
|
||||
if successes:
|
||||
successes += [""] * 2
|
||||
successes += s
|
||||
print()
|
||||
|
||||
if errors:
|
||||
with open(f"new-test-suite-errors.txt", 'w') as stream:
|
||||
for error in errors:
|
||||
stream.write(error[0] + "\n")
|
||||
stream.write(error[1] + "\n")
|
||||
stream.write(error[2] + "\n\n")
|
||||
|
||||
with open("new-test-suite-success.txt", "w") as stream:
|
||||
stream.write(str.join("\n", successes))
|
||||
|
||||
input("Press enter to continue")
|
||||
@@ -0,0 +1,23 @@
|
||||
meta:
|
||||
players: 2
|
||||
settings:
|
||||
1:
|
||||
pottery: cavekeys
|
||||
keysanity: True
|
||||
2:
|
||||
keysanity: True
|
||||
placements:
|
||||
1:
|
||||
Sanctuary: Small Key (Escape)#2
|
||||
'Links House Pot #3': Rupees (20)#2
|
||||
start_inventory:
|
||||
1:
|
||||
- Pegasus Boots
|
||||
- Ocarina (Activated)
|
||||
- Magic Mirror
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Red Mail
|
||||
- Golden Sword
|
||||
@@ -0,0 +1,46 @@
|
||||
meta:
|
||||
players: 1
|
||||
algorithm: dungeon_only
|
||||
settings:
|
||||
1:
|
||||
goal: dungeons
|
||||
pseudoboots: true
|
||||
door_shuffle: crossed
|
||||
decoupledoors: true
|
||||
intensity: 3
|
||||
door_type_mode: all
|
||||
experimental: true
|
||||
dungeon_counters: 'on'
|
||||
compassshuffle: true
|
||||
mapshuffle: true
|
||||
keydropshuffle: true
|
||||
doors:
|
||||
1:
|
||||
lobbies:
|
||||
Hyrule Castle South: Thieves Lobby S
|
||||
doors:
|
||||
Thieves Lobby NE Edge:
|
||||
dest: Hyrule Castle Throne Room South Stairs
|
||||
one-way: True
|
||||
Hyrule Castle Throne Room South Stairs:
|
||||
dest: Desert Beamos Hall NE
|
||||
one-way: True
|
||||
Desert Beamos Hall NE:
|
||||
dest: Ice Spike Cross SE
|
||||
type: Key Door
|
||||
one-way: True
|
||||
Ice Spike Cross SE:
|
||||
dest: Thieves Lobby NE Edge
|
||||
one-way: True
|
||||
Hyrule Castle Throne Room N:
|
||||
dest: Ice Firebar Down Ladder
|
||||
type: Key Door
|
||||
Swamp Lobby S:
|
||||
dest: Thieves Lobby N Edge
|
||||
type: Bomb Door
|
||||
Thieves Lobby E: Thieves Compass Room W
|
||||
Thieves Compass Room NW Edge: Ice Tall Hint SE
|
||||
Thieves Compass Room N Edge: PoD Bow Statue Down Ladder
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
import RaceRandom as random
|
||||
import logging
|
||||
import time
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
@@ -106,7 +108,7 @@ def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links
|
||||
# seed = 635441530
|
||||
random.seed(seed)
|
||||
world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {},
|
||||
{}, {}, {}, {}, {}, True, {}, {}, [], {})
|
||||
{}, {}, {}, {}, {}, True, {}, [], {})
|
||||
world.customizer = False
|
||||
world.shufflelinks = {1: links}
|
||||
if world.mode[1] != 'inverted':
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Possible improvements: account for items that are possibly in logic
|
||||
# Example: Mire Big Key in harmless means all 6 mire smalls required for fire-locked side,
|
||||
# if you have access to harmless via:
|
||||
# 2 pod smalls + bow, hammer or 3 pod small
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: default
|
||||
keysanity: True
|
||||
crystals_needed_for_gt: 0 # to skip trash fill
|
||||
placements:
|
||||
1:
|
||||
Hobo: Big Key (Misery Mire)
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||
Palace of Darkness - Big Chest: Hammer
|
||||
advanced_placements:
|
||||
1:
|
||||
# Contrast with partial_2
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
# Contrast with partial_3
|
||||
- type: Verification
|
||||
item: Big Key (Ganons Tower)
|
||||
locations:
|
||||
Ganons Tower - Big Key Chest: True
|
||||
Ganons Tower - Big Key Room - Left: True
|
||||
Ganons Tower - Big Key Room - Right: True
|
||||
Ganons Tower - Bob's Chest: True
|
||||
# Normal logic doesn't allow this placement
|
||||
# unless hammer is placed before it - no algorithm does this in non-keysanity, but possible in keysanity
|
||||
- type: Verification
|
||||
item: Small Key (Palace of Darkness)
|
||||
locations:
|
||||
Palace of Darkness - Dark Maze - Bottom: True
|
||||
@@ -0,0 +1,24 @@
|
||||
# Even though Lamp is Flipper-locked, this logic considers that a key could be wasted in the dark in mire
|
||||
# Only fire locked mire is off limits
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
keysanity: true
|
||||
placements:
|
||||
1:
|
||||
Hobo: Lamp
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
@@ -0,0 +1,26 @@
|
||||
# For contrast with default logic
|
||||
# This logic is not yet smart enough to allow the crystal blocked chests with two keys (Spike Pot and one other)
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
keysanity: True
|
||||
placements:
|
||||
1:
|
||||
Hobo: Lamp
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
Waterfall Fairy - Right: Small Key (Misery Mire)
|
||||
Swamp Palace - Entrance: Big Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: False
|
||||
Misery Mire - Main Lobby: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
@@ -0,0 +1,160 @@
|
||||
# For contrast with default logic
|
||||
# Examples of valid big key placement that doesn't work with pure worst case scenarios
|
||||
# Basically chests that are obtainable two ways+ of spending keys
|
||||
# (Possible fix: access to the extra door grants access to the mini helma key too)
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: partial
|
||||
crystals_needed_for_gt: 0 # to skip trash fill
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Big Key (Desert Palace)
|
||||
locations:
|
||||
Desert Palace - Big Chest: False
|
||||
Desert Palace - Big Key Chest: True
|
||||
Desert Palace - Boss: False
|
||||
Desert Palace - Compass Chest: True
|
||||
Desert Palace - Map Chest: True
|
||||
Desert Palace - Torch: True
|
||||
- type: Verification
|
||||
item: Big Key (Eastern Palace)
|
||||
locations:
|
||||
Eastern Palace - Big Chest: False
|
||||
Eastern Palace - Big Key Chest: True
|
||||
Eastern Palace - Boss: False
|
||||
Eastern Palace - Cannonball Chest: True
|
||||
Eastern Palace - Compass Chest: True
|
||||
Eastern Palace - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Ganons Tower)
|
||||
locations:
|
||||
# These four require not wasting keys upstairs because the big key is down here
|
||||
Ganons Tower - Big Key Chest: False
|
||||
Ganons Tower - Big Key Room - Left: False
|
||||
Ganons Tower - Big Key Room - Right: False
|
||||
Ganons Tower - Bob's Chest: False
|
||||
# These are normal
|
||||
Ganons Tower - Big Chest: False
|
||||
Ganons Tower - Bob's Torch: True
|
||||
Ganons Tower - Compass Room - Bottom Left: True
|
||||
Ganons Tower - Compass Room - Bottom Right: True
|
||||
Ganons Tower - Compass Room - Top Left: True
|
||||
Ganons Tower - Compass Room - Top Right: True
|
||||
Ganons Tower - DMs Room - Bottom Left: True
|
||||
Ganons Tower - DMs Room - Bottom Right: True
|
||||
Ganons Tower - DMs Room - Top Left: True
|
||||
Ganons Tower - DMs Room - Top Right: True
|
||||
Ganons Tower - Firesnake Room: True
|
||||
Ganons Tower - Hope Room - Left: True
|
||||
Ganons Tower - Hope Room - Right: True
|
||||
Ganons Tower - Map Chest: True
|
||||
Ganons Tower - Mini Helmasaur Room - Left: False
|
||||
Ganons Tower - Mini Helmasaur Room - Right: False
|
||||
Ganons Tower - Pre-Moldorm Chest: False
|
||||
Ganons Tower - Randomizer Room - Bottom Left: True
|
||||
Ganons Tower - Randomizer Room - Bottom Right: True
|
||||
Ganons Tower - Randomizer Room - Top Left: True
|
||||
Ganons Tower - Randomizer Room - Top Right: True
|
||||
Ganons Tower - Tile Room: True
|
||||
Ganons Tower - Validation Chest: False
|
||||
- type: Verification
|
||||
item: Big Key (Ice Palace)
|
||||
locations:
|
||||
Ice Palace - Big Chest: False
|
||||
Ice Palace - Big Key Chest: True
|
||||
Ice Palace - Boss: False
|
||||
Ice Palace - Compass Chest: True
|
||||
Ice Palace - Freezor Chest: True
|
||||
Ice Palace - Iced T Room: True
|
||||
Ice Palace - Map Chest: True
|
||||
Ice Palace - Spike Room: True
|
||||
- type: Verification
|
||||
item: Big Key (Misery Mire)
|
||||
locations:
|
||||
Misery Mire - Big Chest: False
|
||||
Misery Mire - Big Key Chest: True
|
||||
Misery Mire - Boss: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Compass Chest: True
|
||||
Misery Mire - Main Lobby: True
|
||||
Misery Mire - Map Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Palace of Darkness)
|
||||
locations:
|
||||
Palace of Darkness - Big Chest: False
|
||||
Palace of Darkness - Big Key Chest: True
|
||||
Palace of Darkness - Boss: False
|
||||
Palace of Darkness - Compass Chest: True
|
||||
Palace of Darkness - Dark Basement - Left: True
|
||||
Palace of Darkness - Dark Basement - Right: True
|
||||
Palace of Darkness - Dark Maze - Bottom: True
|
||||
Palace of Darkness - Dark Maze - Top: True
|
||||
Palace of Darkness - Harmless Hellway: True
|
||||
Palace of Darkness - Map Chest: True
|
||||
Palace of Darkness - Shooter Room: True
|
||||
Palace of Darkness - Stalfos Basement: True
|
||||
Palace of Darkness - The Arena - Bridge: True
|
||||
Palace of Darkness - The Arena - Ledge: True
|
||||
- type: Verification
|
||||
item: Big Key (Skull Woods)
|
||||
locations:
|
||||
Skull Woods - Big Chest: True
|
||||
Skull Woods - Big Key Chest: True
|
||||
Skull Woods - Boss: True
|
||||
Skull Woods - Bridge Room: True
|
||||
Skull Woods - Compass Chest: True
|
||||
Skull Woods - Map Chest: True
|
||||
Skull Woods - Pinball Room: True
|
||||
Skull Woods - Pot Prison: True
|
||||
- type: Verification
|
||||
item: Big Key (Swamp Palace)
|
||||
locations:
|
||||
Swamp Palace - Big Chest: True
|
||||
Swamp Palace - Big Key Chest: True
|
||||
Swamp Palace - Boss: True
|
||||
Swamp Palace - Compass Chest: True
|
||||
Swamp Palace - Entrance: False
|
||||
Swamp Palace - Flooded Room - Left: True
|
||||
Swamp Palace - Flooded Room - Right: True
|
||||
Swamp Palace - Map Chest: True
|
||||
Swamp Palace - Waterfall Room: True
|
||||
Swamp Palace - West Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Thieves Town)
|
||||
locations:
|
||||
Thieves' Town - Ambush Chest: True
|
||||
Thieves' Town - Attic: False
|
||||
Thieves' Town - Big Chest: False
|
||||
Thieves' Town - Big Key Chest: True
|
||||
Thieves' Town - Blind's Cell: False
|
||||
Thieves' Town - Boss: False
|
||||
Thieves' Town - Compass Chest: True
|
||||
Thieves' Town - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Tower of Hera)
|
||||
locations:
|
||||
Tower of Hera - Basement Cage: True
|
||||
Tower of Hera - Big Chest: False
|
||||
Tower of Hera - Big Key Chest: True
|
||||
Tower of Hera - Boss: False
|
||||
Tower of Hera - Compass Chest: False
|
||||
Tower of Hera - Map Chest: True
|
||||
- type: Verification
|
||||
item: Big Key (Turtle Rock)
|
||||
locations:
|
||||
Turtle Rock - Big Chest: False
|
||||
Turtle Rock - Big Key Chest: True
|
||||
Turtle Rock - Boss: False
|
||||
Turtle Rock - Chain Chomps: True
|
||||
Turtle Rock - Compass Chest: True
|
||||
Turtle Rock - Crystaroller Room: False
|
||||
Turtle Rock - Eye Bridge - Bottom Left: False
|
||||
Turtle Rock - Eye Bridge - Bottom Right: False
|
||||
Turtle Rock - Eye Bridge - Top Left: False
|
||||
Turtle Rock - Eye Bridge - Top Right: False
|
||||
Turtle Rock - Roller Room - Left: True
|
||||
Turtle Rock - Roller Room - Right: True
|
||||
@@ -0,0 +1,22 @@
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
key_logic_algorithm: strict
|
||||
keysanity: true
|
||||
placements:
|
||||
1:
|
||||
Hobo: Big Key (Misery Mire)
|
||||
Waterfall Fairy - Left: Small Key (Misery Mire)
|
||||
advanced_placements:
|
||||
1:
|
||||
- type: Verification
|
||||
item: Flippers
|
||||
locations:
|
||||
Misery Mire - Map Chest: False
|
||||
Misery Mire - Main Lobby: False
|
||||
Misery Mire - Bridge Chest: True
|
||||
Misery Mire - Spike Chest: True
|
||||
Misery Mire - Compass Chest: False
|
||||
Misery Mire - Big Key Chest: False
|
||||
Misery Mire - Boss: False
|
||||
Reference in New Issue
Block a user