Merged DR v1.2.0.0
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
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
*.bmbp
|
||||
*.log
|
||||
*_Spoiler.json
|
||||
*_custom.yaml
|
||||
*_meta.txt
|
||||
*.bps
|
||||
*.pyc
|
||||
*.sfc
|
||||
*.srm
|
||||
|
||||
+115
-53
@@ -21,7 +21,7 @@ from source.dungeon.RoomObject import RoomObject
|
||||
class World(object):
|
||||
|
||||
def __init__(self, players, owShuffle, owCrossed, owMixed, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
|
||||
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
||||
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, custom, customitemarray, hints):
|
||||
self.players = players
|
||||
self.teams = 1
|
||||
self.owShuffle = owShuffle.copy()
|
||||
@@ -34,6 +34,7 @@ class World(object):
|
||||
self.shuffle = shuffle.copy()
|
||||
self.doorShuffle = doorShuffle.copy()
|
||||
self.intensity = {}
|
||||
self.door_type_mode = {}
|
||||
self.logic = logic.copy()
|
||||
self.mode = mode.copy()
|
||||
self.swords = swords.copy()
|
||||
@@ -71,7 +72,6 @@ class World(object):
|
||||
self.fix_trock_exit = {}
|
||||
self.shuffle_ganon = shuffle_ganon
|
||||
self.fix_gtower_exit = self.shuffle_ganon
|
||||
self.retro = retro.copy()
|
||||
self.custom = custom
|
||||
self.customitemarray = customitemarray
|
||||
self.can_take_damage = True
|
||||
@@ -105,10 +105,6 @@ class World(object):
|
||||
self.pot_contents = {}
|
||||
|
||||
for player in range(1, players + 1):
|
||||
# If World State is Retro, set to Open and set Retro flag
|
||||
if self.mode[player] == "retro":
|
||||
self.mode[player] = "open"
|
||||
self.retro[player] = True
|
||||
def set_player_attr(attr, val):
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('_region_cache', {})
|
||||
@@ -132,13 +128,15 @@ 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 ['lean', 'crossed', 'insanity', 'madness_legacy'])
|
||||
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', False)
|
||||
set_player_attr('keyshuffle', 'none')
|
||||
set_player_attr('bigkeyshuffle', False)
|
||||
set_player_attr('restrict_boss_items', 'none')
|
||||
set_player_attr('bombbag', False)
|
||||
set_player_attr('flute_mode', False)
|
||||
set_player_attr('bow_mode', False)
|
||||
set_player_attr('difficulty_requirements', None)
|
||||
set_player_attr('boss_shuffle', 'none')
|
||||
set_player_attr('enemy_shuffle', 'none')
|
||||
@@ -151,6 +149,7 @@ class World(object):
|
||||
set_player_attr('crystals_ganon_orig', {})
|
||||
set_player_attr('crystals_gt_orig', {})
|
||||
set_player_attr('open_pyramid', 'auto')
|
||||
set_player_attr('take_any', 'none')
|
||||
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
||||
set_player_attr('treasure_hunt_count', 0)
|
||||
set_player_attr('treasure_hunt_total', 0)
|
||||
@@ -160,16 +159,23 @@ class World(object):
|
||||
set_player_attr('collection_rate', False)
|
||||
set_player_attr('colorizepots', False)
|
||||
set_player_attr('pot_pool', {})
|
||||
set_player_attr('decoupledoors', False)
|
||||
set_player_attr('door_type_mode', 'original')
|
||||
|
||||
set_player_attr('shopsanity', False)
|
||||
set_player_attr('mixed_travel', 'prevent')
|
||||
set_player_attr('standardize_palettes', 'standardize')
|
||||
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
|
||||
set_player_attr('prizes', {'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0})
|
||||
set_player_attr('prizes', {'dig;': [], 'pull': [0, 0, 0], 'crab': [0, 0], 'stun': 0, 'fish': 0, 'enemies': []})
|
||||
|
||||
set_player_attr('exp_cache', defaultdict(dict))
|
||||
set_player_attr('enabled_entrances', {})
|
||||
|
||||
def finish_init(self):
|
||||
for player in range(1, self.players + 1):
|
||||
if self.mode[player] == 'retro':
|
||||
self.mode[player] == 'open'
|
||||
|
||||
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)})'
|
||||
|
||||
@@ -451,7 +457,8 @@ class World(object):
|
||||
|
||||
def push_precollected(self, item):
|
||||
item.world = self
|
||||
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
||||
if ((item.smallkey and self.keyshuffle[item.player] != 'none')
|
||||
or (item.bigkey and self.bigkeyshuffle[item.player])):
|
||||
item.advancement = True
|
||||
self.precollected_items.append(item)
|
||||
self.state.collect(item, True)
|
||||
@@ -586,6 +593,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):
|
||||
@@ -655,7 +663,7 @@ class CollectionState(object):
|
||||
if key_logic.sm_doors[door]:
|
||||
self.reached_doors[player].add(key_logic.sm_doors[door].name)
|
||||
if not connection.can_reach(self):
|
||||
checklist_key = 'Universal' if self.world.retro[player] else dungeon_name
|
||||
checklist_key = 'Universal' if self.world.keyshuffle[player] == 'universal' else dungeon_name
|
||||
checklist = self.dungeons_to_check[player][checklist_key]
|
||||
checklist[connection.name] = (connection, crystal_state)
|
||||
elif door.name not in self.opened_doors[player]:
|
||||
@@ -827,7 +835,7 @@ class CollectionState(object):
|
||||
return None
|
||||
|
||||
def set_dungeon_limits(self, player, dungeon_name):
|
||||
if self.world.retro[player] and self.world.mode[player] == 'standard':
|
||||
if self.world.keyshuffle[player] == 'universal' and self.world.mode[player] == 'standard':
|
||||
self.dungeon_limits = ['Hyrule Castle', 'Agahnims Tower']
|
||||
else:
|
||||
self.dungeon_limits = [dungeon_name]
|
||||
@@ -851,14 +859,16 @@ class CollectionState(object):
|
||||
door_candidates.append(door.name)
|
||||
return door_candidates
|
||||
door_candidates, skip = [], set()
|
||||
if state.world.accessibility[player] != 'locations' and remaining_keys == 0 and dungeon_name in state.world.key_logic[player]:
|
||||
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 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)
|
||||
@@ -894,6 +904,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):
|
||||
@@ -977,7 +988,7 @@ class CollectionState(object):
|
||||
'Golden Sword', 'Progressive Sword', 'Progressive Glove', 'Silver Arrows', 'Green Pendant',
|
||||
'Blue Pendant', 'Red Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5',
|
||||
'Crystal 6', 'Crystal 7', 'Blue Boomerang', 'Red Boomerang', 'Blue Shield', 'Red Shield',
|
||||
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)'
|
||||
'Mirror Shield', 'Progressive Shield', 'Bug Catching Net', 'Cane of Byrna', 'Ocarina (Activated)',
|
||||
'Boss Heart Container', 'Sanctuary Heart Container', 'Piece of Heart', 'Magic Upgrade (1/2)',
|
||||
'Magic Upgrade (1/4)']
|
||||
or item_name.startswith(('Bottle', 'Small Key', 'Big Key'))
|
||||
@@ -1018,7 +1029,7 @@ class CollectionState(object):
|
||||
new_locations = True
|
||||
while new_locations:
|
||||
reachable_events = [location for location in locations if location.event and
|
||||
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
||||
(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
|
||||
@@ -1078,7 +1089,7 @@ class CollectionState(object):
|
||||
return self.prog_items[item, player] >= count
|
||||
|
||||
def has_sm_key(self, item, player, count=1):
|
||||
if self.world.retro[player]:
|
||||
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)
|
||||
@@ -1207,7 +1218,7 @@ class CollectionState(object):
|
||||
or self.has('Cane of Somaria', player))
|
||||
|
||||
def can_shoot_arrows(self, player):
|
||||
if self.world.retro[player]:
|
||||
if self.world.bow_mode[player] in ['retro', 'retro_silvers']:
|
||||
#todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe
|
||||
return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player))
|
||||
return self.has('Bow', player)
|
||||
@@ -1253,7 +1264,8 @@ class CollectionState(object):
|
||||
if any(map(lambda i: i.name in ['Ocarina', 'Ocarina (Activated)'], self.world.precollected_items)):
|
||||
return True
|
||||
lw = self.world.get_region('Kakariko Area', player)
|
||||
return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player))
|
||||
return self.has('Ocarina (Activated)', player) or (self.has('Ocarina', player) and lw.can_reach(self)
|
||||
and self.is_not_bunny(lw, player))
|
||||
|
||||
def can_melt_things(self, player):
|
||||
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player))
|
||||
@@ -1496,7 +1508,7 @@ class Region(object):
|
||||
return False
|
||||
|
||||
def can_fill(self, item):
|
||||
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
|
||||
inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none')
|
||||
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]))
|
||||
@@ -2276,7 +2288,6 @@ class Sector(object):
|
||||
self.item_logic = set()
|
||||
self.chest_location_set = set()
|
||||
|
||||
|
||||
def region_set(self):
|
||||
if self.r_name_set is None:
|
||||
self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions))
|
||||
@@ -2558,7 +2569,7 @@ class Location(object):
|
||||
def gen_name(self):
|
||||
name = self.name
|
||||
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
||||
if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed':
|
||||
if self.parent_region.dungeon and world and world.doorShuffle[self.player] not in ['basic', 'vanilla']:
|
||||
name += f' @ {self.parent_region.dungeon.name}'
|
||||
if world and world.players > 1:
|
||||
name += f' ({world.get_player_names(self.player)})'
|
||||
@@ -2643,7 +2654,7 @@ class Item(object):
|
||||
return item_dungeon
|
||||
|
||||
def is_inside_dungeon_item(self, world):
|
||||
return ((self.smallkey and not world.keyshuffle[self.player])
|
||||
return ((self.smallkey and world.keyshuffle[self.player] == 'none')
|
||||
or (self.bigkey and not world.bigkeyshuffle[self.player])
|
||||
or (self.compass and not world.compassshuffle[self.player])
|
||||
or (self.map and not world.mapshuffle[self.player]))
|
||||
@@ -2794,9 +2805,10 @@ class Spoiler(object):
|
||||
'versions': {'Door':ERVersion, 'Overworld':ORVersion},
|
||||
'logic': self.world.logic,
|
||||
'mode': self.world.mode,
|
||||
'retro': self.world.retro,
|
||||
'bombbag': self.world.bombbag,
|
||||
'weapons': self.world.swords,
|
||||
'flute_mode': self.world.flute_mode,
|
||||
'bow_mode': self.world.bow_mode,
|
||||
'goal': self.world.goal,
|
||||
'ow_shuffle': self.world.owShuffle,
|
||||
'ow_terrain': self.world.owTerrain,
|
||||
@@ -2809,9 +2821,13 @@ class Spoiler(object):
|
||||
'shuffle': self.world.shuffle,
|
||||
'shuffleganon': self.world.shuffle_ganon,
|
||||
'shufflelinks': self.world.shufflelinks,
|
||||
'shuffletavern': self.world.shuffletavern,
|
||||
'take_any': self.world.take_any,
|
||||
'overworld_map': self.world.overworld_map,
|
||||
'door_shuffle': self.world.doorShuffle,
|
||||
'intensity': self.world.intensity,
|
||||
'door_type_mode': self.world.door_type_mode,
|
||||
'decoupledoors': self.world.decoupledoors,
|
||||
'dungeon_counters': self.world.dungeon_counters,
|
||||
'item_pool': self.world.difficulty,
|
||||
'item_functionality': self.world.difficulty_adjustments,
|
||||
@@ -2996,7 +3012,6 @@ class Spoiler(object):
|
||||
outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player])
|
||||
outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player])
|
||||
outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player])
|
||||
outfile.write('Retro:'.ljust(line_width) + '%s\n' % yn(self.metadata['retro'][player]))
|
||||
outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player])
|
||||
outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player])
|
||||
if self.metadata['goal'][player] in ['triforcehunt', 'trinity']:
|
||||
@@ -3008,6 +3023,9 @@ class Spoiler(object):
|
||||
outfile.write('Restricted Boss Items:'.ljust(line_width) + '%s\n' % self.metadata['restricted_boss_items'][player])
|
||||
outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player])
|
||||
outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player])
|
||||
outfile.write('Flute Mode:'.ljust(line_width) + '%s\n' % self.metadata['flute_mode'][player])
|
||||
outfile.write('Bow Mode:'.ljust(line_width) + '%s\n' % self.metadata['bow_mode'][player])
|
||||
outfile.write('Take Any Caves:'.ljust(line_width) + '%s\n' % self.metadata['take_any'][player])
|
||||
outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % yn(self.metadata['shopsanity'][player]))
|
||||
outfile.write('Bombbag:'.ljust(line_width) + '%s\n' % yn(self.metadata['bombbag'][player]))
|
||||
outfile.write('Pseudoboots:'.ljust(line_width) + '%s\n' % yn(self.metadata['pseudoboots'][player]))
|
||||
@@ -3025,12 +3043,15 @@ class Spoiler(object):
|
||||
if self.metadata['shuffle'][player] != 'vanilla':
|
||||
outfile.write('Shuffle GT/Ganon:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffleganon'][player]))
|
||||
outfile.write('Shuffle Links:'.ljust(line_width) + '%s\n' % yn(self.metadata['shufflelinks'][player]))
|
||||
outfile.write('Shuffle Tavern:'.ljust(line_width) + '%s\n' % yn(self.metadata['shuffletavern'][player]))
|
||||
if self.metadata['shuffle'][player] != 'vanilla' or self.metadata['ow_mixed'][player]:
|
||||
outfile.write('Overworld Map:'.ljust(line_width) + '%s\n' % self.metadata['overworld_map'][player])
|
||||
outfile.write('Pyramid Hole Pre-opened:'.ljust(line_width) + '%s\n' % self.metadata['open_pyramid'][player])
|
||||
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
|
||||
if self.metadata['door_shuffle'][player] != 'vanilla':
|
||||
outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player])
|
||||
outfile.write('Door Type Mode:'.ljust(line_width) + '%s\n' % self.metadata['door_type_mode'][player])
|
||||
outfile.write('Decouple Doors:'.ljust(line_width) + '%s\n' % yn(self.metadata['decoupledoors'][player]))
|
||||
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % yn(self.metadata['experimental'][player]))
|
||||
outfile.write('Dungeon Counters:'.ljust(line_width) + '%s\n' % self.metadata['dungeon_counters'][player])
|
||||
outfile.write('Enemy Drop Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['dropshuffle'][player]))
|
||||
@@ -3038,7 +3059,7 @@ class Spoiler(object):
|
||||
outfile.write('Pot Shuffle (Legacy):'.ljust(line_width) + '%s\n' % yn(self.metadata['potshuffle'][player]))
|
||||
outfile.write('Map Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['mapshuffle'][player]))
|
||||
outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player]))
|
||||
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['keyshuffle'][player]))
|
||||
outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player])
|
||||
outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player]))
|
||||
outfile.write('Boss Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player])
|
||||
outfile.write('Enemy Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player])
|
||||
@@ -3311,12 +3332,12 @@ class Pot(object):
|
||||
return hash((self.x, self.y, self.room))
|
||||
|
||||
|
||||
# byte 0: DDOO OEEE (DR, OR, ER)
|
||||
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0}
|
||||
or_mode = {"vanilla": 0, "parallel": 1, "full": 1}
|
||||
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "lite": 4, "lean": 5, "crossed": 6, "insanity": 7, "dungeonsfull": 8, "dungeonssimple": 9}
|
||||
# byte 0: DDDE EEEE (DR, ER)
|
||||
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3}
|
||||
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8,
|
||||
'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6}
|
||||
|
||||
# byte 1: LLLW WSSR (logic, mode, sword, retro)
|
||||
# byte 1: LLLW WSS? (logic, mode, sword)
|
||||
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4}
|
||||
world_mode = {"open": 0, "standard": 1, "inverted": 2}
|
||||
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
|
||||
@@ -3326,13 +3347,13 @@ goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "cryst
|
||||
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
||||
|
||||
# byte 3: S?MM PIII (shop, unused, mixed, palettes, intensity)
|
||||
# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity)
|
||||
# keydrop now has it's own byte
|
||||
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
|
||||
# intensity is 3 bits (reserves 4-7 levels)
|
||||
|
||||
# new byte 4: ?DDD PPPP (unused, drop, pottery)
|
||||
# dropshuffle reserves 2 bits, pottery needs 2 but reserves 2 for future modes)
|
||||
# new byte 4: TDDD PPPP (tavern shuffle, drop, pottery)
|
||||
# dropshuffle reserves 2 bits, pottery needs 4)
|
||||
pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7,
|
||||
'clustered': 8, 'nonempty': 9}
|
||||
|
||||
@@ -3342,7 +3363,8 @@ counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
|
||||
# byte 6: CCCC CPAA (crystals ganon, pyramid, access
|
||||
access_mode = {"items": 0, "locations": 1, "none": 2}
|
||||
|
||||
# byte 7: BSMC ??EE (big, small, maps, compass, bosses, enemies)
|
||||
# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies)
|
||||
door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3}
|
||||
enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3}
|
||||
|
||||
# byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links)
|
||||
@@ -3356,12 +3378,27 @@ rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2}
|
||||
algo_mode = {"balanced": 0, "equitable": 1, "vanilla_fill": 2, "dungeon_only": 3, "district": 4, 'major_only': 5}
|
||||
boss_mode = {"none": 0, "simple": 1, "full": 2, "chaos": 3, 'random': 3, 'unique': 4}
|
||||
|
||||
# byte 10: settings_version
|
||||
|
||||
# byte 11: OOOT WCCC (OWR layout, free terrain, whirlpools, OWR crossed)
|
||||
or_mode = {"vanilla": 0, "parallel": 1, "full": 2}
|
||||
orcrossed_mode = {"none": 0, "polar": 1, "grouped": 2, "limited": 3, "chaos": 4}
|
||||
|
||||
# byte 12: KMB? FF?? (keep similar, mixed/tile flip, bonk drops, flute spots)
|
||||
flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2}
|
||||
|
||||
# byte 13: 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}
|
||||
|
||||
# additions
|
||||
# psuedoboots does not effect code
|
||||
# sfx_shuffle and other adjust items does not effect settings code
|
||||
|
||||
# Bump this when making changes that are not backwards compatible (nearly all of them)
|
||||
settings_version = 0
|
||||
settings_version = 1
|
||||
|
||||
|
||||
class Settings(object):
|
||||
@@ -3369,43 +3406,52 @@ class Settings(object):
|
||||
@staticmethod
|
||||
def make_code(w, p):
|
||||
code = bytes([
|
||||
(dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owCrossed[p] != 'none' else 0) | (0x08 if w.owMixed[p] else 0) | er_mode[w.shuffle[p]],
|
||||
(dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]],
|
||||
|
||||
(logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3)
|
||||
| (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0),
|
||||
| (sword_mode[w.swords[p]] << 1),
|
||||
|
||||
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
|
||||
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
|
||||
|
||||
(0x80 if w.shopsanity[p] else 0) | (mixed_travel_mode[w.mixed_travel[p]] << 4)
|
||||
(0x80 if w.shopsanity[p] else 0) | (0x40 if w.decoupledoors[p] else 0)
|
||||
| (mixed_travel_mode[w.mixed_travel[p]] << 4)
|
||||
| (0x8 if w.standardize_palettes[p] == "original" else 0)
|
||||
| (0 if w.intensity[p] == "random" else w.intensity[p]),
|
||||
|
||||
(0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
|
||||
(0x80 if w.shuffletavern[p] else 0) | (0x10 if w.dropshuffle[p] else 0) | (pottery_mode[w.pottery[p]]),
|
||||
|
||||
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
|
||||
| (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0),
|
||||
|
||||
((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3)
|
||||
| (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]],
|
||||
| (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]],
|
||||
|
||||
(0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0)
|
||||
(0x80 if w.bigkeyshuffle[p] else 0)
|
||||
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
|
||||
| (enemy_mode[w.enemy_shuffle[p]]),
|
||||
| (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
|
||||
|
||||
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)
|
||||
| (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0),
|
||||
|
||||
(rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]),
|
||||
|
||||
settings_version])
|
||||
settings_version,
|
||||
|
||||
(or_mode[w.owShuffle[p]] << 5) | (0x10 if w.owTerrain[p] else 0) | (0x08 if w.owWhirlpoolShuffle[p] else 0) | orcrossed_mode[w.owCrossed[p]],
|
||||
|
||||
(0x80 if w.owKeepSimilar[p] else 0) | (0x40 if w.owMixed[p] else 0) | (0x20 if w.shuffle_bonk_drops[p] else 0) | (flutespot_mode[w.owFluteShuffle[p]] << 4),
|
||||
|
||||
(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]])
|
||||
])
|
||||
return base64.b64encode(code, "+-".encode()).decode()
|
||||
|
||||
@staticmethod
|
||||
def adjust_args_from_code(code, player, args):
|
||||
settings, p = base64.b64decode(code.encode(), "+-".encode()), player
|
||||
|
||||
if len(settings) < 11:
|
||||
if len(settings) < 14:
|
||||
raise Exception('Provided code is incompatible with this version')
|
||||
if settings[10] != settings_version:
|
||||
raise Exception('Provided code is incompatible with this version')
|
||||
@@ -3413,9 +3459,8 @@ class Settings(object):
|
||||
def r(d):
|
||||
return {y: x for x, y in d.items()}
|
||||
|
||||
args.shuffle[p] = r(er_mode)[settings[0] & 0x0F]
|
||||
args.ow_shuffle[p] = r(or_mode)[(settings[0] & 0x30) >> 4]
|
||||
args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xC0) >> 6]
|
||||
args.shuffle[p] = r(er_mode)[settings[0] & 0x1F]
|
||||
args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xE0) >> 5]
|
||||
args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5]
|
||||
args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3]
|
||||
args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1]
|
||||
@@ -3423,16 +3468,16 @@ class Settings(object):
|
||||
args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1]
|
||||
args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5]
|
||||
args.accessibility[p] = r(access_mode)[settings[6] & 0x3]
|
||||
args.retro[p] = True if settings[1] & 0x01 else False
|
||||
# args.retro[p] = True if settings[1] & 0x01 else False
|
||||
args.hints[p] = True if settings[2] & 0x01 else False
|
||||
args.shopsanity[p] = True if settings[3] & 0x80 else False
|
||||
# args.keydropshuffle[p] = True if settings[3] & 0x40 else False
|
||||
args.decoupledoors[p] = True if settings[3] & 0x40 else False
|
||||
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
|
||||
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
|
||||
intensity = settings[3] & 0x7
|
||||
args.intensity[p] = "random" if intensity == 0 else intensity
|
||||
|
||||
# args.shuffleswitches[p] = True if settings[4] & 0x80 else False
|
||||
args.shuffletavern[p] = True if settings[4] & 0x80 else False
|
||||
args.dropshuffle[p] = True if settings[4] & 0x10 else False
|
||||
args.pottery[p] = r(pottery_mode)[settings[4] & 0x0F]
|
||||
|
||||
@@ -3446,10 +3491,10 @@ class Settings(object):
|
||||
args.openpyramid[p] = True if settings[6] & 0x4 else False
|
||||
|
||||
args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False
|
||||
args.keyshuffle[p] = True if settings[7] & 0x40 else False
|
||||
# args.keyshuffle[p] = True if settings[7] & 0x40 else False
|
||||
args.mapshuffle[p] = True if settings[7] & 0x20 else False
|
||||
args.compassshuffle[p] = True if settings[7] & 0x10 else False
|
||||
# args.shufflebosses[p] = r(boss_mode)[(settings[7] & 0xc) >> 2]
|
||||
args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2]
|
||||
args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3]
|
||||
|
||||
args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5]
|
||||
@@ -3457,11 +3502,28 @@ class Settings(object):
|
||||
args.shufflepots[p] = True if settings[8] & 0x4 else False
|
||||
args.bombbag[p] = True if settings[8] & 0x2 else False
|
||||
args.shufflelinks[p] = True if settings[8] & 0x1 else False
|
||||
|
||||
if len(settings) > 9:
|
||||
args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6]
|
||||
args.algorithm = r(algo_mode)[(settings[9] & 0x38) >> 3]
|
||||
args.shufflebosses[p] = r(boss_mode)[(settings[9] & 0x07)]
|
||||
|
||||
args.ow_shuffle[p] = r(or_mode)[(settings[11] & 0xE0) >> 5]
|
||||
args.ow_terrain[p] = True if settings[11] & 0x10 else False
|
||||
args.ow_whirlpool[p] = True if settings[11] & 0x08 else False
|
||||
args.ow_crossed[p] = r(orcrossed_mode)[(settings[11] & 0x07)]
|
||||
|
||||
args.ow_keepsimilar[p] = True if settings[12] & 0x80 else False
|
||||
args.ow_mixed[p] = True if settings[12] & 0x40 else False
|
||||
args.bonk_drops[p] = True if settings[12] & 0x20 else False
|
||||
args.ow_fluteshuffle[p] = r(flutespot_mode)[(settings[12] & 0x0C) >> 2]
|
||||
|
||||
if len(settings) > 13:
|
||||
args.flute_mode[p] = r(flute_mode)[(settings[13] & 0x80) >> 7]
|
||||
args.bow_mode[p] = r(bow_mode)[(settings[13] & 0x70) >> 4]
|
||||
args.take_any[p] = r(take_any_mode)[(settings[13] & 0xC) >> 2]
|
||||
args.keyshuffle[p] = r(keyshuffle_mode)[settings[13] & 0x3]
|
||||
|
||||
|
||||
class KeyRuleType(FastEnum):
|
||||
WorstCase = 0
|
||||
|
||||
@@ -164,6 +164,22 @@ def place_bosses(world, player):
|
||||
|
||||
all_bosses = sorted(boss_table.keys()) #s orted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
used_bosses = []
|
||||
|
||||
if world.customizer and world.customizer.get_bosses():
|
||||
custom_bosses = world.customizer.get_bosses()
|
||||
if player in custom_bosses:
|
||||
for location, boss in custom_bosses[player].items():
|
||||
level = None
|
||||
if '(' in location:
|
||||
i = location.find('(')
|
||||
level = location[i+1:location.find(')')]
|
||||
location = location[:i-1]
|
||||
if can_place_boss(world, player, boss, location, level):
|
||||
loc_text = location + (' ('+level+')' if level else '')
|
||||
place_boss(boss, level, location, loc_text, world, player)
|
||||
boss_locations.remove([location, level])
|
||||
used_bosses.append((boss, level))
|
||||
|
||||
# temporary hack for swordless kholdstare:
|
||||
if world.boss_shuffle[player] in ["simple", "full", "unique"]:
|
||||
@@ -178,6 +194,8 @@ def place_bosses(world, player):
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = placeable_bosses + random.sample(placeable_bosses, 3)
|
||||
for u, level in used_bosses:
|
||||
placeable_bosses.remove(u)
|
||||
|
||||
logging.getLogger('').debug('Bosses chosen %s', bosses)
|
||||
|
||||
@@ -201,6 +219,9 @@ def place_bosses(world, player):
|
||||
place_boss(boss, level, loc, loc_text, world, player)
|
||||
elif world.boss_shuffle[player] == 'unique':
|
||||
bosses = list(placeable_bosses)
|
||||
for u, level in used_bosses:
|
||||
if not level:
|
||||
bosses.remove(u)
|
||||
gt_bosses = []
|
||||
|
||||
for [loc, level] in boss_locations:
|
||||
|
||||
@@ -9,6 +9,7 @@ import sys
|
||||
from source.classes.BabelFish import BabelFish
|
||||
|
||||
from Utils import update_deprecated_args
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
@@ -32,11 +33,25 @@ def parse_cli(argv, no_defaults=False):
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--settingsfile', help="input json file of settings", type=str)
|
||||
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
|
||||
parser.add_argument('--customizer', help='input yaml file for customizations', type=str)
|
||||
parser.add_argument('--print_custom_yaml', help='print example yaml for current settings',
|
||||
default=False, action="store_true")
|
||||
parser.add_argument('--mystery', dest="mystery", default=False, action="store_true")
|
||||
|
||||
multiargs, _ = parser.parse_known_args(argv)
|
||||
|
||||
if multiargs.settingsfile:
|
||||
settings = apply_settings_file(settings, multiargs.settingsfile)
|
||||
|
||||
player_num = multiargs.multi
|
||||
if multiargs.customizer:
|
||||
custom = CustomSettings()
|
||||
custom.load_yaml(multiargs.customizer)
|
||||
cp = custom.determine_players()
|
||||
if cp:
|
||||
player_num = cp
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# get args
|
||||
@@ -78,38 +93,50 @@ def parse_cli(argv, no_defaults=False):
|
||||
parser.add_argument('--securerandom', default=defval(settings["securerandom"]), action='store_true')
|
||||
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
|
||||
parser.add_argument('--settingsfile', dest="filename", help="input json file of settings", type=str)
|
||||
parser.add_argument('--customizer', dest="customizer", help='input yaml file for customizations', type=str)
|
||||
parser.add_argument('--print_custom_yaml', dest="print_custom_yaml", default=False, action="store_true")
|
||||
|
||||
if multiargs.multi:
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
if player_num:
|
||||
for player in range(1, player_num + 1):
|
||||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||
|
||||
ret = parser.parse_args(argv)
|
||||
|
||||
if ret.keysanity:
|
||||
ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = [True] * 4
|
||||
ret.mapshuffle, ret.compassshuffle, ret.bigkeyshuffle = [True] * 3
|
||||
ret.keyshuffle = 'wild'
|
||||
|
||||
if ret.keydropshuffle:
|
||||
ret.dropshuffle = True
|
||||
ret.pottery = 'keys' if ret.pottery == 'none' else ret.pottery
|
||||
|
||||
if multiargs.multi:
|
||||
if ret.retro or ret.mode == 'retro':
|
||||
if ret.bow_mode == 'progressive':
|
||||
ret.bow_mode = 'retro'
|
||||
elif ret.bow_mode == 'silvers':
|
||||
ret.bow_mode = 'retro_silvers'
|
||||
ret.take_any = 'random' if ret.take_any == 'none' else ret.take_any
|
||||
ret.keyshuffle = 'universal'
|
||||
|
||||
if player_num:
|
||||
defaults = copy.deepcopy(ret)
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
for player in range(1, player_num + 1):
|
||||
playerargs = parse_cli(shlex.split(getattr(ret, f"p{player}")), True)
|
||||
|
||||
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle',
|
||||
'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle',
|
||||
'flute_mode', 'bow_mode', 'take_any', 'boots_hint',
|
||||
'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid',
|
||||
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
|
||||
'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items',
|
||||
'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max',
|
||||
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'pseudoboots',
|
||||
'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
|
||||
'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern',
|
||||
'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters',
|
||||
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
|
||||
'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', 'bonk_drops']:
|
||||
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', 'bonk_drops']:
|
||||
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
|
||||
if player == 1:
|
||||
setattr(ret, name, {1: value})
|
||||
@@ -135,19 +162,22 @@ def parse_settings():
|
||||
"retro": False,
|
||||
"bombbag": False,
|
||||
"mode": "open",
|
||||
"boots_hint": False,
|
||||
"logic": "noglitches",
|
||||
"goal": "ganon",
|
||||
"crystals_gt": "7",
|
||||
"crystals_ganon": "7",
|
||||
"swords": "random",
|
||||
"flute_mode": "normal",
|
||||
"bow_mode": "progressive",
|
||||
"difficulty": "normal",
|
||||
"item_functionality": "normal",
|
||||
"timer": "none",
|
||||
"progressive": "on",
|
||||
"accessibility": "items",
|
||||
"algorithm": "balanced",
|
||||
'mystery': False,
|
||||
'suppress_meta': False,
|
||||
"mystery": False,
|
||||
"suppress_meta": False,
|
||||
"restrict_boss_items": "none",
|
||||
|
||||
# Shuffle Ganon defaults to TRUE
|
||||
@@ -163,7 +193,9 @@ def parse_settings():
|
||||
"bonk_drops": False,
|
||||
"shuffle": "vanilla",
|
||||
"shufflelinks": False,
|
||||
"shuffletavern": False,
|
||||
"overworld_map": "default",
|
||||
"take_any": "none",
|
||||
"pseudoboots": False,
|
||||
|
||||
"shuffleenemies": "none",
|
||||
@@ -173,18 +205,20 @@ def parse_settings():
|
||||
"enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"),
|
||||
|
||||
"shopsanity": False,
|
||||
'keydropshuffle': False,
|
||||
'dropshuffle': False,
|
||||
'pottery': 'none',
|
||||
'colorizepots': False,
|
||||
'shufflepots': False,
|
||||
"keydropshuffle": False,
|
||||
"dropshuffle": False,
|
||||
"pottery": "none",
|
||||
"colorizepots": False,
|
||||
"shufflepots": False,
|
||||
"mapshuffle": False,
|
||||
"compassshuffle": False,
|
||||
"keyshuffle": False,
|
||||
"keyshuffle": "none",
|
||||
"bigkeyshuffle": False,
|
||||
"keysanity": False,
|
||||
"door_shuffle": "vanilla",
|
||||
"intensity": 3,
|
||||
"door_type_mode": "original",
|
||||
"decoupledoors": False,
|
||||
"experimental": False,
|
||||
"dungeon_counters": "default",
|
||||
"mixed_travel": "prevent",
|
||||
@@ -214,8 +248,8 @@ def parse_settings():
|
||||
"uw_palettes": "default",
|
||||
"reduce_flashing": False,
|
||||
"shuffle_sfx": False,
|
||||
'msu_resume': False,
|
||||
'collection_rate': False,
|
||||
"msu_resume": False,
|
||||
"collection_rate": False,
|
||||
|
||||
# Spoiler defaults to TRUE
|
||||
# Playthrough defaults to TRUE
|
||||
|
||||
+1593
-238
File diff suppressed because it is too large
Load Diff
@@ -104,8 +104,10 @@ def create_doors(world, player):
|
||||
create_door(player, 'Sewers Dark Cross Key Door N', Nrml).dir(No, 0x32, Mid, High).small_key().pos(0),
|
||||
create_door(player, 'Sewers Water S', Nrml).dir(So, 0x22, Mid, High).small_key().pos(0).portal(Z, 0x22),
|
||||
create_door(player, 'Sewers Water W', Nrml).dir(We, 0x22, Bot, High).pos(1),
|
||||
create_door(player, 'Sewers Key Rat E', Nrml).dir(Ea, 0x21, Bot, High).pos(1),
|
||||
create_door(player, 'Sewers Key Rat Key Door N', Nrml).dir(No, 0x21, Right, High).small_key().pos(0),
|
||||
create_door(player, 'Sewers Dark Aquabats ES', Nrml).dir(Ea, 0x21, Bot, High).pos(2),
|
||||
create_door(player, 'Sewers Dark Aquabats N', Intr).dir(No, 0x21, Mid, High).pos(1),
|
||||
create_door(player, 'Sewers Key Rat S', Intr).dir(So, 0x21, Mid, High).pos(1),
|
||||
create_door(player, 'Sewers Key Rat NE', Nrml).dir(No, 0x21, Right, High).small_key().pos(0),
|
||||
create_door(player, 'Sewers Secret Room Key Door S', Nrml).dir(So, 0x11, Right, High).small_key().pos(2).portal(X, 0x02),
|
||||
create_door(player, 'Sewers Rat Path WS', Intr).dir(We, 0x11, Bot, High).pos(1),
|
||||
create_door(player, 'Sewers Rat Path WN', Intr).dir(We, 0x11, Top, High).pos(0),
|
||||
@@ -348,7 +350,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Tower Catwalk North Stairs', StrS).dir(No, 0x40, Left, High),
|
||||
create_door(player, 'Tower Antechamber South Stairs', StrS).dir(So, 0x30, Left, High),
|
||||
create_door(player, 'Tower Antechamber NW', Intr).dir(No, 0x30, Left, High).pos(1),
|
||||
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().pos(1),
|
||||
create_door(player, 'Tower Altar SW', Intr).dir(So, 0x30, Left, High).no_exit().trap(0x2).pos(1),
|
||||
create_door(player, 'Tower Altar NW', Nrml).dir(No, 0x30, Left, High).pos(0),
|
||||
create_door(player, 'Tower Agahnim 1 SW', Nrml).dir(So, 0x20, Left, High).no_exit().trap(0x4).pos(0),
|
||||
|
||||
@@ -667,7 +669,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Thieves Conveyor Maze SW', Intr).dir(So, 0xbc, Left, High).pos(6),
|
||||
create_door(player, 'Thieves Pot Alcove Top NW', Intr).dir(No, 0xbc, Left, High).pos(6),
|
||||
create_door(player, 'Thieves Conveyor Maze EN', Intr).dir(Ea, 0xbc, Top, High).pos(2),
|
||||
create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().pos(2),
|
||||
create_door(player, 'Thieves Hallway WN', Intr).dir(We, 0xbc, Top, High).no_exit().trap(0x1).pos(2),
|
||||
create_door(player, 'Thieves Conveyor Maze Down Stairs', Sprl).dir(Dn, 0xbc, 0, HTH).ss(A, 0x11, 0x80, True, True),
|
||||
create_door(player, 'Thieves Boss SE', Nrml).dir(So, 0xac, Right, High).no_exit().trap(0x4).pos(0),
|
||||
create_door(player, 'Thieves Spike Track ES', Nrml).dir(Ea, 0xbb, Bot, High).pos(5),
|
||||
@@ -742,7 +744,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Ice Big Key Push Block', Lgcl),
|
||||
create_door(player, 'Ice Big Key Down Ladder', Lddr).dir(So, 0x1f, 3, High),
|
||||
create_door(player, 'Ice Stalfos Hint SE', Intr).dir(So, 0x3e, Right, High).pos(0),
|
||||
create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().pos(0),
|
||||
create_door(player, 'Ice Conveyor NE', Intr).dir(No, 0x3e, Right, High).no_exit().trap(0x4).pos(0),
|
||||
create_door(player, 'Ice Conveyor to Crystal', Lgcl),
|
||||
create_door(player, 'Ice Conveyor Crystal Exit', Lgcl),
|
||||
create_door(player, 'Ice Conveyor SW', Nrml).dir(So, 0x3e, Left, High).small_key().pos(1).portal(Z, 0x20),
|
||||
@@ -760,7 +762,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Ice Firebar ES', Intr).dir(Ea, 0x5e, Bot, High).pos(3),
|
||||
create_door(player, 'Ice Firebar Down Ladder', Lddr).dir(So, 0x5e, 5, High),
|
||||
create_door(player, 'Ice Spike Cross NE', Intr).dir(No, 0x5e, Right, High).pos(1),
|
||||
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().pos(1),
|
||||
create_door(player, 'Ice Falling Square SE', Intr).dir(So, 0x5e, Right, High).no_exit().trap(0x1).pos(1),
|
||||
create_door(player, 'Ice Falling Square Hole', Hole),
|
||||
create_door(player, 'Ice Spike Room WS', Nrml).dir(We, 0x5f, Bot, High).small_key().pos(0),
|
||||
create_door(player, 'Ice Spike Room Down Stairs', Sprl).dir(Dn, 0x5f, 3, HTH).ss(Z, 0x11, 0x48, True, True),
|
||||
@@ -840,12 +842,16 @@ def create_doors(world, player):
|
||||
create_door(player, 'Mire Hub Top NW', Nrml).dir(No, 0xc2, Left, High).pos(2),
|
||||
create_door(player, 'Mire Lone Shooter WS', Nrml).dir(We, 0xc3, Bot, High).pos(6),
|
||||
create_door(player, 'Mire Lone Shooter ES', Intr).dir(Ea, 0xc3, Bot, High).pos(3),
|
||||
create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().pos(3),
|
||||
create_door(player, 'Mire Falling Bridge WS', Intr).dir(We, 0xc3, Bot, High).no_exit().trap(0x8).pos(3),
|
||||
create_door(player, 'Mire Falling Bridge W', Intr).dir(We, 0xc3, Mid, High).pos(2),
|
||||
create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().pos(2),
|
||||
create_door(player, 'Mire Failure Bridge E', Intr).dir(Ea, 0xc3, Mid, High).no_exit().trap(0x1).pos(2),
|
||||
create_door(player, 'Mire Failure Bridge W', Nrml).dir(We, 0xc3, Mid, High).pos(5),
|
||||
create_door(player, 'Mire Falling Bridge WN', Intr).dir(We, 0xc3, Top, High).pos(1),
|
||||
create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().pos(1),
|
||||
create_door(player, 'Mire Falling Bridge Hook Path', Lgcl),
|
||||
create_door(player, 'Mire Falling Bridge Hook Only Path', Lgcl),
|
||||
create_door(player, 'Mire Falling Bridge Primary Path', Lgcl), # dynamic
|
||||
create_door(player, 'Mire Falling Bridge Failure Path', Lgcl),
|
||||
create_door(player, 'Mire Map Spike Side EN', Intr).dir(Ea, 0xc3, Top, High).no_exit().trap(0x2).pos(1),
|
||||
create_door(player, 'Mire Map Spot WN', Nrml).dir(We, 0xc3, Top, High).small_key().pos(0),
|
||||
create_door(player, 'Mire Crystal Dead End NW', Nrml).dir(No, 0xc3, Left, High).pos(4),
|
||||
create_door(player, 'Mire Map Spike Side Drop Down', Lgcl),
|
||||
@@ -903,7 +909,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Mire Tile Room NW', Intr).dir(No, 0xc1, Left, High).pos(3),
|
||||
create_door(player, 'Mire Compass Room SW', Intr).dir(So, 0xc1, Left, High).pos(3),
|
||||
create_door(player, 'Mire Compass Room EN', Intr).dir(Ea, 0xc1, Top, High).pos(2),
|
||||
create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().pos(2),
|
||||
create_door(player, 'Mire Wizzrobe Bypass WN', Intr).dir(We, 0xc1, Top, High).no_exit().trap(0x1).pos(2),
|
||||
create_door(player, 'Mire Compass Blue Barrier', Lgcl),
|
||||
create_door(player, 'Mire Compass Chest Exit', Lgcl),
|
||||
create_door(player, 'Mire Neglected Room NE', Nrml).dir(No, 0xd1, Right, High).pos(2),
|
||||
@@ -912,7 +918,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'Mire Neglected Room SE', Intr).dir(So, 0xd1, Right, High).pos(3),
|
||||
create_door(player, 'Mire Chest View NE', Intr).dir(No, 0xd1, Right, High).pos(3),
|
||||
create_door(player, 'Mire BK Chest Ledge WS', Intr).dir(We, 0xd1, Bot, High).pos(0),
|
||||
create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().pos(0),
|
||||
create_door(player, 'Mire Warping Pool ES', Intr).dir(Ea, 0xd1, Bot, High).no_exit().trap(0x4).pos(0),
|
||||
create_door(player, 'Mire Warping Pool Warp', Warp),
|
||||
create_door(player, 'Mire Torches Top Down Stairs', Sprl).dir(Dn, 0x97, 0, HTH).ss(A, 0x11, 0xb0, True).kill(),
|
||||
create_door(player, 'Mire Torches Top SW', Intr).dir(So, 0x97, Left, High).pos(1),
|
||||
@@ -1011,7 +1017,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'TR Big Chest Entrance SE', Nrml).dir(So, 0x24, Right, High).pos(4).kill().portal(X, 0x00),
|
||||
create_door(player, 'TR Big Chest Entrance Gap', Lgcl),
|
||||
create_door(player, 'TR Big Chest NE', Intr).dir(No, 0x24, Right, High).pos(3),
|
||||
create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().pos(3),
|
||||
create_door(player, 'TR Dodgers SE', Intr).dir(So, 0x24, Right, High).no_exit().trap(0x8).pos(3),
|
||||
create_door(player, 'TR Dodgers NE', Nrml).dir(No, 0x24, Right, High).big_key().pos(0),
|
||||
create_door(player, 'TR Lazy Eyes SE', Nrml).dir(So, 0x23, Right, High).pos(0).portal(X, 0x00),
|
||||
create_door(player, 'TR Lazy Eyes ES', Nrml).dir(Ea, 0x23, Bot, High).pos(1),
|
||||
@@ -1073,7 +1079,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'GT Hope Room EN', Nrml).dir(Ea, 0x8c, Top, High).trap(0x4).pos(0),
|
||||
create_door(player, 'GT Torch EN', Intr).dir(Ea, 0x8c, Top, High).small_key().pos(2),
|
||||
create_door(player, 'GT Hope Room WN', Intr).dir(We, 0x8c, Top, High).small_key().pos(2),
|
||||
create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().pos(1),
|
||||
create_door(player, 'GT Torch SW', Intr).dir(So, 0x8c, Left, High).no_exit().trap(0x2).pos(1),
|
||||
create_door(player, 'GT Big Chest NW', Intr).dir(No, 0x8c, Left, High).pos(1),
|
||||
create_door(player, 'GT Blocked Stairs Down Stairs', Sprl).dir(Dn, 0x8c, 3, HTH).ss(Z, 0x12, 0x40, True, True).kill(),
|
||||
create_door(player, 'GT Blocked Stairs Block Path', Lgcl),
|
||||
@@ -1179,7 +1185,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'GT Ice Armos NE', Intr).dir(No, 0x1c, Right, High).pos(0),
|
||||
create_door(player, 'GT Big Key Room SE', Intr).dir(So, 0x1c, Right, High).pos(0),
|
||||
create_door(player, 'GT Ice Armos WS', Intr).dir(We, 0x1c, Bot, High).pos(1),
|
||||
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().pos(1),
|
||||
create_door(player, 'GT Four Torches ES', Intr).dir(Ea, 0x1c, Bot, High).no_exit().trap(0x2).pos(1),
|
||||
create_door(player, 'GT Four Torches NW', Intr).dir(No, 0x1c, Left, High).pos(2),
|
||||
create_door(player, 'GT Fairy Abyss SW', Intr).dir(So, 0x1c, Left, High).pos(2),
|
||||
create_door(player, 'GT Four Torches Up Stairs', Sprl).dir(Up, 0x1c, 0, HTH).ss(Z, 0x1b, 0x2c, True, True),
|
||||
@@ -1211,7 +1217,7 @@ def create_doors(world, player):
|
||||
create_door(player, 'GT Beam Dash WS', Intr).dir(We, 0x6c, Bot, High).pos(0),
|
||||
create_door(player, 'GT Lanmolas 2 ES', Intr).dir(Ea, 0x6c, Bot, High).pos(0),
|
||||
create_door(player, 'GT Lanmolas 2 NW', Intr).dir(No, 0x6c, Left, High).pos(1),
|
||||
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().pos(1),
|
||||
create_door(player, 'GT Quad Pot SW', Intr).dir(So, 0x6c, Left, High).no_exit().trap(0x2).pos(1),
|
||||
create_door(player, 'GT Quad Pot Up Stairs', Sprl).dir(Up, 0x6c, 0, HTH).ss(A, 0x1b, 0x6c, True, True),
|
||||
create_door(player, 'GT Wizzrobes 1 Down Stairs', Sprl).dir(Dn, 0xa5, 0, HTH).ss(A, 0x12, 0x80, True, True),
|
||||
create_door(player, 'GT Wizzrobes 1 SW', Intr).dir(So, 0xa5, Left, High).pos(2),
|
||||
@@ -1480,6 +1486,8 @@ def create_doors(world, player):
|
||||
controller_door(south_controller, world.get_door('Ice Cross Top Push Block Bottom', player))
|
||||
controller_door(east_controller, world.get_door('Ice Cross Bottom Push Block Right', player))
|
||||
controller_door(east_controller, world.get_door('Ice Cross Top Push Block Right', player))
|
||||
primary_controller = world.get_door('Mire Falling Bridge WS', player)
|
||||
controller_door(primary_controller, world.get_door('Mire Falling Bridge Primary Path', player))
|
||||
|
||||
assign_entrances(world, player)
|
||||
|
||||
@@ -1572,7 +1580,7 @@ def reset_portals(world, player):
|
||||
|
||||
def create_paired_doors(world, player):
|
||||
world.paired_doors[player] = [
|
||||
PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat Key Door N', True),
|
||||
PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat NE', True),
|
||||
PairedDoor('TR Pokey 2 ES', 'TR Lava Island WS', True), # TR Pokey Key
|
||||
PairedDoor('TR Dodgers NE', 'TR Lava Escape SE', True), # TR Big key door by pipes
|
||||
PairedDoor('PoD Falling Bridge WN', 'PoD Dark Maze EN', True), # Pod Dark maze door
|
||||
|
||||
+204
-71
@@ -10,11 +10,14 @@ 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 RoomData import DoorKind
|
||||
|
||||
from source.dungeon.DungeonStitcher import generate_dungeon_find_proposal
|
||||
from source.dungeon.DungeonStitcher import GenerationException as OtherGenException
|
||||
|
||||
|
||||
class GraphPiece:
|
||||
|
||||
@@ -43,7 +46,10 @@ def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
||||
for sector in builder.sectors:
|
||||
for door in sector.outstanding_doors:
|
||||
doors_to_connect[door.name] = door
|
||||
all_regions.update(sector.regions)
|
||||
if world.mode[player] == 'standard' and builder.name == 'Hyrule Castle Dungeon':
|
||||
all_regions.update([x for x in sector.regions if x.name != 'Hyrule Castle Behind Tapestry'])
|
||||
else:
|
||||
all_regions.update(sector.regions)
|
||||
bk_special |= check_for_special(sector.regions)
|
||||
bk_needed = False
|
||||
for sector in builder.sectors:
|
||||
@@ -59,7 +65,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
|
||||
if builder.valid_proposal: # we made this earlier in gen, just use it
|
||||
proposed_map = builder.valid_proposal
|
||||
else:
|
||||
proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
|
||||
proposed_map = generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player)
|
||||
builder.valid_proposal = proposed_map
|
||||
queue = collections.deque(proposed_map.items())
|
||||
while len(queue) > 0:
|
||||
@@ -77,7 +83,7 @@ def generate_dungeon(builder, entrance_region_names, split_dungeon, world, playe
|
||||
return master_sector
|
||||
|
||||
|
||||
def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
|
||||
def generate_dungeon_find_proposal_old(builder, entrance_region_names, split_dungeon, world, player):
|
||||
logger = logging.getLogger('')
|
||||
name = builder.name
|
||||
entrance_regions = convert_regions(entrance_region_names, world, player)
|
||||
@@ -91,18 +97,27 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
||||
p_region = portal.door.entrance.connected_region
|
||||
access_region = next(x.parent_region for x in p_region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||
if ((access_region.name in world.inaccessible_regions[player] and
|
||||
region.name not in world.enabled_entrances[player])
|
||||
or (world.mode[player] == 'standard' and access_region.name != 'Hyrule Castle Courtyard'
|
||||
and 'Hyrule Castle' in builder.name)):
|
||||
excluded[region] = None
|
||||
else: # for non-portals, holes and sewers in std
|
||||
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'), None)
|
||||
if access_region is None:
|
||||
if builder.sewers_access is None:
|
||||
excluded[region] = None
|
||||
else:
|
||||
if access_region.name == 'Sewer Drop':
|
||||
if world.mode[player] == 'standard' and (builder.sewers_access is None
|
||||
or builder.sewers_access.entrance.parent_region != region):
|
||||
excluded[region] = None
|
||||
access_region = next(x.parent_region for x in access_region.entrances)
|
||||
if (access_region.name in world.inaccessible_regions[player] and
|
||||
region.name not in world.enabled_entrances[player]):
|
||||
excluded[region] = None
|
||||
elif len(region.entrances) == 1: # for holes
|
||||
access_region = next(x.parent_region for x in region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]
|
||||
or x.parent_region.name == 'Sewer Drop')
|
||||
if access_region.name == 'Sewer Drop':
|
||||
access_region = next(x.parent_region for x in access_region.entrances)
|
||||
if (access_region.name in world.inaccessible_regions[player] and
|
||||
region.name not in world.enabled_entrances[player]):
|
||||
excluded[region] = None
|
||||
entrance_regions = [x for x in entrance_regions if x not in excluded.keys()]
|
||||
doors_to_connect = {}
|
||||
all_regions = set()
|
||||
@@ -143,7 +158,11 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon
|
||||
dungeon, hangers, hooks = gen_dungeon_info(name, builder.sectors, entrance_regions, all_regions, proposed_map,
|
||||
doors_to_connect, bk_needed, bk_special, world, player)
|
||||
dungeon_cache[depth] = dungeon, hangers, hooks
|
||||
valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, all_regions,
|
||||
if len(proposed_map) != len(doors_to_connect) and builder.name == 'Hyrule Castle Dungeon':
|
||||
check_regions = all_regions.difference({world.get_region('Hyrule Castle Behind Tapestry', player)})
|
||||
else:
|
||||
check_regions = all_regions
|
||||
valid = check_valid(name, dungeon, hangers, hooks, proposed_map, doors_to_connect, check_regions,
|
||||
bk_needed, bk_special, paths, entrance_regions, world, player)
|
||||
else:
|
||||
dungeon, hangers, hooks = dungeon_cache[depth]
|
||||
@@ -565,9 +584,15 @@ 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':
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
||||
if world.mode[player] == 'standard':
|
||||
if name == 'Hyrule Castle':
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Sanctuary'))
|
||||
if name == 'Hyrule Castle Sewers':
|
||||
paths.append('Sanctuary')
|
||||
if name == 'Hyrule Castle Dungeon':
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
|
||||
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
||||
paths.append('Thieves Attic Window')
|
||||
elif 'Thieves Attic Window' in all_r_names:
|
||||
@@ -786,6 +811,7 @@ class ExplorationState(object):
|
||||
self.prize_door_set = {}
|
||||
self.prize_doors = []
|
||||
self.prize_doors_opened = False
|
||||
self.prize_received = False
|
||||
|
||||
def copy(self):
|
||||
ret = ExplorationState(dungeon=self.dungeon)
|
||||
@@ -817,8 +843,17 @@ class ExplorationState(object):
|
||||
ret.prize_door_set = dict(self.prize_door_set)
|
||||
ret.prize_doors = list(self.prize_doors)
|
||||
ret.prize_doors_opened = self.prize_doors_opened
|
||||
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()
|
||||
@@ -844,9 +879,9 @@ class ExplorationState(object):
|
||||
self.key_locations += 1
|
||||
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
|
||||
self.ttl_locations += 1
|
||||
if location not in self.found_locations: # todo: special logic for TT Boss?
|
||||
if location not in self.found_locations:
|
||||
self.found_locations.append(location)
|
||||
if not bk_Flag:
|
||||
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):
|
||||
@@ -962,6 +997,20 @@ class ExplorationState(object):
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors)
|
||||
|
||||
def add_all_doors_check_big_keys(self, region, big_key_door_proposal, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
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 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:
|
||||
if 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 visited(self, region):
|
||||
if self.crystal == CrystalBarrier.Either:
|
||||
return region in self.visited_blue and region in self.visited_orange
|
||||
@@ -1161,6 +1210,7 @@ class DungeonBuilder(object):
|
||||
self.name = name
|
||||
self.sectors = []
|
||||
self.location_cnt = 0
|
||||
self.location_set = set()
|
||||
self.key_drop_cnt = 0
|
||||
self.dungeon_items = None # during fill how many dungeon items are left
|
||||
self.free_items = None # during fill how many dungeon items are left
|
||||
@@ -1191,6 +1241,8 @@ class DungeonBuilder(object):
|
||||
self.combo_size = None
|
||||
self.flex = 0
|
||||
self.key_door_proposal = None
|
||||
self.bk_door_proposal = None
|
||||
self.trap_door_proposal = None
|
||||
|
||||
self.allowance = None
|
||||
if 'Stonewall' in name:
|
||||
@@ -1206,6 +1258,11 @@ class DungeonBuilder(object):
|
||||
self.split_dungeon_map = None
|
||||
self.exception_list = []
|
||||
|
||||
self.throne_door = None
|
||||
self.throne_sector = None
|
||||
self.chosen_lobby = None
|
||||
self.sewers_access = None
|
||||
|
||||
def polarity_complement(self):
|
||||
pol = Polarity()
|
||||
for sector in self.sectors:
|
||||
@@ -1235,7 +1292,7 @@ def simple_dungeon_builder(name, sector_list):
|
||||
return builder
|
||||
|
||||
|
||||
def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
def create_dungeon_builders(all_sectors, connections_tuple, world, player, dungeon_pool,
|
||||
dungeon_entrances=None, split_dungeon_entrances=None):
|
||||
logger = logging.getLogger('')
|
||||
logger.info('Shuffling Dungeon Sectors')
|
||||
@@ -1251,14 +1308,12 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
global_pole = GlobalPolarity(candidate_sectors)
|
||||
|
||||
dungeon_map = {}
|
||||
for key in dungeon_regions.keys():
|
||||
dungeon_map[key] = DungeonBuilder(key)
|
||||
for key in dungeon_boss_sectors.keys():
|
||||
current_dungeon = dungeon_map[key]
|
||||
for key in dungeon_pool:
|
||||
current_dungeon = dungeon_map[key] = DungeonBuilder(key)
|
||||
for r_name in dungeon_boss_sectors[key]:
|
||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon, candidate_sectors, global_pole)
|
||||
if key == 'Hyrule Castle' and world.mode[player] == 'standard':
|
||||
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary']: # need to deliver zelda
|
||||
for r_name in ['Hyrule Dungeon Cellblock', 'Sanctuary', 'Hyrule Castle Throne Room']: # need to deliver zelda
|
||||
assign_sector(find_sector(r_name, candidate_sectors), current_dungeon,
|
||||
candidate_sectors, global_pole)
|
||||
if key == 'Thieves Town' and world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
||||
@@ -1266,7 +1321,7 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
candidate_sectors, global_pole)
|
||||
entrances_map, potentials, connections = connections_tuple
|
||||
accessible_sectors, reverse_d_map = set(), {}
|
||||
for key in dungeon_entrances.keys():
|
||||
for key in dungeon_pool:
|
||||
current_dungeon = dungeon_map[key]
|
||||
current_dungeon.all_entrances = dungeon_entrances[key]
|
||||
for r_name in current_dungeon.all_entrances:
|
||||
@@ -1286,6 +1341,10 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
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()]
|
||||
|
||||
if not dungeon_map:
|
||||
dungeon_map.update(complete_dungeons)
|
||||
return dungeon_map
|
||||
|
||||
# categorize sectors
|
||||
identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map, connections,
|
||||
dungeon_entrances, split_dungeon_entrances)
|
||||
@@ -1296,8 +1355,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
sanc = find_sector('Sanctuary', candidate_sectors)
|
||||
if sanc: # only run if sanc if a candidate
|
||||
lw_builders = []
|
||||
for name, portal_list in dungeon_portals.items():
|
||||
for portal_name in portal_list:
|
||||
for name in dungeon_pool:
|
||||
for portal_name in dungeon_portals[name]:
|
||||
if world.get_portal(portal_name, player).light_world:
|
||||
lw_builders.append(dungeon_map[name])
|
||||
break
|
||||
@@ -1305,7 +1364,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
sanc_builder = random.choice(lw_builders)
|
||||
assign_sector(sanc, sanc_builder, candidate_sectors, global_pole)
|
||||
|
||||
bow_sectors, retro_std_flag = {}, world.retro[player] 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 = {}
|
||||
@@ -1313,7 +1373,9 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
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:
|
||||
@@ -1324,8 +1386,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
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:
|
||||
@@ -1369,7 +1431,8 @@ def create_dungeon_builders(all_sectors, connections_tuple, world, player,
|
||||
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)
|
||||
@@ -1388,6 +1451,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map,
|
||||
if ent_name in found_connections:
|
||||
continue
|
||||
sector = find_sector(ent_name, reverse_d_map.keys())
|
||||
if sector is None:
|
||||
continue
|
||||
if sector in accessible_sectors:
|
||||
found_connections.add(ent_name)
|
||||
accessible_overworld.add(region) # todo: drops don't give ow access
|
||||
@@ -1398,6 +1463,8 @@ def identify_destination_sectors(accessible_sectors, reverse_d_map, dungeon_map,
|
||||
explored = False
|
||||
else:
|
||||
d_name = reverse_d_map[sector]
|
||||
if d_name not in dungeon_map:
|
||||
return
|
||||
if d_name not in split_dungeon_entrances:
|
||||
for r_name in dungeon_entrances[d_name]:
|
||||
ent_sector = find_sector(r_name, dungeon_map[d_name].sectors)
|
||||
@@ -1451,6 +1518,8 @@ def calc_allowance_and_dead_ends(builder, connections_tuple, world, player):
|
||||
builder.branches -= 1
|
||||
if entrance not in drop_entrances_allowance:
|
||||
needed_connections.append(entrance)
|
||||
if builder.sewers_access:
|
||||
starting_allowance += 1
|
||||
builder.allowance = starting_allowance
|
||||
for entrance in needed_connections:
|
||||
sector = find_sector(entrance, builder.sectors)
|
||||
@@ -1493,7 +1562,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:
|
||||
@@ -1505,6 +1574,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):
|
||||
@@ -1518,6 +1589,7 @@ def assign_sector_helper(sector, builder):
|
||||
builder.sectors.append(sector)
|
||||
builder.location_cnt += sector.chest_locations
|
||||
builder.key_drop_cnt += sector.key_only_locations
|
||||
builder.location_set.update(sector.chest_location_set)
|
||||
if sector.c_switch:
|
||||
builder.c_switch_present = True
|
||||
if sector.blue_barrier:
|
||||
@@ -1555,8 +1627,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:
|
||||
@@ -1565,7 +1637,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):
|
||||
@@ -2077,16 +2149,18 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde
|
||||
while len(problem_builders) > 0:
|
||||
for name, builder in problem_builders.items():
|
||||
candidates = find_branching_candidates(builder, neutral_choices, builder_info)
|
||||
valid, choice = False, None
|
||||
valid, choice, package = False, None, None
|
||||
while not valid:
|
||||
if len(candidates) <= 0:
|
||||
raise GenerationException('Cross Dungeon Builder: Complex branch problems: %s' % name)
|
||||
choice = random.choice(candidates)
|
||||
candidates.remove(choice)
|
||||
choice, package = random.choice(candidates)
|
||||
candidates.remove((choice, package))
|
||||
valid = global_pole.is_valid_choice(dungeon_map, builder, choice) and valid_polarized_assignment(builder, choice)
|
||||
neutral_choices.remove(choice)
|
||||
for sector in choice:
|
||||
assign_sector(sector, builder, polarized_sectors, global_pole)
|
||||
if package:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package
|
||||
builder.unfulfilled.clear()
|
||||
problem_builders = identify_branching_issues(problem_builders, builder_info)
|
||||
|
||||
@@ -2107,16 +2181,21 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, builde
|
||||
chosen_sectors = defaultdict(list)
|
||||
for i, choice in enumerate(choices):
|
||||
chosen_sectors[choice].extend(neutral_choices[i])
|
||||
all_valid = True
|
||||
all_valid, package_map = True, {}
|
||||
for name, sector_list in chosen_sectors.items():
|
||||
if not valid_assignment(dungeon_map[name], sector_list, builder_info):
|
||||
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
|
||||
if not flag:
|
||||
all_valid = False
|
||||
break
|
||||
if package:
|
||||
package_map[dungeon_map[name]] = package
|
||||
if all_valid:
|
||||
for i, choice in enumerate(choices):
|
||||
builder = dungeon_map[choice]
|
||||
for sector in neutral_choices[i]:
|
||||
assign_sector(sector, builder, polarized_sectors, global_pole)
|
||||
if builder in package_map:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
||||
tries += 1
|
||||
|
||||
|
||||
@@ -2629,9 +2708,9 @@ def weed_candidates(builder, candidates, best_charge):
|
||||
def find_branching_candidates(builder, neutral_choices, builder_info):
|
||||
candidates = []
|
||||
for choice in neutral_choices:
|
||||
resolved, problem_list = check_for_valid_layout(builder, choice, builder_info)
|
||||
resolved, problem_list, package = check_for_valid_layout(builder, choice, builder_info)
|
||||
if resolved:
|
||||
candidates.append(choice)
|
||||
candidates.append((choice, package))
|
||||
return candidates
|
||||
|
||||
|
||||
@@ -2786,13 +2865,13 @@ def categorize_groupings(sectors):
|
||||
|
||||
def valid_assignment(builder, sector_list, builder_info):
|
||||
if not valid_entrance(builder, sector_list, builder_info):
|
||||
return False
|
||||
return False, None
|
||||
if not valid_c_switch(builder, sector_list):
|
||||
return False
|
||||
return False, None
|
||||
if not valid_polarized_assignment(builder, sector_list):
|
||||
return False
|
||||
resolved, problems = check_for_valid_layout(builder, sector_list, builder_info)
|
||||
return resolved
|
||||
return False, None
|
||||
resolved, problems, package = check_for_valid_layout(builder, sector_list, builder_info)
|
||||
return resolved, package
|
||||
|
||||
|
||||
def valid_entrance(builder, sector_list, builder_info):
|
||||
@@ -2898,31 +2977,56 @@ def assign_the_rest(dungeon_map, neutral_sectors, global_pole, builder_info):
|
||||
chosen_sectors = defaultdict(list)
|
||||
for i, choice in enumerate(choices):
|
||||
chosen_sectors[choice].append(neutral_sector_list[i])
|
||||
all_valid = True
|
||||
all_valid, package_map = True, {}
|
||||
for name, sector_list in chosen_sectors.items():
|
||||
if not valid_assignment(dungeon_map[name], sector_list, builder_info):
|
||||
flag, package = valid_assignment(dungeon_map[name], sector_list, builder_info)
|
||||
if not flag:
|
||||
all_valid = False
|
||||
break
|
||||
if package:
|
||||
package_map[dungeon_map[name]] = package
|
||||
if all_valid:
|
||||
for name, sector_list in chosen_sectors.items():
|
||||
builder = dungeon_map[name]
|
||||
for sector in sector_list:
|
||||
assign_sector(sector, builder, neutral_sectors, global_pole)
|
||||
if builder in package_map:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
||||
tries += 1
|
||||
|
||||
|
||||
def split_dungeon_builder(builder, split_list, builder_info):
|
||||
ents, splits, c_tuple, world, player = builder_info
|
||||
if builder.split_dungeon_map and len(builder.exception_list) == 0:
|
||||
for name, proposal in builder.valid_proposal.items():
|
||||
builder.split_dungeon_map[name].valid_proposal = proposal
|
||||
if builder.name == 'Hyrule Castle':
|
||||
builder.chosen_lobby.outstanding_doors.remove(builder.throne_door)
|
||||
builder.throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
|
||||
return builder.split_dungeon_map # we made this earlier in gen, just use it
|
||||
|
||||
attempts, comb_w_replace, merge_attempt, merge_limit = 0, None, 0, len(split_list) - 1
|
||||
while attempts < 5: # does not solve coin flips 3% of the time
|
||||
try:
|
||||
candidate_sectors = dict.fromkeys(builder.sectors)
|
||||
global_pole = GlobalPolarity(candidate_sectors)
|
||||
if builder.name == 'Hyrule Castle':
|
||||
throne_sector = find_sector('Hyrule Castle Throne Room', candidate_sectors)
|
||||
chosen_lobbies = {r_name for x in split_list.values() for r_name in x}
|
||||
choices = {}
|
||||
for sector in candidate_sectors:
|
||||
if sector.adj_outflow() > 1 and sector != throne_sector:
|
||||
for door in sector.outstanding_doors:
|
||||
if door.direction == Direction.South and door.entrance.parent_region not in chosen_lobbies:
|
||||
choices[door] = sector
|
||||
chosen_door = random.choice(list(choices.keys()))
|
||||
split_list['Sewers'].append(chosen_door.entrance.parent_region.name)
|
||||
choices[chosen_door].outstanding_doors.remove(chosen_door)
|
||||
builder.throne_door = chosen_door
|
||||
builder.throne_sector = throne_sector
|
||||
builder.chosen_lobby = choices[chosen_door]
|
||||
throne_sector.outstanding_doors.remove(world.get_door('Hyrule Castle Throne Room N', player))
|
||||
|
||||
global_pole = GlobalPolarity(candidate_sectors)
|
||||
dungeon_map, sub_builder, merge_keys = {}, None, []
|
||||
if merge_attempt > 0:
|
||||
candidates = []
|
||||
@@ -2932,7 +3036,6 @@ def split_dungeon_builder(builder, split_list, builder_info):
|
||||
continue
|
||||
elif len(split_entrances) <= 0:
|
||||
continue
|
||||
ents, splits, c_tuple, world, player = builder_info
|
||||
r_name = split_entrances[0]
|
||||
p = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region.name == r_name), None)
|
||||
if p and not p.deadEnd:
|
||||
@@ -2953,14 +3056,29 @@ def split_dungeon_builder(builder, split_list, builder_info):
|
||||
sub_builder.all_entrances = list(split_entrances)
|
||||
for r_name in split_entrances:
|
||||
assign_sector(find_sector(r_name, candidate_sectors), sub_builder, candidate_sectors, global_pole)
|
||||
if builder.name == 'Hyrule Castle':
|
||||
assign_sector(find_sector('Hyrule Castle Throne Room', candidate_sectors),
|
||||
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
|
||||
assign_sector(find_sector('Hyrule Dungeon Cellblock', candidate_sectors),
|
||||
dungeon_map['Hyrule Castle Dungeon'], candidate_sectors, global_pole)
|
||||
dungeon_map['Hyrule Castle Dungeon'].throne_door = world.get_door('Hyrule Castle Throne Room N', player)
|
||||
dungeon_map['Hyrule Castle Sewers'].sewers_access = builder.throne_door
|
||||
if len(candidate_sectors) == 0:
|
||||
return dungeon_map
|
||||
comb_w_replace = len(dungeon_map) ** len(candidate_sectors)
|
||||
return balance_split(candidate_sectors, dungeon_map, global_pole, builder_info)
|
||||
except (GenerationException, NeutralizingException):
|
||||
if comb_w_replace and comb_w_replace <= 10000:
|
||||
if comb_w_replace and comb_w_replace <= 10000 and not builder.throne_door:
|
||||
attempts += 5 # all the combinations were tried already, no use repeating
|
||||
else:
|
||||
attempts += 1
|
||||
if attempts >= 5 and merge_attempt < merge_limit:
|
||||
if builder.throne_door:
|
||||
previous = find_sector(builder.throne_door.entrance.parent_region.name, builder.sectors)
|
||||
previous.outstanding_doors.append(builder.throne_door)
|
||||
builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
||||
split_list['Sewers'].remove(builder.throne_door.entrance.parent_region.name)
|
||||
builder.throne_door = None
|
||||
if attempts >= 5 and merge_attempt < merge_limit and builder.name != 'Hyrule Castle':
|
||||
merge_attempt, attempts = merge_attempt + 1, 0
|
||||
|
||||
raise GenerationException('Unable to resolve in 5 attempts')
|
||||
@@ -2981,16 +3099,21 @@ def balance_split(candidate_sectors, dungeon_map, global_pole, builder_info):
|
||||
chosen_sectors = defaultdict(list)
|
||||
for i, choice in enumerate(choices):
|
||||
chosen_sectors[choice].append(main_sector_list[i])
|
||||
all_valid = True
|
||||
all_valid, package_map = True, {}
|
||||
for name, builder in dungeon_map.items():
|
||||
if not valid_assignment(builder, chosen_sectors[name], builder_info):
|
||||
flag, package = valid_assignment(builder, chosen_sectors[name], builder_info)
|
||||
if not flag:
|
||||
all_valid = False
|
||||
break
|
||||
if package:
|
||||
package_map[builder] = package
|
||||
if all_valid:
|
||||
for name, sector_list in chosen_sectors.items():
|
||||
builder = dungeon_map[name]
|
||||
for sector in sector_list:
|
||||
assign_sector(sector, builder, candidate_sectors, global_pole)
|
||||
if builder in package_map:
|
||||
builder.throne_door, builder.throne_sector, builder.chosen_lobby = package_map[builder]
|
||||
return dungeon_map
|
||||
tries += 1
|
||||
raise GenerationException('Split Dungeon Builder: Impossible dungeon. Ref %s' % next(iter(dungeon_map.keys())))
|
||||
@@ -3380,7 +3503,7 @@ class DungeonAccess:
|
||||
def identify_branching_issues(dungeon_map, builder_info):
|
||||
unconnected_builders = {}
|
||||
for name, builder in dungeon_map.items():
|
||||
resolved, unreached_doors = check_for_valid_layout(builder, [], builder_info)
|
||||
resolved, unreached_doors, package = check_for_valid_layout(builder, [], builder_info)
|
||||
if not resolved:
|
||||
unconnected_builders[name] = builder
|
||||
for hook, door_list in unreached_doors.items():
|
||||
@@ -3402,7 +3525,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():
|
||||
@@ -3421,16 +3545,27 @@ def check_for_valid_layout(builder, sector_list, builder_info):
|
||||
proposal = generate_dungeon_find_proposal(split_build, entrance_regions, split, world, player)
|
||||
# record split proposals
|
||||
builder.valid_proposal[name] = proposal
|
||||
package = None
|
||||
if temp_builder.name == 'Hyrule Castle':
|
||||
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
|
||||
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
||||
package = temp_builder.throne_door, temp_builder.throne_sector, temp_builder.chosen_lobby
|
||||
split_list['Sewers'].remove(temp_builder.throne_door.entrance.parent_region.name)
|
||||
builder.exception_list = list(sector_list)
|
||||
return True, {}
|
||||
except (GenerationException, NeutralizingException):
|
||||
return True, {}, package
|
||||
except (GenerationException, NeutralizingException, OtherGenException):
|
||||
builder.split_dungeon_map = None
|
||||
builder.valid_proposal = None
|
||||
if temp_builder.name == 'Hyrule Castle' and temp_builder.throne_door:
|
||||
temp_builder.chosen_lobby.outstanding_doors.append(temp_builder.throne_door)
|
||||
temp_builder.throne_sector.outstanding_doors.append(world.get_door('Hyrule Castle Throne Room N', player))
|
||||
old_entrance = temp_builder.throne_door.entrance.parent_region.name
|
||||
split_dungeon_entrances[builder.name]['Sewers'].remove(old_entrance)
|
||||
unreached_doors = resolve_equations(builder, sector_list)
|
||||
return False, unreached_doors
|
||||
return False, unreached_doors, None
|
||||
else:
|
||||
unreached_doors = resolve_equations(builder, sector_list)
|
||||
return len(unreached_doors) == 0, unreached_doors
|
||||
return len(unreached_doors) == 0, unreached_doors, None
|
||||
|
||||
|
||||
def find_independent_entrances(entrance_regions, world, player):
|
||||
@@ -3831,22 +3966,20 @@ def find_free_equation(equations):
|
||||
def copy_door_equations(builder, sector_list):
|
||||
equations = {}
|
||||
for sector in builder.sectors + sector_list:
|
||||
if sector.equations is None:
|
||||
# todo: sort equations?
|
||||
sector.equations = calc_sector_equations(sector)
|
||||
sector.equations = calc_sector_equations(sector, builder.sewers_access)
|
||||
curr_list = equations[sector] = []
|
||||
for equation in sector.equations:
|
||||
curr_list.append(equation.copy())
|
||||
return equations
|
||||
|
||||
|
||||
def calc_sector_equations(sector):
|
||||
def calc_sector_equations(sector, sewers_flag=False):
|
||||
equations = []
|
||||
is_entrance = sector.is_entrance_sector() and not sector.destination_entrance
|
||||
is_entrance = (sector.is_entrance_sector() and not sector.destination_entrance) or sewers_flag
|
||||
if is_entrance:
|
||||
flagged_equations = []
|
||||
for door in sector.outstanding_doors:
|
||||
equation, flag = calc_door_equation(door, sector, True)
|
||||
equation, flag = calc_door_equation(door, sector, True, sewers_flag)
|
||||
if flag:
|
||||
flagged_equations.append(equation)
|
||||
equations.append(equation)
|
||||
@@ -3862,9 +3995,9 @@ def calc_sector_equations(sector):
|
||||
return equations
|
||||
|
||||
|
||||
def calc_door_equation(door, sector, look_for_entrance):
|
||||
def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None):
|
||||
if look_for_entrance and not door.blocked:
|
||||
flag = sector.is_entrance_sector()
|
||||
flag = sector.is_entrance_sector() or sewers_flag
|
||||
if flag:
|
||||
eq = DoorEquation(door)
|
||||
eq.benefit[hook_from_door(door)].append(door)
|
||||
|
||||
+15
-13
@@ -5,7 +5,7 @@ from Items import ItemFactory
|
||||
|
||||
def create_dungeons(world, player):
|
||||
def make_dungeon(name, id, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.retro[player] else small_keys,
|
||||
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == 'universal' else small_keys,
|
||||
dungeon_items, player, id)
|
||||
dungeon.boss = BossFactory(default_boss, player)
|
||||
for region in dungeon.regions:
|
||||
@@ -52,10 +52,10 @@ hyrule_castle_regions = [
|
||||
'Hyrule Dungeon South Abyss', 'Hyrule Dungeon South Abyss Catwalk', 'Hyrule Dungeon Guardroom',
|
||||
'Hyrule Dungeon Armory Main', 'Hyrule Dungeon Armory Boomerang', 'Hyrule Dungeon Armory North Branch',
|
||||
'Hyrule Dungeon Staircase', 'Hyrule Dungeon Cellblock', 'Hyrule Dungeon Cell', 'Sewers Behind Tapestry',
|
||||
'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Key Rat', 'Sewers Rat Path',
|
||||
'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats', 'Sewers Pull Switch', 'Sanctuary',
|
||||
'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal', 'Hyrule Castle East Portal'
|
||||
|
||||
'Sewers Rope Room', 'Sewers Dark Cross', 'Sewers Water', 'Sewers Dark Aquabats', 'Sewers Key Rat',
|
||||
'Sewers Rat Path', 'Sewers Secret Room Blocked Path', 'Sewers Secret Room', 'Sewers Yet More Rats',
|
||||
'Sewers Pull Switch', 'Sanctuary', 'Sanctuary Portal', 'Hyrule Castle West Portal', 'Hyrule Castle South Portal',
|
||||
'Hyrule Castle East Portal'
|
||||
]
|
||||
|
||||
eastern_regions = [
|
||||
@@ -155,14 +155,16 @@ ice_regions = [
|
||||
|
||||
mire_regions = [
|
||||
'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Hub Switch',
|
||||
'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot',
|
||||
'Mire Crystal Dead End', 'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge',
|
||||
'Mire BK Door Room', 'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge',
|
||||
'Mire Left Bridge', 'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
|
||||
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room', 'Mire Compass Chest',
|
||||
'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge', 'Mire Warping Pool',
|
||||
'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters', 'Mire Key Rupees',
|
||||
'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal', 'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left',
|
||||
'Mire Lone Shooter', 'Mire Failure Bridge', 'Mire Falling Bridge - Primary', 'Mire Falling Bridge - Failure',
|
||||
'Mire Falling Bridge - Chest', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End',
|
||||
'Mire Hidden Shooters', 'Mire Hidden Shooters Blocked', 'Mire Cross', 'Mire Minibridge', 'Mire BK Door Room',
|
||||
'Mire Spikes', 'Mire Ledgehop', 'Mire Bent Bridge', 'Mire Over Bridge', 'Mire Right Bridge', 'Mire Left Bridge',
|
||||
'Mire Fishbone', 'Mire South Fish', 'Mire Spike Barrier', 'Mire Square Rail', 'Mire Lone Warp',
|
||||
'Mire Wizzrobe Bypass', 'Mire Conveyor Crystal', 'Mire Conveyor - Crystal', 'Mire Tile Room', 'Mire Compass Room',
|
||||
'Mire Compass Chest', 'Mire Neglected Room', 'Mire Chest View', 'Mire Conveyor Barrier', 'Mire BK Chest Ledge',
|
||||
'Mire Warping Pool', 'Mire Torches Top', 'Mire Torches Bottom', 'Mire Attic Hint', 'Mire Dark Shooters',
|
||||
'Mire Key Rupees', 'Mire Block X', 'Mire Tall Dark and Roomy', 'Mire Tall Dark and Roomy - Ranged Crystal',
|
||||
'Mire Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left',
|
||||
'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber',
|
||||
'Mire Boss', 'Mire Portal'
|
||||
]
|
||||
|
||||
@@ -91,6 +91,7 @@ While the exact verbage of location names and item names can be found in the sou
|
||||
|
||||
Overworld Entrance naming:
|
||||
|
||||
Links House: The hero's old residence
|
||||
Turtle Rock: Turtle Rock Main
|
||||
Misery Mire: Misery Mire
|
||||
Ice Palace: Ice Palace
|
||||
@@ -141,6 +142,7 @@ Lake Hylia Fairy: A cave NE of Lake Hylia
|
||||
Light Hype Fairy: The cave south of your house
|
||||
Desert Fairy: The cave near the desert
|
||||
Chicken House: The chicken lady's house
|
||||
Tavern North: A backdoor
|
||||
Aginahs Cave: The open desert cave
|
||||
Sahasrahlas Hut: The house near armos
|
||||
Cave Shop (Lake Hylia): The cave NW Lake Hylia
|
||||
|
||||
+4
-3
@@ -2039,7 +2039,8 @@ Exit_Pool_Base = ['Links House Exit',
|
||||
'Skull Pot Circle',
|
||||
'Pyramid']
|
||||
|
||||
# these are connections that cannot be shuffled and always exist. They link together separate parts of the world we need to divide into regions
|
||||
# these are connections that cannot be shuffled and always exist.
|
||||
# They link together separate parts of the world we need to divide into regions
|
||||
mandatory_connections = [('Old Man S&Q', 'Old Man House'),
|
||||
|
||||
# UW Connections
|
||||
@@ -2378,7 +2379,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c
|
||||
'Light Hype Fairy': (0x6B, (0x0115, 0x34, 0x00a0, 0x0c04, 0x0900, 0x0c58, 0x0988, 0x0c73, 0x0985, 0x0a, 0xf6, 0x0000, 0x0000), 0x02),
|
||||
'Desert Fairy': (0x71, (0x0115, 0x3a, 0x0000, 0x0e00, 0x0400, 0x0e26, 0x0468, 0x0e6d, 0x0485, 0x00, 0x00, 0x0000, 0x0000), 0x00),
|
||||
'Kings Grave': (0x5A, (0x0113, 0x14, 0x0320, 0x0456, 0x0900, 0x04a6, 0x0998, 0x04c3, 0x097d, 0x0a, 0xf6, 0x0000, 0x0000), 0x20),
|
||||
'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x08f9, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00), # do not use, buggy
|
||||
'Tavern North': (0x42, (0x0103, 0x18, 0x1440, 0x08a7, 0x0206, 0x091b, 0x0288, 0x0914, 0x0293, 0xf7, 0x09, 0xFFFF, 0x0000), 0x00),
|
||||
'Chicken House': (0x4A, (0x0108, 0x18, 0x1120, 0x0837, 0x0106, 0x0888, 0x0188, 0x08a4, 0x0193, 0x07, 0xf9, 0x1530, 0x0000), 0x00),
|
||||
'Aginahs Cave': (0x70, (0x010a, 0x30, 0x0656, 0x0cc6, 0x02aa, 0x0d18, 0x0328, 0x0d33, 0x032f, 0x08, 0xf8, 0x0000, 0x0000), 0x00),
|
||||
'Sahasrahlas Hut': (0x44, (0x0105, 0x1e, 0x0610, 0x06d4, 0x0c76, 0x0727, 0x0cf0, 0x0743, 0x0cfb, 0x0a, 0xf6, 0x0000, 0x0000), 0x00),
|
||||
@@ -2659,7 +2660,7 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d),
|
||||
'Light Hype Fairy': (0x940, 0xc80),
|
||||
'Desert Fairy': (0x420, 0xe00),
|
||||
'Kings Grave': (0x920, 0x520),
|
||||
'Tavern North': None, # can't mark this one technically
|
||||
'Tavern North': (0x270, 0x900),
|
||||
'Chicken House': (0x120, 0x880),
|
||||
'Aginahs Cave': (0x2e0, 0xd00),
|
||||
'Sahasrahlas Hut': (0xcf0, 0x6c0),
|
||||
|
||||
@@ -13,7 +13,7 @@ from source.item.FillUtil import filter_pot_locations, valid_pot_items
|
||||
|
||||
|
||||
def get_dungeon_item_pool(world):
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items]
|
||||
return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None]
|
||||
|
||||
|
||||
def promote_dungeon_items(world):
|
||||
@@ -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,11 +34,11 @@ 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():
|
||||
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]):
|
||||
if ((item.smallkey and world.keyshuffle[item.player] != 'none')
|
||||
or (item.bigkey and world.bigkeyshuffle[item.player])):
|
||||
item.advancement = True
|
||||
elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]):
|
||||
item.priority = True
|
||||
@@ -50,7 +49,7 @@ def fill_dungeons_restrictive(world, shuffled_locations):
|
||||
(bigs if i.bigkey else smalls if i.smallkey else others).append(i)
|
||||
unplaced_smalls = list(smalls)
|
||||
for i in world.itempool:
|
||||
if i.smallkey and world.keyshuffle[i.player]:
|
||||
if i.smallkey and world.keyshuffle[i.player] != 'none':
|
||||
unplaced_smalls.append(i)
|
||||
|
||||
def fill(base_state, items, key_pool):
|
||||
@@ -72,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 = []
|
||||
@@ -93,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:
|
||||
@@ -160,7 +161,7 @@ def valid_key_placement(item, location, key_pool, world):
|
||||
if not valid_reserved_placement(item, location, world):
|
||||
return False
|
||||
if ((not item.smallkey and not item.bigkey) or item.player != location.player
|
||||
or world.retro[item.player] or world.logic[item.player] == 'nologic'):
|
||||
or world.keyshuffle[item.player] == 'universal' or world.logic[item.player] == 'nologic'):
|
||||
return True
|
||||
dungeon = location.parent_region.dungeon
|
||||
if dungeon:
|
||||
@@ -172,7 +173,7 @@ def valid_key_placement(item, location, key_pool, world):
|
||||
if key_logic.prize_location:
|
||||
prize_loc = world.get_location(key_logic.prize_location, location.player)
|
||||
cr_count = world.crystals_needed_for_gt[location.player]
|
||||
wild_keys = world.keyshuffle[item.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)
|
||||
else:
|
||||
return not item.is_inside_dungeon_item(world)
|
||||
@@ -216,7 +217,7 @@ def track_dungeon_items(item, location, world):
|
||||
|
||||
|
||||
def is_dungeon_item(item, world):
|
||||
return ((item.smallkey and not world.keyshuffle[item.player])
|
||||
return ((item.smallkey and world.keyshuffle[item.player] == 'none')
|
||||
or (item.bigkey and not world.bigkeyshuffle[item.player])
|
||||
or (item.compass and not world.compassshuffle[item.player])
|
||||
or (item.map and not world.mapshuffle[item.player]))
|
||||
@@ -288,7 +289,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)
|
||||
@@ -426,7 +427,8 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
|
||||
|
||||
# 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] and world.mode[item.player] == 'standard' else 0)
|
||||
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)
|
||||
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
|
||||
@@ -490,14 +492,15 @@ def calc_trash_locations(world, player):
|
||||
|
||||
def ensure_good_pots(world, write_skips=False):
|
||||
for loc in world.get_locations():
|
||||
# # convert Arrows 5 when necessary
|
||||
# if (loc.item.name in {'Arrows (5)'}
|
||||
# and loc.type not in [LocationType.Pot, LocationType.Bonk]):
|
||||
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
|
||||
# convert Nothing when necessary
|
||||
if loc.item is None:
|
||||
loc.item = ItemFactory('Nothing', loc.player)
|
||||
# convert Arrows 5 and Nothing when necessary
|
||||
if (loc.item.name in {'Nothing'}
|
||||
and (loc.type != LocationType.Pot or loc.item.player != loc.player)):
|
||||
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
|
||||
if (loc.item.name in {'Arrows (5)'}
|
||||
and (loc.type not in [LocationType.Pot, LocationType.Bonk] or loc.item.player != loc.player)):
|
||||
loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.item.player)
|
||||
# # can be placed here by multiworld balancing or shop balancing
|
||||
# # change it to something normal for the player it got swapped to
|
||||
# elif (loc.item.name in {'Chicken', 'Big Magic'}
|
||||
@@ -507,7 +510,7 @@ def ensure_good_pots(world, write_skips=False):
|
||||
# else:
|
||||
# loc.item = ItemFactory(invalid_location_replacement[loc.item.name], loc.player)
|
||||
# do the arrow retro check
|
||||
if world.retro[loc.item.player] and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
|
||||
if world.bow_mode[loc.item.player].startswith('retro') and loc.item.name in {'Arrows (5)', 'Arrows (10)'}:
|
||||
loc.item = ItemFactory('Rupees (5)', loc.item.player)
|
||||
# don't write out all pots to spoiler
|
||||
if write_skips:
|
||||
@@ -528,10 +531,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):
|
||||
@@ -672,7 +682,7 @@ def balance_multiworld_progression(world):
|
||||
candidate_items = collections.defaultdict(set)
|
||||
while True:
|
||||
for location in balancing_sphere:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
balancing_state.collect(location.item, True, location)
|
||||
player = location.item.player
|
||||
if player in balancing_players and not location.locked and location.player != player:
|
||||
@@ -747,7 +757,7 @@ def balance_multiworld_progression(world):
|
||||
sphere_locations.add(location)
|
||||
|
||||
for location in sphere_locations:
|
||||
if location.event and (world.keyshuffle[location.item.player] or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
if location.event and (world.keyshuffle[location.item.player] != 'none' or not location.item.smallkey) and (world.bigkeyshuffle[location.item.player] or not location.item.bigkey):
|
||||
state.collect(location.item, True, location)
|
||||
checked_locations |= sphere_locations
|
||||
|
||||
@@ -823,7 +833,9 @@ def balance_money_progression(world):
|
||||
return True
|
||||
if item.name in ['Progressive Armor', 'Blue Mail', 'Red Mail']:
|
||||
return True
|
||||
if world.retro[player] and (item.name in ['Single Arrow', 'Small Key (Universal)']):
|
||||
if world.keyshuffle[player] == 'universal' and item.name == 'Small Key (Universal)':
|
||||
return True
|
||||
if world.bow_mode[player].startswith('retro') and item.name == 'Single Arrow':
|
||||
return True
|
||||
if location.name in pay_for_locations:
|
||||
return True
|
||||
@@ -963,31 +975,85 @@ def balance_money_progression(world):
|
||||
wallet[location.item.player] += rupee_chart[location.item.name]
|
||||
|
||||
def set_prize_drops(world, player):
|
||||
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,
|
||||
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
|
||||
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
|
||||
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
|
||||
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
|
||||
|
||||
# randomize last 7 slots
|
||||
new_prizes = random.sample(prizes, 7)
|
||||
def chunk(l,n):
|
||||
return [l[i:i+n] for i in range(0, len(l), n)]
|
||||
|
||||
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
|
||||
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
|
||||
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
|
||||
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
|
||||
|
||||
if world.retro[player]:
|
||||
if world.bow_mode[player].startswith('retro'):
|
||||
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
|
||||
0xE2: 0xDB} #10 Arrows -> Red Rupee
|
||||
new_prizes = [prize_replacements.get(prize, prize) for prize in new_prizes]
|
||||
prizes = [prize_replacements.get(prize, prize) for prize in prizes]
|
||||
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
|
||||
|
||||
# write tree pull prizes
|
||||
world.prizes[player]['pull'] = [ new_prizes.pop(), new_prizes.pop(), new_prizes.pop() ]
|
||||
world.prizes[player]['dig'] = dig_prizes
|
||||
|
||||
# write tree pull prizes
|
||||
world.prizes[player]['pull'] = [ prizes.pop(), prizes.pop(), prizes.pop() ]
|
||||
|
||||
# rupee crab prizes
|
||||
world.prizes[player]['crab'] = [ new_prizes.pop(), new_prizes.pop() ]
|
||||
world.prizes[player]['crab'] = [ prizes.pop(), prizes.pop() ]
|
||||
|
||||
# stunned enemy prize
|
||||
world.prizes[player]['stun'] = new_prizes.pop()
|
||||
world.prizes[player]['stun'] = prizes.pop()
|
||||
|
||||
# saved fish prize
|
||||
world.prizes[player]['fish'] = new_prizes.pop()
|
||||
world.prizes[player]['fish'] = prizes.pop()
|
||||
|
||||
world.prizes[player]['enemies'] = prizes
|
||||
@@ -190,7 +190,7 @@ def guiMain(args=None):
|
||||
self.pages["startinventory"].content.pack(side=TOP, fill=BOTH, expand=True)
|
||||
|
||||
# Custom Controls
|
||||
self.pages["custom"].content = custom_page(self,self.pages["custom"])
|
||||
self.pages["custom"].content = custom_page(self, self.pages["custom"])
|
||||
self.pages["custom"].content.pack(side=TOP, fill=BOTH, expand=True)
|
||||
|
||||
def validation(P):
|
||||
|
||||
+3
-2
@@ -76,7 +76,7 @@ class InitialSram:
|
||||
if startingstate.has('Bow', player):
|
||||
equip[0x340] = 3 if startingstate.has('Silver Arrows', player) else 1
|
||||
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
|
||||
if not world.retro[player]:
|
||||
if not world.bow_mode[player].startswith('retro'):
|
||||
equip[0x38E] |= 0x80
|
||||
if startingstate.has('Silver Arrows', player):
|
||||
equip[0x38E] |= 0x40
|
||||
@@ -147,6 +147,7 @@ class InitialSram:
|
||||
'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),
|
||||
'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)}
|
||||
keys = {'Small Key (Eastern Palace)': [0x37E], 'Small Key (Desert Palace)': [0x37F],
|
||||
@@ -190,7 +191,7 @@ class InitialSram:
|
||||
elif item.name in bombs:
|
||||
starting_bombs += bombs[item.name]
|
||||
elif item.name in arrows:
|
||||
if world.retro[player]:
|
||||
if world.bow_mode[player].startswith('retro'):
|
||||
equip[0x38E] |= 0x80
|
||||
starting_arrows = 1
|
||||
else:
|
||||
|
||||
+349
-109
@@ -1,4 +1,4 @@
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
import logging
|
||||
import math
|
||||
import RaceRandom as random
|
||||
@@ -6,7 +6,7 @@ import RaceRandom as random
|
||||
from BaseClasses import LocationType, 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, fast_fill, get_dungeon_item_pool
|
||||
from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys
|
||||
from PotShuffle import vanilla_pots
|
||||
from Tables import bonk_prize_lookup
|
||||
from Items import ItemFactory
|
||||
@@ -279,13 +279,17 @@ def generate_itempool(world, player):
|
||||
world.get_location('Zelda Drop Off', player).locked = True
|
||||
|
||||
# set up item pool
|
||||
if world.custom and player in world.customitemarray:
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.customitemarray[player])
|
||||
skip_pool_adjustments = False
|
||||
if world.customizer and world.customizer.get_item_pool():
|
||||
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = make_customizer_pool(world, player)
|
||||
skip_pool_adjustments = True
|
||||
elif world.custom and player in world.customitemarray:
|
||||
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.customitemarray[player])
|
||||
world.rupoor_cost = min(world.customitemarray[player]["rupoorcost"], 9999)
|
||||
else:
|
||||
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.is_tile_swapped(0x18, player))
|
||||
(pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) = get_pool_core(world, player, world.progressive, world.shuffle[player], world.difficulty[player], world.treasure_hunt_total[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.bombbag[player], world.doorShuffle[player], world.logic[player], world.flute_mode[player] == 'active' or world.is_tile_swapped(0x18, player))
|
||||
|
||||
if player in world.pool_adjustment.keys():
|
||||
if player in world.pool_adjustment.keys() and not skip_pool_adjustments:
|
||||
amt = world.pool_adjustment[player]
|
||||
if amt < 0:
|
||||
trash_options = [x for x in pool if x in trash_items]
|
||||
@@ -311,7 +315,8 @@ def generate_itempool(world, player):
|
||||
if not found_sword and world.swords[player] != 'swordless':
|
||||
found_sword = True
|
||||
possible_weapons.append(item)
|
||||
if item in ['Progressive Bow', 'Bow'] and not found_bow and not world.retro[player]:
|
||||
if (item in ['Progressive Bow', 'Bow'] and not found_bow
|
||||
and not world.bow_mode[player].startswith('retro')):
|
||||
found_bow = True
|
||||
possible_weapons.append(item)
|
||||
if item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
||||
@@ -331,7 +336,7 @@ def generate_itempool(world, player):
|
||||
world.get_location(location, player).event = True
|
||||
world.get_location(location, player).locked = True
|
||||
|
||||
if world.shopsanity[player]:
|
||||
if world.shopsanity[player] and not skip_pool_adjustments:
|
||||
for shop in world.shops[player]:
|
||||
if shop.region.name in shop_to_location_table:
|
||||
for index, slot in enumerate(shop.inventory):
|
||||
@@ -365,7 +370,7 @@ def generate_itempool(world, player):
|
||||
world.treasure_hunt_icon[player] = 'Triforce Piece'
|
||||
|
||||
world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player
|
||||
and ((item.smallkey and world.keyshuffle[player])
|
||||
and ((item.smallkey and world.keyshuffle[player] != 'none')
|
||||
or (item.bigkey and world.bigkeyshuffle[player])
|
||||
or (item.map and world.mapshuffle[player])
|
||||
or (item.compass and world.compassshuffle[player]))])
|
||||
@@ -374,7 +379,9 @@ def generate_itempool(world, player):
|
||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartcontainer"] == 0):
|
||||
next(item for item in items if item.name == 'Boss Heart Container').advancement = True
|
||||
container = next((item for item in items if item.name == 'Boss Heart Container'), None)
|
||||
if container:
|
||||
container.advancement = True
|
||||
elif world.difficulty[player] in ['expert'] and not (world.custom and player in world.customitemarray and world.customitemarray[player]["heartpiece"] < 4):
|
||||
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
|
||||
for i in range(4):
|
||||
@@ -391,11 +398,33 @@ def generate_itempool(world, player):
|
||||
return item if not choice else ItemFactory("Bee Trap", player) if choice == 'trap' else ItemFactory("Bee", player)
|
||||
return item
|
||||
|
||||
world.itempool += [beemizer(item) for item in items]
|
||||
if not skip_pool_adjustments:
|
||||
world.itempool += [beemizer(item) for item in items]
|
||||
else:
|
||||
world.itempool += items
|
||||
|
||||
# shuffle medallions
|
||||
mm_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
mm_medallion, tr_medallion = None, None
|
||||
if world.customizer and world.customizer.get_medallions() and player in world.customizer.get_medallions():
|
||||
medal_map = world.customizer.get_medallions()
|
||||
if player in medal_map:
|
||||
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:
|
||||
tr_medallion = ['Ether', 'Quake', 'Bombos'][random.randint(0, 2)]
|
||||
world.required_medallions[player] = (mm_medallion, tr_medallion)
|
||||
|
||||
# shuffle bottle refills
|
||||
@@ -409,16 +438,17 @@ def generate_itempool(world, player):
|
||||
|
||||
set_up_shops(world, player)
|
||||
|
||||
if world.retro[player]:
|
||||
set_up_take_anys(world, player)
|
||||
if world.dropshuffle[player]:
|
||||
if world.take_any[player] != 'none':
|
||||
set_up_take_anys(world, player, skip_pool_adjustments)
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
if world.dropshuffle[player] and not skip_pool_adjustments:
|
||||
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 13
|
||||
if world.pottery[player] not in ['none', 'cave']:
|
||||
if world.pottery[player] not in ['none', 'cave'] and not skip_pool_adjustments:
|
||||
world.itempool += [ItemFactory('Small Key (Universal)', player)] * 19
|
||||
|
||||
create_dynamic_shop_locations(world, player)
|
||||
|
||||
if world.pottery[player] not in ['none', 'keys']:
|
||||
if world.pottery[player] not in ['none', 'keys'] and not skip_pool_adjustments:
|
||||
add_pot_contents(world, player)
|
||||
|
||||
if world.shuffle_bonk_drops[player]:
|
||||
@@ -436,8 +466,12 @@ take_any_locations = [
|
||||
'Palace of Darkness Hint', 'East Dark World Hint', 'Archery Game', 'Dark Lake Hylia Ledge Hint',
|
||||
'Dark Lake Hylia Ledge Spike Cave', 'Fortune Teller (Dark)', 'Dark Sanctuary Hint', 'Dark Desert Hint']
|
||||
|
||||
fixed_take_anys = [
|
||||
'Desert Healer Fairy', 'Swamp Healer Fairy', 'Dark Death Mountain Healer Fairy',
|
||||
'Dark Lake Hylia Ledge Healer Fairy', 'Bonk Fairy (Dark)']
|
||||
|
||||
def set_up_take_anys(world, player):
|
||||
|
||||
def set_up_take_anys(world, player, skip_adjustments=False):
|
||||
if world.mode[player] == 'inverted':
|
||||
if 'Dark Sanctuary Hint' in take_any_locations:
|
||||
take_any_locations.remove('Dark Sanctuary Hint')
|
||||
@@ -445,9 +479,12 @@ def set_up_take_anys(world, player):
|
||||
if 'Archery Game' in take_any_locations:
|
||||
take_any_locations.remove('Archery Game')
|
||||
|
||||
take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0]
|
||||
|
||||
regions = random.sample(take_any_candidates, 5)
|
||||
if world.take_any[player] == 'random':
|
||||
take_any_candidates = [x for x in take_any_locations if len(world.get_region(x, player).locations) == 0]
|
||||
regions = random.sample(take_any_candidates, 5)
|
||||
elif world.take_any[player] == 'fixed':
|
||||
regions = list(fixed_take_anys)
|
||||
random.shuffle(regions)
|
||||
|
||||
old_man_take_any = Region("Old Man Sword Cave", RegionType.Cave, 'the sword cave', player)
|
||||
world.regions.append(old_man_take_any)
|
||||
@@ -462,12 +499,13 @@ def set_up_take_anys(world, player):
|
||||
|
||||
sword = next((item for item in world.itempool if item.type == 'Sword' and item.player == player), None)
|
||||
if sword:
|
||||
world.itempool.append(ItemFactory('Rupees (20)', player))
|
||||
if not world.shopsanity[player]:
|
||||
world.itempool.remove(sword)
|
||||
if not skip_adjustments:
|
||||
world.itempool.append(ItemFactory('Rupees (20)', player))
|
||||
if not world.shopsanity[player]:
|
||||
world.itempool.remove(sword)
|
||||
old_man_take_any.shop.add_inventory(0, sword.name, 0, 0, create_location=True)
|
||||
else:
|
||||
if world.shopsanity[player]:
|
||||
if world.shopsanity[player] and not skip_adjustments:
|
||||
world.itempool.append(ItemFactory('Rupees (300)', player))
|
||||
old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0, create_location=world.shopsanity[player])
|
||||
|
||||
@@ -485,7 +523,7 @@ def set_up_take_anys(world, player):
|
||||
world.shops[player].append(take_any.shop)
|
||||
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0, create_location=world.shopsanity[player])
|
||||
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=world.shopsanity[player])
|
||||
if world.shopsanity[player]:
|
||||
if world.shopsanity[player] and not skip_adjustments:
|
||||
world.itempool.append(ItemFactory('Blue Potion', player))
|
||||
world.itempool.append(ItemFactory('Boss Heart Container', player))
|
||||
|
||||
@@ -668,38 +706,47 @@ def fill_prizes(world, attempts=15):
|
||||
|
||||
|
||||
def set_up_shops(world, player):
|
||||
if world.retro[player]:
|
||||
retro_bow = world.bow_mode[player].startswith('retro')
|
||||
universal_keys = world.keyshuffle[player] == 'universal'
|
||||
if retro_bow or universal_keys:
|
||||
if world.shopsanity[player]:
|
||||
removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)]
|
||||
red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5]
|
||||
shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player]
|
||||
removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player])
|
||||
removals.extend(red_pots)
|
||||
removals.extend(random.sample(shields_n_hearts, 5))
|
||||
removals = []
|
||||
if retro_bow:
|
||||
removals = [next(item for item in world.itempool if item.name == 'Arrows (10)' and item.player == player)]
|
||||
removals.extend([item for item in world.itempool if item.name == 'Arrow Upgrade (+5)' and item.player == player])
|
||||
shields_n_hearts = [item for item in world.itempool if item.name in ['Blue Shield', 'Small Heart'] and item.player == player]
|
||||
removals.extend(random.sample(shields_n_hearts, 5))
|
||||
if universal_keys:
|
||||
red_pots = [item for item in world.itempool if item.name == 'Red Potion' and item.player == player][:5]
|
||||
removals.extend(red_pots)
|
||||
for remove in removals:
|
||||
world.itempool.remove(remove)
|
||||
for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield
|
||||
arrow_item = ItemFactory('Single Arrow', player)
|
||||
arrow_item.advancement = True
|
||||
world.itempool.append(arrow_item)
|
||||
for i in range(5): # replace the red potions
|
||||
world.itempool.append(ItemFactory('Small Key (Universal)', player))
|
||||
world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade
|
||||
if retro_bow:
|
||||
for i in range(6): # replace the Arrows (10) and randomly selected hearts/blue shield
|
||||
arrow_item = ItemFactory('Single Arrow', player)
|
||||
arrow_item.advancement = True
|
||||
world.itempool.append(arrow_item)
|
||||
world.itempool.append(ItemFactory('Rupees (50)', player)) # replaces the arrow upgrade
|
||||
if universal_keys:
|
||||
for i in range(5): # replace the red potions
|
||||
world.itempool.append(ItemFactory('Small Key (Universal)', player))
|
||||
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
||||
else:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
if not rss.locked:
|
||||
rss.custom = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
if retro_bow:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
if not rss.locked:
|
||||
rss.custom = True
|
||||
rss.add_inventory(2, 'Single Arrow', 80)
|
||||
rss.locked = True
|
||||
cap_shop = world.get_region('Capacity Upgrade', player).shop
|
||||
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
|
||||
for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5):
|
||||
shop.custom = True
|
||||
shop.locked = True
|
||||
shop.add_inventory(0, 'Single Arrow', 80)
|
||||
shop.add_inventory(1, 'Small Key (Universal)', 100)
|
||||
shop.add_inventory(2, 'Bombs (10)', 50)
|
||||
rss.locked = True
|
||||
cap_shop = world.get_region('Capacity Upgrade', player).shop
|
||||
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
|
||||
if retro_bow:
|
||||
shop.add_inventory(0, 'Single Arrow', 80)
|
||||
if universal_keys:
|
||||
shop.add_inventory(1, 'Small Key (Universal)', 100)
|
||||
if world.bombbag[player]:
|
||||
if world.shopsanity[player]:
|
||||
removals = [item for item in world.itempool if item.name == 'Bomb Upgrade (+5)' and item.player == player]
|
||||
@@ -712,10 +759,11 @@ def set_up_shops(world, player):
|
||||
|
||||
|
||||
def customize_shops(world, player):
|
||||
found_bomb_upgrade, found_arrow_upgrade = False, world.retro[player]
|
||||
retro_bow = world.bow_mode[player].startswith('retro')
|
||||
found_bomb_upgrade, found_arrow_upgrade = False, retro_bow
|
||||
possible_replacements = []
|
||||
shops_to_customize = shop_to_location_table.copy()
|
||||
if world.retro[player]:
|
||||
if world.take_any[player] != 'none':
|
||||
shops_to_customize.update(retro_shops)
|
||||
for shop_name, loc_list in shops_to_customize.items():
|
||||
shop = world.get_region(shop_name, player).shop
|
||||
@@ -738,7 +786,7 @@ def customize_shops(world, player):
|
||||
price = 0
|
||||
else:
|
||||
price = 120 if shop_name == 'Potion Shop' and item.name == 'Red Potion' else item.price
|
||||
if world.retro[player] and item.name == 'Single Arrow':
|
||||
if retro_bow and item.name == 'Single Arrow':
|
||||
price = 80
|
||||
# randomize price
|
||||
shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player)
|
||||
@@ -765,7 +813,7 @@ def customize_shops(world, player):
|
||||
if not found_arrow_upgrade and len(possible_replacements) > 0:
|
||||
choices = []
|
||||
for shop, idx, loc, item in possible_replacements:
|
||||
if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not world.retro[player]):
|
||||
if item.name == 'Arrows (10)' or (item.name == 'Single Arrow' and not retro_bow):
|
||||
choices.append((shop, idx, loc, item))
|
||||
if len(choices) > 0:
|
||||
shop, idx, loc, item = random.choice(choices)
|
||||
@@ -909,11 +957,12 @@ rupee_chart = {'Rupee (1)': 1, 'Rupees (5)': 5, 'Rupees (20)': 20, 'Rupees (50)'
|
||||
|
||||
|
||||
def add_pot_contents(world, player):
|
||||
retro_bow = world.bow_mode[player].startswith('retro')
|
||||
for super_tile, pot_list in vanilla_pots.items():
|
||||
for pot in pot_list:
|
||||
if pot.item not in [PotItem.Hole, PotItem.Key, PotItem.Switch]:
|
||||
if valid_pot_location(pot, world.pot_pool[player], world, player):
|
||||
item = ('Rupees (5)' if world.retro[player] and pot_items[pot.item] == 'Arrows (5)'
|
||||
item = ('Rupees (5)' if retro_bow and pot_items[pot.item] == 'Arrows (5)'
|
||||
else pot_items[pot.item])
|
||||
world.itempool.append(ItemFactory(item, player))
|
||||
|
||||
@@ -929,7 +978,8 @@ def add_bonkdrop_contents(world, player):
|
||||
count -= 1
|
||||
|
||||
|
||||
def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer, goal, mode, swords, retro, 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 = []
|
||||
@@ -965,12 +1015,8 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
||||
|
||||
lamps_needed_for_dark_rooms = 1
|
||||
|
||||
# insanity shuffle doesn't have fake LW/DW logic so for now guaranteed Mirror and Moon Pearl at the start
|
||||
if shuffle == 'insanity_legacy':
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
else:
|
||||
pool.extend(['Magic Mirror', 'Moon Pearl'])
|
||||
# old insanity shuffle didn't have fake LW/DW logic so this used to be conditional
|
||||
pool.extend(['Magic Mirror', 'Moon Pearl'])
|
||||
|
||||
if timer == 'display':
|
||||
clock_mode = 'stopwatch'
|
||||
@@ -1004,7 +1050,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
||||
else:
|
||||
pool.extend(diff.basicarmor)
|
||||
|
||||
if want_progressives():
|
||||
if 'silvers' not in world.bow_mode[player]:
|
||||
pool.extend(['Progressive Bow'] * 2)
|
||||
elif swords != 'swordless':
|
||||
pool.extend(diff.basicbow)
|
||||
@@ -1052,13 +1098,14 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
||||
|
||||
if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
|
||||
place_item('Master Sword Pedestal', 'Triforce')
|
||||
if retro:
|
||||
pool = [item.replace('Single Arrow','Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrows (10)','Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrow Upgrade (+5)','Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrow Upgrade (+10)','Rupees (5)') for item in pool]
|
||||
if world.bow_mode[player].startswith('retro'):
|
||||
pool = [item.replace('Single Arrow', 'Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrows (10)', 'Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrow Upgrade (+5)', 'Rupees (5)') for item in pool]
|
||||
pool = [item.replace('Arrow Upgrade (+10)', 'Rupees (5)') for item in pool]
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
pool.extend(diff.retro)
|
||||
if door_shuffle != 'vanilla': # door shuffle needs more keys for retro
|
||||
if door_shuffle != 'vanilla': # door shuffle needs more keys for universal keys
|
||||
replace = 'Rupees (20)' if difficulty == 'normal' else 'Rupees (5)'
|
||||
indices = [i for i, x in enumerate(pool) if x == replace]
|
||||
for i in range(0, min(10, len(indices))):
|
||||
@@ -1074,7 +1121,7 @@ def get_pool_core(progressive, shuffle, difficulty, treasure_hunt_total, timer,
|
||||
return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms)
|
||||
|
||||
|
||||
def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag, customitemarray):
|
||||
def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer, goal, mode, swords, bombbag, customitemarray):
|
||||
pool = []
|
||||
placed_items = {}
|
||||
precollected_items = []
|
||||
@@ -1155,7 +1202,7 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
||||
itemtotal = itemtotal + 1
|
||||
|
||||
if mode == 'standard':
|
||||
if retro:
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
key_location = random.choice(['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||
place_item(key_location, 'Small Key (Universal)')
|
||||
pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0))
|
||||
@@ -1166,17 +1213,10 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
|
||||
|
||||
pool.extend(['Fighter Sword'] * customitemarray["sword1"])
|
||||
pool.extend(['Progressive Sword'] * customitemarray["progressivesword"])
|
||||
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
|
||||
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
|
||||
|
||||
if shuffle == 'insanity_legacy':
|
||||
place_item('Link\'s House', 'Magic Mirror')
|
||||
place_item('Sanctuary', 'Moon Pearl')
|
||||
pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0))
|
||||
pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0))
|
||||
else:
|
||||
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
|
||||
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
|
||||
|
||||
if retro:
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
|
||||
if itemtotal < total_items_to_place:
|
||||
nothings = total_items_to_place - itemtotal
|
||||
@@ -1208,12 +1248,62 @@ def make_customizer_pool(world, player):
|
||||
assert loc not in placed_items
|
||||
placed_items[loc] = item
|
||||
|
||||
dungeon_locations, dungeon_count = defaultdict(set), defaultdict(int)
|
||||
for l in world.get_unfilled_locations(player):
|
||||
if l.parent_region.dungeon:
|
||||
dungeon = l.parent_region.dungeon
|
||||
dungeon_locations[dungeon.name].add(l)
|
||||
if dungeon.name not in dungeon_count:
|
||||
d_count = 1 if dungeon.big_key else 0
|
||||
d_count += len(dungeon.small_keys) + len(dungeon.dungeon_items)
|
||||
dungeon_count[dungeon.name] = d_count
|
||||
|
||||
diff = difficulties[world.difficulty[player]]
|
||||
for item_name, amount in world.customizer.get_item_pool()[player].items():
|
||||
if isinstance(amount, int):
|
||||
if item_name == 'Bottle (Random)':
|
||||
for _ in range(amount):
|
||||
pool.append(random.choice(diff.bottles))
|
||||
elif item_name.startswith('Small Key') and item_name != 'Small Key (Universal)':
|
||||
d_item = ItemFactory(item_name, player)
|
||||
if world.keyshuffle[player] == 'none':
|
||||
d_name = d_item.dungeon
|
||||
dungeon = world.get_dungeon(d_name, player)
|
||||
target_amount = max(amount, len(dungeon.small_keys))
|
||||
additional_amount = target_amount - len(dungeon.small_keys)
|
||||
possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name])
|
||||
if possible_fit > 0:
|
||||
dungeon_count[d_name] += possible_fit
|
||||
dungeon.small_keys.extend([d_item] * amount)
|
||||
additional_amount -= possible_fit
|
||||
if additional_amount > 0:
|
||||
pool.extend([item_name] * amount)
|
||||
else:
|
||||
dungeon = world.get_dungeon(d_item.dungeon, player)
|
||||
target_amount = max(amount, len(dungeon.small_keys))
|
||||
additional_amount = target_amount - len(dungeon.small_keys)
|
||||
dungeon.small_keys.extend([d_item] * additional_amount)
|
||||
elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'):
|
||||
d_item = ItemFactory(item_name, player)
|
||||
if ((d_item.bigkey and not world.bigkeyshuffle[player])
|
||||
or (d_item.compass and not world.compassshuffle[player])
|
||||
or (d_item.map and not world.mapshuffle[player])):
|
||||
d_name = d_item.dungeon
|
||||
dungeon = world.get_dungeon(d_name, player)
|
||||
current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0
|
||||
additional_amount = amount - current_amount
|
||||
possible_fit = min(additional_amount, len(dungeon_locations[d_name])-dungeon_count[d_name])
|
||||
if possible_fit > 0:
|
||||
dungeon_count[d_name] += possible_fit
|
||||
dungeon.dungeon_items.extend([d_item] * amount)
|
||||
additional_amount -= possible_fit
|
||||
if additional_amount > 0:
|
||||
pool.extend([item_name] * amount)
|
||||
else:
|
||||
dungeon = world.get_dungeon(d_item.dungeon, player)
|
||||
current_amount = 1 if d_item == dungeon.big_key or d_item in dungeon.dungeon_items else 0
|
||||
additional_amount = amount - current_amount
|
||||
dungeon.dungeon_items.extend([d_item] * additional_amount)
|
||||
else:
|
||||
pool.extend([item_name] * amount)
|
||||
|
||||
@@ -1225,12 +1315,73 @@ def make_customizer_pool(world, player):
|
||||
elif timer == 'ohko':
|
||||
clock_mode = 'ohko'
|
||||
|
||||
if world.goal[player] == 'pedestal':
|
||||
if world.goal[player] in ['pedestal', 'trinity']:
|
||||
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:
|
||||
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):
|
||||
missing_items.append('Progressive Glove')
|
||||
|
||||
if world.bombbag[player]:
|
||||
if 'Bomb Upgrade (+10)' not in pool:
|
||||
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):
|
||||
missing_items.append('Progressive Sword')
|
||||
|
||||
bow_found = next((i for i in pool if i in {'Bow', 'Progressive Bow'}), None)
|
||||
if not bow_found:
|
||||
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])
|
||||
if t != 0:
|
||||
pieces = sum(1 for i in pool if i == 'Triforce Piece')
|
||||
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))
|
||||
|
||||
return pool, placed_items, precollected_items, clock_mode, 1
|
||||
|
||||
|
||||
filler_items = {
|
||||
'Arrows (10)': 12,
|
||||
'Bombs (3)': 16,
|
||||
'Rupees (300)': 5,
|
||||
'Rupees (100)': 1,
|
||||
'Rupees (50)': 7,
|
||||
'Rupees (20)': 28,
|
||||
'Rupees (5)': 4,
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
# 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
|
||||
@@ -1246,31 +1397,120 @@ def set_default_triforce(goal, custom_goal, custom_total):
|
||||
|
||||
|
||||
# A quick test to ensure all combinations generate the correct amount of items.
|
||||
def test():
|
||||
for difficulty in ['normal', 'hard', 'expert']:
|
||||
for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']:
|
||||
for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
|
||||
for mode in ['open', 'standard', 'inverted', 'retro']:
|
||||
for swords in ['random', 'assured', 'swordless', 'vanilla']:
|
||||
for progressive in ['on', 'off']:
|
||||
for shuffle in ['vanilla', 'full', 'crossed', 'insanity']:
|
||||
for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
|
||||
for retro in [True, False]:
|
||||
for door_shuffle in ['basic', 'crossed', 'vanilla']:
|
||||
for bombbag in [True, False]:
|
||||
out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False)
|
||||
count = len(out[0]) + len(out[1])
|
||||
# def test():
|
||||
# for difficulty in ['normal', 'hard', 'expert']:
|
||||
# for goal in ['ganon', 'triforcehunt', 'pedestal', 'trinity']:
|
||||
# for timer in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown']:
|
||||
# for mode in ['open', 'standard', 'inverted', 'retro']:
|
||||
# for swords in ['random', 'assured', 'swordless', 'vanilla']:
|
||||
# for progressive in ['on', 'off']:
|
||||
# for shuffle in ['vanilla', 'full', 'crossed', 'insanity']:
|
||||
# for logic in ['noglitches', 'minorglitches', 'owglitches', 'nologic']:
|
||||
# for retro in [True, False]:
|
||||
# for door_shuffle in ['basic', 'crossed', 'vanilla']:
|
||||
# for bombbag in [True, False]:
|
||||
# out = get_pool_core(progressive, shuffle, difficulty, 30, timer, goal, mode, swords, retro, bombbag, door_shuffle, logic, False)
|
||||
# count = len(out[0]) + len(out[1])
|
||||
|
||||
correct_count = total_items_to_place
|
||||
if goal in ['pedestal', 'trinity'] and swords != 'vanilla':
|
||||
# pedestal goals generate one extra item
|
||||
correct_count += 1
|
||||
if retro:
|
||||
correct_count += 28
|
||||
try:
|
||||
assert count == correct_count, "expected {0} items but found {1} items for {2}".format(correct_count, count, (progressive, shuffle, difficulty, timer, goal, mode, swords, retro, bombbag))
|
||||
except AssertionError as e:
|
||||
print(e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
def fill_specific_items(world):
|
||||
if world.customizer:
|
||||
placements = world.customizer.get_placements()
|
||||
dungeon_pool = get_dungeon_item_pool(world)
|
||||
prize_pool = []
|
||||
prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2',
|
||||
'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'}
|
||||
for p in range(1, world.players + 1):
|
||||
prize_pool.extend(prize_set)
|
||||
if placements:
|
||||
for player, placement_list in placements.items():
|
||||
for location, item in placement_list.items():
|
||||
loc = world.get_location(location, player)
|
||||
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
|
||||
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']
|
||||
|
||||
|
||||
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):
|
||||
return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none')
|
||||
or (item.startswith('Big Key') and not world.bigkeyshuffle[player])
|
||||
or (item.startswith('Compass') and not world.compassshuffle[player])
|
||||
or (item.startswith('Map') and not world.mapshuffle[player]))
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ def ItemFactory(items, player):
|
||||
advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text = item_table[item]
|
||||
ret.append(Item(item, advancement, priority, type, code, price, pedestal_hint, pedestal_credit, sickkid_credit, zora_credit, witch_credit, fluteboy_credit, hint_text, player))
|
||||
else:
|
||||
logging.getLogger('').warning('Unknown Item: %s', item)
|
||||
return None
|
||||
raise RuntimeError(f'Unknown Item: {item}')
|
||||
|
||||
if singleton:
|
||||
return ret[0]
|
||||
|
||||
+72
-20
@@ -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)
|
||||
|
||||
@@ -223,13 +224,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
|
||||
|
||||
|
||||
@@ -251,7 +253,7 @@ def find_all_locations(sector):
|
||||
|
||||
|
||||
def calc_max_chests(builder, key_layout, world, player):
|
||||
if world.doorShuffle[player] != 'crossed':
|
||||
if world.doorShuffle[player] in ['basic', 'vanilla']:
|
||||
return len(world.get_dungeon(key_layout.sector.name, player).small_keys)
|
||||
return max(0, builder.key_doors_num - key_layout.max_drops)
|
||||
|
||||
@@ -275,7 +277,7 @@ def analyze_dungeon(key_layout, world, player):
|
||||
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
|
||||
key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations))
|
||||
key_logic.prize_location = dungeon_table[key_layout.sector.name].prize
|
||||
if world.retro[player] and world.mode[player] != 'standard':
|
||||
if world.keyshuffle[player] == 'universal' and world.mode[player] != 'standard':
|
||||
return
|
||||
|
||||
original_key_counter = find_counter({}, False, key_layout, False)
|
||||
@@ -301,10 +303,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():
|
||||
@@ -926,7 +928,7 @@ def self_lock_possible(counter):
|
||||
|
||||
|
||||
def available_chest_small_keys(key_counter, world, player):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
if world.keyshuffle[player] == 'none':
|
||||
cnt = 0
|
||||
for loc in key_counter.free_locations:
|
||||
if key_counter.big_key_opened or '- Big Chest' not in loc.name:
|
||||
@@ -937,7 +939,7 @@ def available_chest_small_keys(key_counter, world, player):
|
||||
|
||||
|
||||
def available_chest_small_keys_logic(key_counter, world, player, sm_restricted):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
if world.keyshuffle[player] == 'none':
|
||||
cnt = 0
|
||||
for loc in key_counter.free_locations:
|
||||
if loc not in sm_restricted and (key_counter.big_key_opened or '- Big Chest' not in loc.name):
|
||||
@@ -1172,6 +1174,16 @@ def expand_key_state(state, flat_proposal, world, player):
|
||||
state.add_all_doors_check_keys(connect_region, flat_proposal, world, player)
|
||||
|
||||
|
||||
def expand_big_key_state(state, flat_proposal, world, player):
|
||||
while len(state.avail_doors) > 0:
|
||||
exp_door = state.next_avail_door()
|
||||
door = exp_door.door
|
||||
connect_region = world.get_entrance(door.name, player).connected_region
|
||||
if state.validate(door, connect_region, world, player):
|
||||
state.visit_region(connect_region, key_checks=True)
|
||||
state.add_all_doors_check_big_keys(connect_region, flat_proposal, world, player)
|
||||
|
||||
|
||||
def flatten_pair_list(paired_list):
|
||||
flat_list = []
|
||||
for d in paired_list:
|
||||
@@ -1401,13 +1413,51 @@ def prize_relevance(key_layout, dungeon_entrance, is_atgt_swapped):
|
||||
return None
|
||||
|
||||
|
||||
def prize_relevance_sig2(start_regions, d_name, dungeon_entrance, world, player):
|
||||
if len(start_regions) > 1 and dungeon_entrance and dungeon_table[d_name].prize:
|
||||
if dungeon_entrance.name == ('Agahmins Tower' if world.is_atgt_swapped(player) else 'Ganons Tower'):
|
||||
return 'GT'
|
||||
elif dungeon_entrance.name == 'Pyramid Fairy':
|
||||
return 'BigBomb'
|
||||
return None
|
||||
|
||||
|
||||
def validate_bk_layout(proposal, builder, start_regions, world, player):
|
||||
bk_special = check_bk_special(builder.master_sector.regions, world, player)
|
||||
if world.bigkeyshuffle[player] and (world.dropshuffle[player] or not bk_special):
|
||||
return True
|
||||
flat_proposal = flatten_pair_list(proposal)
|
||||
state = ExplorationState(dungeon=builder.name)
|
||||
state.big_key_special = bk_special
|
||||
for region in start_regions:
|
||||
dungeon_entrance, portal_door = find_outside_connection(region)
|
||||
prize_relevant_flag = prize_relevance_sig2(start_regions, builder.name, dungeon_entrance, world, player)
|
||||
if prize_relevant_flag:
|
||||
state.append_door_to_list(portal_door, state.prize_doors)
|
||||
state.prize_door_set[portal_door] = dungeon_entrance
|
||||
# key_layout.prize_relevant = prize_relevant_flag
|
||||
else:
|
||||
state.visit_region(region, key_checks=True)
|
||||
state.add_all_doors_check_big_keys(region, flat_proposal, world, player)
|
||||
expand_big_key_state(state, flat_proposal, world, player)
|
||||
if bk_special:
|
||||
for loc in state.found_locations:
|
||||
if loc.forced_big_key():
|
||||
return True
|
||||
else:
|
||||
return len(state.bk_found) > 0
|
||||
return False
|
||||
|
||||
|
||||
# Soft lock stuff
|
||||
def validate_key_layout(key_layout, world, player):
|
||||
# retro is all good - except for hyrule castle in standard mode
|
||||
if (world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')) or world.logic[player] == 'nologic':
|
||||
# universal key is all good - except for hyrule castle in standard mode
|
||||
if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
|
||||
(world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
|
||||
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:
|
||||
@@ -1442,7 +1492,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
|
||||
# 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
|
||||
@@ -1532,8 +1583,8 @@ def enough_small_locations(state, avail_small_loc):
|
||||
|
||||
|
||||
def determine_prize_lock(key_layout, world, player):
|
||||
if ((world.retro[player] and (world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle'))
|
||||
or world.logic[player] == 'nologic'):
|
||||
if world.logic[player] == 'nologic' or (world.keyshuffle[player] == 'universal' and
|
||||
(world.mode[player] != 'standard' or key_layout.sector.name != 'Hyrule Castle')):
|
||||
return # done, doesn't matter what
|
||||
flat_proposal = key_layout.flat_prop
|
||||
state = ExplorationState(dungeon=key_layout.sector.name)
|
||||
@@ -1565,7 +1616,7 @@ def determine_prize_lock(key_layout, world, player):
|
||||
|
||||
|
||||
def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
if world.keyshuffle[player] == 'none':
|
||||
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)
|
||||
return max(0, avail_chest_keys + key_only - state.used_smalls)
|
||||
@@ -1573,7 +1624,7 @@ def cnt_avail_small_locations(free_locations, key_only, state, world, player):
|
||||
|
||||
|
||||
def cnt_avail_small_locations_by_ctr(free_locations, counter, layout, world, player):
|
||||
if not world.keyshuffle[player] and not world.retro[player]:
|
||||
if world.keyshuffle[player] == 'none':
|
||||
bk_adj = 1 if counter.big_key_opened and not layout.big_key_special else 0
|
||||
avail_chest_keys = min(free_locations - bk_adj, layout.max_chests)
|
||||
return max(0, avail_chest_keys + len(counter.key_only_locations) - counter.used_keys)
|
||||
@@ -1599,12 +1650,13 @@ 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
|
||||
else:
|
||||
builder = world.dungeon_layouts[player][key_layout.sector.name]
|
||||
state.key_locations = builder.total_keys - builder.key_drop_cnt
|
||||
state.key_locations = max(0, builder.total_keys - builder.key_drop_cnt)
|
||||
state.big_key_special = False
|
||||
for region in key_layout.sector.regions:
|
||||
for location in region.locations:
|
||||
@@ -1865,7 +1917,7 @@ def val_hyrule(key_logic, world, player):
|
||||
val_rule(key_logic.door_rules['Hyrule Dungeon Map Room Key Door S'], 1)
|
||||
val_rule(key_logic.door_rules['Hyrule Dungeon Armory Interior Key Door N'], 2)
|
||||
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 3)
|
||||
val_rule(key_logic.door_rules['Sewers Key Rat Key Door N'], 4)
|
||||
val_rule(key_logic.door_rules['Sewers Key Rat NE'], 4)
|
||||
else:
|
||||
val_rule(key_logic.door_rules['Sewers Secret Room Key Door S'], 2)
|
||||
val_rule(key_logic.door_rules['Sewers Dark Cross Key Door N'], 2)
|
||||
@@ -2016,14 +2068,14 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
|
||||
|
||||
# Soft lock stuff
|
||||
def validate_key_placement(key_layout, world, player):
|
||||
if world.retro[player] or world.accessibility[player] == 'none':
|
||||
if world.keyshuffle[player] == 'universal' or world.accessibility[player] == 'none':
|
||||
return True # Can't keylock in retro. Expected if beatable only.
|
||||
max_counter = find_max_counter(key_layout)
|
||||
keys_outside = 0
|
||||
big_key_outside = False
|
||||
smallkey_name = dungeon_keys[key_layout.sector.name]
|
||||
bigkey_name = dungeon_bigs[key_layout.sector.name]
|
||||
if world.keyshuffle[player]:
|
||||
if world.keyshuffle[player] != 'none':
|
||||
keys_outside = key_layout.max_chests - sum(1 for i in max_counter.free_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player)
|
||||
if world.bigkeyshuffle[player]:
|
||||
max_counter = find_max_counter(key_layout)
|
||||
|
||||
@@ -25,14 +25,17 @@ 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_pots
|
||||
from Fill import dungeon_tracking
|
||||
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, create_farm_locations
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
from source.item.FillUtil import create_item_pool_config, massage_item_pool, district_item_pool_config
|
||||
from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
|
||||
__version__ = '1.0.1.3-u'
|
||||
__version__ = '1.2.0.0-u'
|
||||
|
||||
from source.classes.BabelFish import BabelFish
|
||||
|
||||
@@ -58,32 +61,44 @@ def main(args, seed=None, fish=None):
|
||||
|
||||
if args.securerandom:
|
||||
random.use_secure()
|
||||
|
||||
seeded = False
|
||||
# initialize the world
|
||||
if args.code:
|
||||
for player, code in args.code.items():
|
||||
if code:
|
||||
Settings.adjust_args_from_code(code, player, args)
|
||||
customized = None
|
||||
if args.customizer:
|
||||
customized = CustomSettings()
|
||||
customized.load_yaml(args.customizer)
|
||||
seed = customized.determine_seed(seed)
|
||||
seeded = True
|
||||
customized.adjust_args(args)
|
||||
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
|
||||
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
|
||||
args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints)
|
||||
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints)
|
||||
world.customizer = customized if customized else None
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
world.seed = random.randint(0, 999999999)
|
||||
else:
|
||||
world.seed = int(seed)
|
||||
random.seed(world.seed)
|
||||
if not seeded:
|
||||
random.seed(world.seed)
|
||||
|
||||
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()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.bombbag = args.bombbag.copy()
|
||||
world.flute_mode = args.flute_mode.copy()
|
||||
world.bow_mode = args.bow_mode.copy()
|
||||
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
||||
@@ -100,6 +115,8 @@ def main(args, seed=None, fish=None):
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
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.decoupledoors = args.decoupledoors.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.fish = fish
|
||||
@@ -112,13 +129,16 @@ def main(args, seed=None, fish=None):
|
||||
world.treasure_hunt_count = {k: int(v) for k, v in args.triforce_goal.items()}
|
||||
world.treasure_hunt_total = {k: int(v) for k, v in args.triforce_pool.items()}
|
||||
world.shufflelinks = args.shufflelinks.copy()
|
||||
world.shuffletavern = args.shuffletavern.copy()
|
||||
world.pseudoboots = args.pseudoboots.copy()
|
||||
world.overworld_map = args.overworld_map.copy()
|
||||
world.take_any = args.take_any.copy()
|
||||
world.restrict_boss_items = args.restrict_boss_items.copy()
|
||||
world.collection_rate = args.collection_rate.copy()
|
||||
world.colorizepots = args.colorizepots.copy()
|
||||
|
||||
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
|
||||
world.finish_init()
|
||||
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
logger.info(
|
||||
@@ -139,6 +159,8 @@ def main(args, seed=None, fish=None):
|
||||
for player, name in enumerate(team, 1):
|
||||
world.player_names[player].append(name)
|
||||
logger.info('')
|
||||
world.settings = CustomSettings()
|
||||
world.settings.create_from_world(world)
|
||||
|
||||
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
|
||||
|
||||
@@ -151,7 +173,9 @@ def main(args, seed=None, fish=None):
|
||||
|
||||
if args.usestartinventory[player]:
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
item = ItemFactory(tok.strip(), player)
|
||||
name = tok.strip()
|
||||
name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)'
|
||||
item = ItemFactory(name, player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
@@ -172,6 +196,25 @@ def main(args, seed=None, fish=None):
|
||||
adjust_locations(world, player)
|
||||
place_bosses(world, player)
|
||||
|
||||
if world.customizer and world.customizer.get_start_inventory():
|
||||
for p, inv_list in world.customizer.get_start_inventory().items():
|
||||
for inv_item in inv_list:
|
||||
item = ItemFactory(inv_item.strip(), p)
|
||||
if item:
|
||||
world.push_precollected(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:
|
||||
d.big_key.remove(match)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_info(world)
|
||||
|
||||
if any(world.potshuffle.values()):
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
||||
for player in range(1, world.players + 1):
|
||||
@@ -196,9 +239,12 @@ def main(args, seed=None, fish=None):
|
||||
link_entrances(world, player)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.prep"))
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
link_doors_prep(world, player)
|
||||
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_entrances(world)
|
||||
create_item_pool_config(world)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
|
||||
@@ -206,6 +252,9 @@ def main(args, seed=None, fish=None):
|
||||
for player in range(1, world.players + 1):
|
||||
link_doors(world, player)
|
||||
mark_light_dark_world_regions(world, player)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_doors(world)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
@@ -224,19 +273,21 @@ def main(args, seed=None, fish=None):
|
||||
for player in range(1, world.players + 1):
|
||||
if world.shopsanity[player]:
|
||||
sell_potions(world, player)
|
||||
if world.retro[player]:
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
sell_keys(world, player)
|
||||
else:
|
||||
lock_shop_locations(world, player)
|
||||
|
||||
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"))
|
||||
|
||||
fill_prizes(world)
|
||||
|
||||
# used for debugging
|
||||
# fill_specific_items(world)
|
||||
|
||||
logger.info(world.fish.translate("cli","cli","placing.dungeon.items"))
|
||||
|
||||
if args.algorithm != 'equitable':
|
||||
@@ -270,6 +321,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"))
|
||||
|
||||
@@ -280,6 +332,10 @@ def main(args, seed=None, fish=None):
|
||||
balance_money_progression(world)
|
||||
ensure_good_pots(world, True)
|
||||
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_item_placements(world)
|
||||
world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml'))
|
||||
|
||||
rom_names = []
|
||||
jsonout = {}
|
||||
enemized = False
|
||||
@@ -398,7 +454,7 @@ def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
|
||||
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.remote_items = world.remote_items.copy()
|
||||
@@ -427,6 +483,7 @@ def copy_world(world):
|
||||
ret.keyshuffle = world.keyshuffle.copy()
|
||||
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
|
||||
ret.bombbag = world.bombbag.copy()
|
||||
ret.flute_mode = world.flute_mode.copy()
|
||||
ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy()
|
||||
ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy()
|
||||
ret.crystals_ganon_orig = world.crystals_ganon_orig.copy()
|
||||
@@ -445,6 +502,7 @@ def copy_world(world):
|
||||
ret.enemy_damage = world.enemy_damage.copy()
|
||||
ret.beemizer = world.beemizer.copy()
|
||||
ret.intensity = world.intensity.copy()
|
||||
ret.decoupledoors = world.decoupledoors.copy()
|
||||
ret.experimental = world.experimental.copy()
|
||||
ret.shopsanity = world.shopsanity.copy()
|
||||
ret.dropshuffle = world.dropshuffle.copy()
|
||||
@@ -517,12 +575,9 @@ 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
|
||||
if location.skip:
|
||||
new_location.skip = True
|
||||
new_location.event = location.event
|
||||
new_location.locked = location.locked
|
||||
new_location.skip = location.skip
|
||||
# these need to be modified properly by set_rules
|
||||
new_location.access_rule = lambda state: True
|
||||
new_location.item_rule = lambda state: True
|
||||
@@ -576,7 +631,7 @@ def copy_world_premature(world, player):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm,
|
||||
world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||
world.accessibility, world.shuffle_ganon, world.custom, world.customitemarray, world.hints)
|
||||
ret.teams = world.teams
|
||||
ret.player_names = copy.deepcopy(world.player_names)
|
||||
ret.remote_items = world.remote_items.copy()
|
||||
@@ -775,8 +830,8 @@ def create_playthrough(world):
|
||||
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:
|
||||
|
||||
+4
-197
@@ -1,22 +1,21 @@
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import os
|
||||
import RaceRandom as random
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import yaml
|
||||
|
||||
from DungeonRandomizer import parse_cli
|
||||
from Main import main as DRMain
|
||||
from source.classes.BabelFish import BabelFish
|
||||
from yaml.constructor import SafeConstructor
|
||||
|
||||
from source.tools.MysteryUtils import roll_settings, get_weights
|
||||
|
||||
|
||||
def add_bool(self, node):
|
||||
return self.construct_scalar(node)
|
||||
|
||||
SafeConstructor.add_constructor(u'tag:yaml.org,2002:bool', add_bool)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
|
||||
@@ -107,198 +106,6 @@ def main():
|
||||
|
||||
DRMain(erargs, seed, BabelFish())
|
||||
|
||||
def get_weights(path):
|
||||
if os.path.exists(Path(path)):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
|
||||
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
|
||||
|
||||
def roll_settings(weights):
|
||||
def get_choice(option, root=None):
|
||||
root = weights if root is None else root
|
||||
if option not in root:
|
||||
return None
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return None
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int,root[option].values())))[0]
|
||||
|
||||
def get_choice_default(option, root=weights, default=None):
|
||||
choice = get_choice(option, root)
|
||||
if choice is None and default is not None:
|
||||
return default
|
||||
return choice
|
||||
|
||||
while True:
|
||||
subweights = weights.get('subweights', {})
|
||||
if len(subweights) == 0:
|
||||
break
|
||||
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
|
||||
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
|
||||
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
|
||||
subweights['subweights'] = subweights.get('subweights', {})
|
||||
weights = {**weights, **subweights}
|
||||
|
||||
ret = argparse.Namespace()
|
||||
|
||||
ret.algorithm = get_choice('algorithm')
|
||||
|
||||
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
|
||||
'owg': 'owglitches', 'minorglitches': 'minorglitches'}
|
||||
glitches_required = get_choice('glitches_required')
|
||||
if glitches_required is not None:
|
||||
if glitches_required not in glitch_map.keys():
|
||||
print(f'Logic did not match one of: {", ".join(glitch_map.keys())}')
|
||||
glitches_required = 'none'
|
||||
ret.logic = glitch_map[glitches_required]
|
||||
|
||||
item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else dungeon_items in ['mc', 'mcs', 'full']
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle') == 'on' if 'smallkey_shuffle' in weights else dungeon_items in ['mcs', 'full']
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else dungeon_items in ['full']
|
||||
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
ret.restrict_boss_items = get_choice('restrict_boss_items')
|
||||
|
||||
overworld_shuffle = get_choice('overworld_shuffle')
|
||||
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
|
||||
ret.ow_terrain = get_choice('overworld_terrain') == 'on'
|
||||
valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'}
|
||||
ret.ow_crossed = get_choice('overworld_crossed')
|
||||
ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none'
|
||||
ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on'
|
||||
ret.ow_mixed = get_choice('overworld_swap') == 'on'
|
||||
ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on'
|
||||
overworld_flute = get_choice('flute_shuffle')
|
||||
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
|
||||
ret.bonk_drops = get_choice('bonk_drops') == 'on'
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
overworld_map = get_choice('overworld_map')
|
||||
ret.overworld_map = overworld_map if overworld_map != 'default' else 'default'
|
||||
door_shuffle = get_choice('door_shuffle')
|
||||
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
|
||||
ret.intensity = get_choice('intensity')
|
||||
ret.experimental = get_choice('experimental') == 'on'
|
||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
|
||||
if ret.dungeon_counters == 'default':
|
||||
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
|
||||
|
||||
ret.pseudoboots = get_choice('pseudoboots') == 'on'
|
||||
ret.shopsanity = get_choice('shopsanity') == 'on'
|
||||
ret.dropshuffle = get_choice('dropshuffle') == 'on'
|
||||
ret.pottery = get_choice('pottery') if 'pottery' in weights else 'none'
|
||||
ret.colorizepots = get_choice('colorizepots') == '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 else 'standardize'
|
||||
|
||||
goal = get_choice('goals')
|
||||
if goal is not None:
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt',
|
||||
'trinity': 'trinity'
|
||||
}[goal]
|
||||
|
||||
ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto'
|
||||
|
||||
ret.shuffleganon = get_choice('shuffleganon') == 'on'
|
||||
ret.shufflelinks = get_choice('shufflelinks') == 'on'
|
||||
|
||||
ret.crystals_gt = get_choice('tower_open')
|
||||
ret.crystals_ganon = get_choice('ganon_open')
|
||||
|
||||
from ItemList import set_default_triforce
|
||||
default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0)
|
||||
goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal)
|
||||
goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal)
|
||||
pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool)
|
||||
pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool)
|
||||
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
|
||||
min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal))
|
||||
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
|
||||
|
||||
ret.mode = get_choice('world_state')
|
||||
if ret.mode == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
|
||||
|
||||
ret.bombbag = get_choice('bombbag') == 'on'
|
||||
|
||||
ret.hints = get_choice('hints') == 'on'
|
||||
|
||||
swords = get_choice('weapons')
|
||||
if swords is not None:
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[swords]
|
||||
|
||||
ret.difficulty = get_choice('item_pool')
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality')
|
||||
|
||||
old_style_bosses = {'basic': 'simple',
|
||||
'normal': 'full',
|
||||
'chaos': 'random'}
|
||||
boss_choice = get_choice('boss_shuffle')
|
||||
if boss_choice in old_style_bosses.keys():
|
||||
boss_choice = old_style_bosses[boss_choice]
|
||||
ret.shufflebosses = boss_choice
|
||||
|
||||
enemy_choice = get_choice('enemy_shuffle')
|
||||
if enemy_choice == 'chaos':
|
||||
enemy_choice = 'random'
|
||||
ret.shuffleenemies = enemy_choice
|
||||
|
||||
old_style_damage = {'none': 'default',
|
||||
'chaos': 'random'}
|
||||
damage_choice = get_choice('enemy_damage')
|
||||
if damage_choice in old_style_damage:
|
||||
damage_choice = old_style_damage[damage_choice]
|
||||
ret.enemy_damage = damage_choice
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health')
|
||||
|
||||
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
if get_choice(item, inventoryweights) == 'on':
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
if len(startitems) > 0:
|
||||
ret.usestartinventory = True
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
ret.sprite = get_choice('sprite', romweights)
|
||||
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
|
||||
ret.quickswap = get_choice('quickswap', romweights) == 'on'
|
||||
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
|
||||
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
|
||||
ret.fastmenu = get_choice('menuspeed', romweights)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights)
|
||||
ret.heartbeep = get_choice('heartbeep', romweights)
|
||||
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'
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
+10
-9
@@ -93,12 +93,12 @@ vanilla_pots = {
|
||||
Pot(28, 23, PotItem.Nothing, 'Ice Pengator Switch', obj=RoomObject(0x1FC388, [0x3B, 0xBB, 0xFA])),
|
||||
Pot(86, 26, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC397, [0xAF, 0xD3, 0xFA])),
|
||||
Pot(86, 27, PotItem.Nothing, 'Ice Big Key', obj=RoomObject(0x1FC39A, [0xAF, 0xDB, 0xFA]))],
|
||||
0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])),
|
||||
Pot(168, 24, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])),
|
||||
Pot(48, 28, PotItem.Heart, 'Sewers Key Rat', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])),
|
||||
Pot(82, 28, PotItem.SmallMagic, 'Sewers Key Rat', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])),
|
||||
Pot(100, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])),
|
||||
Pot(104, 28, PotItem.Nothing, 'Sewers Key Rat', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))],
|
||||
0x21: [Pot(160, 20, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C71, [0x43, 0xA7, 0xFA])),
|
||||
Pot(168, 24, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7A, [0x53, 0xC7, 0xFA])),
|
||||
Pot(48, 28, PotItem.Heart, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C80, [0x63, 0xE3, 0xFA])),
|
||||
Pot(82, 28, PotItem.SmallMagic, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C7D, [0xA7, 0xE3, 0xFA])),
|
||||
Pot(100, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C74, [0xCB, 0xE3, 0xFA])),
|
||||
Pot(104, 28, PotItem.Nothing, 'Sewers Dark Aquabats', obj=RoomObject(0x0A8C77, [0xD3, 0xE3, 0xFA]))],
|
||||
0x23: [Pot(86, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED09, [0xAF, 0xD3, 0xFA])),
|
||||
Pot(90, 26, PotItem.Heart, 'TR Lazy Eyes', obj=RoomObject(0x1FED0C, [0xB7, 0xD3, 0xFA])),
|
||||
Pot(94, 26, PotItem.OneRupee, 'TR Lazy Eyes', obj=RoomObject(0x1FED0F, [0xBF, 0xD3, 0xFA])),
|
||||
@@ -879,7 +879,8 @@ def shuffle_pots(world, player):
|
||||
elif old_pot.item == PotItem.Switch:
|
||||
available_pots = (pot for pot in new_pots if (pot.room == old_pot.room or pot.room in movable_switch_rooms[old_pot.room]) and not (pot.flags & PotFlags.NoSwitch))
|
||||
elif old_pot.item == PotItem.Key:
|
||||
if world.doorShuffle[player] == 'vanilla' and not world.retro[player] and world.pottery[player] == 'none' and world.logic[player] != 'nologic':
|
||||
if (world.doorShuffle[player] == 'vanilla' and world.keyshuffle[player] != 'universal'
|
||||
and world.pottery[player] == 'none' and world.logic[player] != 'nologic'):
|
||||
available_pots = (pot for pot in new_pots if pot.room not in invalid_key_rooms)
|
||||
else:
|
||||
available_pots = new_pots
|
||||
@@ -890,7 +891,7 @@ def shuffle_pots(world, player):
|
||||
|
||||
new_pot = random.choice(available_pots)
|
||||
new_pot.item = old_pot.item
|
||||
if world.retro[player] and new_pot.item == PotItem.FiveArrows:
|
||||
if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows:
|
||||
new_pot.item = PotItem.FiveRupees
|
||||
|
||||
if new_pot.item == PotItem.Key:
|
||||
@@ -938,7 +939,7 @@ def shuffle_pot_switches(world, player):
|
||||
|
||||
new_pot = random.choice(available_pots)
|
||||
new_pot.item, old_pot.item = old_pot.item, new_pot.item
|
||||
if world.retro[player] and new_pot.item == PotItem.FiveArrows:
|
||||
if world.bow_mode[player].startswith('retro') and new_pot.item == PotItem.FiveArrows:
|
||||
new_pot.item = PotItem.FiveRupees
|
||||
|
||||
if new_pot.item == PotItem.Switch and (new_pot.flags & PotFlags.SwitchLogicChange):
|
||||
|
||||
+69
-230
@@ -1,270 +1,109 @@
|
||||
## 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 pot 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
|
||||
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.
|
||||
|
||||
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.
|
||||
## Customizer
|
||||
|
||||
### Goal: Trinity
|
||||
Please see [Customizer documentation](docs/Customizer.md) on how to create custom seeds.
|
||||
|
||||
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.
|
||||
## Standard Generation Change
|
||||
|
||||
### Boss Shuffle: Unique
|
||||
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.
|
||||
|
||||
At least one boss each of the prize bosses will be present guarding the prizes. GT bosses can be anything.
|
||||
## ER Features
|
||||
|
||||
### MSU Resume
|
||||
### New Experimental Algorithm
|
||||
|
||||
Turns on msu resume support. Found on "Game Options" tab, the "Adjust/Patch" tab, or use the `--msu_resume` CLI option.
|
||||
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.
|
||||
|
||||
### BPS Patch
|
||||
### Lite/Lean ER (Experimental required)
|
||||
|
||||
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.
|
||||
Designed by Codemann, these are available now (only with experimental turned on - they otherwise fail)
|
||||
|
||||
## New Font
|
||||
#### 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
|
||||
|
||||
Font updated to support lowercase English. Lowercase vs. uppercase typos may exist. Note, you can use lowercase English letters on the file name.
|
||||
#### 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
|
||||
|
||||
## Restricted Item Placement Algorithm
|
||||
### Back of Tavern Shuffle (Experimental required)
|
||||
|
||||
The "Item Sorting" option or ```--algorithm``` has been updated with new placement algorithms. Older algorithms have been removed.
|
||||
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.
|
||||
|
||||
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.
|
||||
#### Take Any Caves
|
||||
|
||||
Here are the current fill options:
|
||||
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:
|
||||
|
||||
### Balanced
|
||||
* 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)
|
||||
|
||||
This one stays the same as before and is recommended for the most random distribution of items.
|
||||
# Bug Fixes and Notes
|
||||
|
||||
### Vanilla Fill
|
||||
None yet
|
||||
|
||||
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.
|
||||
# Known Issues
|
||||
|
||||
### 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.
|
||||
|
||||
## 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
|
||||
|
||||
#### 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
|
||||
* Standing in the doorway when defeating Aga 1 and being teleported to the Dark World will not clear door state. It may cause issues requiring a Save & Quit to fix.
|
||||
* 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. A bomb jump to get to those pot may be required if you don't have boots to bonk across.
|
||||
+11
-3
@@ -421,7 +421,10 @@ def create_dungeon_regions(world, player):
|
||||
create_dungeon_region(player, 'Sewers Rope Room', 'Hyrule Castle', None, ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs']),
|
||||
create_dungeon_region(player, 'Sewers Dark Cross', 'Hyrule Castle', ['Sewers - Dark Cross'], ['Sewers Dark Cross Key Door N', 'Sewers Dark Cross South Stairs']),
|
||||
create_dungeon_region(player, 'Sewers Water', 'Hyrule Castle', None, ['Sewers Water S', 'Sewers Water W']),
|
||||
create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'], ['Sewers Key Rat E', 'Sewers Key Rat Key Door N']),
|
||||
create_dungeon_region(player, 'Sewers Dark Aquabats', 'Hyrule Castle', None,
|
||||
['Sewers Dark Aquabats ES', 'Sewers Dark Aquabats N']),
|
||||
create_dungeon_region(player, 'Sewers Key Rat', 'Hyrule Castle', ['Hyrule Castle - Key Rat Key Drop'],
|
||||
['Sewers Key Rat S', 'Sewers Key Rat NE']),
|
||||
create_dungeon_region(player, 'Sewers Secret Room Blocked Path', 'Hyrule Castle', None, ['Sewers Secret Room Up Stairs']),
|
||||
create_dungeon_region(player, 'Sewers Rat Path', 'Hyrule Castle', None, ['Sewers Secret Room Key Door S', 'Sewers Secret Room Push Block', 'Sewers Rat Path WS', 'Sewers Rat Path WN']),
|
||||
create_dungeon_region(player, 'Sewers Secret Room', 'Hyrule Castle', ['Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right'],
|
||||
@@ -770,7 +773,12 @@ def create_dungeon_regions(world, player):
|
||||
create_dungeon_region(player, 'Mire Hub Switch', 'Misery Mire', ['Misery Mire - Main Lobby'], ['Mire Hub Switch Blue Barrier N', 'Mire Hub Switch Blue Barrier S']),
|
||||
create_dungeon_region(player, 'Mire Lone Shooter', 'Misery Mire', None, ['Mire Lone Shooter WS', 'Mire Lone Shooter ES']),
|
||||
create_dungeon_region(player, 'Mire Failure Bridge', 'Misery Mire', None, ['Mire Failure Bridge W', 'Mire Failure Bridge E']),
|
||||
create_dungeon_region(player, 'Mire Falling Bridge', 'Misery Mire', ['Misery Mire - Big Chest'], ['Mire Falling Bridge WS', 'Mire Falling Bridge W', 'Mire Falling Bridge WN']),
|
||||
create_dungeon_region(player, 'Mire Falling Bridge - Failure', 'Misery Mire', None,
|
||||
['Mire Falling Bridge W', 'Mire Falling Bridge Hook Only Path', 'Mire Falling Bridge Primary Path']),
|
||||
create_dungeon_region(player, 'Mire Falling Bridge - Primary', 'Misery Mire', None,
|
||||
['Mire Falling Bridge WS', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Failure Path']),
|
||||
create_dungeon_region(player, 'Mire Falling Bridge - Chest', 'Misery Mire', ['Misery Mire - Big Chest'],
|
||||
['Mire Falling Bridge WN']),
|
||||
create_dungeon_region(player, 'Mire Map Spike Side', 'Misery Mire', None, ['Mire Map Spike Side EN', 'Mire Map Spike Side Drop Down', 'Mire Map Spike Side Blue Barrier']),
|
||||
create_dungeon_region(player, 'Mire Map Spot', 'Misery Mire', ['Misery Mire - Map Chest'], ['Mire Map Spot WN', 'Mire Map Spot Blue Barrier']),
|
||||
create_dungeon_region(player, 'Mire Crystal Dead End', 'Misery Mire', None, ['Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier', 'Mire Crystal Dead End NW']),
|
||||
@@ -1174,7 +1182,7 @@ def adjust_locations(world, player):
|
||||
loc.event = False
|
||||
item_dungeon = key_item.dungeon
|
||||
dungeon = world.get_dungeon(item_dungeon, player)
|
||||
if key_item.smallkey and not world.retro[player]:
|
||||
if key_item.smallkey and world.keyshuffle[player] != 'universal':
|
||||
dungeon.small_keys.append(key_item)
|
||||
elif key_item.bigkey:
|
||||
dungeon.big_key = key_item
|
||||
|
||||
@@ -38,7 +38,7 @@ from source.dungeon.RoomList import Room0127
|
||||
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '3954c4e449c908a9e086a1fd5797c98b'
|
||||
RANDOMIZERBASEHASH = '7a6619830c3521f2a09c4c64cd8d7d75'
|
||||
|
||||
|
||||
class JsonRom(object):
|
||||
@@ -829,7 +829,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
# Thanks to Zarby89 for originally finding these values
|
||||
# todo fix screen scrolling
|
||||
|
||||
if world.shuffle[player] not in ['insanity', 'insanity_legacy', 'madness_legacy'] and \
|
||||
if world.shuffle[player] not in ['insanity'] and \
|
||||
exit.name in ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit', 'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)']:
|
||||
@@ -860,16 +860,18 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
else:
|
||||
# patch door table
|
||||
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
||||
if exit.name == 'Tavern North':
|
||||
rom.write_byte(0x157D0, exit.target)
|
||||
if world.mode[player] == 'inverted':
|
||||
patch_shuffled_dark_sanc(world, rom, player)
|
||||
|
||||
# setup dr option flags based on experimental, etc.
|
||||
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
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']:
|
||||
dr_flags |= DROptions.Debug
|
||||
if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic'] and world.logic[player] != 'nologic'\
|
||||
and world.mixed_travel[player] == 'prevent':
|
||||
# PoD Falling Bridge or Hammjump
|
||||
# 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05
|
||||
@@ -890,6 +892,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
dr_flags |= DROptions.DarkWorld_Spawns
|
||||
if world.logic[player] != 'nologic':
|
||||
dr_flags |= DROptions.Fix_EG
|
||||
if world.door_type_mode[player] in ['big', 'all', 'chaos']:
|
||||
dr_flags |= DROptions.BigKeyDoor_Shuffle
|
||||
|
||||
my_locations = world.get_filled_locations(player)
|
||||
valid_locations = [l for l in my_locations if ((l.type == LocationType.Pot and not l.forced_item)
|
||||
@@ -900,7 +904,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
valid_loc_by_dungeon = valid_dungeon_locations(valid_locations)
|
||||
|
||||
# fix hc big key problems (map and compass too)
|
||||
if world.doorShuffle[player] == 'crossed' or world.dropshuffle[player] or world.pottery[player] not in ['none', 'cave']:
|
||||
if (world.doorShuffle[player] not in ['vanilla', 'basic'] or world.dropshuffle[player]
|
||||
or world.pottery[player] not in ['none', 'cave']):
|
||||
rom.write_byte(0x151f1, 2)
|
||||
rom.write_byte(0x15270, 2)
|
||||
sanctuary = world.get_region('Sanctuary', player)
|
||||
@@ -929,19 +934,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
rom.write_bytes(0x13fff4, [0xe4, 0x00])
|
||||
|
||||
# patch doors
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
rom.write_byte(0x138002, 2)
|
||||
for name, layout in world.key_layout[player].items():
|
||||
offset = compass_data[name][4]//2
|
||||
if world.retro[player]:
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
rom.write_byte(0x13f030+offset, layout.max_chests + layout.max_drops)
|
||||
else:
|
||||
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
|
||||
rom.write_byte(0x13f030+offset, layout.max_chests)
|
||||
builder = world.dungeon_layouts[player][name]
|
||||
valid_cnt = len(valid_loc_by_dungeon[name])
|
||||
if valid_cnt > 99:
|
||||
logging.getLogger('').warning(f'{name} exceeds 99 in locations ({valid_cnt})')
|
||||
if valid_cnt > 256:
|
||||
logging.getLogger('').warning(f'{name} exceeds 256 in locations ({valid_cnt})')
|
||||
rom.write_byte(0x13f080+offset, valid_cnt % 10)
|
||||
rom.write_byte(0x13f090+offset, valid_cnt // 10)
|
||||
rom.write_byte(0x13f0a0+offset, valid_cnt)
|
||||
@@ -969,7 +974,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
if world.doorShuffle[player] != 'vanilla':
|
||||
for name, pair in boss_indicator.items():
|
||||
dungeon_id, boss_door = pair
|
||||
opposite_door = world.get_door(boss_door, player).dest
|
||||
boss_region = world.get_door(boss_door, player).entrance.parent_region
|
||||
opposite_door = next(iter(x for x in boss_region.entrances if x.name != 'Skull Final Drop WS')).door
|
||||
if opposite_door and isinstance(opposite_door, Door) and opposite_door.roomIndex > -1:
|
||||
dungeon_name = opposite_door.entrance.parent_region.dungeon.name
|
||||
dungeon_id = boss_indicator[dungeon_name][0]
|
||||
@@ -1033,7 +1039,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
rom.write_byte(0x142A50, 1) # StandingItemsOn
|
||||
multiClientFlags = ((0x1 if world.dropshuffle[player] else 0)
|
||||
| (0x2 if world.shopsanity[player] else 0)
|
||||
| (0x4 if world.retro[player] else 0)
|
||||
| (0x4 if world.take_any[player] != 'none' else 0)
|
||||
| (0x8 if world.pottery[player] != 'none' else 0)
|
||||
| (0x10 if is_mystery else 0))
|
||||
rom.write_byte(0x142A51, multiClientFlags)
|
||||
@@ -1219,42 +1225,8 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
for i in range(1024):
|
||||
rom.write_byte(0x178000 + i, random.randint(0, 255))
|
||||
|
||||
# shuffle prize packs
|
||||
pack_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]
|
||||
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,
|
||||
0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDE, 0xDE, 0xDE, 0xDE, 0xDE,
|
||||
0xDF, 0xDF, 0xDF, 0xDF, 0xDF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
|
||||
0xE1, 0xE1, 0xE1, 0xE1, 0xE1, 0xE2, 0xE2, 0xE2, 0xE2, 0xE2,
|
||||
0xE3, 0xE3, 0xE3, 0xE3, 0xE3]
|
||||
|
||||
def chunk(l,n):
|
||||
return [l[i:i+n] for i in range(0, len(l), n)]
|
||||
|
||||
#shuffle order of 7 main packs
|
||||
packs = chunk(pack_prizes, 8)
|
||||
random.shuffle(packs)
|
||||
pack_prizes = [drop for pack in packs for drop in pack]
|
||||
|
||||
if world.difficulty_adjustments[player] in ['hard', 'expert']:
|
||||
prize_replacements = {0xE0: 0xDF, # Fairy -> heart
|
||||
0xE3: 0xD8} # Big magic -> small magic
|
||||
pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes]
|
||||
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
|
||||
|
||||
if world.retro[player]:
|
||||
prize_replacements = {0xE1: 0xDA, #5 Arrows -> Blue Rupee
|
||||
0xE2: 0xDB} #10 Arrows -> Red Rupee
|
||||
pack_prizes = [prize_replacements.get(prize, prize) for prize in pack_prizes]
|
||||
dig_prizes = [prize_replacements.get(prize, prize) for prize in dig_prizes]
|
||||
rom.write_bytes(0x180100, dig_prizes)
|
||||
# write dig prizes
|
||||
rom.write_bytes(0x180100, world.prizes[player]['dig'])
|
||||
|
||||
# write tree pull prizes
|
||||
rom.write_byte(0xEFBD4, world.prizes[player]['pull'][0])
|
||||
@@ -1272,7 +1244,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
rom.write_byte(0xE82CC, world.prizes[player]['fish'])
|
||||
|
||||
# fill enemy prize packs
|
||||
rom.write_bytes(0x37A78, pack_prizes)
|
||||
rom.write_bytes(0x37A78, world.prizes[player]['enemies'])
|
||||
|
||||
# Fill in item substitutions table
|
||||
rom.write_bytes(0x184000, [
|
||||
@@ -1280,7 +1252,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
|
||||
0x51, 0x00 if world.bombbag[player] else 0x06, 0x31 if world.bombbag[player] else 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade. If bombbag -> turns into Bombs (10)
|
||||
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
||||
0x58, 0x01, 0x36 if world.retro[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
||||
0x58, 0x01, 0x36 if world.bow_mode[player].startswith('retro') else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
||||
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
||||
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
|
||||
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
|
||||
@@ -1329,7 +1301,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
ERtimeincrease = 10
|
||||
else:
|
||||
ERtimeincrease = 20
|
||||
if world.keyshuffle[player] or world.bigkeyshuffle[player] or world.mapshuffle[player]:
|
||||
if world.keyshuffle[player] != 'none' or world.bigkeyshuffle[player] or world.mapshuffle[player]:
|
||||
ERtimeincrease = ERtimeincrease + 15
|
||||
if world.clock_mode == 'none':
|
||||
rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode
|
||||
@@ -1469,7 +1441,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
# m - enabled for inside maps
|
||||
# c - enabled for inside compasses
|
||||
# s - enabled for inside small keys
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] else 0x00)
|
||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] == 'wild' else 0x00)
|
||||
| (0x02 if world.compassshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] else 0x00)
|
||||
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes
|
||||
@@ -1520,7 +1492,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
if len(portal_list) == 1:
|
||||
portal_idx = 0
|
||||
else:
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
# the random choice excludes sanctuary
|
||||
portal_idx = next((i for i, elem in enumerate(portal_list)
|
||||
if world.get_portal(elem, player).chosen), random.choice([1, 2, 3]))
|
||||
@@ -1544,7 +1516,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
rom.write_byte(0x53EA6+ow_map_index, world_indicator)
|
||||
|
||||
# in crossed doors - flip the compass exists flags
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
for dungeon, portal_list in dungeon_portals.items():
|
||||
ow_map_index = dungeon_table[dungeon].map_index
|
||||
exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass')
|
||||
@@ -1561,7 +1533,7 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
# a - Small Key
|
||||
#
|
||||
enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'none'
|
||||
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] else 0x00)
|
||||
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00)
|
||||
| (0x02 if world.bigkeyshuffle[player] else 0x00)
|
||||
| (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00)
|
||||
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu
|
||||
@@ -1592,17 +1564,17 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal
|
||||
write_int16(rom, 0x18017C, get_reveal_bytes('Crystal 5')|get_reveal_bytes('Crystal 6') if world.mapshuffle[player] else 0x0000) # Bomb Shop Reveal
|
||||
|
||||
rom.write_byte(0x180172, 0x01 if world.retro[player] else 0x00) # universal keys
|
||||
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
|
||||
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
|
||||
rom.write_byte(0x180178, 0x32 if world.retro[player] else 0x00) # silver arrow cost
|
||||
rom.write_byte(0x301FC, 0xDA if world.retro[player] else 0xE1) # rupees replace arrows under pots
|
||||
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == 'universal' else 0x00) # universal keys
|
||||
rom.write_byte(0x180175, 0x01 if world.bow_mode[player].startswith('retro') else 0x00) # rupee bow
|
||||
rom.write_byte(0x180176, 0x0A if world.bow_mode[player].startswith('retro') else 0x00) # wood arrow cost
|
||||
rom.write_byte(0x180178, 0x32 if world.bow_mode[player].startswith('retro') else 0x00) # silver arrow cost
|
||||
rom.write_byte(0x301FC, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1) # rupees replace arrows under pots
|
||||
if enemized:
|
||||
rom.write_byte(0x1B152e, 0xDA if world.retro[player] else 0xE1)
|
||||
rom.write_byte(0x30052, 0xDB if world.retro[player] else 0xE2) # replace arrows in fish prize from bottle merchant
|
||||
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
|
||||
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro[player] else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
|
||||
rom.write_bytes(0xEDA5, [0x35, 0x41] if world.retro[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows
|
||||
rom.write_byte(0x1B152e, 0xDA if world.bow_mode[player].startswith('retro') else 0xE1)
|
||||
rom.write_byte(0x30052, 0xDB if world.bow_mode[player].startswith('retro') else 0xE2) # replace arrows in fish prize from bottle merchant
|
||||
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
|
||||
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.bow_mode[player].startswith('retro') else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
|
||||
rom.write_bytes(0xEDA5, [0x35, 0x41] if world.bow_mode[player].startswith('retro') else [0x43, 0x44]) # Chest game gives rupees instead of arrows
|
||||
digging_game_rng = random.randint(1, 30) # set rng for digging game
|
||||
rom.write_byte(0x180020, digging_game_rng)
|
||||
rom.write_byte(0xEFD95, digging_game_rng)
|
||||
@@ -1647,11 +1619,13 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False):
|
||||
rom.write_bytes(0x180188, [0x20, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||
rom.write_bytes(0x18018B, [0x20, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||
magic_max, magic_small = 0x80, 0x20
|
||||
if world.doorShuffle[player] == 'crossed':
|
||||
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)
|
||||
@@ -2175,7 +2149,7 @@ def write_strings(rom, world, player, team):
|
||||
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
|
||||
else:
|
||||
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
|
||||
if world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
|
||||
if world.shuffle[player] in ['simple', 'restricted']:
|
||||
for entrance in all_entrances:
|
||||
if entrance.name in entrances_to_hint:
|
||||
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(entrance.connected_region) + '.'
|
||||
@@ -2187,7 +2161,7 @@ def write_strings(rom, world, player, team):
|
||||
entrances_to_hint.update(InconvenientOtherEntrances)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'lite', 'lean']:
|
||||
hint_count = 0
|
||||
elif world.shuffle[player] in ['simple', 'restricted', 'restricted_legacy']:
|
||||
elif world.shuffle[player] in ['simple', 'restricted']:
|
||||
hint_count = 2
|
||||
else:
|
||||
hint_count = 4
|
||||
@@ -2202,7 +2176,7 @@ def write_strings(rom, world, player, team):
|
||||
break
|
||||
|
||||
# Next we handle hints for randomly selected other entrances, curating the selection intelligently based on shuffle.
|
||||
if world.shuffle[player] not in ['simple', 'restricted', 'restricted_legacy']:
|
||||
if world.shuffle[player] not in ['simple', 'restricted']:
|
||||
entrances_to_hint.update(ConnectorEntrances)
|
||||
entrances_to_hint.update(DungeonEntrances)
|
||||
if world.shuffle_ganon[player]:
|
||||
@@ -2232,8 +2206,10 @@ def write_strings(rom, world, player, team):
|
||||
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
|
||||
else:
|
||||
entrances_to_hint.update({'Links House': 'The hero\'s old residence'})
|
||||
if world.shuffletavern[player]:
|
||||
entrances_to_hint.update({'Tavern North': 'A backdoor'})
|
||||
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
|
||||
if world.shuffle[player] in ['insanity', 'madness_legacy', 'insanity_legacy']:
|
||||
if world.shuffle[player] in ['insanity']:
|
||||
entrances_to_hint.update(InsanityEntrances)
|
||||
if world.shuffle_ganon[player]:
|
||||
if world.is_tile_swapped(0x1b, player):
|
||||
@@ -2254,13 +2230,13 @@ def write_strings(rom, world, player, team):
|
||||
|
||||
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
||||
locations_to_hint = InconvenientLocations.copy()
|
||||
if world.doorShuffle[player] != 'crossed':
|
||||
if world.doorShuffle[player] == 'vanilla':
|
||||
locations_to_hint.extend(InconvenientDungeonLocations)
|
||||
if world.shuffle[player] in ['vanilla', 'dungeonssimple', 'dungeonsfull']:
|
||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||
random.shuffle(locations_to_hint)
|
||||
hint_count = 3 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 5
|
||||
hint_count -= 2 if world.doorShuffle[player] == 'crossed' else 0
|
||||
hint_count -= 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
|
||||
del locations_to_hint[hint_count:]
|
||||
for location in locations_to_hint:
|
||||
if location == 'Swamp Left':
|
||||
@@ -2330,27 +2306,27 @@ def write_strings(rom, world, player, team):
|
||||
this_hint = this_hint[0].upper() + this_hint[1:]
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
items_to_hint.remove(flute_item)
|
||||
if world.keyshuffle[player]:
|
||||
if world.keyshuffle[player] == 'wild':
|
||||
items_to_hint.extend(SmallKeys)
|
||||
if world.bigkeyshuffle[player]:
|
||||
items_to_hint.extend(BigKeys)
|
||||
random.shuffle(items_to_hint)
|
||||
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] else 8
|
||||
hint_count += 2 if world.doorShuffle[player] == 'crossed' else 0
|
||||
hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0
|
||||
hint_count += 1 if world.owShuffle[player] != 'vanilla' or world.owCrossed[player] != 'none' or world.owMixed[player] else 0
|
||||
while hint_count > 0:
|
||||
while hint_count > 0 and len(items_to_hint) > 0:
|
||||
this_item = items_to_hint.pop(0)
|
||||
this_location = world.find_items_not_key_only(this_item, player)
|
||||
random.shuffle(this_location)
|
||||
if this_location:
|
||||
random.shuffle(this_location)
|
||||
item_name = this_location[0].item.hint_text
|
||||
item_name = item_name[0].upper() + item_name[1:]
|
||||
this_hint = f'{item_name} can be found {hint_text(this_location[0])}.'
|
||||
tt[hint_locations.pop(0)] = this_hint
|
||||
hint_count -= 1
|
||||
|
||||
# Adding a hint for the Thieves' Town Attic location in Crossed door shuffle.
|
||||
if world.doorShuffle[player] in ['crossed']:
|
||||
# Adding a hint for the Thieves' Town Attic location in mixed door shuffles.
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name
|
||||
this_hint = 'A cracked floor can be found in ' + attic_hint + '.'
|
||||
if world.intensity[player] < 2 and hint_locations[0] == 'telepathic_tile_thieves_town_upstairs':
|
||||
@@ -2468,7 +2444,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)]
|
||||
|
||||
@@ -2509,7 +2502,7 @@ def write_strings(rom, world, player, team):
|
||||
tt['tablet_bombos_book'] = bombos_text
|
||||
|
||||
# attic hint
|
||||
if world.doorShuffle[player] in ['crossed']:
|
||||
if world.doorShuffle[player] not in ['vanilla', 'basic']:
|
||||
attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name
|
||||
tt['blind_not_that_way'] = f'{attic_hint} is too bright for my eyes'
|
||||
# see tagalog.asm tables at 957,967 or Follower_HandleTrigger in JPDASM
|
||||
|
||||
+14
-2
@@ -316,6 +316,18 @@ class Room(object):
|
||||
byte_array.append(kind.value)
|
||||
return byte_array
|
||||
|
||||
def next_free(self, pos=4):
|
||||
for i, door in enumerate(self.doorList):
|
||||
if i >= pos:
|
||||
return None
|
||||
position, kind = door
|
||||
if kind not in [DoorKind.SmallKey, DoorKind.Dashable, DoorKind.Bombable, DoorKind.TrapTriggerable,
|
||||
DoorKind.Trap, DoorKind.Trap2, DoorKind.TrapTriggerableLow, DoorKind.TrapLowE3,
|
||||
DoorKind.BigKey, DoorKind.StairKey, DoorKind.StairKey2, DoorKind.StairKeyLow,
|
||||
DoorKind.BlastWall, DoorKind.BombableEntrance]:
|
||||
return i
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
@@ -383,8 +395,8 @@ class DoorKind(Enum):
|
||||
Bombable = 0x2E
|
||||
BlastWall = 0x30
|
||||
Hidden = 0x32
|
||||
TrapTriggerable = 0x36 # right side trap or south side trap
|
||||
Trap2 = 0x38 # left side trap or north side trap
|
||||
TrapTriggerable = 0x36 # right side trap or bottom side trap (door directions: West, North)
|
||||
Trap2 = 0x38 # left side trap or top side trap (door directions: East, South)
|
||||
NormalLow2 = 0x40
|
||||
TrapTriggerableLow = 0x44
|
||||
Warp = 0x46
|
||||
|
||||
@@ -355,7 +355,8 @@ def global_rules(world, player):
|
||||
|
||||
set_rule(world.get_entrance('Mire Lobby Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Mire Post-Gap Gap', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Mire Falling Bridge WN', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player)) # this is due to the fact the the door opposite is blocked
|
||||
set_rule(world.get_entrance('Mire Falling Bridge Hook Path', player), lambda state: state.has_Boots(player) or state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Mire Falling Bridge Hook Only Path', player), lambda state: state.has('Hookshot', player))
|
||||
set_rule(world.get_entrance('Mire 2 NE', player), lambda state: state.has_sword(player) or
|
||||
(state.has('Fire Rod', player) and (state.can_use_bombs(player) or state.can_extend_magic(player, 9))) or # 9 fr shots or 8 with some bombs
|
||||
(state.has('Ice Rod', player) and state.can_use_bombs(player)) or # freeze popo and throw, bomb to finish
|
||||
@@ -425,7 +426,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))
|
||||
@@ -1604,7 +1606,8 @@ def add_conditional_lamps(world, player):
|
||||
'Sewers Behind Tapestry': {'sewer': True, 'entrances': ['Sewers Behind Tapestry S', 'Sewers Behind Tapestry Down Stairs'], 'locations': []},
|
||||
'Sewers Rope Room': {'sewer': True, 'entrances': ['Sewers Rope Room Up Stairs', 'Sewers Rope Room North Stairs'], 'locations': []},
|
||||
'Sewers Water': {'sewer': True, 'entrances': ['Sewers Water S', 'Sewers Water W'], 'locations': []},
|
||||
'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat E', 'Sewers Key Rat Key Door N'], 'locations': ['Hyrule Castle - Key Rat Key Drop']},
|
||||
'Sewers Dark Aquabats': {'sewer': True, 'entrances': ['Sewers Dark Aquabats N', 'Sewers Dark Aquabats ES'], 'locations': []},
|
||||
'Sewers Key Rat': {'sewer': True, 'entrances': ['Sewers Key Rat S', 'Sewers Key Rat NE'], 'locations': ['Hyrule Castle - Key Rat Key Drop']},
|
||||
'Old Man Cave': {'sewer': False, 'entrances': ['Old Man Cave Exit (East)']},
|
||||
'Old Man House Back': {'sewer': False, 'entrances': ['Old Man House Back to Front', 'Old Man House Exit (Top)']},
|
||||
'Death Mountain Return Cave (left)': {'sewer': False, 'entrances': ['Death Mountain Return Cave E', 'Death Mountain Return Cave Exit (West)']},
|
||||
@@ -2041,9 +2044,9 @@ bunny_impassible_doors = {
|
||||
'Ice Backwards Room Hole', 'Ice Switch Room SE', 'Ice Antechamber NE', 'Ice Antechamber Hole', 'Mire Lobby Gap',
|
||||
'Mire Post-Gap Gap', 'Mire 2 NE', 'Mire Hub Upper Blue Barrier', 'Mire Hub Lower Blue Barrier',
|
||||
'Mire Hub Right Blue Barrier', 'Mire Hub Top Blue Barrier', 'Mire Hub Switch Blue Barrier N',
|
||||
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge WN', 'Mire Map Spike Side Blue Barrier',
|
||||
'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier', 'Mire Crystal Dead End Right Barrier',
|
||||
'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
|
||||
'Mire Hub Switch Blue Barrier S', 'Mire Falling Bridge Hook Path', 'Mire Falling Bridge Hook Only Path',
|
||||
'Mire Map Spike Side Blue Barrier', 'Mire Map Spot Blue Barrier', 'Mire Crystal Dead End Left Barrier',
|
||||
'Mire Crystal Dead End Right Barrier', 'Mire Cross ES', 'Mire Left Bridge Hook Path', 'Mire Fishbone Blue Barrier',
|
||||
'Mire South Fish Blue Barrier', 'Mire Tile Room NW', 'Mire Compass Blue Barrier', 'Mire Attic Hint Hole',
|
||||
'Mire Dark Shooters SW', 'Mire Crystal Mid Blue Barrier', 'Mire Crystal Left Blue Barrier', 'TR Main Lobby Gap',
|
||||
'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path',
|
||||
@@ -2089,7 +2092,7 @@ def add_key_logic_rules(world, player):
|
||||
add_rule(big_chest, create_rule(d_logic.bk_name, player))
|
||||
if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1:
|
||||
set_always_allow(big_chest, allow_big_key_in_big_chest(d_logic.bk_name, player))
|
||||
if world.retro[player]:
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
for d_name, layout in world.key_layout[player].items():
|
||||
for door in layout.flat_prop:
|
||||
if world.mode[player] != 'standard' or not retro_in_hc(door.entrance):
|
||||
@@ -2108,8 +2111,8 @@ 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):
|
||||
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
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ def main(args=None):
|
||||
|
||||
test("Vanilla ", "--shuffle vanilla")
|
||||
test("Retro ", "--retro --shuffle vanilla")
|
||||
test("Keysanity ", "--shuffle vanilla --dropshuffle --keysanity")
|
||||
test("Keysanity ", "--shuffle vanilla --keydropshuffle --keysanity")
|
||||
test("Shopsanity", "--shuffle vanilla --shopsanity")
|
||||
test("Simple ", "--shuffle simple")
|
||||
test("Full ", "--shuffle full")
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,213 @@
|
||||
## Customizer Files
|
||||
|
||||
A customizer file is [yaml file](https://yaml.org/) with different settings. This documentation is intended to be read with an [example customizer file](customizer_example.yaml).
|
||||
|
||||
This can also be used to roll a mystery or mutli-mystery seed via the GUI. [Example](multi_mystery_example.yaml)
|
||||
|
||||
The cli includes a couple arguments to help:
|
||||
|
||||
`--print_custom_yaml` will create a yaml file based on the seed rolled. Treat it like a spoiler.
|
||||
|
||||
`--customizer` takes a file as an argument.
|
||||
|
||||
Present on the GUI as `Print Customizer File` and `Customizer File` on the Generation Setup tab.
|
||||
|
||||
### meta
|
||||
|
||||
Supported values:
|
||||
|
||||
* `players`: number of players
|
||||
* `seed`: you can set the seed number for a set experience on things that are randomized instead of specified
|
||||
* `algorithm`: fill algorithm
|
||||
* `names`: for naming mutliworld players
|
||||
|
||||
###### Not Yet Implemented
|
||||
|
||||
* `mystery`: indicates the seed should be treated as a mystery (hides total collection_rate) (This may end up player specific)
|
||||
|
||||
### settings
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate settings.
|
||||
|
||||
in each player section you can set up default settings for that player or you can specify a mystery yaml file.
|
||||
|
||||
```
|
||||
1: player1.yaml
|
||||
2:
|
||||
shuffle: crossed
|
||||
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 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.
|
||||
|
||||
###### Not Yet Implemented
|
||||
|
||||
Rom/Adjust flags like sprite, quickswap are not outputing with the print_custom_yaml settings
|
||||
|
||||
### item_pool
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate pool.
|
||||
|
||||
Then each player can have the entire item pool defined. The name of item should be followed by the number of that item in the pool. Many key items will be added to the pool if not detected.
|
||||
|
||||
`Bottle (Random)` is supported to randomize bottle contents according to those allowed by difficulty. Pendants and crystals are supported here.
|
||||
|
||||
|
||||
##### Caveat
|
||||
|
||||
Dungeon items amount can be increased (but not decreased as the minimum of each dungeon item is either pre-determined or calculated by door rando) if the type of dungeon item is not shuffled then it is attempted to be placed in the dungeon. Extra item beyond dungeon capacity not be confined to the dungeon.
|
||||
|
||||
### placements
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate placement list.
|
||||
|
||||
You may list each location for a player and the item you wish to place there. A location name requires to be enclosed by single quotes if the location name contains a `#` (Most pot locations have the `#`). (Currently no location names have both a `'` and a `#` so you don't need to worry about escaping the `'`)
|
||||
|
||||
For multiworld you can specify which player the item is for using this syntax:
|
||||
|
||||
`<item name>#<player number>`
|
||||
|
||||
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
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `entrances`, `exits`, and `two-way`.
|
||||
|
||||
#### two-way
|
||||
|
||||
`two-way` should be used for connectors, dungeons that you wish to couple. (as opposite to decoupled in the insanity shuffle). Links house should be placed using this method as is can be decoupled logically. (Haven't tested to see if it works in game). The overworld entrance is listed first, followed by the interior exit that it leads to. (The exit will then be linked to exit at that entrance).
|
||||
|
||||
`50 Rupee Cave: Desert Palace Exit (North)` The 50 Rupee entrance leads to Desert North entrance, and leaving there will spit you out at the same place.
|
||||
|
||||
#### exits
|
||||
|
||||
`exits` is used for the Chris Houlihan Room Exit and connectors and dungeons that you wish to be decoupled from their entrances. Perhaps counter-intuitively, the exit is listed after the entrance from which it emerges.
|
||||
|
||||
`Light Hype Fairy: Chris Houlihan Room Exit` leaving Chris Houlihan Room will spit you out at the Light Hype Fairy.
|
||||
|
||||
(I can easily switch this syntax around if people would like me too)
|
||||
|
||||
#### entrances
|
||||
|
||||
`entrances` is used for single entrances caves, houses, shops, etc. and drops. Single entrance caves always exit to where you enter, they cannot be decoupled. Dungeons and connectors which are decoupled can also be listed here.
|
||||
|
||||
`Chicken House: Kakariko Shop` if you walk into Chicken House door, you will in the Kakariko Shop.
|
||||
|
||||
##### Known Issues
|
||||
|
||||
Chris Houlihan and Links House should be specified together or not at all.
|
||||
|
||||
### doors
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate sections. This section has three primary subsections: `lobbies` and `doors`.
|
||||
|
||||
`lobbies` lists the doors by which each dungeon is entered
|
||||
|
||||
`<lobby name>: <door name>` Ex. `Turtle Rock Chest: TR Lava Escape SE`
|
||||
|
||||
`doors` lists pairs of doors. The first door name is listed is the key. The value of this object may be the paired door name or optionally it can have two properties: `dest` and `type`. If you want a type, you must use the second option.
|
||||
|
||||
The destination door is listed under `dest`
|
||||
Supported `type`s are `Key Door`, `Bomb Door`, and `Dash Door`
|
||||
|
||||
Here are the two examples of the syntax:
|
||||
|
||||
```Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W```
|
||||
```
|
||||
Sewers Rat Path WS:
|
||||
dest: Sewers Secret Room ES
|
||||
two-way: true
|
||||
type: Key Door
|
||||
```
|
||||
|
||||
You'll note that sub-tile door do not need to be listed, but if you want them to be key doors you will have to list them.
|
||||
|
||||
###### Not Yet Implemented
|
||||
|
||||
`one-way` to indicate decoupled doors
|
||||
|
||||
##### Known Issue
|
||||
|
||||
If you specify a door type and those doors cannot be a stateful door due to the nature of the supertile (or you've placed too many on the supertile) an exception is thrown.
|
||||
|
||||
### medallions
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate info.
|
||||
|
||||
Example:
|
||||
```
|
||||
Misery Mire: Ether
|
||||
Turtle Rock: Quake
|
||||
```
|
||||
|
||||
Leave blank or omit if you wish it to be random. Alternatively, a weighted dictionary is supported and a 'Random' option
|
||||
|
||||
### bosses
|
||||
|
||||
This must be defined by player. Each player number should be listed with the appropriate boss list.
|
||||
|
||||
This is done as `<dungeon>: <boss>`
|
||||
|
||||
E.g. `Skull Woods: Helmasaur King` for helmacopter. Be sure to turn on at least one enemizer setting for the bosses to actually be randomized.
|
||||
|
||||
### start_inventory
|
||||
|
||||
This must be defined by player. Each player number should be listed with a list of items to start with.
|
||||
|
||||
This is a yaml list (note the hyphens):
|
||||
|
||||
```
|
||||
start_inventory:
|
||||
1:
|
||||
- Pegasus Boots
|
||||
- Progressive Sword
|
||||
```
|
||||
|
||||
To start with multiple copies of progressive items, list them more than once.
|
||||
|
||||
##### Known Issue
|
||||
|
||||
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,125 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
door_shuffle: vanilla
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
goal: ganon
|
||||
hints: false
|
||||
intensity: 1
|
||||
pseudoboots: false
|
||||
pottery: lottery
|
||||
shopsanity: false
|
||||
shuffle: vanilla
|
||||
shufflelinks: false
|
||||
shufflebosses: false
|
||||
item_pool:
|
||||
1:
|
||||
Ice Rod: 1
|
||||
Progressive Sword: 1
|
||||
placements:
|
||||
1:
|
||||
Turtle Rock - Boss: Triforce
|
||||
start_inventory:
|
||||
1:
|
||||
- Progressive Bow
|
||||
- Progressive Bow
|
||||
- Blue Boomerang
|
||||
- Red Boomerang
|
||||
- Hookshot
|
||||
- Mushroom
|
||||
- Magic Powder
|
||||
- Fire Rod
|
||||
- Bombos
|
||||
- Ether
|
||||
- Quake
|
||||
- Lamp
|
||||
- Hammer
|
||||
- Ocarina
|
||||
- Bug Catching Net
|
||||
- Book of Mudora
|
||||
- Shovel
|
||||
- Cane of Somaria
|
||||
- Cane of Byrna
|
||||
- Cape
|
||||
- Magic Mirror
|
||||
- Moon Pearl
|
||||
- Titans Mitts
|
||||
- Tempered Sword
|
||||
- Pegasus Boots
|
||||
- Flippers
|
||||
- Red Mail
|
||||
- Progressive Shield
|
||||
- Progressive Shield
|
||||
- Bottle (Red Potion)
|
||||
- Bottle (Green Potion)
|
||||
- Bottle (Blue Potion)
|
||||
- Bottle (Fairy)
|
||||
- Magic Upgrade (1/2)
|
||||
- Magic Upgrade (1/2)
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Boss Heart Container
|
||||
- Arrows (10)
|
||||
- Arrows (10)
|
||||
- Arrows (10)
|
||||
- Bombs (10)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (300)
|
||||
- Rupees (50)
|
||||
- Rupees (20)
|
||||
- Rupees (20)
|
||||
- Rupees (5)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
- Rupee (1)
|
||||
|
||||
|
||||
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,108 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 1
|
||||
seed: 42
|
||||
names: Lonk
|
||||
settings:
|
||||
1:
|
||||
door_shuffle: basic
|
||||
dropshuffle: true
|
||||
experimental: true
|
||||
goal: ganon
|
||||
hints: true
|
||||
intensity: 3
|
||||
overworld_map: compass
|
||||
pseudoboots: true
|
||||
pottery: keys
|
||||
shopsanity: true
|
||||
shuffle: crossed
|
||||
shufflelinks: true
|
||||
shufflebosses: unique
|
||||
item_pool:
|
||||
1:
|
||||
Blue Boomerang: 3
|
||||
Bombos: 3
|
||||
Book of Mudora: 3
|
||||
Boss Heart Container: 30
|
||||
Bottle (Random): 12
|
||||
Bug Catching Net: 3
|
||||
Cane of Byrna: 3
|
||||
Cane of Somaria: 3
|
||||
Cape: 3
|
||||
Ether: 3
|
||||
Fire Rod: 3
|
||||
Flippers: 3
|
||||
Hammer: 3
|
||||
Hookshot: 3
|
||||
Ice Rod: 3
|
||||
Lamp: 3
|
||||
Magic Mirror: 3
|
||||
Magic Powder: 3
|
||||
Magic Upgrade (1/2): 3
|
||||
Moon Pearl: 3
|
||||
Mushroom: 3
|
||||
Ocarina: 3
|
||||
Pegasus Boots: 3
|
||||
Piece of Heart: 24
|
||||
Progressive Armor: 6
|
||||
Progressive Bow: 6
|
||||
Progressive Glove: 6
|
||||
Progressive Shield: 9
|
||||
Progressive Sword: 12
|
||||
Quake: 3
|
||||
Red Boomerang: 3
|
||||
Rupees (300): 10
|
||||
Bombs (3): 20
|
||||
Sanctuary Heart Container: 3
|
||||
Shovel: 3
|
||||
Single Arrow: 1
|
||||
placements:
|
||||
1:
|
||||
Palace of Darkness - Big Chest: Hammer
|
||||
Capacity Upgrade - Left: Moon Pearl
|
||||
Turtle Rock - Pokey 2 Key Drop: Ice Rod
|
||||
entrances:
|
||||
1:
|
||||
entrances:
|
||||
Bat Cave Drop: Pyramid
|
||||
Dam: Chicken House
|
||||
Snitch Lady (West): Cave 45
|
||||
exits:
|
||||
Misery Mire: Chris Houlihan Room Exit
|
||||
two-way:
|
||||
Bat Cave Cave: Pyramid Exit
|
||||
Bumper Cave (Bottom): Bumper Cave Exit (Bottom)
|
||||
Bumper Cave (Top): Bumper Cave Exit (Top)
|
||||
Skull Woods Final Section: Ganons Tower Exit
|
||||
Misery Mire: Links House Exit
|
||||
doors:
|
||||
1:
|
||||
lobbies:
|
||||
Sanctuary: Sewers Pull Switch S
|
||||
Skull 2 East: Skull 1 Lobby S
|
||||
Skull 3: Skull 2 West Lobby S
|
||||
Turtle Rock Lazy Eyes: TR Roller Room SW
|
||||
doors:
|
||||
Hyrule Dungeon Guardroom Abyss Edge: Hyrule Castle Lobby W
|
||||
Sewers Rat Path WS:
|
||||
dest: Sewers Secret Room ES
|
||||
type: Key Door
|
||||
Sewers Rat Path WN:
|
||||
dest: Sewers Secret Room EN
|
||||
type: Key Door
|
||||
Tower Pacifist Run WS:
|
||||
dest: Tower Circle of Pots ES
|
||||
type: Key Door
|
||||
Hera Lobby Up Stairs: Hera 4F Down Stairs
|
||||
medallions:
|
||||
1:
|
||||
Misery Mire: Bombos
|
||||
bosses:
|
||||
1:
|
||||
Palace of Darkness: Arrghus
|
||||
Thieves Town: Blind
|
||||
Ganons Tower (top): Vitreous
|
||||
start_inventory:
|
||||
1:
|
||||
- Pegasus Boots
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
meta:
|
||||
algorithm: balanced
|
||||
players: 3
|
||||
seed: 722495389
|
||||
names: Link,Zelda,Ganon
|
||||
settings:
|
||||
1: player1.yml
|
||||
2: player2.yml
|
||||
3: player3.yml
|
||||
start_inventory:
|
||||
1:
|
||||
- Progressive Sword
|
||||
- Pegasus Boots
|
||||
2:
|
||||
- Lamp
|
||||
3:
|
||||
- Flippers
|
||||
@@ -0,0 +1,19 @@
|
||||
description: I like my keysanity distributed weirdly, but no doors please
|
||||
door_shuffle: vanilla
|
||||
dungeon_items:
|
||||
standard: 12
|
||||
m: 3
|
||||
c: 3
|
||||
s: 3
|
||||
b: 3
|
||||
mc: 2
|
||||
ms: 2
|
||||
mb: 2
|
||||
cs: 2
|
||||
cb: 2
|
||||
sb: 2
|
||||
mcs: 3
|
||||
mcb: 3
|
||||
msb: 3
|
||||
csb: 3
|
||||
full: 12
|
||||
@@ -0,0 +1,16 @@
|
||||
description: Some sort of er with unknown goals
|
||||
door_shuffle: basic
|
||||
entrance_shuffle:
|
||||
dungeonssimple: 3
|
||||
dungeonsfull: 2
|
||||
simple: 2
|
||||
restricted: 2
|
||||
full: 2
|
||||
crossed: 3
|
||||
insanity: 1
|
||||
goals:
|
||||
ganon: 2
|
||||
fast_ganon: 2
|
||||
dungeons: 1
|
||||
pedestal: 2
|
||||
triforce-hunt: 1
|
||||
@@ -0,0 +1,25 @@
|
||||
description: Door feature fan
|
||||
door_shuffle: crossed
|
||||
world_state:
|
||||
standard: 2
|
||||
open: 2
|
||||
inverted: 1
|
||||
intensity:
|
||||
1: 1
|
||||
2: 1
|
||||
3: 2
|
||||
keydropshuffle:
|
||||
on: 1
|
||||
off: 1
|
||||
shopsanity:
|
||||
on: 1
|
||||
off: 4
|
||||
restrict_boss_items:
|
||||
none: 1
|
||||
dungeon: 1
|
||||
boss_shuffle:
|
||||
none: 4
|
||||
simple: 1
|
||||
full: 1
|
||||
unique: 1
|
||||
random: 1
|
||||
+24
-3
@@ -36,6 +36,12 @@
|
||||
1: 2
|
||||
2: 2
|
||||
3: 4
|
||||
door_type_mode:
|
||||
original: 2
|
||||
big: 2
|
||||
all: 1
|
||||
chaos: 1
|
||||
decoupledoors: off
|
||||
dropshuffle:
|
||||
on: 1
|
||||
off: 1
|
||||
@@ -58,6 +64,14 @@
|
||||
bombbag:
|
||||
on: 1
|
||||
off: 4
|
||||
flute_mode:
|
||||
normal: 3
|
||||
active: 1
|
||||
bow_mode:
|
||||
progressive: 5
|
||||
silvers: 5
|
||||
retro: 1
|
||||
retro_silvers: 1
|
||||
entrance_shuffle:
|
||||
none: 15
|
||||
dungeonssimple: 3
|
||||
@@ -88,9 +102,10 @@
|
||||
open: 1
|
||||
inverted: 1
|
||||
retro: 0
|
||||
retro:
|
||||
on: 1
|
||||
off: 4
|
||||
take_any:
|
||||
random: 1
|
||||
fixed: 3
|
||||
none: 16
|
||||
goals:
|
||||
ganon: 2
|
||||
fast_ganon: 2
|
||||
@@ -118,6 +133,12 @@
|
||||
mc: 3
|
||||
mcs: 2
|
||||
full: 5
|
||||
mcu: 1 # map, compass, universal smalls
|
||||
# for use when you aren't using the dungeon_items above
|
||||
# smallkey_shuffle:
|
||||
# standard: 5
|
||||
# wild: 1
|
||||
# universal: 1
|
||||
dungeon_counters:
|
||||
on: 5
|
||||
off: 0
|
||||
|
||||
@@ -43,11 +43,13 @@ entrance_shuffle:
|
||||
shufflelinks:
|
||||
on: 1
|
||||
off: 1
|
||||
shuffletavern:
|
||||
on: 1
|
||||
off: 1
|
||||
world_state:
|
||||
standard: 1
|
||||
open: 1
|
||||
inverted: 1
|
||||
retro: 0
|
||||
retro:
|
||||
on: 1
|
||||
off: 1
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"retro"
|
||||
]
|
||||
},
|
||||
"boots_hint": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"swords": {
|
||||
"choices": [
|
||||
"random",
|
||||
@@ -42,6 +46,20 @@
|
||||
"vanilla"
|
||||
]
|
||||
},
|
||||
"flute_mode": {
|
||||
"choices": [
|
||||
"normal",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"bow_mode": {
|
||||
"choices": [
|
||||
"progressive",
|
||||
"silvers",
|
||||
"retro",
|
||||
"retro_silvers"
|
||||
]
|
||||
},
|
||||
"goal": {
|
||||
"choices": [
|
||||
"ganon",
|
||||
@@ -198,6 +216,7 @@
|
||||
"door_shuffle": {
|
||||
"choices": [
|
||||
"basic",
|
||||
"partitioned",
|
||||
"crossed",
|
||||
"vanilla"
|
||||
]
|
||||
@@ -210,6 +229,18 @@
|
||||
"random"
|
||||
]
|
||||
},
|
||||
"door_type_mode": {
|
||||
"choices":[
|
||||
"original",
|
||||
"big",
|
||||
"all",
|
||||
"chaos"
|
||||
]
|
||||
},
|
||||
"decoupledoors": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"experimental": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
@@ -312,8 +343,11 @@
|
||||
"type": "bool"
|
||||
},
|
||||
"keyshuffle": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
"choices": [
|
||||
"none",
|
||||
"wild",
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
"bigkeyshuffle": {
|
||||
"action": "store_true",
|
||||
@@ -332,6 +366,13 @@
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"take_any": {
|
||||
"choices": [
|
||||
"none",
|
||||
"random",
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
"startinventory": {},
|
||||
"usestartinventory": {
|
||||
"type": "bool"
|
||||
@@ -429,6 +470,10 @@
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"shuffletavern": {
|
||||
"action": "store_true",
|
||||
"type": "bool"
|
||||
},
|
||||
"overworld_map": {
|
||||
"choices": [
|
||||
"default",
|
||||
|
||||
@@ -122,6 +122,18 @@
|
||||
"Hard: Reduced functionality.",
|
||||
"Expert: Greatly reduced functionality."
|
||||
],
|
||||
"flute_mode": [
|
||||
"Determines if you need to wake up the bird or not on flute pickup (default: %(default)s)",
|
||||
"Normal: Normal functionality.",
|
||||
"Active: Flute is activated on pickup."
|
||||
],
|
||||
"bow_mode": [
|
||||
"Determines how the bow acts in the pool (default: %(default)s)",
|
||||
"Progressive: Two progressive bows placed. First picked up is the bow. Second is silvers.",
|
||||
"Silvers Separate: Bow and silvers are completely separate items.",
|
||||
"Retro: Z1 Bow where arrows cost money and the Single Arrow must be bought or found to shoot",
|
||||
"Retro + Silvers: Bow and silvers are completely separate items."
|
||||
],
|
||||
"timer": [
|
||||
"Select game timer setting. Affects available itempool. (default: %(default)s)",
|
||||
"None: No timer.",
|
||||
@@ -250,6 +262,7 @@
|
||||
"door_shuffle": [
|
||||
"Select Door Shuffling Algorithm. (default: %(default)s)",
|
||||
"Basic: Doors are mixed within a single dungeon.",
|
||||
"Partitioned Doors are mixed in 3 partitions: L1-3+HC+AT, D1-4, D5-8",
|
||||
"Crossed: Doors are mixed between all dungeons.",
|
||||
"Vanilla: All doors are connected the same way they were in the",
|
||||
" base game."
|
||||
@@ -261,6 +274,14 @@
|
||||
"3: And shuffles dungeon lobbies",
|
||||
"random: Picks one of those at random"
|
||||
],
|
||||
"door_type_mode" : [
|
||||
"Door Types to Shuffle (default: %(default)s)",
|
||||
"original: Shuffles key doors, bombable, and dashable doors",
|
||||
"big: Adds big key doors",
|
||||
"all: Adds traps doors (and any future supported door types)",
|
||||
"chaos: Increases the number of door types in all dungeon pools"
|
||||
],
|
||||
"decoupledoors" : [ "Door entrances and exits are decoupled" ],
|
||||
"experimental": [ "Enable experimental features. (default: %(default)s)" ],
|
||||
"dungeon_counters": [ "Enable dungeon chest counters. (default: %(default)s)" ],
|
||||
"crystals_ganon": [
|
||||
@@ -331,10 +352,17 @@
|
||||
"Keys are universal, shooting arrows costs rupees,",
|
||||
"and a few other little things make this more like Zelda-1. (default: %(default)s)"
|
||||
],
|
||||
"pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s)"],
|
||||
"take_any": [
|
||||
"Take Any caves from Zelda 1 (default: %(default)s)",
|
||||
"None: No take any caves",
|
||||
"Random: Take any caves can replace a random set of un-interesting caves. See documentation for full list",
|
||||
"Fixed: Take any caves will replace certain location. See documentation for full list"
|
||||
],
|
||||
"pseudoboots": [ " Start with pseudo boots that allow dashing but no item checks (default: %(default)s)"],
|
||||
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
|
||||
"startinventory": [ "Specifies a list of items that will be in your starting inventory (separated by commas). (default: %(default)s)" ],
|
||||
"usestartinventory": [ "Toggle usage of Starting Inventory." ],
|
||||
"customizer": ["Path to a customizer file."],
|
||||
"custom": [ "Not supported." ],
|
||||
"customitemarray": [ "Not supported." ],
|
||||
"accessibility": [
|
||||
@@ -359,6 +387,9 @@
|
||||
"shufflelinks": [
|
||||
"Include Link's House in the entrance shuffle pool. (default: %(default)s)"
|
||||
],
|
||||
"shuffletavern": [
|
||||
"Include the back of the tavern in the entrance shuffle pool. (default: %(default)s)"
|
||||
],
|
||||
"overworld_map": [
|
||||
"Control if and how the overworld map indicators show the locations of dungeons (default: %(default)s)"
|
||||
],
|
||||
|
||||
@@ -53,8 +53,12 @@
|
||||
"randomizer.dungeon.mapshuffle": "Maps",
|
||||
"randomizer.dungeon.compassshuffle": "Compasses",
|
||||
"randomizer.dungeon.smallkeyshuffle": "Small Keys",
|
||||
"randomizer.dungeon.smallkeyshuffle.none": "In Dungeon",
|
||||
"randomizer.dungeon.smallkeyshuffle.wild": "Randomized",
|
||||
"randomizer.dungeon.smallkeyshuffle.universal": "Universal",
|
||||
"randomizer.dungeon.bigkeyshuffle": "Big Keys",
|
||||
"randomizer.dungeon.keydropshuffle": "Key Drop Shuffle (Legacy)",
|
||||
"randomizer.dungeon.decoupledoors": "Decouple Doors",
|
||||
"randomizer.dungeon.dropshuffle": "Shuffle Enemy Key Drops",
|
||||
"randomizer.dungeon.potshuffle": "Pot Shuffle (Legacy)",
|
||||
"randomizer.dungeon.pottery": "Pottery",
|
||||
@@ -72,6 +76,7 @@
|
||||
"randomizer.dungeon.dungeondoorshuffle": "Dungeon Door Shuffle",
|
||||
"randomizer.dungeon.dungeondoorshuffle.vanilla": "Vanilla",
|
||||
"randomizer.dungeon.dungeondoorshuffle.basic": "Basic",
|
||||
"randomizer.dungeon.dungeondoorshuffle.partitioned": "Partitioned",
|
||||
"randomizer.dungeon.dungeondoorshuffle.crossed": "Crossed",
|
||||
|
||||
"randomizer.dungeon.dungeonintensity": "Intensity Level",
|
||||
@@ -80,6 +85,12 @@
|
||||
"randomizer.dungeon.dungeonintensity.3": "3: Dungeon Lobbies",
|
||||
"randomizer.dungeon.dungeonintensity.random": "Random",
|
||||
|
||||
"randomizer.dungeon.door_type_mode": "Door Types to Shuffle",
|
||||
"randomizer.dungeon.door_type_mode.original": "Small Key Doors, Bomb Doors, Dash Doors",
|
||||
"randomizer.dungeon.door_type_mode.big": "Adds Big Key Doors",
|
||||
"randomizer.dungeon.door_type_mode.all": "Adds Trap Doors",
|
||||
"randomizer.dungeon.door_type_mode.chaos": "Increases all door types",
|
||||
|
||||
"randomizer.dungeon.experimental": "Enable Experimental Features",
|
||||
|
||||
"randomizer.dungeon.dungeon_counters": "Dungeon Chest Counters",
|
||||
@@ -160,6 +171,7 @@
|
||||
"randomizer.entrance.openpyramid.no": "No",
|
||||
"randomizer.entrance.shuffleganon": "Include Ganon's Tower and Pyramid Hole in shuffle pool",
|
||||
"randomizer.entrance.shufflelinks": "Include Link's House in the shuffle pool",
|
||||
"randomizer.entrance.shuffletavern": "Include the back of the tavern in the entrance shuffle pool",
|
||||
"randomizer.entrance.overworld_map": "Overworld Map",
|
||||
"randomizer.entrance.overworld_map.default": "Default (no location display)",
|
||||
"randomizer.entrance.overworld_map.compass": "Compass indicates location",
|
||||
@@ -167,21 +179,20 @@
|
||||
|
||||
"randomizer.entrance.entranceshuffle": "Entrance Shuffle",
|
||||
"randomizer.entrance.entranceshuffle.vanilla": "Vanilla",
|
||||
"randomizer.entrance.entranceshuffle.lite": "Lite",
|
||||
"randomizer.entrance.entranceshuffle.simple": "Simple",
|
||||
"randomizer.entrance.entranceshuffle.restricted": "Restricted",
|
||||
"randomizer.entrance.entranceshuffle.full": "Full",
|
||||
"randomizer.entrance.entranceshuffle.lite": "Lite",
|
||||
"randomizer.entrance.entranceshuffle.lean": "Lean",
|
||||
"randomizer.entrance.entranceshuffle.crossed": "Crossed",
|
||||
"randomizer.entrance.entranceshuffle.insanity": "Insanity",
|
||||
"randomizer.entrance.entranceshuffle.restricted_legacy": "Restricted (Legacy)",
|
||||
"randomizer.entrance.entranceshuffle.full_legacy": "Full (Legacy)",
|
||||
"randomizer.entrance.entranceshuffle.madness_legacy": "Madness (Legacy)",
|
||||
"randomizer.entrance.entranceshuffle.insanity_legacy": "Insanity (Legacy)",
|
||||
"randomizer.entrance.entranceshuffle.dungeonsfull": "Dungeons + Full",
|
||||
"randomizer.entrance.entranceshuffle.dungeonssimple": "Dungeons + Simple",
|
||||
|
||||
|
||||
"randomizer.entrance.take_any": "Take Any Caves",
|
||||
"randomizer.entrance.take_any.none": "None",
|
||||
"randomizer.entrance.take_any.random": "Random",
|
||||
"randomizer.entrance.take_any.fixed": "Fixed",
|
||||
|
||||
"randomizer.gameoptions.nobgm": "Disable Music & MSU-1",
|
||||
"randomizer.gameoptions.quickswap": "L/R Quickswapping",
|
||||
@@ -230,6 +241,7 @@
|
||||
"randomizer.generation.createspoiler": "Create Spoiler Log",
|
||||
"randomizer.generation.createrom": "Create Patched ROM",
|
||||
"randomizer.generation.calcplaythrough": "Calculate Playthrough",
|
||||
"randomizer.generation.print_custom_yaml": "Print Customizer File",
|
||||
"randomizer.generation.usestartinventory": "Use Starting Inventory",
|
||||
"randomizer.generation.usecustompool": "Use Custom Item Pool",
|
||||
|
||||
@@ -244,7 +256,7 @@
|
||||
"randomizer.generation.rom.dialog.allfiles": "All Files",
|
||||
|
||||
"randomizer.item.hints": "Include Helpful Hints",
|
||||
"randomizer.item.retro": "Retro mode (universal keys)",
|
||||
"randomizer.item.retro": "Retro mode",
|
||||
"randomizer.item.pseudoboots": "Start with Pseudo Boots",
|
||||
"randomizer.item.bombbag": "Bombbag",
|
||||
|
||||
@@ -315,6 +327,16 @@
|
||||
"randomizer.item.itemfunction.hard": "Hard",
|
||||
"randomizer.item.itemfunction.expert": "Expert",
|
||||
|
||||
"randomizer.item.flute_mode": "Flute Mode",
|
||||
"randomizer.item.flute_mode.normal": "Normal",
|
||||
"randomizer.item.flute_mode.active": "Pre-Activated",
|
||||
|
||||
"randomizer.item.bow_mode": "Bow Mode",
|
||||
"randomizer.item.bow_mode.progressive": "Progressive",
|
||||
"randomizer.item.bow_mode.silvers": "Silvers Separate",
|
||||
"randomizer.item.bow_mode.retro": "Retro (Progressive)",
|
||||
"randomizer.item.bow_mode.retro_silvers": "Retro + Silvers",
|
||||
|
||||
"randomizer.item.timer": "Timer Setting",
|
||||
"randomizer.item.timer.none": "No Timer",
|
||||
"randomizer.item.timer.display": "Stopwatch",
|
||||
@@ -323,11 +345,6 @@
|
||||
"randomizer.item.timer.ohko": "OHKO",
|
||||
"randomizer.item.timer.timed-countdown": "Timed Countdown",
|
||||
|
||||
"randomizer.item.progressives": "Progressive Items",
|
||||
"randomizer.item.progressives.on": "On",
|
||||
"randomizer.item.progressives.off": "Off",
|
||||
"randomizer.item.progressives.random": "Random",
|
||||
|
||||
"randomizer.item.accessibility": "Accessibility",
|
||||
"randomizer.item.accessibility.items": "100% Inventory",
|
||||
"randomizer.item.accessibility.locations": "100% Locations",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"keysanity": {
|
||||
"mapshuffle": { "type": "checkbox" },
|
||||
"compassshuffle": { "type": "checkbox" },
|
||||
"smallkeyshuffle": { "type": "checkbox" },
|
||||
"bigkeyshuffle": { "type": "checkbox" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
{
|
||||
"widgets": {
|
||||
"smallkeyshuffle": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"none",
|
||||
"wild",
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
"dungeondoorshuffle": {
|
||||
"type": "selectbox",
|
||||
"default": "vanilla",
|
||||
"options": [
|
||||
"vanilla",
|
||||
"basic",
|
||||
"partitioned",
|
||||
"crossed"
|
||||
]
|
||||
},
|
||||
@@ -22,6 +31,20 @@
|
||||
"width": 45
|
||||
}
|
||||
},
|
||||
"door_type_mode": {
|
||||
"type": "selectbox",
|
||||
"default": "basic",
|
||||
"options": [
|
||||
"original",
|
||||
"big",
|
||||
"all",
|
||||
"chaos"
|
||||
],
|
||||
"config": {
|
||||
"width": 45
|
||||
}
|
||||
},
|
||||
"decoupledoors": { "type": "checkbox" },
|
||||
"keydropshuffle": { "type": "checkbox" },
|
||||
"pottery": {
|
||||
"type": "selectbox",
|
||||
|
||||
@@ -11,8 +11,17 @@
|
||||
"width": 10
|
||||
}
|
||||
},
|
||||
"take_any": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"none",
|
||||
"random",
|
||||
"fixed"
|
||||
]
|
||||
},
|
||||
"shuffleganon": { "type": "checkbox" },
|
||||
"shufflelinks": { "type": "checkbox" },
|
||||
"shuffletavern": { "type": "checkbox" },
|
||||
"overworld_map": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bps": { "type": "checkbox" },
|
||||
"createspoiler": { "type": "checkbox" },
|
||||
"calcplaythrough": { "type": "checkbox" },
|
||||
"print_custom_yaml": { "type": "checkbox" },
|
||||
"usestartinventory": { "type": "checkbox" },
|
||||
"usecustompool": { "type": "checkbox" }
|
||||
}
|
||||
|
||||
@@ -86,6 +86,22 @@
|
||||
"expert"
|
||||
]
|
||||
},
|
||||
"flute_mode": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"normal",
|
||||
"active"
|
||||
]
|
||||
},
|
||||
"bow_mode": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"progressive",
|
||||
"silvers",
|
||||
"retro",
|
||||
"retro_silvers"
|
||||
]
|
||||
},
|
||||
"timer": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
@@ -97,14 +113,6 @@
|
||||
"timed-countdown"
|
||||
]
|
||||
},
|
||||
"progressives": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
"on",
|
||||
"off",
|
||||
"random"
|
||||
]
|
||||
},
|
||||
"accessibility": {
|
||||
"type": "selectbox",
|
||||
"options": [
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import yaml
|
||||
from yaml.representer import Representer
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import RaceRandom as random
|
||||
from BaseClasses import LocationType, DoorType
|
||||
from source.tools.MysteryUtils import roll_settings, get_weights
|
||||
|
||||
|
||||
class CustomSettings(object):
|
||||
|
||||
def __init__(self):
|
||||
self.file_source = None
|
||||
self.relative_dir = None
|
||||
self.world_rep = {}
|
||||
self.player_range = None
|
||||
|
||||
def load_yaml(self, file):
|
||||
self.file_source = load_yaml(file)
|
||||
head, filename = os.path.split(file)
|
||||
self.relative_dir = head
|
||||
|
||||
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:
|
||||
return None
|
||||
meta = defaultdict(lambda: None, self.file_source['meta'])
|
||||
return meta['players']
|
||||
|
||||
def adjust_args(self, args):
|
||||
def get_setting(value, default):
|
||||
if 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'])
|
||||
args.multi = get_setting(meta['players'], args.multi)
|
||||
args.algorithm = get_setting(meta['algorithm'], args.algorithm)
|
||||
args.outputname = get_setting(meta['name'], args.outputname)
|
||||
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)
|
||||
self.player_range = range(1, args.multi + 1)
|
||||
if 'settings' in self.file_source:
|
||||
for p in self.player_range:
|
||||
player_setting = self.file_source['settings'][p]
|
||||
if isinstance(player_setting, str):
|
||||
weights = get_weights(os.path.join(self.relative_dir, player_setting))
|
||||
settings = defaultdict(lambda: None, vars(roll_settings(weights)))
|
||||
args.mystery = True
|
||||
else:
|
||||
settings = defaultdict(lambda: None, player_setting)
|
||||
args.shuffle[p] = get_setting(settings['shuffle'], args.shuffle[p])
|
||||
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])
|
||||
args.item_functionality[p] = get_setting(settings['item_functionality'], args.item_functionality[p])
|
||||
args.goal[p] = get_setting(settings['goal'], args.goal[p])
|
||||
args.difficulty[p] = get_setting(settings['difficulty'], args.difficulty[p])
|
||||
args.accessibility[p] = get_setting(settings['accessibility'], args.accessibility[p])
|
||||
args.retro[p] = get_setting(settings['retro'], args.retro[p])
|
||||
args.take_any[p] = get_setting(settings['take_any'], args.take_any[p])
|
||||
args.hints[p] = get_setting(settings['hints'], args.hints[p])
|
||||
args.shopsanity[p] = get_setting(settings['shopsanity'], args.shopsanity[p])
|
||||
args.dropshuffle[p] = get_setting(settings['dropshuffle'], args.dropshuffle[p])
|
||||
args.pottery[p] = get_setting(settings['pottery'], args.pottery[p])
|
||||
|
||||
if get_setting(settings['keydropshuffle'], args.keydropshuffle[p]):
|
||||
args.dropshuffle[p] = True
|
||||
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.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])
|
||||
args.crystals_ganon[p] = get_setting(settings['crystals_ganon'], args.crystals_ganon[p])
|
||||
args.experimental[p] = get_setting(settings['experimental'], args.experimental[p])
|
||||
args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p])
|
||||
args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p])
|
||||
args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p])
|
||||
args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p])
|
||||
args.compassshuffle[p] = get_setting(settings['compassshuffle'], args.compassshuffle[p])
|
||||
|
||||
if get_setting(settings['keysanity'], args.keysanity):
|
||||
args.bigkeyshuffle[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.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])
|
||||
args.bombbag[p] = get_setting(settings['bombbag'], args.bombbag[p])
|
||||
args.shufflelinks[p] = get_setting(settings['shufflelinks'], args.shufflelinks[p])
|
||||
args.shuffletavern[p] = get_setting(settings['shuffletavern'], args.shuffletavern[p])
|
||||
args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p])
|
||||
args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p])
|
||||
args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p])
|
||||
args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p])
|
||||
args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p])
|
||||
args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p])
|
||||
|
||||
# mystery usage
|
||||
args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p])
|
||||
args.startinventory[p] = get_setting(settings['startinventory'], args.startinventory[p])
|
||||
|
||||
# rom adjust stuff
|
||||
args.sprite[p] = get_setting(settings['sprite'], args.sprite[p])
|
||||
args.disablemusic[p] = get_setting(settings['disablemusic'], args.disablemusic[p])
|
||||
args.quickswap[p] = get_setting(settings['quickswap'], args.quickswap[p])
|
||||
args.reduce_flashing[p] = get_setting(settings['reduce_flashing'], args.reduce_flashing[p])
|
||||
args.fastmenu[p] = get_setting(settings['fastmenu'], args.fastmenu[p])
|
||||
args.heartcolor[p] = get_setting(settings['heartcolor'], args.heartcolor[p])
|
||||
args.heartbeep[p] = get_setting(settings['heartbeep'], args.heartbeep[p])
|
||||
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])
|
||||
|
||||
def get_item_pool(self):
|
||||
if 'item_pool' in self.file_source:
|
||||
return self.file_source['item_pool']
|
||||
return None
|
||||
|
||||
def get_placements(self):
|
||||
if 'placements' in self.file_source:
|
||||
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']
|
||||
return None
|
||||
|
||||
def get_doors(self):
|
||||
if 'doors' in self.file_source:
|
||||
return self.file_source['doors']
|
||||
return None
|
||||
|
||||
def get_bosses(self):
|
||||
if 'bosses' in self.file_source:
|
||||
return self.file_source['bosses']
|
||||
return None
|
||||
|
||||
def get_start_inventory(self):
|
||||
if 'start_inventory' in self.file_source:
|
||||
return self.file_source['start_inventory']
|
||||
return None
|
||||
|
||||
def get_medallions(self):
|
||||
if 'medallions' in self.file_source:
|
||||
return self.file_source['medallions']
|
||||
return None
|
||||
|
||||
def get_drops(self):
|
||||
if 'drops' in self.file_source:
|
||||
return self.file_source['drops']
|
||||
return None
|
||||
|
||||
def create_from_world(self, world):
|
||||
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
|
||||
self.world_rep['settings'] = settings_dict
|
||||
for p in self.player_range:
|
||||
settings_dict[p] = {}
|
||||
settings_dict[p]['shuffle'] = world.shuffle[p]
|
||||
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]['decoupledoors'] = world.decoupledoors[p]
|
||||
settings_dict[p]['logic'] = world.logic[p]
|
||||
settings_dict[p]['mode'] = world.mode[p]
|
||||
settings_dict[p]['swords'] = world.swords[p]
|
||||
settings_dict[p]['flute_mode'] = world.flute_mode[p]
|
||||
settings_dict[p]['bow_mode'] = world.bow_mode[p]
|
||||
settings_dict[p]['difficulty'] = world.difficulty[p]
|
||||
settings_dict[p]['goal'] = world.goal[p]
|
||||
settings_dict[p]['accessibility'] = world.accessibility[p]
|
||||
settings_dict[p]['item_functionality'] = world.difficulty_adjustments[p]
|
||||
settings_dict[p]['take_any'] = world.take_any[p]
|
||||
settings_dict[p]['hints'] = world.hints[p]
|
||||
settings_dict[p]['shopsanity'] = world.shopsanity[p]
|
||||
settings_dict[p]['dropshuffle'] = world.dropshuffle[p]
|
||||
settings_dict[p]['pottery'] = world.pottery[p]
|
||||
settings_dict[p]['mixed_travel'] = world.mixed_travel[p]
|
||||
settings_dict[p]['standardize_palettes'] = world.standardize_palettes[p]
|
||||
settings_dict[p]['dungeon_counters'] = world.dungeon_counters[p]
|
||||
settings_dict[p]['crystals_gt'] = world.crystals_gt_orig[p]
|
||||
settings_dict[p]['crystals_ganon'] = world.crystals_ganon_orig[p]
|
||||
settings_dict[p]['experimental'] = world.experimental[p]
|
||||
settings_dict[p]['openpyramid'] = world.open_pyramid[p]
|
||||
settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p]
|
||||
settings_dict[p]['keyshuffle'] = world.keyshuffle[p]
|
||||
settings_dict[p]['mapshuffle'] = world.mapshuffle[p]
|
||||
settings_dict[p]['compassshuffle'] = world.compassshuffle[p]
|
||||
settings_dict[p]['shufflebosses'] = world.boss_shuffle[p]
|
||||
settings_dict[p]['shuffleenemies'] = world.enemy_shuffle[p]
|
||||
settings_dict[p]['enemy_health'] = world.enemy_health[p]
|
||||
settings_dict[p]['enemy_damage'] = world.enemy_damage[p]
|
||||
settings_dict[p]['shufflepots'] = world.potshuffle[p]
|
||||
settings_dict[p]['bombbag'] = world.bombbag[p]
|
||||
settings_dict[p]['shufflelinks'] = world.shufflelinks[p]
|
||||
settings_dict[p]['shuffletavern'] = world.shuffletavern[p]
|
||||
settings_dict[p]['overworld_map'] = world.overworld_map[p]
|
||||
settings_dict[p]['pseudoboots'] = world.pseudoboots[p]
|
||||
settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p]
|
||||
settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p]
|
||||
settings_dict[p]['beemizer'] = world.beemizer[p]
|
||||
|
||||
# rom adjust stuff
|
||||
# settings_dict[p]['sprite'] = world.sprite[p]
|
||||
# settings_dict[p]['disablemusic'] = world.disablemusic[p]
|
||||
# settings_dict[p]['quickswap'] = world.quickswap[p]
|
||||
# settings_dict[p]['reduce_flashing'] = world.reduce_flashing[p]
|
||||
# settings_dict[p]['fastmenu'] = world.fastmenu[p]
|
||||
# settings_dict[p]['heartcolor'] = world.heartcolor[p]
|
||||
# settings_dict[p]['heartbeep'] = world.heartbeep[p]
|
||||
# settings_dict[p]['ow_palettes'] = world.ow_palettes[p]
|
||||
# settings_dict[p]['uw_palettes'] = world.uw_palettes[p]
|
||||
# settings_dict[p]['shuffle_sfx'] = world.shuffle_sfx[p]
|
||||
# more settings?
|
||||
|
||||
def record_info(self, world):
|
||||
self.world_rep['bosses'] = bosses = {}
|
||||
self.world_rep['start_inventory'] = start_inv = {}
|
||||
for p in self.player_range:
|
||||
bosses[p] = {}
|
||||
start_inv[p] = []
|
||||
for dungeon in world.dungeons:
|
||||
for level, boss in dungeon.bosses.items():
|
||||
location = dungeon.name if level is None else f'{dungeon.name} ({level})'
|
||||
if boss and 'Agahnim' not in boss.name:
|
||||
bosses[dungeon.player][location] = boss.name
|
||||
for item in world.precollected_items:
|
||||
start_inv[item.player].append(item.name)
|
||||
|
||||
def record_item_pool(self, world):
|
||||
self.world_rep['item_pool'] = item_pool = {}
|
||||
self.world_rep['medallions'] = medallions = {}
|
||||
for p in self.player_range:
|
||||
item_pool[p] = defaultdict(int)
|
||||
medallions[p] = {}
|
||||
for item in world.itempool:
|
||||
item_pool[item.player][item.name] += 1
|
||||
for p, req_medals in world.required_medallions.items():
|
||||
medallions[p]['Misery Mire'] = req_medals[0]
|
||||
medallions[p]['Turtle Rock'] = req_medals[1]
|
||||
|
||||
def record_item_placements(self, world):
|
||||
self.world_rep['placements'] = placements = {}
|
||||
for p in self.player_range:
|
||||
placements[p] = {}
|
||||
for location in world.get_locations():
|
||||
if location.type != LocationType.Logical:
|
||||
if location.player != location.item.player:
|
||||
placements[location.player][location.name] = f'{location.item.name}#{location.item.player}'
|
||||
else:
|
||||
placements[location.player][location.name] = location.item.name
|
||||
|
||||
def record_entrances(self, world):
|
||||
self.world_rep['entrances'] = entrances = {}
|
||||
world.custom_entrances = {}
|
||||
for p in self.player_range:
|
||||
connections = entrances[p] = {}
|
||||
connections['entrances'] = {}
|
||||
connections['exits'] = {}
|
||||
connections['two-way'] = {}
|
||||
for key, data in world.spoiler.entrances.items():
|
||||
player = data['player'] if 'player' in data else 1
|
||||
connections = entrances[player]
|
||||
sub = 'two-way' if data['direction'] == 'both' else 'exits' if data['direction'] == 'exit' else 'entrances'
|
||||
connections[sub][data['entrance']] = data['exit']
|
||||
|
||||
def record_doors(self, world):
|
||||
self.world_rep['doors'] = doors = {}
|
||||
for p in self.player_range:
|
||||
meta_doors = doors[p] = {}
|
||||
lobbies = meta_doors['lobbies'] = {}
|
||||
door_map = meta_doors['doors'] = {}
|
||||
for portal in world.dungeon_portals[p]:
|
||||
lobbies[portal.name] = portal.door.name
|
||||
door_types = {DoorType.Normal, DoorType.SpiralStairs, DoorType.Interior}
|
||||
if world.intensity[p] > 1:
|
||||
door_types.update([DoorType.Open, DoorType.StraightStairs, DoorType.Ladder])
|
||||
door_kinds, skip = {}, set()
|
||||
for key, info in world.spoiler.doorTypes.items():
|
||||
if key[1] == p:
|
||||
if ' <-> ' in info['doorNames']:
|
||||
dns = info['doorNames'].split(' <-> ')
|
||||
for dn in dns:
|
||||
door_kinds[dn] = info['type'] # Key Door, Bomb Door, Dash Door
|
||||
else:
|
||||
door_kinds[info['doorNames']] = info['type']
|
||||
for door in world.doors:
|
||||
if door.player == p and not door.entranceFlag and door.type in door_types and door not in skip:
|
||||
if door.type == DoorType.Interior:
|
||||
if door.name in door_kinds:
|
||||
door_value = {'type': door_kinds[door.name]}
|
||||
door_map[door.name] = door_value # intra-tile note
|
||||
skip.add(door.dest)
|
||||
elif door.dest:
|
||||
if door.dest.dest == door:
|
||||
door_value = door.dest.name
|
||||
skip.add(door.dest)
|
||||
if door.name in door_kinds:
|
||||
door_value = {'dest': door_value, 'type': door_kinds[door.name]}
|
||||
if door.name not in door_kinds and door.dest.name in door_kinds:
|
||||
# tricky swap thing
|
||||
door_value = {'dest': door.name, 'type': door_kinds[door.dest.name]}
|
||||
door = door.dest # this is weird
|
||||
elif door.name in door_kinds:
|
||||
door_value = {'dest': door.dest.name, 'one-way': True, 'type': door_kinds[door.name]}
|
||||
else:
|
||||
door_value = {'dest': door.dest.name, 'one-way': True}
|
||||
door_map[door.name] = door_value
|
||||
|
||||
def record_medallions(self):
|
||||
pass
|
||||
|
||||
def write_to_file(self, destination):
|
||||
yaml.add_representer(defaultdict, Representer.represent_dict)
|
||||
with open(destination, 'w') as file:
|
||||
yaml.dump(self.world_rep, file)
|
||||
|
||||
|
||||
def load_yaml(path):
|
||||
if os.path.exists(Path(path)):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
|
||||
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
|
||||
|
||||
@@ -68,8 +68,9 @@ SETTINGSTOPROCESS = {
|
||||
"weapons": "swords",
|
||||
"itempool": "difficulty",
|
||||
"itemfunction": "item_functionality",
|
||||
"flute_mode": "flute_mode",
|
||||
"bow_mode": "bow_mode",
|
||||
"timer": "timer",
|
||||
"progressives": "progressive",
|
||||
"accessibility": "accessibility",
|
||||
"sortingalgo": "algorithm",
|
||||
"beemizer": "beemizer",
|
||||
@@ -89,8 +90,10 @@ SETTINGSTOPROCESS = {
|
||||
"openpyramid": "openpyramid",
|
||||
"shuffleganon": "shuffleganon",
|
||||
"shufflelinks": "shufflelinks",
|
||||
"shuffletavern": "shuffletavern",
|
||||
"entranceshuffle": "shuffle",
|
||||
"overworld_map": "overworld_map",
|
||||
"take_any": "take_any",
|
||||
},
|
||||
"enemizer": {
|
||||
"enemyshuffle": "shuffleenemies",
|
||||
@@ -105,6 +108,8 @@ SETTINGSTOPROCESS = {
|
||||
"bigkeyshuffle": "bigkeyshuffle",
|
||||
"dungeondoorshuffle": "door_shuffle",
|
||||
"dungeonintensity": "intensity",
|
||||
"door_type_mode": "door_type_mode",
|
||||
"decoupledoors": "decoupledoors",
|
||||
"keydropshuffle": "keydropshuffle",
|
||||
"dropshuffle": "dropshuffle",
|
||||
"pottery": "pottery",
|
||||
@@ -133,6 +138,7 @@ SETTINGSTOPROCESS = {
|
||||
"createspoiler": "create_spoiler",
|
||||
"createrom": "create_rom",
|
||||
"calcplaythrough": "calc_playthrough",
|
||||
"print_custom_yaml": "print_custom_yaml",
|
||||
"usestartinventory": "usestartinventory",
|
||||
"usecustompool": "custom",
|
||||
"saveonexit": "saveonexit"
|
||||
|
||||
@@ -0,0 +1,888 @@
|
||||
import RaceRandom as random
|
||||
import collections
|
||||
import logging
|
||||
import time
|
||||
|
||||
from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector
|
||||
from BaseClasses import hook_from_door, flooded_keys
|
||||
from Regions import dungeon_events, flooded_keys_reverse
|
||||
|
||||
|
||||
def pre_validate(builder, entrance_region_names, split_dungeon, world, player):
|
||||
pass
|
||||
# todo: determine the part of check_valid that are necessary here
|
||||
|
||||
|
||||
def generate_dungeon(builder, entrance_region_names, split_dungeon, world, player):
|
||||
if builder.valid_proposal: # we made this earlier in gen, just use it
|
||||
proposed_map = builder.valid_proposal
|
||||
else:
|
||||
proposed_map = generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player)
|
||||
builder.valid_proposal = proposed_map
|
||||
queue = collections.deque(proposed_map.items())
|
||||
while len(queue) > 0:
|
||||
a, b = queue.popleft()
|
||||
if world.decoupledoors[player]:
|
||||
connect_doors_one_way(a, b)
|
||||
else:
|
||||
connect_doors(a, b)
|
||||
queue.remove((b, a))
|
||||
if len(builder.sectors) == 0:
|
||||
return Sector()
|
||||
available_sectors = list(builder.sectors)
|
||||
master_sector = available_sectors.pop()
|
||||
for sub_sector in available_sectors:
|
||||
master_sector.regions.extend(sub_sector.regions)
|
||||
master_sector.outstanding_doors.clear()
|
||||
master_sector.r_name_set = None
|
||||
return master_sector
|
||||
|
||||
|
||||
def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon, world, player):
|
||||
logger = logging.getLogger('')
|
||||
name = builder.name
|
||||
logger.debug(f'Generating Dungeon: {name}')
|
||||
entrance_regions = convert_regions(entrance_region_names, world, player)
|
||||
excluded = {}
|
||||
for region in entrance_regions:
|
||||
portal = next((x for x in world.dungeon_portals[player] if x.door.entrance.parent_region == region), None)
|
||||
if portal:
|
||||
if portal.destination:
|
||||
excluded[region] = None
|
||||
elif len(entrance_regions) > 1:
|
||||
p_region = portal.door.entrance.connected_region
|
||||
access_region = next(x.parent_region for x in p_region.entrances
|
||||
if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld])
|
||||
if (access_region.name in world.inaccessible_regions[player] and
|
||||
region.name not in world.enabled_entrances[player]):
|
||||
excluded[region] = None
|
||||
elif split_dungeon and builder.sewers_access and builder.sewers_access.entrance.parent_region == region:
|
||||
continue
|
||||
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()
|
||||
for sector in builder.sectors:
|
||||
for door in sector.outstanding_doors:
|
||||
doors_to_connect[door.name] = door, idx
|
||||
idx += 1
|
||||
all_regions.update(sector.regions)
|
||||
finished = False
|
||||
# flag if standard and this is hyrule castle
|
||||
paths = determine_paths_for_dungeon(world, player, all_regions, name)
|
||||
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||
itr = 0
|
||||
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||
hash_code_set = set()
|
||||
start = time.time()
|
||||
while not finished:
|
||||
if itr > 1000:
|
||||
elasped = time.time() - start
|
||||
raise GenerationException(f'Generation taking too long. {elasped}. Ref {name}')
|
||||
if hash_code in hash_code_set:
|
||||
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||
if hash_code not in hash_code_set:
|
||||
hash_code_set.add(hash_code)
|
||||
explored_state = explore_proposal(name, entrance_regions, all_regions, proposed_map, doors_to_connect,
|
||||
world, player)
|
||||
if check_valid(name, explored_state, proposed_map, doors_to_connect, all_regions,
|
||||
paths, entrance_regions, world, player):
|
||||
finished = True
|
||||
else:
|
||||
proposed_map, hash_code = modify_proposal(proposed_map, explored_state, doors_to_connect,
|
||||
hash_code_set, world, player)
|
||||
itr += 1
|
||||
return proposed_map
|
||||
|
||||
|
||||
def create_random_proposal(doors_to_connect, world, player):
|
||||
logger = logging.getLogger('')
|
||||
hooks = [Hook.North, Hook.South, Hook.East, Hook.West, Hook.Stairs]
|
||||
primary_bucket = collections.defaultdict(list)
|
||||
secondary_bucket = collections.defaultdict(list)
|
||||
for name, door in doors_to_connect.items():
|
||||
door, idx = door
|
||||
primary_bucket[hook_from_door(door)].append(door)
|
||||
secondary_bucket[hook_from_door(door)].append(door)
|
||||
proposal = {}
|
||||
while True:
|
||||
hooks_left, left = [], 0
|
||||
for hook in hooks:
|
||||
hook_len = len(primary_bucket[hook])
|
||||
if hook_len > 0:
|
||||
hooks_left.append(hook)
|
||||
left += hook_len
|
||||
if left == 0:
|
||||
return proposal
|
||||
next_hook = random.choice(hooks_left)
|
||||
primary_door = random.choice(primary_bucket[next_hook])
|
||||
opp_hook, secondary_door = type_map[next_hook], None
|
||||
while (secondary_door is None or secondary_door == primary_door
|
||||
or decouple_check(primary_bucket[next_hook], secondary_bucket[opp_hook],
|
||||
primary_door, secondary_door, world, player)):
|
||||
secondary_door = random.choice(secondary_bucket[opp_hook])
|
||||
proposal[primary_door] = secondary_door
|
||||
primary_bucket[next_hook].remove(primary_door)
|
||||
secondary_bucket[opp_hook].remove(secondary_door)
|
||||
if not world.decoupledoors[player]:
|
||||
proposal[secondary_door] = primary_door
|
||||
primary_bucket[opp_hook].remove(secondary_door)
|
||||
secondary_bucket[next_hook].remove(primary_door)
|
||||
logger.debug(f' Linking {primary_door.name} <-> {secondary_door.name}')
|
||||
else:
|
||||
logger.debug(f' Linking {primary_door.name} -> {secondary_door.name}')
|
||||
|
||||
|
||||
def decouple_check(primary_list, secondary_list, primary_door, secondary_door, world, player):
|
||||
if world.decoupledoors[player] and len(primary_list) == 2 and len(secondary_list) == 2:
|
||||
primary_alone = next(d for d in primary_list if d != primary_door)
|
||||
secondary_alone = next(d for d in secondary_list if d != secondary_door)
|
||||
return primary_alone == secondary_alone
|
||||
return False
|
||||
|
||||
|
||||
def proposal_hash(doors_to_connect, proposed_map):
|
||||
hash_code = ''
|
||||
for name, door_pair in doors_to_connect.items():
|
||||
door, idx = door_pair
|
||||
hash_code += str(idx) + str(doors_to_connect[proposed_map[door].name][1])
|
||||
return hash_code
|
||||
|
||||
|
||||
def modify_proposal(proposed_map, explored_state, doors_to_connect, hash_code_set, world, player):
|
||||
logger = logging.getLogger('')
|
||||
hash_code, itr = None, 0
|
||||
while hash_code is None or hash_code in hash_code_set:
|
||||
if itr > 10:
|
||||
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||
return proposed_map, hash_code
|
||||
visited_bucket = collections.defaultdict(list)
|
||||
unvisted_bucket = collections.defaultdict(list)
|
||||
visited_choices = []
|
||||
unvisted_count = 0
|
||||
for door_one, door_two in proposed_map.items():
|
||||
if door_one in explored_state.visited_doors:
|
||||
visited_bucket[hook_from_door(door_one)].append(door_one)
|
||||
visited_choices.append(door_one)
|
||||
else:
|
||||
unvisted_bucket[hook_from_door(door_one)].append(door_one)
|
||||
unvisted_count += 1
|
||||
if unvisted_count == 0:
|
||||
# something is wrong beyond connectedness, crystal switch puzzle or bk layout - reshuffle
|
||||
proposed_map = create_random_proposal(doors_to_connect, world, player)
|
||||
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||
return proposed_map, hash_code
|
||||
|
||||
attempt, opp_hook = None, None
|
||||
opp_hook_len, possible_swaps = 0, list(visited_choices)
|
||||
while opp_hook_len == 0:
|
||||
if len(possible_swaps) == 0:
|
||||
break
|
||||
attempt = random.choice(possible_swaps)
|
||||
possible_swaps.remove(attempt)
|
||||
opp_hook = type_map[hook_from_door(attempt)]
|
||||
opp_hook_len = len(unvisted_bucket[opp_hook])
|
||||
if opp_hook_len == 0:
|
||||
itr += 1
|
||||
continue
|
||||
unvisted_bucket[opp_hook].sort(key=lambda d: d.name)
|
||||
new_door = random.choice(unvisted_bucket[opp_hook])
|
||||
old_target = proposed_map[attempt]
|
||||
proposed_map[attempt] = new_door
|
||||
if not world.decoupledoors[player]:
|
||||
old_attempt = proposed_map[new_door]
|
||||
else:
|
||||
old_attempt = next(x for x in proposed_map if proposed_map[x] == new_door)
|
||||
proposed_map[old_attempt] = old_target
|
||||
if not world.decoupledoors[player]:
|
||||
proposed_map[old_target] = old_attempt
|
||||
proposed_map[new_door] = attempt
|
||||
hash_code = proposal_hash(doors_to_connect, proposed_map)
|
||||
itr += 1
|
||||
|
||||
if not world.decoupledoors[player]:
|
||||
logger.debug(f' Re-linking {attempt.name} <-> {new_door.name}')
|
||||
logger.debug(f' Re-linking {old_attempt.name} <-> {old_target.name}')
|
||||
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
|
||||
|
||||
|
||||
def explore_proposal(name, entrance_regions, all_regions, proposed_map, valid_doors, world, player):
|
||||
start = ExplorationState(dungeon=name)
|
||||
original_state = extend_reachable_state_lenient(entrance_regions, start, proposed_map,
|
||||
all_regions, valid_doors, world, player)
|
||||
return original_state
|
||||
|
||||
|
||||
def check_valid(name, exploration_state, proposed_map, doors_to_connect, all_regions,
|
||||
paths, entrance_regions, world, player):
|
||||
all_visited = set()
|
||||
all_visited.update(exploration_state.visited_blue)
|
||||
all_visited.update(exploration_state.visited_orange)
|
||||
if len(all_regions.difference(all_visited)) > 0:
|
||||
return False
|
||||
if not valid_paths(name, paths, entrance_regions, doors_to_connect, all_regions, proposed_map, world, player):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def determine_if_bk_needed(sector, split_dungeon, bk_special, world, player):
|
||||
if not split_dungeon or bk_special:
|
||||
for region in sector.regions:
|
||||
for ext in region.exits:
|
||||
door = world.check_for_door(ext.name, player)
|
||||
if door is not None and door.bigKey:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_for_special(regions):
|
||||
for region in regions:
|
||||
for loc in region.locations:
|
||||
if loc.forced_big_key():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def valid_paths(name, paths, entrance_regions, valid_doors, all_regions, proposed_map, world, player):
|
||||
for path in paths:
|
||||
if type(path) is tuple:
|
||||
target = path[1]
|
||||
start_regions = []
|
||||
for region in all_regions:
|
||||
if path[0] == region.name:
|
||||
start_regions.append(region)
|
||||
break
|
||||
else:
|
||||
target = path
|
||||
start_regions = entrance_regions
|
||||
if not valid_path(name, start_regions, target, valid_doors, proposed_map, all_regions, world, player):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def valid_path(name, starting_regions, target, valid_doors, proposed_map, all_regions, world, player):
|
||||
target_regions = set()
|
||||
if type(target) is not list:
|
||||
for region in all_regions:
|
||||
if target == region.name:
|
||||
target_regions.add(region)
|
||||
break
|
||||
else:
|
||||
for region in all_regions:
|
||||
if region.name in target:
|
||||
target_regions.add(region)
|
||||
|
||||
start = ExplorationState(dungeon=name)
|
||||
original_state = extend_reachable_state_lenient(starting_regions, start, proposed_map, all_regions,
|
||||
valid_doors, world, player)
|
||||
|
||||
for exp_door in original_state.unattached_doors:
|
||||
if not exp_door.door.blocked or exp_door.door.trapFlag != 0:
|
||||
return True # outstanding connection possible
|
||||
for target in target_regions:
|
||||
if original_state.visited_at_all(target):
|
||||
return True
|
||||
return False # couldn't find an outstanding door or the target
|
||||
|
||||
|
||||
boss_path_checks = ['Eastern Boss', 'Desert Boss', 'Hera Boss', 'Tower Agahnim 1', 'PoD Boss', 'Swamp Boss',
|
||||
'Skull Boss', 'Ice Boss', 'Mire Boss', 'TR Boss', 'GT Agahnim 2']
|
||||
|
||||
# pinball is allowed to orphan you
|
||||
drop_path_checks = ['Skull Pot Circle', 'Skull Left Drop', 'Skull Back Drop', 'Sewers Rat Path']
|
||||
|
||||
|
||||
def determine_paths_for_dungeon(world, player, all_regions, name):
|
||||
all_r_names = set(x.name for x in all_regions)
|
||||
paths = []
|
||||
non_hole_portals = []
|
||||
for portal in world.dungeon_portals[player]:
|
||||
if portal.door.entrance.parent_region in all_regions:
|
||||
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 Dungeon':
|
||||
paths.append('Hyrule Dungeon Cellblock')
|
||||
paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room'))
|
||||
if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town':
|
||||
paths.append('Thieves Attic Window')
|
||||
elif 'Thieves Attic Window' in all_r_names:
|
||||
paths.append('Thieves Attic Window')
|
||||
for boss in boss_path_checks:
|
||||
if boss in all_r_names:
|
||||
paths.append(boss)
|
||||
if 'Thieves Boss' in all_r_names:
|
||||
paths.append('Thieves Boss')
|
||||
if world.get_dungeon("Thieves Town", player).boss.enemizer_name == 'Blind':
|
||||
paths.append(('Thieves Blind\'s Cell', 'Thieves Boss'))
|
||||
for drop_check in drop_path_checks:
|
||||
if drop_check in all_r_names:
|
||||
paths.append((drop_check, non_hole_portals))
|
||||
return paths
|
||||
|
||||
|
||||
def convert_regions(region_names, world, player):
|
||||
region_list = []
|
||||
for name in region_names:
|
||||
region_list.append(world.get_region(name, player))
|
||||
return region_list
|
||||
|
||||
|
||||
type_map = {
|
||||
Hook.Stairs: Hook.Stairs,
|
||||
Hook.North: Hook.South,
|
||||
Hook.South: Hook.North,
|
||||
Hook.West: Hook.East,
|
||||
Hook.East: Hook.West
|
||||
}
|
||||
|
||||
|
||||
def connect_doors(a, b):
|
||||
# Return on unsupported types.
|
||||
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
|
||||
return
|
||||
# Connect supported types
|
||||
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||
connect_two_way(a.entrance, b.entrance)
|
||||
dep_doors, target = [], None
|
||||
if len(a.dependents) > 0:
|
||||
dep_doors, target = a.dependents, b
|
||||
elif len(b.dependents) > 0:
|
||||
dep_doors, target = b.dependents, a
|
||||
if target is not None:
|
||||
target_region = target.entrance.parent_region
|
||||
for dep in dep_doors:
|
||||
connect_simple_door(dep, target_region)
|
||||
return
|
||||
# If we failed to account for a type, panic
|
||||
raise RuntimeError('Unknown door type ' + a.type.name)
|
||||
|
||||
|
||||
def connect_doors_one_way(a, b):
|
||||
# Return on unsupported types.
|
||||
if a.type in [DoorType.Hole, DoorType.Warp, DoorType.Interior, DoorType.Logical]:
|
||||
return
|
||||
# Connect supported types
|
||||
if a.type in [DoorType.Normal, DoorType.SpiralStairs, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
||||
connect_one_way(a.entrance, b.entrance)
|
||||
dep_doors, target = [], None
|
||||
if len(a.dependents) > 0:
|
||||
dep_doors, target = a.dependents, b
|
||||
if target is not None:
|
||||
target_region = target.entrance.parent_region
|
||||
for dep in dep_doors:
|
||||
connect_simple_door(dep, target_region)
|
||||
return
|
||||
# If we failed to account for a type, panic
|
||||
raise RuntimeError('Unknown door type ' + a.type.name)
|
||||
|
||||
|
||||
def connect_two_way(entrance, ext):
|
||||
|
||||
# if these were already connected somewhere, remove the backreference
|
||||
if entrance.connected_region is not None:
|
||||
entrance.connected_region.entrances.remove(entrance)
|
||||
if ext.connected_region is not None:
|
||||
ext.connected_region.entrances.remove(ext)
|
||||
|
||||
entrance.connect(ext.parent_region)
|
||||
ext.connect(entrance.parent_region)
|
||||
if entrance.parent_region.dungeon:
|
||||
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||
x = entrance.door
|
||||
y = ext.door
|
||||
if x is not None:
|
||||
x.dest = y
|
||||
if y is not None:
|
||||
y.dest = x
|
||||
|
||||
|
||||
def connect_one_way(entrance, ext):
|
||||
|
||||
# if these were already connected somewhere, remove the backreference
|
||||
if entrance.connected_region is not None:
|
||||
entrance.connected_region.entrances.remove(entrance)
|
||||
|
||||
entrance.connect(ext.parent_region)
|
||||
if entrance.parent_region.dungeon:
|
||||
ext.parent_region.dungeon = entrance.parent_region.dungeon
|
||||
x = entrance.door
|
||||
if x is not None:
|
||||
x.dest = ext.door
|
||||
|
||||
|
||||
def connect_simple_door(exit_door, region):
|
||||
exit_door.entrance.connect(region)
|
||||
exit_door.dest = region
|
||||
|
||||
|
||||
special_big_key_doors = ['Hyrule Dungeon Cellblock Door', "Thieves Blind's Cell Door"]
|
||||
|
||||
|
||||
class ExplorationState(object):
|
||||
|
||||
def __init__(self, init_crystal=CrystalBarrier.Orange, dungeon=None):
|
||||
|
||||
self.unattached_doors = []
|
||||
self.avail_doors = []
|
||||
self.event_doors = []
|
||||
|
||||
self.visited_orange = []
|
||||
self.visited_blue = []
|
||||
self.visited_doors = set()
|
||||
self.events = set()
|
||||
self.crystal = init_crystal
|
||||
|
||||
# key region stuff
|
||||
self.door_krs = {}
|
||||
|
||||
# key validation stuff
|
||||
self.small_doors = []
|
||||
self.big_doors = []
|
||||
self.opened_doors = []
|
||||
self.big_key_opened = False
|
||||
self.big_key_special = False
|
||||
|
||||
self.found_locations = []
|
||||
self.ttl_locations = 0
|
||||
self.used_locations = 0
|
||||
self.key_locations = 0
|
||||
self.used_smalls = 0
|
||||
self.bk_found = set()
|
||||
|
||||
self.non_door_entrances = []
|
||||
self.dungeon = dungeon
|
||||
self.pinball_used = False
|
||||
|
||||
self.prize_door_set = {}
|
||||
self.prize_doors = []
|
||||
self.prize_doors_opened = False
|
||||
|
||||
def copy(self):
|
||||
ret = ExplorationState(dungeon=self.dungeon)
|
||||
ret.unattached_doors = list(self.unattached_doors)
|
||||
ret.avail_doors = list(self.avail_doors)
|
||||
ret.event_doors = list(self.event_doors)
|
||||
ret.visited_orange = list(self.visited_orange)
|
||||
ret.visited_blue = list(self.visited_blue)
|
||||
ret.events = set(self.events)
|
||||
ret.crystal = self.crystal
|
||||
ret.door_krs = self.door_krs.copy()
|
||||
|
||||
ret.small_doors = list(self.small_doors)
|
||||
ret.big_doors = list(self.big_doors)
|
||||
ret.opened_doors = list(self.opened_doors)
|
||||
ret.big_key_opened = self.big_key_opened
|
||||
ret.big_key_special = self.big_key_special
|
||||
ret.ttl_locations = self.ttl_locations
|
||||
ret.key_locations = self.key_locations
|
||||
ret.used_locations = self.used_locations
|
||||
ret.used_smalls = self.used_smalls
|
||||
ret.found_locations = list(self.found_locations)
|
||||
ret.bk_found = set(self.bk_found)
|
||||
|
||||
ret.non_door_entrances = list(self.non_door_entrances)
|
||||
ret.dungeon = self.dungeon
|
||||
ret.pinball_used = self.pinball_used
|
||||
|
||||
ret.prize_door_set = dict(self.prize_door_set)
|
||||
ret.prize_doors = list(self.prize_doors)
|
||||
ret.prize_doors_opened = self.prize_doors_opened
|
||||
return ret
|
||||
|
||||
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):
|
||||
if region.type != RegionType.Dungeon:
|
||||
self.crystal = CrystalBarrier.Orange
|
||||
if self.crystal == CrystalBarrier.Either:
|
||||
if region not in self.visited_blue:
|
||||
self.visited_blue.append(region)
|
||||
if region not in self.visited_orange:
|
||||
self.visited_orange.append(region)
|
||||
elif self.crystal == CrystalBarrier.Orange:
|
||||
self.visited_orange.append(region)
|
||||
elif self.crystal == CrystalBarrier.Blue:
|
||||
self.visited_blue.append(region)
|
||||
if region.type == RegionType.Dungeon:
|
||||
for location in region.locations:
|
||||
if key_checks and location not in self.found_locations:
|
||||
if location.forced_item and 'Small Key' in location.item.name:
|
||||
self.key_locations += 1
|
||||
if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']:
|
||||
self.ttl_locations += 1
|
||||
if location not in self.found_locations:
|
||||
self.found_locations.append(location)
|
||||
if not bk_flag:
|
||||
self.bk_found.add(location)
|
||||
if location.name in dungeon_events and location.name not in self.events:
|
||||
if self.flooded_key_check(location):
|
||||
self.perform_event(location.name, key_region)
|
||||
if location.name in flooded_keys_reverse.keys() and self.location_found(
|
||||
flooded_keys_reverse[location.name]):
|
||||
self.perform_event(flooded_keys_reverse[location.name], key_region)
|
||||
if '- Prize' in location.name:
|
||||
self.prize_received = True
|
||||
|
||||
def flooded_key_check(self, location):
|
||||
if location.name not in flooded_keys.keys():
|
||||
return True
|
||||
return flooded_keys[location.name] in [x.name for x in self.found_locations]
|
||||
|
||||
def location_found(self, location_name):
|
||||
for l in self.found_locations:
|
||||
if l.name == location_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def perform_event(self, location_name, key_region):
|
||||
self.events.add(location_name)
|
||||
queue = collections.deque(self.event_doors)
|
||||
while len(queue) > 0:
|
||||
exp_door = queue.popleft()
|
||||
if exp_door.door.req_event == location_name:
|
||||
self.avail_doors.append(exp_door)
|
||||
self.event_doors.remove(exp_door)
|
||||
if key_region is not None:
|
||||
d_name = exp_door.door.name
|
||||
if d_name not in self.door_krs.keys():
|
||||
self.door_krs[d_name] = key_region
|
||||
|
||||
def add_all_entrance_doors_check_unattached(self, region, world, player):
|
||||
door_list = [x for x in get_doors(world, region, player) if x.type in [DoorType.Normal, DoorType.SpiralStairs]]
|
||||
door_list.extend(get_entrance_doors(world, region, player))
|
||||
for door in door_list:
|
||||
if self.can_traverse(door):
|
||||
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
|
||||
self.append_door_to_list(door, self.unattached_doors)
|
||||
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)
|
||||
for entrance in region.entrances:
|
||||
door = world.check_for_door(entrance.name, player)
|
||||
if door is None:
|
||||
self.non_door_entrances.append(entrance)
|
||||
|
||||
def add_all_doors_check_unattached(self, region, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if self.can_traverse(door):
|
||||
if door.controller is not None:
|
||||
door = door.controller
|
||||
if door.dest is None and not self.in_door_list_ic(door, self.unattached_doors):
|
||||
self.append_door_to_list(door, self.unattached_doors)
|
||||
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(self, region, proposed_map, valid_doors, flag, world, player, exception):
|
||||
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 door.blocked and exception(door):
|
||||
self.pinball_used = True
|
||||
if self.can_traverse(door, exception):
|
||||
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, flag)
|
||||
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, flag)
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors, flag)
|
||||
|
||||
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:
|
||||
self.visited_doors.add(door)
|
||||
if self.can_traverse_ignore_traps(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:
|
||||
if door.controller is not None:
|
||||
door = door.controller
|
||||
if 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, False)
|
||||
elif not self.in_door_list(door, self.avail_doors):
|
||||
self.append_door_to_list(door, self.avail_doors, False)
|
||||
|
||||
def add_all_doors_check_key_region(self, region, key_region, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if self.can_traverse(door):
|
||||
if 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)
|
||||
if door.name not in self.door_krs.keys():
|
||||
self.door_krs[door.name] = key_region
|
||||
else:
|
||||
if door.name not in self.door_krs.keys():
|
||||
self.door_krs[door.name] = key_region
|
||||
|
||||
def add_all_doors_check_keys(self, region, key_door_proposal, world, player):
|
||||
for door in get_doors(world, region, player):
|
||||
if self.can_traverse(door):
|
||||
if door.controller:
|
||||
door = door.controller
|
||||
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:
|
||||
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:
|
||||
if 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 visited(self, region):
|
||||
if self.crystal == CrystalBarrier.Either:
|
||||
return region in self.visited_blue and region in self.visited_orange
|
||||
elif self.crystal == CrystalBarrier.Orange:
|
||||
return region in self.visited_orange
|
||||
elif self.crystal == CrystalBarrier.Blue:
|
||||
return region in self.visited_blue
|
||||
return False
|
||||
|
||||
def visited_at_all(self, region):
|
||||
return region in self.visited_blue or region in self.visited_orange
|
||||
|
||||
def found_forced_bk(self):
|
||||
for location in self.found_locations:
|
||||
if location.forced_big_key():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_traverse(self, door, exception=None):
|
||||
if door.blocked:
|
||||
return exception(door) if exception else False
|
||||
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
|
||||
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
|
||||
return True
|
||||
|
||||
def can_traverse_ignore_traps(self, door):
|
||||
if door.blocked and door.trapFlag == 0:
|
||||
return False
|
||||
if door.crystal not in [CrystalBarrier.Null, CrystalBarrier.Either]:
|
||||
return self.crystal == CrystalBarrier.Either or door.crystal == self.crystal
|
||||
return True
|
||||
|
||||
def count_locations_exclude_specials(self, world, player):
|
||||
return count_locations_exclude_big_chest(self.found_locations, world, player)
|
||||
|
||||
def validate(self, door, region, world, player):
|
||||
return self.can_traverse(door) and not self.visited(region) and valid_region_to_explore(region, self.dungeon,
|
||||
world, player)
|
||||
|
||||
def in_door_list(self, door, door_list):
|
||||
for d in door_list:
|
||||
if d.door == door and d.crystal == self.crystal:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def in_door_list_ic(door, door_list):
|
||||
for d in door_list:
|
||||
if d.door == door:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def find_door_in_list(door, door_list):
|
||||
for d in door_list:
|
||||
if d.door == door:
|
||||
return d
|
||||
return None
|
||||
|
||||
def append_door_to_list(self, door, door_list, flag=False):
|
||||
if door.crystal == CrystalBarrier.Null:
|
||||
door_list.append(ExplorableDoor(door, self.crystal, flag))
|
||||
else:
|
||||
door_list.append(ExplorableDoor(door, door.crystal, flag))
|
||||
|
||||
def key_door_sort(self, d):
|
||||
if d.door.smallKey:
|
||||
if d.door in self.opened_doors:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
return 2
|
||||
|
||||
|
||||
def count_locations_exclude_big_chest(locations, world, player):
|
||||
cnt = 0
|
||||
for loc in locations:
|
||||
if ('- Big Chest' not in loc.name and not loc.forced_item and not reserved_location(loc, world, player)
|
||||
and not prize_or_event(loc) and not blind_boss_unavail(loc, locations, world, player)):
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
|
||||
def prize_or_event(loc):
|
||||
return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']
|
||||
|
||||
|
||||
def reserved_location(loc, world, player):
|
||||
return hasattr(world, 'item_pool_config') and loc.name in world.item_pool_config.reserved_locations[player]
|
||||
|
||||
|
||||
def blind_boss_unavail(loc, locations, world, player):
|
||||
if loc.name == "Thieves' Town - Boss":
|
||||
return (loc.parent_region.dungeon.boss.name == 'Blind' and
|
||||
(not any(x for x in locations if x.name == 'Suspicious Maiden') or
|
||||
(world.get_region('Thieves Attic Window', player).dungeon.name == 'Thieves Town' and
|
||||
not any(x for x in locations if x.name == 'Attic Cracked Floor'))))
|
||||
return False
|
||||
|
||||
|
||||
class ExplorableDoor(object):
|
||||
|
||||
def __init__(self, door, crystal, flag):
|
||||
self.door = door
|
||||
self.crystal = crystal
|
||||
self.flag = flag
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s (%s)' % (self.door.name, self.crystal.name)
|
||||
|
||||
|
||||
def extend_reachable_state_improved(search_regions, state, proposed_map, all_regions, valid_doors, bk_flag, world, player, exception):
|
||||
local_state = state.copy()
|
||||
for region in search_regions:
|
||||
local_state.visit_region(region)
|
||||
local_state.add_all_doors_check_proposed(region, proposed_map, valid_doors, False, world, player, exception)
|
||||
while len(local_state.avail_doors) > 0:
|
||||
explorable_door = local_state.next_avail_door()
|
||||
if explorable_door.door.bigKey:
|
||||
if bk_flag:
|
||||
big_not_found = (not special_big_key_found(local_state) if local_state.big_key_special
|
||||
else local_state.count_locations_exclude_specials(world, player) == 0)
|
||||
if big_not_found:
|
||||
continue # we can't open this door
|
||||
if explorable_door.door in proposed_map:
|
||||
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
|
||||
else:
|
||||
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
|
||||
if connect_region is not None:
|
||||
if valid_region_to_explore_in_regions(connect_region, all_regions, world, player) and not local_state.visited(
|
||||
connect_region):
|
||||
flag = explorable_door.flag or explorable_door.door.bigKey
|
||||
local_state.visit_region(connect_region, bk_flag=flag)
|
||||
local_state.add_all_doors_check_proposed(connect_region, proposed_map, valid_doors, flag, world, player, exception)
|
||||
return local_state
|
||||
|
||||
|
||||
def extend_reachable_state_lenient(search_regions, state, proposed_map, all_regions, valid_doors, world, player):
|
||||
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)
|
||||
while len(local_state.avail_doors) > 0:
|
||||
explorable_door = local_state.next_avail_door()
|
||||
if explorable_door.door in proposed_map:
|
||||
connect_region = world.get_entrance(proposed_map[explorable_door.door].name, player).parent_region
|
||||
else:
|
||||
connect_region = world.get_entrance(explorable_door.door.name, player).connected_region
|
||||
if connect_region is not None:
|
||||
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)
|
||||
return local_state
|
||||
|
||||
|
||||
def special_big_key_found(state):
|
||||
for location in state.found_locations:
|
||||
if location.forced_item and location.forced_item.bigkey:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def valid_region_to_explore_in_regions(region, all_regions, world, player):
|
||||
if region is None:
|
||||
return False
|
||||
return ((region.type == RegionType.Dungeon and region in all_regions)
|
||||
or region.name in world.inaccessible_regions[player]
|
||||
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||
|
||||
|
||||
def valid_region_to_explore(region, name, world, player):
|
||||
if region is None:
|
||||
return False
|
||||
return ((region.type == RegionType.Dungeon and region.dungeon and region.dungeon.name in name)
|
||||
or region.name in world.inaccessible_regions[player]
|
||||
or (region.name == 'Hyrule Castle Ledge' and world.mode[player] == 'standard'))
|
||||
|
||||
|
||||
def get_doors(world, region, player):
|
||||
res = []
|
||||
for ext in region.exits:
|
||||
door = world.check_for_door(ext.name, player)
|
||||
if door is not None:
|
||||
res.append(door)
|
||||
return res
|
||||
|
||||
|
||||
def get_entrance_doors(world, region, player):
|
||||
res = []
|
||||
for ext in region.entrances:
|
||||
door = world.check_for_door(ext.name, player)
|
||||
if door is not None:
|
||||
res.append(door)
|
||||
return res
|
||||
|
||||
|
||||
class GenerationException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@@ -157,8 +157,8 @@ def adjust_page(top, parent, settings):
|
||||
"quickswap": "quickswap",
|
||||
"nobgm": "disablemusic",
|
||||
"reduce_flashing": "reduce_flashing",
|
||||
'msu_resume': 'msu_resume',
|
||||
"shuffle_sfx": "shuffle_sfx",
|
||||
"msu_resume": "msu_resume"
|
||||
}
|
||||
guiargs = Namespace()
|
||||
for option in options:
|
||||
|
||||
@@ -220,6 +220,11 @@ def create_guiargs(parent):
|
||||
# Get baserom path
|
||||
guiargs.rom = parent.pages["randomizer"].pages["generation"].widgets["rom"].storageVar.get()
|
||||
|
||||
# Get customizer path
|
||||
customizer_value = parent.pages["randomizer"].pages["generation"].widgets["customizer"].storageVar.get()
|
||||
if customizer_value and customizer_value != 'None':
|
||||
guiargs.customizer = customizer_value
|
||||
|
||||
# Get if we're using the Custom Item Pool
|
||||
guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get())
|
||||
|
||||
@@ -280,4 +285,12 @@ def create_guiargs(parent):
|
||||
guiargs.dropshuffle = 1
|
||||
guiargs.pottery = 'keys' if guiargs.pottery == 'none' else guiargs.pottery
|
||||
|
||||
if guiargs.retro or guiargs.mode == 'retro':
|
||||
if guiargs.bow_mode == 'progressive':
|
||||
guiargs.bow_mode = 'retro'
|
||||
elif guiargs.bow_mode == 'silvers':
|
||||
guiargs.bow_mode = 'retro_silvers'
|
||||
guiargs.take_any = 'random' if guiargs.take_any == 'none' else guiargs.take_any
|
||||
guiargs.keyshuffle = 'universal'
|
||||
|
||||
return guiargs
|
||||
|
||||
@@ -27,7 +27,7 @@ def entrando_page(parent):
|
||||
for key in dictWidgets:
|
||||
self.widgets[key] = dictWidgets[key]
|
||||
packAttrs = {"anchor":E}
|
||||
if self.widgets[key].type == "checkbox" or key == "openpyramid":
|
||||
if self.widgets[key].type == "checkbox" or key in ["openpyramid", "take_any"]:
|
||||
packAttrs["anchor"] = W
|
||||
self.widgets[key].pack(packAttrs)
|
||||
|
||||
|
||||
@@ -79,6 +79,48 @@ def generation_page(parent,settings):
|
||||
# frame: pack
|
||||
self.widgets[widget].pieces["frame"].pack(fill=X)
|
||||
|
||||
|
||||
self.frames["customizer"] = Frame(self)
|
||||
self.frames["customizer"].pack(anchor=W, fill=X)
|
||||
## Customizer file
|
||||
# This one's more-complicated, build it and stuff it
|
||||
# widget ID
|
||||
widget = "customizer"
|
||||
|
||||
# Empty object
|
||||
self.widgets[widget] = Empty()
|
||||
# pieces
|
||||
self.widgets[widget].pieces = {}
|
||||
|
||||
# frame
|
||||
self.widgets[widget].pieces["frame"] = Frame(self.frames["customizer"])
|
||||
# frame: label
|
||||
self.widgets[widget].pieces["frame"].label = Label(self.widgets[widget].pieces["frame"], text='Customizer File: ')
|
||||
# storage var
|
||||
self.widgets[widget].storageVar = StringVar()
|
||||
# textbox
|
||||
self.widgets[widget].pieces["textbox"] = Entry(self.widgets[widget].pieces["frame"],
|
||||
textvariable=self.widgets[widget].storageVar)
|
||||
self.widgets[widget].storageVar.set(settings["customizer"])
|
||||
|
||||
# FIXME: Translate these
|
||||
def FileSelect():
|
||||
file = filedialog.askopenfilename(filetypes=[("Yaml Files", (".yaml", ".yml")), ("All Files", "*")],
|
||||
initialdir=os.path.join("."))
|
||||
self.widgets["customizer"].storageVar.set(file)
|
||||
# dialog button
|
||||
self.widgets[widget].pieces["button"] = Button(self.widgets[widget].pieces["frame"],
|
||||
text='Select File', command=FileSelect)
|
||||
|
||||
# frame label: pack
|
||||
self.widgets[widget].pieces["frame"].label.pack(side=LEFT)
|
||||
# textbox: pack
|
||||
self.widgets[widget].pieces["textbox"].pack(side=LEFT, fill=X, expand=True)
|
||||
# button: pack
|
||||
self.widgets[widget].pieces["button"].pack(side=LEFT)
|
||||
# frame: pack
|
||||
self.widgets[widget].pieces["frame"].pack(fill=X)
|
||||
|
||||
## Run Diagnostics
|
||||
# This one's more-complicated, build it and stuff it
|
||||
# widget ID
|
||||
|
||||
+46
-10
@@ -18,6 +18,8 @@ class ItemPoolConfig(object):
|
||||
self.item_pool = None
|
||||
self.placeholders = None
|
||||
self.reserved_locations = defaultdict(set)
|
||||
self.restricted = {}
|
||||
self.preferred = {}
|
||||
|
||||
self.recorded_choices = []
|
||||
|
||||
@@ -82,10 +84,11 @@ def create_item_pool_config(world):
|
||||
if world.shopsanity[player]:
|
||||
for item, locs in shop_vanilla_mapping.items():
|
||||
config.static_placement[player][item].extend(locs)
|
||||
if world.retro[player]:
|
||||
if world.take_any[player] != 'none':
|
||||
for item, locs in retro_vanilla_mapping.items():
|
||||
config.static_placement[player][item].extend(locs)
|
||||
# universal keys
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
universal_key_locations = []
|
||||
for item, locs in vanilla_mapping.items():
|
||||
if 'Small Key' in item:
|
||||
@@ -98,12 +101,13 @@ def create_item_pool_config(world):
|
||||
for item, locs in potkeys_vanilla_mapping.items():
|
||||
universal_key_locations.extend(locs)
|
||||
if world.shopsanity[player]:
|
||||
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
|
||||
single_arrow_placement.append('Red Shield Shop - Right')
|
||||
config.static_placement[player]['Single Arrow'] = single_arrow_placement
|
||||
universal_key_locations.extend(shop_vanilla_mapping['Small Heart'])
|
||||
universal_key_locations.extend(shop_vanilla_mapping['Blue Shield'])
|
||||
config.static_placement[player]['Small Key (Universal)'] = universal_key_locations
|
||||
if world.bow_mode[player].startswith('retro') and world.shopsanity[player]:
|
||||
single_arrow_placement = list(shop_vanilla_mapping['Red Potion'])
|
||||
single_arrow_placement.append('Red Shield Shop - Right')
|
||||
config.static_placement[player]['Single Arrow'] = single_arrow_placement
|
||||
config.location_groups[player] = [
|
||||
LocationGroup('Major').locs(mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers']),
|
||||
LocationGroup('bkhp').locs(mode_grouping['Heart Pieces']),
|
||||
@@ -124,7 +128,7 @@ def create_item_pool_config(world):
|
||||
groups.locations.extend(mode_grouping['Big Keys'])
|
||||
if world.dropshuffle[player] != 'none':
|
||||
groups.locations.extend(mode_grouping['Big Key Drops'])
|
||||
if world.keyshuffle[player]:
|
||||
if world.keyshuffle[player] != 'none':
|
||||
groups.locations.extend(mode_grouping['Small Keys'])
|
||||
if world.dropshuffle[player] != 'none':
|
||||
groups.locations.extend(mode_grouping['Key Drops'])
|
||||
@@ -137,7 +141,7 @@ def create_item_pool_config(world):
|
||||
if world.shopsanity[player]:
|
||||
groups.locations.append('Capacity Upgrade - Left')
|
||||
groups.locations.append('Capacity Upgrade - Right')
|
||||
if world.retro[player]:
|
||||
if world.take_any[player] != 'none':
|
||||
if world.shopsanity[player]:
|
||||
groups.locations.extend(retro_vanilla_mapping['Heart Container'])
|
||||
groups.locations.append('Old Man Sword Cave Item 1')
|
||||
@@ -249,7 +253,7 @@ def previously_reserved(location, world, player):
|
||||
if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player]
|
||||
or not world.mapshuffle[player]
|
||||
or not world.bigkeyshuffle[player]
|
||||
or not (world.keyshuffle[player] or world.retro[player])):
|
||||
or world.keyshuffle[player] == 'none'):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -335,7 +339,7 @@ def determine_major_items(world, player):
|
||||
pass # now what?
|
||||
if world.bigkeyshuffle[player]:
|
||||
major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'})
|
||||
if world.keyshuffle[player]:
|
||||
if world.keyshuffle[player] != 'none':
|
||||
major_item_set.update({x for x, y in item_table.items() if y[2] == 'SmallKey'})
|
||||
if world.compassshuffle[player]:
|
||||
major_item_set.update({x for x, y in item_table.items() if y[2] == 'Compass'})
|
||||
@@ -344,8 +348,9 @@ def determine_major_items(world, player):
|
||||
if world.shopsanity[player]:
|
||||
major_item_set.add('Bomb Upgrade (+5)')
|
||||
major_item_set.add('Arrow Upgrade (+5)')
|
||||
if world.retro[player]:
|
||||
if world.bow_mode[player].startswith('retro'):
|
||||
major_item_set.add('Single Arrow')
|
||||
if world.keyshuffle[player] == 'universal':
|
||||
major_item_set.add('Small Key (Universal)')
|
||||
if world.goal in ['triforcehunt', 'trinity']:
|
||||
major_item_set.add('Triforce Piece')
|
||||
@@ -377,8 +382,10 @@ 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
|
||||
item_name = 'Bottle' if item_to_place.name.startswith('Bottle') else item_to_place.name
|
||||
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]
|
||||
@@ -415,6 +422,12 @@ 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)
|
||||
return locations
|
||||
|
||||
|
||||
@@ -811,3 +824,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)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
||||
import argparse
|
||||
import RaceRandom as random
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import yaml
|
||||
|
||||
|
||||
def get_weights(path):
|
||||
if os.path.exists(Path(path)):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return yaml.load(f, Loader=yaml.SafeLoader)
|
||||
elif urllib.parse.urlparse(path).scheme in ['http', 'https']:
|
||||
return yaml.load(urllib.request.urlopen(path), Loader=yaml.FullLoader)
|
||||
|
||||
def roll_settings(weights):
|
||||
def get_choice(option, root=None):
|
||||
root = weights if root is None else root
|
||||
if option not in root:
|
||||
return None
|
||||
if type(root[option]) is not dict:
|
||||
return root[option]
|
||||
if not root[option]:
|
||||
return None
|
||||
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
|
||||
|
||||
def get_choice_default(option, root=weights, default=None):
|
||||
choice = get_choice(option, root)
|
||||
if choice is None and default is not None:
|
||||
return default
|
||||
return choice
|
||||
|
||||
while True:
|
||||
subweights = weights.get('subweights', {})
|
||||
if len(subweights) == 0:
|
||||
break
|
||||
chances = ({k: int(v['chance']) for (k, v) in subweights.items()})
|
||||
subweight_name = random.choices(list(chances.keys()), weights=list(chances.values()))[0]
|
||||
subweights = weights.get('subweights', {}).get(subweight_name, {}).get('weights', {})
|
||||
subweights['subweights'] = subweights.get('subweights', {})
|
||||
weights = {**weights, **subweights}
|
||||
|
||||
ret = argparse.Namespace()
|
||||
|
||||
ret.algorithm = get_choice('algorithm')
|
||||
|
||||
glitch_map = {'none': 'noglitches', 'no_logic': 'nologic', 'owglitches': 'owglitches',
|
||||
'owg': 'owglitches', 'minorglitches': 'minorglitches'}
|
||||
glitches_required = get_choice('glitches_required')
|
||||
if glitches_required is not None:
|
||||
if glitches_required not in glitch_map.keys():
|
||||
print(f'Logic did not match one of: {", ".join(glitch_map.keys())}')
|
||||
glitches_required = 'none'
|
||||
ret.logic = glitch_map[glitches_required]
|
||||
|
||||
# item_placement = get_choice('item_placement')
|
||||
# not supported in ER
|
||||
|
||||
dungeon_items = get_choice('dungeon_items')
|
||||
dungeon_items = '' if dungeon_items == 'standard' or dungeon_items is None else dungeon_items
|
||||
dungeon_items = 'mcsb' if dungeon_items == 'full' else dungeon_items
|
||||
ret.mapshuffle = get_choice('map_shuffle') == 'on' if 'map_shuffle' in weights else 'm' in dungeon_items
|
||||
ret.compassshuffle = get_choice('compass_shuffle') == 'on' if 'compass_shuffle' in weights else 'c' in dungeon_items
|
||||
if 'smallkey_shuffle' in weights:
|
||||
ret.keyshuffle = get_choice('smallkey_shuffle')
|
||||
else:
|
||||
if 's' in dungeon_items:
|
||||
ret.keyshuffle = 'wild'
|
||||
if 'u' in dungeon_items:
|
||||
ret.keyshuffle = 'universal'
|
||||
ret.bigkeyshuffle = get_choice('bigkey_shuffle') == 'on' if 'bigkey_shuffle' in weights else 'b' in dungeon_items
|
||||
|
||||
ret.accessibility = get_choice('accessibility')
|
||||
ret.restrict_boss_items = get_choice('restrict_boss_items')
|
||||
|
||||
overworld_shuffle = get_choice('overworld_shuffle')
|
||||
ret.ow_shuffle = overworld_shuffle if overworld_shuffle != 'none' else 'vanilla'
|
||||
ret.ow_terrain = get_choice('overworld_terrain') == 'on'
|
||||
valid_options = {'none', 'polar', 'grouped', 'limited', 'chaos'}
|
||||
ret.ow_crossed = get_choice('overworld_crossed')
|
||||
ret.ow_crossed = ret.ow_crossed if ret.ow_crossed in valid_options else 'none'
|
||||
ret.ow_keepsimilar = get_choice('overworld_keepsimilar') == 'on'
|
||||
ret.ow_mixed = get_choice('overworld_swap') == 'on'
|
||||
ret.ow_whirlpool = get_choice('whirlpool_shuffle') == 'on'
|
||||
overworld_flute = get_choice('flute_shuffle')
|
||||
ret.ow_fluteshuffle = overworld_flute if overworld_flute != 'none' else 'vanilla'
|
||||
ret.bonk_drops = get_choice('bonk_drops') == 'on'
|
||||
entrance_shuffle = get_choice('entrance_shuffle')
|
||||
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
|
||||
overworld_map = get_choice('overworld_map')
|
||||
ret.overworld_map = overworld_map if overworld_map != 'default' else 'default'
|
||||
door_shuffle = get_choice('door_shuffle')
|
||||
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.decoupledoors = get_choice('decoupledoors') == 'on'
|
||||
ret.experimental = get_choice('experimental') == 'on'
|
||||
ret.collection_rate = get_choice('collection_rate') == 'on'
|
||||
|
||||
ret.dungeon_counters = get_choice('dungeon_counters') if 'dungeon_counters' in weights else 'default'
|
||||
if ret.dungeon_counters == 'default':
|
||||
ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off'
|
||||
|
||||
ret.pseudoboots = get_choice('pseudoboots') == 'on'
|
||||
ret.shopsanity = get_choice('shopsanity') == 'on'
|
||||
keydropshuffle = get_choice('keydropshuffle') == 'on'
|
||||
ret.dropshuffle = get_choice('dropshuffle') == 'on' or keydropshuffle
|
||||
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.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
|
||||
else 'standardize')
|
||||
|
||||
goal = get_choice('goals')
|
||||
if goal is not None:
|
||||
ret.goal = {'ganon': 'ganon',
|
||||
'fast_ganon': 'crystals',
|
||||
'dungeons': 'dungeons',
|
||||
'pedestal': 'pedestal',
|
||||
'triforce-hunt': 'triforcehunt',
|
||||
'trinity': 'trinity'
|
||||
}[goal]
|
||||
|
||||
ret.openpyramid = get_choice('open_pyramid') if 'open_pyramid' in weights else 'auto'
|
||||
|
||||
ret.shuffleganon = get_choice('shuffleganon') == 'on'
|
||||
ret.shufflelinks = get_choice('shufflelinks') == 'on'
|
||||
ret.shuffletavern = get_choice('shuffletavern') == 'on'
|
||||
|
||||
ret.crystals_gt = get_choice('tower_open')
|
||||
ret.crystals_ganon = get_choice('ganon_open')
|
||||
|
||||
from ItemList import set_default_triforce
|
||||
default_tf_goal, default_tf_pool = set_default_triforce(ret.goal, 0, 0)
|
||||
goal_min = get_choice_default('triforce_goal_min', default=default_tf_goal)
|
||||
goal_max = get_choice_default('triforce_goal_max', default=default_tf_goal)
|
||||
pool_min = get_choice_default('triforce_pool_min', default=default_tf_pool)
|
||||
pool_max = get_choice_default('triforce_pool_max', default=default_tf_pool)
|
||||
ret.triforce_goal = random.randint(int(goal_min), int(goal_max))
|
||||
min_diff = get_choice_default('triforce_min_difference', default=(default_tf_pool-default_tf_goal))
|
||||
ret.triforce_pool = random.randint(max(int(pool_min), ret.triforce_goal + int(min_diff)), int(pool_max))
|
||||
|
||||
ret.mode = get_choice('world_state')
|
||||
if ret.mode == 'retro':
|
||||
ret.mode = 'open'
|
||||
ret.retro = True
|
||||
ret.retro = get_choice('retro') == 'on' # this overrides world_state if used
|
||||
ret.take_any = get_choice_default('take_any', default='none')
|
||||
|
||||
ret.bombbag = get_choice('bombbag') == 'on'
|
||||
|
||||
ret.hints = get_choice('hints') == 'on'
|
||||
|
||||
swords = get_choice('weapons')
|
||||
if swords is not None:
|
||||
ret.swords = {'randomized': 'random',
|
||||
'assured': 'assured',
|
||||
'vanilla': 'vanilla',
|
||||
'swordless': 'swordless'
|
||||
}[swords]
|
||||
|
||||
ret.difficulty = get_choice('item_pool')
|
||||
ret.flute_mode = get_choice_default('flute_mode', default='normal')
|
||||
ret.bow_mode = get_choice_default('bow_mode', default='progressive')
|
||||
|
||||
ret.item_functionality = get_choice('item_functionality')
|
||||
|
||||
old_style_bosses = {'basic': 'simple',
|
||||
'normal': 'full',
|
||||
'chaos': 'random'}
|
||||
boss_choice = get_choice('boss_shuffle')
|
||||
if boss_choice in old_style_bosses.keys():
|
||||
boss_choice = old_style_bosses[boss_choice]
|
||||
ret.shufflebosses = boss_choice
|
||||
|
||||
enemy_choice = get_choice('enemy_shuffle')
|
||||
if enemy_choice == 'chaos':
|
||||
enemy_choice = 'random'
|
||||
ret.shuffleenemies = enemy_choice
|
||||
|
||||
old_style_damage = {'none': 'default',
|
||||
'chaos': 'random'}
|
||||
damage_choice = get_choice('enemy_damage')
|
||||
if damage_choice in old_style_damage:
|
||||
damage_choice = old_style_damage[damage_choice]
|
||||
ret.enemy_damage = damage_choice
|
||||
|
||||
ret.enemy_health = get_choice('enemy_health')
|
||||
|
||||
ret.beemizer = get_choice('beemizer') if 'beemizer' in weights else '0'
|
||||
|
||||
inventoryweights = weights.get('startinventory', {})
|
||||
startitems = []
|
||||
for item in inventoryweights.keys():
|
||||
if get_choice(item, inventoryweights) == 'on':
|
||||
startitems.append(item)
|
||||
ret.startinventory = ','.join(startitems)
|
||||
if len(startitems) > 0:
|
||||
ret.usestartinventory = True
|
||||
|
||||
if 'rom' in weights:
|
||||
romweights = weights['rom']
|
||||
ret.sprite = get_choice('sprite', romweights)
|
||||
ret.disablemusic = get_choice('disablemusic', romweights) == 'on'
|
||||
ret.quickswap = get_choice('quickswap', romweights) == 'on'
|
||||
ret.reduce_flashing = get_choice('reduce_flashing', romweights) == 'on'
|
||||
ret.msu_resume = get_choice('msu_resume', romweights) == 'on'
|
||||
ret.fastmenu = get_choice('menuspeed', romweights)
|
||||
ret.heartcolor = get_choice('heartcolor', romweights)
|
||||
ret.heartbeep = get_choice('heartbeep', romweights)
|
||||
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'
|
||||
|
||||
return ret
|
||||
@@ -0,0 +1,13 @@
|
||||
meta:
|
||||
players: 1
|
||||
settings:
|
||||
1:
|
||||
door_shuffle: crossed
|
||||
mode: standard
|
||||
shuffle: crossed
|
||||
doors:
|
||||
1:
|
||||
doors:
|
||||
Hyrule Castle East Lobby N:
|
||||
dest: Sewers Secret Room Key Door S
|
||||
type: Key Door
|
||||
@@ -0,0 +1,144 @@
|
||||
import RaceRandom as random
|
||||
import logging
|
||||
import time
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from EntranceShuffle import link_entrances
|
||||
from BaseClasses import World
|
||||
from Regions import create_regions, create_dungeon_regions
|
||||
|
||||
|
||||
# tested: open + crossed (lh) Mar. 17 (made changes)
|
||||
# tested: open + simple (lh) Mar. 22
|
||||
|
||||
def run_stats():
|
||||
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
|
||||
random.seed(None)
|
||||
tests = 10000
|
||||
|
||||
for main_mode in ['open', 'standard', 'inverted']:
|
||||
for shuffle_mode in ['dungeonssimple', 'dungeonsfull', 'lite', 'lean',
|
||||
'simple', 'restricted', 'full', 'crossed']:
|
||||
for ls in [True, False]:
|
||||
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
|
||||
continue
|
||||
|
||||
def runner_new(world):
|
||||
link_entrances_new(world, 1)
|
||||
|
||||
def runner_old(world):
|
||||
link_entrances(world, 1)
|
||||
compare_tests(tests, shuffle_mode, main_mode, ls, runner_old, runner_new)
|
||||
|
||||
|
||||
def run_test_stats():
|
||||
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
|
||||
random.seed(None)
|
||||
tests = 10000
|
||||
|
||||
for main_mode in ['open']:
|
||||
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
|
||||
'simple']:#, 'restricted', 'full', 'crossed']:
|
||||
for ls in [True, False]:
|
||||
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
|
||||
continue
|
||||
|
||||
def runner_new(world):
|
||||
link_entrances_new(world, 1)
|
||||
|
||||
run_tests(tests, shuffle_mode, main_mode, ls, runner_new)
|
||||
|
||||
|
||||
def run_old_stats():
|
||||
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
|
||||
random.seed(None)
|
||||
tests = 10000
|
||||
|
||||
for main_mode in ['open']:
|
||||
for shuffle_mode in [#'dungeonssimple', 'dungeonsfull',
|
||||
'simple']:#, 'restricted', 'full', 'crossed']:
|
||||
for ls in [True, False]:
|
||||
if ls and (main_mode == 'standard' or shuffle_mode in ['dungeonssimple', 'dungeonsfull']):
|
||||
continue
|
||||
|
||||
def runner(world):
|
||||
link_entrances(world, 1)
|
||||
run_tests(tests, shuffle_mode, main_mode, ls, runner)
|
||||
|
||||
|
||||
def compare_tests(tests, shuffle_mode, main_mode, links=False, runner_old=None, runner_new=None):
|
||||
print(f'Testing {shuffle_mode} {main_mode}')
|
||||
entrance_set = set()
|
||||
exit_set = set()
|
||||
ctr = defaultdict(Counter)
|
||||
start = time.time()
|
||||
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_old)
|
||||
print(f'Old test took {time.time() - start}s')
|
||||
start = time.time()
|
||||
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new, -1)
|
||||
# test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner_new)
|
||||
print(f'New test took {time.time() - start}s')
|
||||
dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links)
|
||||
|
||||
|
||||
def run_tests(tests, shuffle_mode, main_mode, links=False, runner=None):
|
||||
print(f'Testing {shuffle_mode} {main_mode}')
|
||||
entrance_set = set()
|
||||
exit_set = set()
|
||||
ctr = defaultdict(Counter)
|
||||
test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner)
|
||||
|
||||
|
||||
def test_loop(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links, runner, value=1):
|
||||
for i in range(0, tests):
|
||||
if i % 1000 == 0:
|
||||
print(f'Test {i}')
|
||||
seed = random.randint(0, 999999999)
|
||||
# seed = 635441530
|
||||
random.seed(seed)
|
||||
world = World(1, {1: shuffle_mode}, {1: 'vanilla'}, {1: 'noglitches'}, {1: main_mode}, {}, {}, {},
|
||||
{}, {}, {}, {}, {}, True, {}, {}, [], {})
|
||||
world.customizer = False
|
||||
world.shufflelinks = {1: links}
|
||||
create_regions(world, 1)
|
||||
create_dungeon_regions(world, 1)
|
||||
# print(f'Linking seed {seed}')
|
||||
# try:
|
||||
runner(world)
|
||||
# except Exception as e:
|
||||
# print(f'Failure during seed {seed} with {e}')
|
||||
for data in world.spoiler.entrances.values():
|
||||
ent = data['entrance']
|
||||
ext = data['exit']
|
||||
# drc = data['direction']
|
||||
entrance_set.add(ent)
|
||||
exit_set.add(ext)
|
||||
ctr[ent][ext] += value
|
||||
|
||||
|
||||
def dump_file(tests, entrance_set, exit_set, ctr, shuffle_mode, main_mode, links):
|
||||
columns = sorted(list(exit_set))
|
||||
rows = sorted(list(entrance_set))
|
||||
filename = f'er_stats_{shuffle_mode}_{main_mode}_{"ls" if links else "v"}.csv'
|
||||
with open(filename, 'w') as stat_file:
|
||||
stat_file.write(',')
|
||||
stat_file.write(','.join(columns))
|
||||
stat_file.write('\n')
|
||||
for r in rows:
|
||||
stat_file.write(f'{r},')
|
||||
formatted = []
|
||||
for c in columns:
|
||||
occurance = ctr[r][c] / tests
|
||||
formatted.append(f'{occurance:.5%}')
|
||||
stat_file.write(','.join(formatted))
|
||||
stat_file.write('\n')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# logging.basicConfig(format='%(message)s', level=logging.DEBUG)
|
||||
# run_tests(1, 'restricted', 'inverted', True, lambda world: link_entrances_new(world, 1))
|
||||
# run_test_stats()
|
||||
# run_old_stats()
|
||||
run_stats()
|
||||
Reference in New Issue
Block a user