Merge pull request #4 from aerinon/DoorDev

Update DoorDev
This commit is contained in:
Mike A. Trethewey
2020-03-03 21:20:11 -08:00
committed by GitHub
65 changed files with 5176 additions and 2293 deletions
+6
View File
@@ -16,7 +16,13 @@ README.html
EnemizerCLI/
.mypy_cache/
RaceRom.py
upx/
weights/
settings.json
working_dirs.json
*.exe
venv
test
+3
View File
@@ -21,6 +21,9 @@ def adjust(args):
else:
raise RuntimeError('Provided Rom is not a valid Link to the Past Randomizer Rom. Please provide one for adjusting.')
if not hasattr(args,"sprite"):
args.sprite = None
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, args.sprite, args.ow_palettes, args.uw_palettes)
rom.write_to_file(output_path('%s.sfc' % outfilebase))
+26 -6
View File
@@ -2,7 +2,7 @@ import copy
from enum import Enum, unique, Flag
import logging
import json
from collections import OrderedDict, deque
from collections import OrderedDict, deque, defaultdict
from EntranceShuffle import door_addresses
from _vendor.collections_extended import bag
@@ -69,6 +69,8 @@ class World(object):
self.dungeon_layouts = {}
self.inaccessible_regions = {}
self.key_logic = {}
self.pool_adjustment = {}
self.key_layout = defaultdict(dict)
for player in range(1, players + 1):
def set_player_attr(attr, val):
@@ -767,6 +769,16 @@ class CollectionState(object):
else:
self.prog_items.add(('Bow', item.player))
changed = True
elif 'Armor' in item.name:
if self.has('Red Mail', item.player):
pass
elif self.has('Blue Mail', item.player):
self.prog_items.add(('Red Mail', item.player))
changed = True
else:
self.prog_items.add(('Blue Mail', item.player))
changed = True
elif item.name.startswith('Bottle'):
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
self.prog_items.add((item.name, item.player))
@@ -1515,11 +1527,11 @@ class Spoiler(object):
else:
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def set_door(self, entrance, exit, direction, player):
def set_door(self, entrance, exit, direction, player, d_name):
if self.world.players == 1:
self.doors[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction), ('dname', d_name)])
else:
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction), ('dname', d_name)])
def set_door_type(self, doorNames, type, player):
if self.world.players == 1:
@@ -1625,7 +1637,8 @@ class Spoiler(object):
'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage,
'players': self.world.players,
'teams': self.world.teams
'teams': self.world.teams,
'experimental' : self.world.experimental
}
def to_json(self):
@@ -1683,9 +1696,16 @@ class Spoiler(object):
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No'))
if self.doors:
outfile.write('\n\nDoors:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.doors.values()]))
outfile.write('\n'.join(
['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '',
entry['entrance'],
'<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>',
entry['exit'],
'({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for
entry in self.doors.values()]))
if self.doorTypes:
outfile.write('\n\nDoor Types:\n\n')
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', entry['doorNames'], entry['type']) for entry in self.doorTypes.values()]))
+8
View File
@@ -183,6 +183,10 @@ def place_bosses(world, player):
raise FillError('Could not place boss for location %s' % loc_text)
bosses.remove(boss)
# GT Bosses can move dungeon - find the real dungeon to place them in
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
elif world.boss_shuffle[player] == "chaos": #all bosses chosen at random
@@ -193,5 +197,9 @@ def place_bosses(world, player):
except IndexError:
raise FillError('Could not place boss for location %s' % loc_text)
# GT Bosses can move dungeon - find the real dungeon to place them in
if level:
loc = [x.name for x in world.dungeons if x.player == player and level in x.bosses.keys()][0]
loc_text = loc + ' (' + level + ')'
logging.getLogger('').debug('Placing boss %s at %s', boss, loc_text)
world.get_dungeon(loc, player).bosses[level] = BossFactory(boss, player)
+508
View File
@@ -0,0 +1,508 @@
import argparse
import copy
import json
import os
import logging
import random
import textwrap
import shlex
import sys
from Main import main
from Utils import is_bundled, close_console
from Fill import FillError
import classes.constants as CONST
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
def parse_arguments(argv, no_defaults=False):
def defval(value):
return value if not no_defaults else None
# get settings
settings = get_settings()
# we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', default=defval(settings["create_spoiler"] != 0), help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default=defval(settings["logic"]), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
help='''\
Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches:
Minor Glitches: May require Fake Flippers, Bunny Revival
and Dark Room Navigation.
No Logic: Distribute items without regard for
item requirements.
''')
parser.add_argument('--mode', default=defval(settings["mode"]), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
help='''\
Select game mode. (default: %(default)s)
Open: World starts with Zelda rescued.
Standard: Fixes Hyrule Castle Secret Entrance and Front Door
but may lead to weird rain state issues if you exit
through the Hyrule Castle side exits before rescuing
Zelda in a full shuffle.
Inverted: Starting locations are Dark Sanctuary in West Dark
World or at Link's House, which is shuffled freely.
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
parser.add_argument('--swords', default=defval(settings["swords"]), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
help='''\
Select sword placement. (default: %(default)s)
Random: All swords placed randomly.
Assured: Start game with a sword already.
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer
(and Book). Bombos pads have been added in Ice
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default=defval(settings["goal"]), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
defeat Ganon.
Crystals: Collect all crystals then defeat Ganon.
Pedestal: Places the Triforce at the Master Sword Pedestal.
All Dungeons: Collect all crystals, pendants, beat both
Agahnim fights and then defeat Ganon.
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
parser.add_argument('--difficulty', default=defval(settings["difficulty"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select game difficulty. Affects available itempool. (default: %(default)s)
Normal: Normal difficulty.
Hard: A harder setting with less equipment and reduced health.
Expert: A harder yet setting with minimum equipment and health.
''')
parser.add_argument('--item_functionality', default=defval(settings["item_functionality"]), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select limits on item functionality to increase difficulty. (default: %(default)s)
Normal: Normal functionality.
Hard: Reduced functionality.
Expert: Greatly reduced functionality.
''')
parser.add_argument('--timer', default=defval(settings["timer"]), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
help='''\
Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer.
Display: Displays a timer but does not affect
the itempool.
Timed: Starts with clock at zero. Green Clocks
subtract 4 minutes (Total: 20), Blue Clocks
subtract 2 minutes (Total: 10), Red Clocks add
2 minutes (Total: 10). Winner is player with
lowest time at the end.
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
5 minutes (Total: 25). As long as clock is at 0,
Link will die in one hit.
OHKO: Like Timed OHKO, but no clock items are present
and the clock is permenantly at zero.
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--progressive', default=defval(settings["progressive"]), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval(settings["algorithm"]), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\
Select item filling algorithm. (default: %(default)s
balanced: vt26 derivative that aims to strike a balance between
the overworld heavy vt25 and the dungeon heavy vt26
algorithm.
vt26: Shuffle items and place them in a random location
that it is not impossible to be in. This includes
dungeon keys and items.
vt25: Shuffle items and place them in a random location
that it is not impossible to be in.
vt21: Unbiased in its selection, but has tendency to put
Ice Rod in Turtle Rock.
vt22: Drops off stale locations after 1/3 of progress
items were placed to try to circumvent vt21\'s
shortcomings.
Freshness: Keep track of stale locations (ones that cannot be
reached yet) and decrease likeliness of selecting
them the more often they were found unreachable.
Flood: Push out items starting from Link\'s House and
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval(settings["shuffle"]), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
multi-entrance caves to one world.
Simple: Shuffle Dungeon Entrances/Exits between each other
and keep all 4-entrance dungeons confined to one
location. All caves outside of death mountain are
shuffled in pairs and matched by original type.
Restricted: Use Dungeons shuffling from Simple but freely
connect remaining entrances.
Crossed: Mix cave and dungeon entrances freely while allowing
caves to cross between worlds.
Insanity: Decouple entrances and exits from each other and
shuffle them freely. Caves that used to be single
entrance will still exit to the same location from
which they are entered.
Vanilla: All entrances are in the same locations they were
in the base game.
Legacy shuffles preserve behavior from older versions of the
entrance randomizer including significant technical limitations.
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval(settings["door_shuffle"]), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.
(Not yet implemented)
Crossed: Doors are mixed between all dungeons.
(Not yet implemented)
Vanilla: All doors are connected the same way they were in the
base game.
''')
parser.add_argument('--experimental', default=defval(settings["experimental"] != 0), help='Enable experimental features', action='store_true')
parser.add_argument('--dungeon_counters', default=defval(settings["dungeon_counters"]), help='Enable dungeon chest counters', const='off', nargs='?', choices=['off', 'on', 'pickup'])
parser.add_argument('--crystals_ganon', default=defval(settings["crystals_ganon"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default=defval(settings["crystals_gt"]), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--openpyramid', default=defval(settings["openpyramid"] != 0), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
''', action='store_true')
parser.add_argument('--rom', default=defval(settings["rom"]), help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', default=defval(int(settings["seed"]) if settings["seed"] != "" and settings["seed"] is not None else None), help='Define seed number to generate.', type=int)
parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else None), help='''\
Use to batch generate multiple seeds with same settings.
If --seed is provided, it will be used for the first seed, then
used to derive the next seed (i.e. generating 10 seeds with
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default=defval(settings["fastmenu"]), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', default=defval(settings["quickswap"] != 0), help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', default=defval(settings["disablemusic"] != 0), help='Disables game music.', action='store_true')
parser.add_argument('--mapshuffle', default=defval(settings["mapshuffle"] != 0), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', default=defval(settings["compassshuffle"] != 0), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(settings["keyshuffle"] != 0), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(settings["bigkeyshuffle"] != 0), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keysanity', default=defval(settings["keysanity"] != 0), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(settings["retro"] != 0), help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--startinventory', default=defval(settings["startinventory"]), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--usestartinventory', default=defval(settings["usestartinventory"] != 0), help='Not supported.')
parser.add_argument('--custom', default=defval(settings["custom"] != 0), help='Not supported.')
parser.add_argument('--customitemarray', default={}, help='Not supported.')
parser.add_argument('--accessibility', default=defval(settings["accessibility"]), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys.
Locations: You will be able to reach every location in the game.
None: You will be able to reach enough locations to beat the game.
''')
parser.add_argument('--hints', default=defval(settings["hints"] != 0), help='''\
Make telepathic tiles and storytellers give helpful hints.
''', action='store_true')
# included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(settings["shuffleganon"] != 0))
parser.add_argument('--no-shuffleganon', help='''\
If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool.
''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default=defval(settings["heartbeep"]), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default=defval(settings["heartcolor"]), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval(settings["ow_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval(settings["uw_palettes"]), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', default=defval(settings["sprite"]), help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--suppress_rom', default=defval(settings["suppress_rom"] != 0), help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--skip_playthrough', action='store_true', default=defval(settings["skip_playthrough"] != 0))
parser.add_argument('--enemizercli', default=defval(settings["enemizercli"]))
parser.add_argument('--shufflebosses', default=defval(settings["shufflebosses"]), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval(settings["shuffleenemies"]), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval(settings["enemy_health"]), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval(settings["enemy_damage"]), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(settings["shufflepots"] != 0), action='store_true')
parser.add_argument('--beemizer', default=defval(settings["beemizer"]), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--remote_items', default=defval(settings["remote_items"] != 0), action='store_true')
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(settings["names"]))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath', default=defval(settings["outputpath"]))
parser.add_argument('--race', default=defval(settings["race"] != 0), action='store_true')
parser.add_argument('--saveonexit', default=defval(settings["saveonexit"]), choices=['never', 'ask', 'always'])
parser.add_argument('--outputname')
if multiargs.multi:
for player in range(1, multiargs.multi + 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
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret,f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'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']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
else:
getattr(ret, name)[player] = value
return ret
def get_settings():
# set default settings
settings = {
"retro": False,
"mode": "open",
"logic": "noglitches",
"goal": "ganon",
"crystals_gt": "7",
"crystals_ganon": "7",
"swords": "random",
"difficulty": "normal",
"item_functionality": "normal",
"timer": "none",
"progressive": "on",
"accessibility": "items",
"algorithm": "balanced",
"openpyramid": False,
"shuffleganon": False,
"shuffle": "vanilla",
"shufflepots": False,
"shuffleenemies": "none",
"shufflebosses": "none",
"enemy_damage": "default",
"enemy_health": "default",
"enemizercli": os.path.join(".", "EnemizerCLI", "EnemizerCLI.Core"),
"mapshuffle": False,
"compassshuffle": False,
"keyshuffle": False,
"bigkeyshuffle": False,
"keysanity": False,
"door_shuffle": "basic",
"experimental": 0,
"dungeon_counters": "off",
"multi": 1,
"names": "",
"hints": True,
"disablemusic": False,
"quickswap": False,
"heartcolor": "red",
"heartbeep": "normal",
"sprite": None,
"fastmenu": "normal",
"ow_palettes": "default",
"uw_palettes": "default",
"create_spoiler": False,
"skip_playthrough": False,
"suppress_rom": False,
"usestartinventory": False,
"custom": False,
"rom": os.path.join(".", "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"),
"seed": None,
"count": None,
"startinventory": "",
"beemizer": 0,
"remote_items": False,
"race": False,
"customitemarray": {
"bow": 0,
"progressivebow": 2,
"boomerang": 1,
"redmerang": 1,
"hookshot": 1,
"mushroom": 1,
"powder": 1,
"firerod": 1,
"icerod": 1,
"bombos": 1,
"ether": 1,
"quake": 1,
"lamp": 1,
"hammer": 1,
"shovel": 1,
"flute": 1,
"bugnet": 1,
"book": 1,
"bottle": 4,
"somaria": 1,
"byrna": 1,
"cape": 1,
"mirror": 1,
"boots": 1,
"powerglove": 0,
"titansmitt": 0,
"progressiveglove": 2,
"flippers": 1,
"pearl": 1,
"heartpiece": 24,
"heartcontainer": 10,
"sancheart": 1,
"sword1": 0,
"sword2": 0,
"sword3": 0,
"sword4": 0,
"progressivesword": 4,
"shield1": 0,
"shield2": 0,
"shield3": 0,
"progressiveshield": 3,
"mail2": 0,
"mail3": 0,
"progressivemail": 2,
"halfmagic": 1,
"quartermagic": 0,
"bombsplus5": 0,
"bombsplus10": 0,
"arrowsplus5": 0,
"arrowsplus10": 0,
"arrow1": 1,
"arrow10": 12,
"bomb1": 0,
"bomb3": 16,
"bomb10": 1,
"rupee1": 2,
"rupee5": 4,
"rupee20": 28,
"rupee50": 7,
"rupee100": 1,
"rupee300": 5,
"blueclock": 0,
"greenclock": 0,
"redclock": 0,
"silversupgrade": 0,
"generickeys": 0,
"triforcepieces": 0,
"triforcepiecesgoal": 0,
"triforce": 0,
"rupoor": 0,
"rupoorcost": 10
},
"randomSprite": False,
"outputpath": os.path.join("."),
"saveonexit": "ask",
"startinventoryarray": {}
}
if sys.platform.lower().find("windows"):
settings["enemizercli"] += ".exe"
# read saved settings file if it exists and set these
settings_path = os.path.join(".", "resources", "user", "settings.json")
if os.path.exists(settings_path):
with(open(settings_path)) as json_file:
data = json.load(json_file)
for k, v in data.items():
settings[k] = v
return settings
def get_args_priority(settings_args, gui_args, cli_args):
args = {}
args["settings"] = get_settings() if settings_args is None else settings_args
args["gui"] = {} if gui_args is None else gui_args
args["cli"] = cli_args
args["load"] = args["settings"]
if args["gui"] is not None:
for k in args["gui"]:
if k not in args["load"] or args["load"][k] != args["gui"]:
args["load"][k] = args["gui"][k]
if args["cli"] is None:
args["cli"] = {}
cli = vars(parse_arguments(None))
for k, v in cli.items():
if isinstance(v, dict) and 1 in v:
args["cli"][k] = v[1]
else:
args["cli"][k] = v
load_doesnt_have_key = k not in args["load"]
different_val = (k in args["load"] and k in args["cli"]) and (args["load"][k] != args["cli"][k])
cli_has_empty_dict = k in args["cli"] and isinstance(args["cli"][k], dict) and len(args["cli"][k]) == 0
if load_doesnt_have_key or different_val:
if not cli_has_empty_dict:
args["load"][k] = args["cli"][k]
return args
+240 -69
View File
@@ -1,6 +1,5 @@
import random
import collections
from collections import defaultdict
from collections import defaultdict, deque
import logging
import operator as op
import time
@@ -11,7 +10,7 @@ from BaseClasses import RegionType, Door, DoorType, Direction, Sector, CrystalBa
from Regions import key_only_locations
from Dungeons import hyrule_castle_regions, eastern_regions, desert_regions, hera_regions, tower_regions, pod_regions
from Dungeons import dungeon_regions, region_starts, split_region_starts, flexible_starts
from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys
from Dungeons import drop_entrances, dungeon_bigs, dungeon_keys, dungeon_hints
from Items import ItemFactory
from RoomData import DoorKind, PairedDoor
from DungeonGenerator import ExplorationState, convert_regions, generate_dungeon, validate_tr
@@ -54,8 +53,6 @@ def link_doors(world, player):
within_dungeon(world, player)
elif world.doorShuffle[player] == 'crossed':
cross_dungeon(world, player)
elif world.doorShuffle[player] == 'experimental':
experiment(world, player)
else:
logging.getLogger('').error('Invalid door shuffle setting: %s' % world.doorShuffle[player])
raise Exception('Invalid door shuffle setting: %s' % world.doorShuffle[player])
@@ -69,7 +66,7 @@ def mark_regions(world, player):
# traverse dungeons and make sure dungeon property is assigned
player_dungeons = [dungeon for dungeon in world.dungeons if dungeon.player == player]
for dungeon in player_dungeons:
queue = collections.deque(dungeon.regions)
queue = deque(dungeon.regions)
while len(queue) > 0:
region = world.get_region(queue.popleft(), player)
if region.name not in dungeon.regions:
@@ -87,31 +84,42 @@ def mark_regions(world, player):
def create_door_spoiler(world, player):
logger = logging.getLogger('')
queue = collections.deque((door for door in world.doors if door.player == player))
queue = deque(world.dungeon_layouts[player].values())
while len(queue) > 0:
door_a = queue.popleft()
if door_a.type in [DoorType.Normal, DoorType.SpiralStairs]:
door_b = door_a.dest
if door_b is not None:
logger.debug('spoiler: %s connected to %s', door_a.name, door_b.name)
if not door_a.blocked and not door_b.blocked:
world.spoiler.set_door(door_a.name, door_b.name, 'both', player)
elif door_a.blocked:
world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player)
elif door_b.blocked:
world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player)
else:
logger.warning('This is a bug')
if door_b in queue:
queue.remove(door_b)
else:
logger.debug('Door not found in queue: %s connected to %s', door_b.name, door_a.name)
else:
logger.warning('Door not connected: %s', door_a.name)
builder = queue.popleft()
done = set()
start_regions = set(convert_regions(builder.layout_starts, world, player)) # todo: set all_entrances for basic
reg_queue = deque(start_regions)
visited = set(start_regions)
while len(reg_queue) > 0:
next = reg_queue.pop()
for ext in next.exits:
door_a = ext.door
connect = ext.connected_region
if door_a and door_a.type in [DoorType.Normal, DoorType.SpiralStairs] and door_a not in done:
done.add(door_a)
door_b = door_a.dest
if door_b:
done.add(door_b)
if not door_a.blocked and not door_b.blocked:
world.spoiler.set_door(door_a.name, door_b.name, 'both', player, builder.name)
elif door_a.blocked:
world.spoiler.set_door(door_b.name, door_a.name, 'entrance', player, builder.name)
elif door_b.blocked:
world.spoiler.set_door(door_a.name, door_b.name, 'entrance', player, builder.name)
else:
logger.warning('This is a bug during door spoiler')
else:
logger.warning('Door not connected: %s', door_a.name)
if connect and connect.type == RegionType.Dungeon and connect not in visited:
visited.add(connect)
reg_queue.append(connect)
def vanilla_key_logic(world, player):
builders = []
world.dungeon_layouts[player] = {}
for dungeon in [dungeon for dungeon in world.dungeons if dungeon.player == player]:
sector = Sector()
sector.name = dungeon.name
@@ -119,12 +127,13 @@ def vanilla_key_logic(world, player):
builder = simple_dungeon_builder(sector.name, [sector])
builder.master_sector = sector
builders.append(builder)
world.dungeon_layouts[player][builder.name] = builder
overworld_prep(world, player)
entrances_map, potentials, connections = determine_entrance_list(world, player)
enabled_entrances = {}
sector_queue = collections.deque(builders)
sector_queue = deque(builders)
last_key = None
while len(sector_queue) > 0:
builder = sector_queue.popleft()
@@ -149,8 +158,9 @@ def vanilla_key_logic(world, player):
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
world.key_logic[player][builder.name] = key_layout.key_logic
log_key_logic(builder.name, key_layout.key_logic)
last_key = None
if world.shuffle[player] == 'vanilla':
if world.shuffle[player] == 'vanilla' and world.accessibility[player] == 'items':
validate_vanilla_key_logic(world, player)
@@ -187,8 +197,7 @@ def connect_simple_door(world, exit_name, region_name, player):
d.dest = region
def connect_door_only(world, exit_name, region_name, player):
region = world.get_region(region_name, player)
def connect_door_only(world, exit_name, region, player):
d = world.check_for_door(exit_name, player)
if d is not None:
d.dest = region
@@ -313,6 +322,7 @@ def within_dungeon(world, player):
for builder in world.dungeon_layouts[player].values():
shuffle_key_doors(builder, world, player)
logging.getLogger('').info('Key door shuffle time: %s', time.process_time()-start)
smooth_door_pairs(world, player)
def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map):
@@ -334,7 +344,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map)
def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player):
entrances_map, potentials, connections = connections_tuple
enabled_entrances = {}
sector_queue = collections.deque(dungeon_builders.values())
sector_queue = deque(dungeon_builders.values())
last_key = None
while len(sector_queue) > 0:
builder = sector_queue.popleft()
@@ -425,7 +435,7 @@ def find_new_entrances(sector, connections, potentials, enabled, world, player):
for potential in potentials.pop(new_region):
enabled[potential] = (region.name, region.dungeon)
# see if this unexplored region connects elsewhere
queue = collections.deque(new_region.exits)
queue = deque(new_region.exits)
visited = set()
while len(queue) > 0:
ext = queue.popleft()
@@ -671,6 +681,33 @@ def cross_dungeon(world, player):
gt = world.get_dungeon('Ganons Tower', player)
del gt.dungeon_items[0] # removes map
assign_cross_keys(dungeon_builders, world, player)
all_dungeon_items = [y for x in world.dungeons if x.player == player for y in x.all_items]
target_items = 34 if world.retro[player] else 63
d_items = target_items - len(all_dungeon_items)
if d_items > 0:
if d_items >= 1: # restore HC map
world.get_dungeon('Hyrule Castle', player).dungeon_items.append(ItemFactory('Map (Escape)', player))
if d_items >= 2: # restore GT map
world.get_dungeon('Ganons Tower', player).dungeon_items.append(ItemFactory('Map (Ganons Tower)', player))
if d_items > 2:
world.pool_adjustment[player] = d_items - 2
elif d_items < 0:
world.pool_adjustment[player] = d_items
smooth_door_pairs(world, player)
# Re-assign dungeon bosses
gt = world.get_dungeon('Ganons Tower', player)
for name, builder in dungeon_builders.items():
reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player)
reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player)
reassign_boss('GT Moldorm', 'top', builder, gt, world, player)
if world.hints[player]:
refine_hints(dungeon_builders)
def assign_cross_keys(dungeon_builders, world, player):
start = time.process_time()
total_keys = remaining = 29
total_candidates = 0
@@ -688,6 +725,7 @@ def cross_dungeon(world, player):
total_candidates += builder.key_doors_num
start_regions_map[name] = start_regions
# Step 2: Initial Key Number Assignment & Calculate Flexibility
for name, builder in dungeon_builders.items():
calculated = int(round(builder.key_doors_num*total_keys/total_candidates))
@@ -717,7 +755,7 @@ def cross_dungeon(world, player):
# Step 4: Try to assign remaining keys
builder_order = [x for x in dungeon_builders.values() if x.flex > 0]
builder_order.sort(key=lambda b: b.combo_size)
queue = collections.deque(builder_order)
queue = deque(builder_order)
logger = logging.getLogger('')
while len(queue) > 0 and remaining > 0:
builder = queue.popleft()
@@ -731,7 +769,7 @@ def cross_dungeon(world, player):
if builder.flex > 0:
builder.combo_size = ncr(len(builder.candidates), builder.key_doors_num)
queue.append(builder)
queue = collections.deque(sorted(queue, key=lambda b: b.combo_size))
queue = deque(sorted(queue, key=lambda b: b.combo_size))
else:
logger.info('Cross Dungeon: Increase failed for %s', name)
builder.key_doors_num -= 1
@@ -739,22 +777,17 @@ def cross_dungeon(world, player):
logger.info('Cross Dungeon: Keys unable to assign in pool %s', remaining)
# Last Step: Adjust Small Key Dungeon Pool
for name, builder in dungeon_builders.items():
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
dungeon = world.get_dungeon(name, player)
if actual_chest_keys == 0:
dungeon.small_keys = []
else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
if not world.retro[player]:
for name, builder in dungeon_builders.items():
reassign_key_doors(builder, world, player)
log_key_logic(builder.name, world.key_logic[player][builder.name])
actual_chest_keys = max(builder.key_doors_num - builder.key_drop_cnt, 0)
dungeon = world.get_dungeon(name, player)
if actual_chest_keys == 0:
dungeon.small_keys = []
else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
logging.getLogger('').info('Cross Dungeon: Key door shuffle time: %s', time.process_time()-start)
# todo: pair down paired doors - excessive rom writes ATM
# Re-assign dungeon bosses
gt = world.get_dungeon('Ganons Tower', player)
for name, builder in dungeon_builders.items():
reassign_boss('GT Ice Armos', 'bottom', builder, gt, world, player)
reassign_boss('GT Lanmolas 2', 'middle', builder, gt, world, player)
reassign_boss('GT Moldorm', 'top', builder, gt, world, player)
def reassign_boss(boss_region, boss_key, builder, gt, world, player):
@@ -765,9 +798,12 @@ def reassign_boss(boss_region, boss_key, builder, gt, world, player):
new_dungeon.bosses[boss_key] = gt_boss
def experiment(world, player):
# print_wiki_doors(dungeon_regions, world, player)
cross_dungeon(world, player)
def refine_hints(dungeon_builders):
for name, builder in dungeon_builders.items():
for region in builder.master_sector.regions:
for location in region.locations:
if not location.event and '- Boss' not in location.name and '- Prize' not in location.name and location.name != 'Sanctuary':
location.hint_text = dungeon_hints[name]
def convert_to_sectors(region_names, world, player):
@@ -817,7 +853,7 @@ def convert_to_sectors(region_names, world, player):
# those with split region starts like Desert/Skull combine for key layouts
def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
for recombine in recombinant_builders.values():
queue = collections.deque(dungeon_builders.values())
queue = deque(dungeon_builders.values())
while len(queue) > 0:
builder = queue.pop()
if builder.name.startswith(recombine.name):
@@ -825,8 +861,11 @@ def combine_layouts(recombinant_builders, dungeon_builders, entrances_map):
if recombine.master_sector is None:
recombine.master_sector = builder.master_sector
recombine.master_sector.name = recombine.name
recombine.pre_open_stonewall = builder.pre_open_stonewall
else:
recombine.master_sector.regions.extend(builder.master_sector.regions)
if builder.pre_open_stonewall:
recombine.pre_open_stonewall = builder.pre_open_stonewall
recombine.layout_starts = list(entrances_map[recombine.name])
dungeon_builders[recombine.name] = recombine
@@ -859,14 +898,16 @@ def shuffle_key_doors(builder, world, player):
builder.key_doors_num = num_key_doors
find_small_key_door_candidates(builder, start_regions, world, player)
find_valid_combination(builder, start_regions, world, player)
reassign_key_doors(builder, world, player)
log_key_logic(builder.name, world.key_logic[player][builder.name])
def find_current_key_doors(builder, world, player):
def find_current_key_doors(builder):
current_doors = []
for region in builder.master_sector.regions:
for ext in region.exits:
d = world.check_for_door(ext.name, player)
if d is not None and d.smallKey:
d = ext.door
if d and d.smallKey:
current_doors.append(d)
return current_doors
@@ -948,9 +989,9 @@ def find_valid_combination(builder, start_regions, world, player, drop_keys=True
if player not in world.key_logic.keys():
world.key_logic[player] = {}
analyze_dungeon(key_layout, world, player)
reassign_key_doors(builder, proposal, world, player)
log_key_logic(builder.name, key_layout.key_logic)
builder.key_door_proposal = proposal
world.key_logic[player][builder.name] = key_layout.key_logic
world.key_layout[player][builder.name] = key_layout
return True
@@ -972,11 +1013,18 @@ def log_key_logic(d_name, key_logic):
if rule.alternate_small_key is not None:
for loc in rule.alternate_big_key_loc:
logger.debug('---BK Loc %s', loc.name)
logger.debug('Placement rules for %s', d_name)
for rule in key_logic.placement_rules:
logger.debug('*Rule for %s:', rule.door_reference)
if rule.bk_conditional_set:
logger.debug('**BK Checks %s', ','.join([x.name for x in rule.bk_conditional_set]))
logger.debug('**BK Blocked By Door (%s) : %s', rule.needed_keys_wo_bk, ','.join([x.name for x in rule.check_locations_wo_bk]))
logger.debug('**BK Elsewhere (%s) : %s', rule.needed_keys_w_bk, ','.join([x.name for x in rule.check_locations_w_bk]))
def build_pair_list(flat_list):
paired_list = []
queue = collections.deque(flat_list)
queue = deque(flat_list)
while len(queue) > 0:
d = queue.pop()
if d.dest in queue and d.type != DoorType.SpiralStairs:
@@ -1002,11 +1050,13 @@ def find_key_door_candidates(region, checked, world, player):
dungeon = region.dungeon
candidates = []
checked_doors = list(checked)
queue = collections.deque([(region, None, None)])
queue = deque([(region, None, None)])
while len(queue) > 0:
current, last_door, last_region = queue.pop()
for ext in current.exits:
d = world.check_for_door(ext.name, player)
d = ext.door
if d and d.controller:
d = d.controller
if d is not None and not d.blocked and d.dest is not last_door and d.dest is not last_region and d not in checked_doors:
valid = False
if 0 <= d.doorListPos < 4 and d.type in [DoorType.Interior, DoorType.Normal, DoorType.SpiralStairs]:
@@ -1025,9 +1075,11 @@ def find_key_door_candidates(region, checked, world, player):
okay_normals = [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable,
DoorKind.Dashable, DoorKind.DungeonChanger]
valid = kind in okay_normals and kind_b in okay_normals
if valid and 0 <= d2.doorListPos < 4:
candidates.append(d2)
else:
valid = True
if valid:
if valid and d not in candidates:
candidates.append(d)
if ext.connected_region.type != RegionType.Dungeon or ext.connected_region.dungeon == dungeon:
queue.append((ext.connected_region, d, current))
@@ -1058,10 +1110,12 @@ def ncr(n, r):
return numerator / denominator
def reassign_key_doors(builder, proposal, world, player):
def reassign_key_doors(builder, world, player):
logger = logging.getLogger('')
logger.debug('Key doors for %s', builder.name)
proposal = builder.key_door_proposal
flat_proposal = flatten_pair_list(proposal)
queue = collections.deque(find_current_key_doors(builder, world, player))
queue = deque(find_current_key_doors(builder))
while len(queue) > 0:
d = queue.pop()
if d.type is DoorType.SpiralStairs and d not in proposal:
@@ -1070,7 +1124,7 @@ def reassign_key_doors(builder, proposal, world, player):
room.delete(d.doorListPos)
else:
if len(room.doorList) > 1:
room.mirror(d.doorListPos) # todo: I don't think this works for crossed - maybe it will
room.mirror(d.doorListPos) # I think this works for crossed now
else:
room.delete(d.doorListPos)
d.smallKey = False
@@ -1129,6 +1183,104 @@ def change_door_to_small_key(d, world, player):
room.change(d.doorListPos, DoorKind.SmallKey)
def smooth_door_pairs(world, player):
all_doors = [x for x in world.doors if x.player == player]
skip = set()
for door in all_doors:
if door.type in [DoorType.Normal, DoorType.Interior] and door not in skip:
partner = door.dest
skip.add(partner)
room_a = world.get_room(door.roomIndex, player)
room_b = world.get_room(partner.roomIndex, player)
type_a = room_a.kind(door)
type_b = room_b.kind(partner)
valid_pair = stateful_door(door, type_a) and stateful_door(partner, type_b)
if door.type == DoorType.Normal:
if type_a == DoorKind.SmallKey or type_b == DoorKind.SmallKey:
if valid_pair:
if type_a != DoorKind.SmallKey:
room_a.change(door.doorListPos, DoorKind.SmallKey)
if type_b != DoorKind.SmallKey:
room_b.change(partner.doorListPos, DoorKind.SmallKey)
add_pair(door, partner, world, player)
else:
if type_a == DoorKind.SmallKey:
remove_pair(door, world, player)
if type_b == DoorKind.SmallKey:
remove_pair(door, world, player)
elif type_a in [DoorKind.Bombable, DoorKind.Dashable] or type_b in [DoorKind.Bombable, DoorKind.Dashable]:
if valid_pair:
if type_a == type_b:
add_pair(door, partner, world, player)
spoiler_type = 'Bomb Door' if type_a == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player)
else:
new_type = DoorKind.Dashable if type_a == DoorKind.Dashable or type_b == DoorKind.Dashable else DoorKind.Bombable
if type_a != new_type:
room_a.change(door.doorListPos, new_type)
if type_b != new_type:
room_b.change(partner.doorListPos, new_type)
add_pair(door, partner, world, player)
spoiler_type = 'Bomb Door' if new_type == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player)
else:
if type_a in [DoorKind.Bombable, DoorKind.Dashable]:
room_a.change(door.doorListPos, DoorKind.Normal)
remove_pair(door, world, player)
elif type_b in [DoorKind.Bombable, DoorKind.Dashable]:
room_b.change(partner.doorListPos, DoorKind.Normal)
remove_pair(partner, world, player)
elif world.experimental[player] and valid_pair and type_a != DoorKind.SmallKey and type_b != DoorKind.SmallKey:
random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b)
world.paired_doors[player] = [x for x in world.paired_doors[player] if x.pair or x.original]
def add_pair(door_a, door_b, world, player):
pair_a, pair_b = None, None
for paired_door in world.paired_doors[player]:
if paired_door.door_a == door_a.name and paired_door.door_b == door_b.name:
paired_door.pair = True
return
if paired_door.door_a == door_b.name and paired_door.door_b == door_a.name:
paired_door.pair = True
return
if paired_door.door_a == door_a.name or paired_door.door_b == door_a.name:
pair_a = paired_door
if paired_door.door_a == door_b.name or paired_door.door_b == door_b.name:
pair_b = paired_door
if pair_a:
pair_a.pair = False
if pair_b:
pair_b.pair = False
world.paired_doors[player].append(PairedDoor(door_a, door_b))
def remove_pair(door, world, player):
for paired_door in world.paired_doors[player]:
if paired_door.door_a == door.name or paired_door.door_b == door.name:
paired_door.pair = False
break
def stateful_door(door, kind):
if 0 <= door.doorListPos < 4:
return kind in [DoorKind.Normal, DoorKind.SmallKey, DoorKind.Bombable, DoorKind.Dashable] #, DoorKind.BigKey]
return False
def random_door_type(door, partner, world, player, type_a, type_b, room_a, room_b):
r_kind = random.choices([DoorKind.Normal, DoorKind.Bombable, DoorKind.Dashable], [5, 2, 3], k=1)[0]
if r_kind != DoorKind.Normal:
if door.type == DoorType.Normal:
add_pair(door, partner, world, player)
if type_a != r_kind:
room_a.change(door.doorListPos, r_kind)
if type_b != r_kind:
room_b.change(partner.doorListPos, r_kind)
spoiler_type = 'Bomb Door' if r_kind == DoorKind.Bombable else 'Dash Door'
world.spoiler.set_door_type(door.name + ' <-> ' + partner.name, spoiler_type, player)
def determine_required_paths(world, player):
paths = {
'Hyrule Castle': ['Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby'],
@@ -1168,13 +1320,18 @@ def find_inaccessible_regions(world, player):
regs = convert_regions(start_regions, world, player)
all_regions = set([r for r in world.regions if r.player == player and r.type is not RegionType.Dungeon])
visited_regions = set()
queue = collections.deque(regs)
queue = deque(regs)
while len(queue) > 0:
next_region = queue.popleft()
visited_regions.add(next_region)
if next_region.name == 'Inverted Dark Sanctuary': # special spawn point in cave
for ent in next_region.entrances:
parent = ent.parent_region
if parent and parent.type is not RegionType.Dungeon and parent not in queue and parent not in visited_regions:
queue.append(parent)
for ext in next_region.exits:
connect = ext.connected_region
if connect is not None and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions:
if connect and connect.type is not RegionType.Dungeon and connect not in queue and connect not in visited_regions:
queue.append(connect)
world.inaccessible_regions[player].extend([r.name for r in all_regions.difference(visited_regions) if valid_inaccessible_region(r)])
if world.mode[player] == 'standard':
@@ -1194,8 +1351,8 @@ def add_inaccessible_doors(world, player):
# todo: ignore standard mode hyrule castle ledge?
for inaccessible_region in world.inaccessible_regions[player]:
region = world.get_region(inaccessible_region, player)
for exit in region.exits:
create_door(world, player, exit.name, region.name)
for ext in region.exits:
create_door(world, player, ext.name, region.name)
def create_door(world, player, entName, region_name):
@@ -1301,7 +1458,9 @@ def check_for_pinball_fix(state, bad_region, world, player):
@unique
class DROptions(Flag):
NoOptions = 0x00
Eternal_Mini_Bosses = 0x01 # If on, GT minibosses marked as defeated when they try to spawn a heart
Town_Portal = 0x02 # If on, Players will start with mirror scroll
Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required
# DATA GOES DOWN HERE
@@ -1312,7 +1471,13 @@ logical_connections = [
('Eastern Hint Tile Push Block', 'Eastern Hint Tile'),
('Eastern Map Balcony Hook Path', 'Eastern Map Room'),
('Eastern Map Room Drop Down', 'Eastern Map Balcony'),
('Desert Main Lobby Left Path', 'Desert Left Alcove'),
('Desert Main Lobby Right Path', 'Desert Right Alcove'),
('Desert Left Alcove Path', 'Desert Main Lobby'),
('Desert Right Alcove Path', 'Desert Main Lobby'),
('Hera Big Chest Landing Exit', 'Hera 4F'),
('PoD Pit Room Block Path N', 'PoD Pit Room Blocked'),
('PoD Pit Room Block Path S', 'PoD Pit Room'),
('PoD Arena Bonk Path', 'PoD Arena Bridge'),
('PoD Arena Main Crystal Path', 'PoD Arena Crystal'),
('PoD Arena Crystal Path', 'PoD Arena Main'),
@@ -1321,6 +1486,8 @@ logical_connections = [
('PoD Arena Bridge Drop Down', 'PoD Arena Main'),
('PoD Map Balcony Drop Down', 'PoD Sexy Statue'),
('PoD Basement Ledge Drop Down', 'PoD Stalfos Basement'),
('PoD Falling Bridge Path N', 'PoD Falling Bridge Ledge'),
('PoD Falling Bridge Path S', 'PoD Falling Bridge'),
('Swamp Lobby Moat', 'Swamp Entrance'),
('Swamp Entrance Moat', 'Swamp Lobby'),
('Swamp Trench 1 Approach Dry', 'Swamp Trench 1 Nexus'),
@@ -1389,6 +1556,8 @@ logical_connections = [
('Mire Map Spot Blue Barrier', 'Mire Crystal Dead End'),
('Mire Crystal Dead End Left Barrier', 'Mire Map Spot'),
('Mire Crystal Dead End Right Barrier', 'Mire Map Spike Side'),
('Mire Hidden Shooters Block Path S', 'Mire Hidden Shooters'),
('Mire Hidden Shooters Block Path N', 'Mire Hidden Shooters Blocked'),
('Mire Left Bridge Hook Path', 'Mire Right Bridge'),
('Mire Crystal Right Orange Barrier', 'Mire Crystal Mid'),
('Mire Crystal Mid Orange Barrier', 'Mire Crystal Right'),
@@ -1409,6 +1578,8 @@ logical_connections = [
('TR Crystal Maze Blue Path', 'TR Crystal Maze'),
('TR Crystal Maze Cane Path', 'TR Crystal Maze'),
('GT Blocked Stairs Block Path', 'GT Big Chest'),
('GT Speed Torch South Path', 'GT Speed Torch'),
('GT Speed Torch North Path', 'GT Speed Torch Upper'),
('GT Hookshot East-North Path', 'GT Hookshot North Platform'),
('GT Hookshot East-South Path', 'GT Hookshot South Platform'),
('GT Hookshot North-East Path', 'GT Hookshot East Platform'),
+53 -39
View File
@@ -182,6 +182,10 @@ def create_doors(world, player):
create_door(player, 'Desert Main Lobby N Edge', Open).dir(No, 0x84, None, High),
create_door(player, 'Desert Main Lobby NE Edge', Open).dir(No, 0x84, None, High),
create_door(player, 'Desert Main Lobby E Edge', Open).dir(Ea, 0x84, None, High),
create_door(player, 'Desert Main Lobby Left Path', Lgcl),
create_door(player, 'Desert Main Lobby Right Path', Lgcl),
create_door(player, 'Desert Left Alcove Path', Lgcl),
create_door(player, 'Desert Right Alcove Path', Lgcl),
create_door(player, 'Desert Dead End Edge', Open).dir(So, 0x74, None, High),
create_door(player, 'Desert East Wing W Edge', Open).dir(We, 0x85, None, High),
create_door(player, 'Desert East Wing N Edge', Open).dir(No, 0x85, None, High),
@@ -321,6 +325,8 @@ def create_doors(world, player):
create_door(player, 'PoD Pit Room NE', Nrml).dir(No, 0x3a, Right, High).pos(2),
create_door(player, 'PoD Pit Room Freefall', Hole),
create_door(player, 'PoD Pit Room Bomb Hole', Hole),
create_door(player, 'PoD Pit Room Block Path N', Lgcl),
create_door(player, 'PoD Pit Room Block Path S', Lgcl),
create_door(player, 'PoD Big Key Landing Hole', Hole),
create_door(player, 'PoD Big Key Landing Down Stairs', Sprl).dir(Dn, 0x3a, 0, HTH).ss(A, 0x11, 0x00).kill(),
create_door(player, 'PoD Basement Ledge Up Stairs', Sprl).dir(Up, 0x0a, 0, HTH).ss(A, 0x1a, 0xec).small_key().pos(0),
@@ -354,6 +360,8 @@ def create_doors(world, player):
create_door(player, 'PoD Falling Bridge SW', Nrml).dir(So, 0x1a, Left, High).small_key().pos(3),
create_door(player, 'PoD Falling Bridge WN', Nrml).dir(We, 0x1a, Top, High).small_key().pos(1),
create_door(player, 'PoD Falling Bridge EN', Intr).dir(Ea, 0x1a, Top, High).pos(4),
create_door(player, 'PoD Falling Bridge Path N', Lgcl),
create_door(player, 'PoD Falling Bridge Path S', Lgcl),
create_door(player, 'PoD Big Chest Balcony W', Nrml).dir(We, 0x1a, Mid, High).pos(2),
create_door(player, 'PoD Dark Maze EN', Nrml).dir(Ea, 0x19, Top, High).small_key().pos(1),
create_door(player, 'PoD Dark Maze E', Nrml).dir(Ea, 0x19, Mid, High).pos(0),
@@ -658,7 +666,7 @@ def create_doors(world, player):
create_door(player, 'Ice Freezors Bomb Hole', Hole), # combine these two? -- they have to lead to the same spot
create_door(player, 'Ice Freezors Ledge Hole', Hole),
create_door(player, 'Ice Freezors Ledge ES', Intr).dir(Ea, 0x7e, Bot, High).pos(2),
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(1),
create_door(player, 'Ice Tall Hint WS', Intr).dir(We, 0x7e, Bot, High).pos(2),
create_door(player, 'Ice Tall Hint EN', Nrml).dir(Ea, 0x7e, Top, High).pos(1),
create_door(player, 'Ice Tall Hint SE', Nrml).dir(So, 0x7e, Right, High).small_key().pos(0),
create_door(player, 'Ice Hookshot Ledge WN', Nrml).dir(We, 0x7f, Top, High).no_exit().trap(0x4).pos(0).kill(),
@@ -735,6 +743,8 @@ def create_doors(world, player):
create_door(player, 'Mire Hidden Shooters ES', Nrml).dir(Ea, 0xb2, Bot, High).pos(7),
create_door(player, 'Mire Hidden Shooters WS', Intr).dir(We, 0xb2, Bot, High).pos(1),
create_door(player, 'Mire Cross ES', Intr).dir(Ea, 0xb2, Bot, High).pos(1),
create_door(player, 'Mire Hidden Shooters Block Path S', Lgcl),
create_door(player, 'Mire Hidden Shooters Block Path N', Lgcl),
create_door(player, 'Mire Hidden Shooters NE', Intr).dir(No, 0xb2, Right, High).pos(2),
create_door(player, 'Mire Minibridge SE', Intr).dir(So, 0xb2, Right, High).pos(2),
create_door(player, 'Mire Cross SW', Nrml).dir(So, 0xb2, Left, High).pos(5),
@@ -912,6 +922,8 @@ def create_doors(world, player):
create_door(player, 'GT Tile Room EN', Intr).dir(Ea, 0x8d, Top, High).small_key().pos(1),
create_door(player, 'GT Speed Torch WN', Intr).dir(We, 0x8d, Top, High).small_key().pos(1),
create_door(player, 'GT Speed Torch NE', Nrml).dir(No, 0x8d, Right, High).pos(3),
create_door(player, 'GT Speed Torch South Path', Lgcl),
create_door(player, 'GT Speed Torch North Path', Lgcl),
create_door(player, 'GT Speed Torch WS', Intr).dir(We, 0x8d, Bot, High).pos(4),
create_door(player, 'GT Pots n Blocks ES', Intr).dir(Ea, 0x8d, Bot, High).pos(4),
create_door(player, 'GT Speed Torch SE', Nrml).dir(So, 0x8d, Right, High).trap(0x4).pos(0),
@@ -1098,6 +1110,8 @@ def create_doors(world, player):
world.get_door('PoD Arena Crystal Path', player).barrier(CrystalBarrier.Blue)
world.get_door('PoD Sexy Statue W', player).c_switch()
world.get_door('PoD Sexy Statue NW', player).c_switch()
world.get_door('PoD Map Balcony WS', player).c_switch()
world.get_door('PoD Map Balcony South Stairs', player).c_switch()
world.get_door('PoD Bow Statue SW', player).c_switch()
world.get_door('PoD Bow Statue Down Ladder', player).c_switch()
world.get_door('PoD Dark Pegs Up Ladder', player).c_switch()
@@ -1211,44 +1225,44 @@ def create_doors(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'),
PairedDoor('TR Pokey 2 ES', 'TR Lava Island WS'), # TR Pokey Key
PairedDoor('TR Dodgers NE', 'TR Lava Escape SE'), # TR Big key door by pipes
PairedDoor('PoD Falling Bridge WN', 'PoD Dark Maze EN'), # Pod Dark maze door
PairedDoor('PoD Dark Maze E', 'PoD Big Chest Balcony W'), # PoD Bombable by Big Chest
PairedDoor('PoD Arena Main NW', 'PoD Falling Bridge SW'), # Pod key door by bridge
PairedDoor('Sewers Dark Cross Key Door N', 'Sewers Dark Cross Key Door S'),
PairedDoor('Swamp Hub WN', 'Swamp Crystal Switch EN'), # Swamp key door crystal switch
PairedDoor('Swamp Hub North Ledge N', 'Swamp Push Statue S'), # Swamp key door above big chest
PairedDoor('PoD Map Balcony WS', 'PoD Arena Ledge ES'), # Pod bombable by arena
PairedDoor('Swamp Hub Dead Ledge EN', 'Swamp Hammer Switch WN'), # Swamp bombable to random pots
PairedDoor('Swamp Pot Row WN', 'Swamp Map Ledge EN'), # Swamp bombable to map chest
PairedDoor('Swamp Pot Row WS', 'Swamp Trench 1 Approach ES'), # Swamp key door early room $38
PairedDoor('PoD Middle Cage N', 'PoD Pit Room S'),
PairedDoor('GT Crystal Circles SW', 'GT Left Moldorm Ledge NW'), # GT moldorm key door
PairedDoor('Ice Conveyor SW', 'Ice Bomb Jump NW'), # Ice BJ key door
PairedDoor('Desert Tiles 2 SE', 'Desert Beamos Hall NE'),
PairedDoor('Skull 3 Lobby NW', 'Skull Star Pits SW'), # Skull 3 key door
PairedDoor('Skull 1 Lobby WS', 'Skull Pot Prison ES'), # Skull 1 key door - pot prison to big chest
PairedDoor('Skull Map Room SE', 'Skull Pinball NE'), # Skull 1 - pinball key door
PairedDoor('GT Dash Hall NE', 'GT Hidden Spikes SE'), # gt main big key door
PairedDoor('Ice Spike Cross ES', 'Ice Spike Room WS'), # ice door to spike chest
PairedDoor('GT Conveyor Star Pits EN', 'GT Falling Bridge WN'), # gt right side key door to cape bridge
PairedDoor('GT Warp Maze (Rails) WS', 'GT Randomizer Room ES'), # gt bombable to rando room
PairedDoor('Ice Tall Hint SE', 'Ice Lonely Freezor NE'), # ice's big icy room key door to lonely freezor
PairedDoor('Eastern Courtyard N', 'Eastern Darkness S'),
PairedDoor('Mire Fishbone SE', 'Mire Spike Barrier NE'), # mire fishbone key door
PairedDoor('Mire BK Door Room N', 'Mire Left Bridge S'), # mire big key door to bridges
PairedDoor('Eastern Big Key NE', 'Eastern Hint Tile Blocked Path SE'),
PairedDoor('TR Hub NW', 'TR Pokey 1 SW'), # TR somaria hub to pokey
PairedDoor('Eastern Dark Square Key Door WN', 'Eastern Cannonball Ledge Key Door EN'),
PairedDoor('Thieves Rail Ledge NW', 'Thieves Pot Alcove Bottom SW'), # TT random bomb to pots
PairedDoor('Thieves BK Corner NE', 'Thieves Hallway SE'), # TT big key door
PairedDoor('Ice Switch Room ES', 'Ice Refill WS'), # Ice last key door to crystal switch
PairedDoor('Mire Hub WS', 'Mire Conveyor Crystal ES'), # mire hub key door to attic
PairedDoor('Mire Hub Right EN', 'Mire Map Spot WN'), # mire hub key door to map
PairedDoor('TR Dash Bridge WS', 'TR Crystal Maze ES'), # tr last key door to switch maze
PairedDoor('Thieves Ambush E', 'Thieves Rail Ledge W') # TT dashable above
PairedDoor('Sewers Secret Room Key Door S', 'Sewers Key Rat Key Door N', 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
PairedDoor('PoD Dark Maze E', 'PoD Big Chest Balcony W', True), # PoD Bombable by Big Chest
PairedDoor('PoD Arena Main NW', 'PoD Falling Bridge SW', True), # Pod key door by bridge
PairedDoor('Sewers Dark Cross Key Door N', 'Sewers Dark Cross Key Door S', True),
PairedDoor('Swamp Hub WN', 'Swamp Crystal Switch EN', True), # Swamp key door crystal switch
PairedDoor('Swamp Hub North Ledge N', 'Swamp Push Statue S', True), # Swamp key door above big chest
PairedDoor('PoD Map Balcony WS', 'PoD Arena Ledge ES', True), # Pod bombable by arena
PairedDoor('Swamp Hub Dead Ledge EN', 'Swamp Hammer Switch WN', True), # Swamp bombable to random pots
PairedDoor('Swamp Pot Row WN', 'Swamp Map Ledge EN', True), # Swamp bombable to map chest
PairedDoor('Swamp Pot Row WS', 'Swamp Trench 1 Approach ES', True), # Swamp key door early room $38
PairedDoor('PoD Middle Cage N', 'PoD Pit Room S', True),
PairedDoor('GT Crystal Circles SW', 'GT Left Moldorm Ledge NW', True), # GT moldorm key door
PairedDoor('Ice Conveyor SW', 'Ice Bomb Jump NW', True), # Ice BJ key door
PairedDoor('Desert Tiles 2 SE', 'Desert Beamos Hall NE', True),
PairedDoor('Skull 3 Lobby NW', 'Skull Star Pits SW', True), # Skull 3 key door
PairedDoor('Skull 1 Lobby WS', 'Skull Pot Prison ES', True), # Skull 1 key door - pot prison to big chest
PairedDoor('Skull Map Room SE', 'Skull Pinball NE', True), # Skull 1 - pinball key door
PairedDoor('GT Dash Hall NE', 'GT Hidden Spikes SE', True), # gt main big key door
PairedDoor('Ice Spike Cross ES', 'Ice Spike Room WS', True), # ice door to spike chest
PairedDoor('GT Conveyor Star Pits EN', 'GT Falling Bridge WN', True), # gt right side key door to cape bridge
PairedDoor('GT Warp Maze (Rails) WS', 'GT Randomizer Room ES', True), # gt bombable to rando room
PairedDoor('Ice Tall Hint SE', 'Ice Lonely Freezor NE', True), # ice's big icy room key door to lonely freezor
PairedDoor('Eastern Courtyard N', 'Eastern Darkness S', True),
PairedDoor('Mire Fishbone SE', 'Mire Spike Barrier NE', True), # mire fishbone key door
PairedDoor('Mire BK Door Room N', 'Mire Left Bridge S', True), # mire big key door to bridges
PairedDoor('Eastern Big Key NE', 'Eastern Hint Tile Blocked Path SE', True),
PairedDoor('TR Hub NW', 'TR Pokey 1 SW', True), # TR somaria hub to pokey
PairedDoor('Eastern Dark Square Key Door WN', 'Eastern Cannonball Ledge Key Door EN', True),
PairedDoor('Thieves Rail Ledge NW', 'Thieves Pot Alcove Bottom SW', True), # TT random bomb to pots
PairedDoor('Thieves BK Corner NE', 'Thieves Hallway SE', True), # TT big key door
PairedDoor('Ice Switch Room ES', 'Ice Refill WS', True), # Ice last key door to crystal switch
PairedDoor('Mire Hub WS', 'Mire Conveyor Crystal ES', True), # mire hub key door to attic
PairedDoor('Mire Hub Right EN', 'Mire Map Spot WN', True), # mire hub key door to map
PairedDoor('TR Dash Bridge WS', 'TR Crystal Maze ES', True), # tr last key door to switch maze
PairedDoor('Thieves Ambush E', 'Thieves Rail Ledge W', True) # TT dashable above
]
+116 -16
View File
@@ -1,5 +1,6 @@
import random
import collections
import itertools
from collections import defaultdict, deque
from enum import Enum, unique
import logging
@@ -447,9 +448,12 @@ def stonewall_valid(stonewall):
if bad_door.blocked:
return True # great we're done with this one
loop_region = stonewall.entrance.parent_region
start_region = bad_door.entrance.parent_region
queue = deque([start_region])
visited = {start_region}
start_regions = [bad_door.entrance.parent_region]
if bad_door.dependents:
for dep in bad_door.dependents:
start_regions.append(dep.entrance.parent_region)
queue = deque(start_regions)
visited = set(start_regions)
while len(queue) > 0:
region = queue.popleft()
if region == loop_region:
@@ -1027,6 +1031,7 @@ class DungeonBuilder(object):
self.key_doors_num = None
self.combo_size = None
self.flex = 0
self.key_door_proposal = None
if name in dungeon_dead_end_allowance.keys():
self.allowance = dungeon_dead_end_allowance[name]
@@ -1463,20 +1468,32 @@ def assign_polarized_sectors(dungeon_map, polarized_sectors, global_pole, logger
problem_builders = identify_branching_issues_2(problem_builders)
# step 5: assign randomly until gone - must maintain connectedness, neutral polarity, branching, lack, etc.
comb_w_replace = len(dungeon_map) ** len(neutral_choices)
combinations = None
if comb_w_replace <= 1000:
combinations = list(itertools.product(dungeon_map.keys(), repeat=len(neutral_choices)))
random.shuffle(combinations)
tries = 0
while len(polarized_sectors) > 0:
if tries > 100:
if tries > 1000 or (combinations and tries >= len(combinations)):
raise Exception('No valid assignment found. Ref: %s' % next(iter(dungeon_map.keys())))
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
valid = []
if combinations:
choices = combinations[tries]
else:
choices = random.choices(list(dungeon_map.keys()), k=len(neutral_choices))
chosen_sectors = defaultdict(list)
for i, choice in enumerate(choices):
builder = dungeon_map[choice]
if valid_assignment(builder, neutral_choices[i]):
chosen_sectors[choice].extend(neutral_choices[i])
all_valid = True
for name, sector_list in chosen_sectors.items():
if not valid_assignment(dungeon_map[name], sector_list):
all_valid = False
break
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)
valid.append(neutral_choices[i])
for c in valid:
neutral_choices.remove(c)
tries += 1
@@ -1927,6 +1944,7 @@ def resolve_equations(builder, sector_list):
# negative benefit transforms (dead end)
def find_priority_equation(equations, current_access):
flex = calc_flex(equations, current_access)
required = calc_required(equations, current_access)
best_profit = None
triplet_candidates = []
local_profit_map = {}
@@ -1947,14 +1965,17 @@ def find_priority_equation(equations, current_access):
else:
triplet_candidates.append((eq, eq_list, sector))
local_profit_map[sector] = best_local_profit
if len(triplet_candidates) == 0:
filtered_candidates = filter_requirements(triplet_candidates, equations, required, current_access)
if len(filtered_candidates) == 0:
filtered_candidates = triplet_candidates
if len(filtered_candidates) == 0:
return None, None, None # can't pay for anything
if len(triplet_candidates) == 1:
return triplet_candidates[0]
if len(filtered_candidates) == 1:
return filtered_candidates[0]
required_candidates = [x for x in triplet_candidates if x[0].required]
required_candidates = [x for x in filtered_candidates if x[0].required]
if len(required_candidates) == 0:
required_candidates = triplet_candidates
required_candidates = filtered_candidates
if len(required_candidates) == 1:
return required_candidates[0]
@@ -1970,6 +1991,46 @@ def find_priority_equation(equations, current_access):
return good_local_candidates[0] # just pick one I guess
def calc_required(equations, current_access):
ttl = 0
for num in current_access.values():
ttl += num
local_profit_map = {}
for sector, eq_list in equations.items():
best_local_profit = None
for eq in eq_list:
profit = eq.profit()
if best_local_profit is None or profit > best_local_profit:
best_local_profit = profit
local_profit_map[sector] = best_local_profit
ttl += best_local_profit
if ttl == 0:
new_lists = {}
for sector, eq_list in equations.items():
if len(eq_list) > 1:
rem_list = []
for eq in eq_list:
if eq.profit() < local_profit_map[sector]:
rem_list.append(eq)
if len(rem_list) > 0:
new_lists[sector] = [x for x in eq_list if x not in rem_list]
for sector, eq_list in new_lists.items():
if len(eq_list) <= 1:
for eq in eq_list:
eq.required = True
equations[sector] = eq_list
required_costs = defaultdict(int)
required_benefits = defaultdict(int)
for sector, eq_list in equations.items():
for eq in eq_list:
if eq.required:
for key, door_list in eq.cost.items():
required_costs[key] += len(door_list)
for key, door_list in eq.benefit.items():
required_benefits[key] += len(door_list)
return required_costs, required_benefits
def calc_flex(equations, current_access):
flex_spending = defaultdict(int)
required_costs = defaultdict(int)
@@ -1983,6 +2044,45 @@ def calc_flex(equations, current_access):
return flex_spending
def filter_requirements(triplet_candidates, equations, required, current_access):
r_costs, r_exits = required
valid_candidates = []
for cand, cand_list, cand_sector in triplet_candidates:
valid = True
if not cand.required:
potential_benefit = defaultdict(int)
potential_costs = defaultdict(int)
for h_type, benefit in current_access.items():
cur_cost = len(cand.cost[h_type])
if benefit - cur_cost > 0:
potential_benefit[h_type] += benefit - cur_cost
for h_type, benefit_list in cand.benefit.items():
potential_benefit[h_type] += len(benefit_list)
for sector, eq_list in equations.items():
if sector == cand_sector:
affected_doors = [d for x in cand.benefit.values() for d in x] + [d for x in cand.cost.values() for d in x]
adj_list = [x for x in eq_list if x.door not in affected_doors]
else:
adj_list = eq_list
for eq in adj_list:
for h_type, benefit_list in eq.benefit.items():
potential_benefit[h_type] += len(benefit_list)
for h_type, cost_list in eq.cost.items():
potential_costs[h_type] += len(cost_list)
for h_type, requirement in r_costs.items():
if requirement > 0 and potential_benefit[h_type] < requirement:
valid = False
break
if valid:
for h_type, requirement in r_exits.items():
if requirement > 0 and potential_costs[h_type] < requirement:
valid = False
break
if valid:
valid_candidates.append((cand, cand_list, cand_sector))
return valid_candidates
def resolve_equation(equation, eq_list, sector, current_access, reached_doors, equations):
for key, door_list in equation.cost.items():
if current_access[key] - len(door_list) < 0:
+7 -302
View File
@@ -8,312 +8,12 @@ import textwrap
import shlex
import sys
from CLI import parse_arguments
from Main import main
from Rom import get_sprite_from_name
from Utils import is_bundled, close_console
from Fill import FillError
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def _get_help_string(self, action):
return textwrap.dedent(action.help)
def parse_arguments(argv, no_defaults=False):
def defval(value):
return value if not no_defaults else None
# we need to know how many players we have first
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args(argv)
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--create_spoiler', help='Output a Spoiler File', action='store_true')
parser.add_argument('--logic', default=defval('noglitches'), const='noglitches', nargs='?', choices=['noglitches', 'minorglitches', 'nologic'],
help='''\
Select Enforcement of Item Requirements. (default: %(default)s)
No Glitches:
Minor Glitches: May require Fake Flippers, Bunny Revival
and Dark Room Navigation.
No Logic: Distribute items without regard for
item requirements.
''')
parser.add_argument('--mode', default=defval('open'), const='open', nargs='?', choices=['standard', 'open', 'inverted'],
help='''\
Select game mode. (default: %(default)s)
Open: World starts with Zelda rescued.
Standard: Fixes Hyrule Castle Secret Entrance and Front Door
but may lead to weird rain state issues if you exit
through the Hyrule Castle side exits before rescuing
Zelda in a full shuffle.
Inverted: Starting locations are Dark Sanctuary in West Dark
World or at Link's House, which is shuffled freely.
Requires the moon pearl to be Link in the Light World
instead of a bunny.
''')
parser.add_argument('--swords', default=defval('random'), const='random', nargs='?', choices= ['random', 'assured', 'swordless', 'vanilla'],
help='''\
Select sword placement. (default: %(default)s)
Random: All swords placed randomly.
Assured: Start game with a sword already.
Swordless: No swords. Curtains in Skull Woods and Agahnim\'s
Tower are removed, Agahnim\'s Tower barrier can be
destroyed with hammer. Misery Mire and Turtle Rock
can be opened without a sword. Hammer damages Ganon.
Ether and Bombos Tablet can be activated with Hammer
(and Book). Bombos pads have been added in Ice
Palace, to allow for an alternative to firerod.
Vanilla: Swords are in vanilla locations.
''')
parser.add_argument('--goal', default=defval('ganon'), const='ganon', nargs='?', choices=['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals'],
help='''\
Select completion goal. (default: %(default)s)
Ganon: Collect all crystals, beat Agahnim 2 then
defeat Ganon.
Crystals: Collect all crystals then defeat Ganon.
Pedestal: Places the Triforce at the Master Sword Pedestal.
All Dungeons: Collect all crystals, pendants, beat both
Agahnim fights and then defeat Ganon.
Triforce Hunt: Places 30 Triforce Pieces in the world, collect
20 of them to beat the game.
''')
parser.add_argument('--difficulty', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select game difficulty. Affects available itempool. (default: %(default)s)
Normal: Normal difficulty.
Hard: A harder setting with less equipment and reduced health.
Expert: A harder yet setting with minimum equipment and health.
''')
parser.add_argument('--item_functionality', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'hard', 'expert'],
help='''\
Select limits on item functionality to increase difficulty. (default: %(default)s)
Normal: Normal functionality.
Hard: Reduced functionality.
Expert: Greatly reduced functionality.
''')
parser.add_argument('--timer', default=defval('none'), const='normal', nargs='?', choices=['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'],
help='''\
Select game timer setting. Affects available itempool. (default: %(default)s)
None: No timer.
Display: Displays a timer but does not affect
the itempool.
Timed: Starts with clock at zero. Green Clocks
subtract 4 minutes (Total: 20), Blue Clocks
subtract 2 minutes (Total: 10), Red Clocks add
2 minutes (Total: 10). Winner is player with
lowest time at the end.
Timed OHKO: Starts clock at 10 minutes. Green Clocks add
5 minutes (Total: 25). As long as clock is at 0,
Link will die in one hit.
OHKO: Like Timed OHKO, but no clock items are present
and the clock is permenantly at zero.
Timed Countdown: Starts with clock at 40 minutes. Same clocks as
Timed mode. If time runs out, you lose (but can
still keep playing).
''')
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', choices=['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26', 'balanced'],
help='''\
Select item filling algorithm. (default: %(default)s
balanced: vt26 derivative that aims to strike a balance between
the overworld heavy vt25 and the dungeon heavy vt26
algorithm.
vt26: Shuffle items and place them in a random location
that it is not impossible to be in. This includes
dungeon keys and items.
vt25: Shuffle items and place them in a random location
that it is not impossible to be in.
vt21: Unbiased in its selection, but has tendency to put
Ice Rod in Turtle Rock.
vt22: Drops off stale locations after 1/3 of progress
items were placed to try to circumvent vt21\'s
shortcomings.
Freshness: Keep track of stale locations (ones that cannot be
reached yet) and decrease likeliness of selecting
them the more often they were found unreachable.
Flood: Push out items starting from Link\'s House and
slightly biased to placing progression items with
less restrictions.
''')
parser.add_argument('--shuffle', default=defval('vanilla'), const='full', nargs='?', choices=['vanilla', 'simple', 'restricted', 'full', 'crossed', 'insanity', 'restricted_legacy', 'full_legacy', 'madness_legacy', 'insanity_legacy', 'dungeonsfull', 'dungeonssimple'],
help='''\
Select Entrance Shuffling Algorithm. (default: %(default)s)
Full: Mix cave and dungeon entrances freely while limiting
multi-entrance caves to one world.
Simple: Shuffle Dungeon Entrances/Exits between each other
and keep all 4-entrance dungeons confined to one
location. All caves outside of death mountain are
shuffled in pairs and matched by original type.
Restricted: Use Dungeons shuffling from Simple but freely
connect remaining entrances.
Crossed: Mix cave and dungeon entrances freely while allowing
caves to cross between worlds.
Insanity: Decouple entrances and exits from each other and
shuffle them freely. Caves that used to be single
entrance will still exit to the same location from
which they are entered.
Vanilla: All entrances are in the same locations they were
in the base game.
Legacy shuffles preserve behavior from older versions of the
entrance randomizer including significant technical limitations.
The dungeon variants only mix up dungeons and keep the rest of
the overworld vanilla.
''')
parser.add_argument('--door_shuffle', default=defval('basic'), const='vanilla', nargs='?', choices=['vanilla', 'basic', 'crossed', 'experimental'],
help='''\
Select Door Shuffling Algorithm. (default: %(default)s)
Basic: Doors are mixed within a single dungeon.
(Not yet implemented)
Crossed: Doors are mixed between all dungeons.
(Not yet implemented)
Vanilla: All doors are connected the same way they were in the
base game.
Experimental: Experimental mixes live here. Use at your own risk.
''')
parser.add_argument('--crystals_ganon', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to defeat ganon. Any other
requirements for ganon for the selected goal still apply.
This setting does not apply when the all dungeons goal is
selected. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--crystals_gt', default=defval('7'), const='7', nargs='?', choices=['random', '0', '1', '2', '3', '4', '5', '6', '7'],
help='''\
How many crystals are needed to open GT. For inverted mode
this applies to the castle tower door instead. (default: %(default)s)
Random: Picks a random value between 0 and 7 (inclusive).
0-7: Number of crystals needed
''')
parser.add_argument('--openpyramid', default=defval(False), help='''\
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
''', action='store_true')
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'), help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--count', help='''\
Use to batch generate multiple seeds with same settings.
If --seed is provided, it will be used for the first seed, then
used to derive the next seed (i.e. generating 10 seeds with
--seed given will produce the same 10 (different) roms each
time).
''', type=int)
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?', choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', default=defval(False), help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', default=defval(False), help='Disables game music.', action='store_true')
parser.add_argument('--mapshuffle', default=defval(False), help='Maps are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--compassshuffle', default=defval(False), help='Compasses are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keyshuffle', default=defval(False), help='Small Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--bigkeyshuffle', default=defval(False), help='Big Keys are no longer restricted to their dungeons, but can be anywhere', action='store_true')
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1.
''', action='store_true')
parser.add_argument('--startinventory', default=defval(''), help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--custom', default=defval(False), help='Not supported.')
parser.add_argument('--customitemarray', default=defval(False), help='Not supported.')
parser.add_argument('--accessibility', default=defval('items'), const='items', nargs='?', choices=['items', 'locations', 'none'], help='''\
Select Item/Location Accessibility. (default: %(default)s)
Items: You can reach all unique inventory items. No guarantees about
reaching all locations or all keys.
Locations: You will be able to reach every location in the game.
None: You will be able to reach enough locations to beat the game.
''')
parser.add_argument('--hints', default=defval(False), help='''\
Make telepathic tiles and storytellers give helpful hints.
''', action='store_true')
# included for backwards compatibility
parser.add_argument('--shuffleganon', help=argparse.SUPPRESS, action='store_true', default=defval(True))
parser.add_argument('--no-shuffleganon', help='''\
If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool.
''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout'])
parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in
binary format and have a length of 0x7000 (28672) bytes,
or 0x7078 (28792) bytes including palette data.
Alternatively, can be a ALttP Rom patched with a Link
sprite that will be extracted.
''')
parser.add_argument('--suppress_rom', help='Do not create an output rom file.', action='store_true')
parser.add_argument('--gui', help='Launch the GUI', action='store_true')
parser.add_argument('--jsonout', action='store_true', help='''\
Output .json patch to stdout instead of a patched rom. Used
for VT site integration, do not use otherwise.
''')
parser.add_argument('--skip_playthrough', action='store_true', default=defval(False))
parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core'))
parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos'])
parser.add_argument('--shuffleenemies', default=defval('none'), choices=['none', 'shuffled', 'chaos'])
parser.add_argument('--enemy_health', default=defval('default'), choices=['default', 'easy', 'normal', 'hard', 'expert'])
parser.add_argument('--enemy_damage', default=defval('default'), choices=['default', 'shuffled', 'chaos'])
parser.add_argument('--shufflepots', default=defval(False), action='store_true')
parser.add_argument('--beemizer', default=defval(0), type=lambda value: min(max(int(value), 0), 4))
parser.add_argument('--remote_items', default=defval(False), action='store_true')
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath')
parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname')
if multiargs.multi:
for player in range(1, multiargs.multi + 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
if multiargs.multi:
defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1):
playerargs = parse_arguments(shlex.split(getattr(ret,f"p{player}")), True)
for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'door_shuffle', 'crystals_ganon', 'crystals_gt', 'openpyramid',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
else:
getattr(ret, name)[player] = value
return ret
def start():
args = parse_arguments(None)
@@ -359,7 +59,12 @@ def start():
seed = random.randint(0, 999999999)
for fail in failures:
logger.info('%s seed failed with: %s', fail[1], fail[0])
logger.info('Generation fail rate: %f%%', 100*len(failures)/args.count)
fail_rate = 100 * len(failures) / args.count
success_rate = 100 * (args.count - len(failures)) / args.count
fail_rate = str(fail_rate).split('.')
success_rate = str(success_rate).split('.')
logger.info('Generation fail rate: ' + str(fail_rate[0] ).rjust(3, " ") + '.' + str(fail_rate[1] ).ljust(6, '0') + '%')
logger.info('Generation success rate: ' + str(success_rate[0]).rjust(3, " ") + '.' + str(success_rate[1]).ljust(6, '0') + '%')
else:
main(seed=args.seed, args=args)
+59
View File
@@ -0,0 +1,59 @@
# -*- mode: python -*-
block_cipher = None
console = True
def recurse_for_py_files(names_so_far):
returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)):
if name != "__pycache__":
subdir_name = os.path.join(*names_so_far, name)
if os.path.isdir(subdir_name):
new_name_list = names_so_far + [name]
for filename in os.listdir(os.path.join(*new_name_list)):
base_file,file_extension = os.path.splitext(filename)
if file_extension == ".py":
new_name = ".".join(new_name_list+[base_file])
if not new_name in returnvalue:
returnvalue.append(new_name)
returnvalue.extend(recurse_for_py_files(new_name_list))
returnvalue.append("PIL._tkinter_finder") #Linux needs this
return returnvalue
hiddenimports = []
a = Analysis(['DungeonRandomizer.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller
excluded_binaries = [
'VCRUNTIME140.dll',
'msvcp140.dll',
'mfc140u.dll']
a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries])
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='DungeonRandomizer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=console ) # <--- change this to True to enable command prompt when the app runs
+40 -21
View File
@@ -182,11 +182,12 @@ eastern_regions = [
]
desert_regions = [
'Desert Main Lobby', 'Desert Dead End', 'Desert East Lobby', 'Desert East Wing', 'Desert Compass Room',
'Desert Cannonball', 'Desert Arrow Pot Corner', 'Desert Trap Room', 'Desert North Hall', 'Desert Map Room',
'Desert Sandworm Corner', 'Desert Bonk Torch', 'Desert Circle of Pots', 'Desert Big Chest Room', 'Desert West Wing',
'Desert West Lobby', 'Desert Fairy Fountain', 'Desert Back Lobby', 'Desert Tiles 1',
'Desert Bridge', 'Desert Four Statues', 'Desert Beamos Hall', 'Desert Tiles 2', 'Desert Wall Slide', 'Desert Boss',
'Desert Main Lobby', 'Desert Left Alcove', 'Desert Right Alcove', 'Desert Dead End', 'Desert East Lobby',
'Desert East Wing', 'Desert Compass Room', 'Desert Cannonball', 'Desert Arrow Pot Corner', 'Desert Trap Room',
'Desert North Hall', 'Desert Map Room', 'Desert Sandworm Corner', 'Desert Bonk Torch', 'Desert Circle of Pots',
'Desert Big Chest Room', 'Desert West Wing', 'Desert West Lobby', 'Desert Fairy Fountain', 'Desert Back Lobby',
'Desert Tiles 1', 'Desert Bridge', 'Desert Four Statues', 'Desert Beamos Hall', 'Desert Tiles 2',
'Desert Wall Slide', 'Desert Boss'
]
hera_regions = [
@@ -203,12 +204,13 @@ tower_regions = [
]
pod_regions = [
'PoD Lobby', 'PoD Left Cage', 'PoD Middle Cage', 'PoD Shooter Room', 'PoD Pit Room', 'PoD Arena Main',
'PoD Arena North', 'PoD Arena Crystal', 'PoD Arena Bridge', 'PoD Arena Ledge', 'PoD Sexy Statue', 'PoD Map Balcony',
'PoD Conveyor', 'PoD Mimics 1', 'PoD Jelly Hall', 'PoD Warp Hint', 'PoD Warp Room', 'PoD Stalfos Basement',
'PoD Basement Ledge', 'PoD Big Key Landing', 'PoD Falling Bridge', 'PoD Dark Maze', 'PoD Big Chest Balcony',
'PoD Compass Room', 'PoD Dark Basement', 'PoD Harmless Hellway', 'PoD Mimics 2', 'PoD Bow Statue', 'PoD Dark Pegs',
'PoD Lonely Turtle', 'PoD Turtle Party', 'PoD Dark Alley', 'PoD Callback', 'PoD Boss'
'PoD Lobby', 'PoD Left Cage', 'PoD Middle Cage', 'PoD Shooter Room', 'PoD Pit Room', 'PoD Pit Room Blocked',
'PoD Arena Main', 'PoD Arena North', 'PoD Arena Crystal', 'PoD Arena Bridge', 'PoD Arena Ledge', 'PoD Sexy Statue',
'PoD Map Balcony', 'PoD Conveyor', 'PoD Mimics 1', 'PoD Jelly Hall', 'PoD Warp Hint', 'PoD Warp Room',
'PoD Stalfos Basement', 'PoD Basement Ledge', 'PoD Big Key Landing', 'PoD Falling Bridge',
'PoD Falling Bridge Ledge', 'PoD Dark Maze', 'PoD Big Chest Balcony', 'PoD Compass Room', 'PoD Dark Basement',
'PoD Harmless Hellway', 'PoD Mimics 2', 'PoD Bow Statue', 'PoD Dark Pegs', 'PoD Lonely Turtle', 'PoD Turtle Party',
'PoD Dark Alley', 'PoD Callback', 'PoD Boss'
]
swamp_regions = [
@@ -255,14 +257,15 @@ ice_regions = [
mire_regions = [
'Mire Lobby', 'Mire Post-Gap', 'Mire 2', 'Mire Hub', 'Mire Hub Right', 'Mire Hub Top', 'Mire Lone Shooter',
'Mire Failure Bridge', 'Mire Falling Bridge', 'Mire Map Spike Side', 'Mire Map Spot', 'Mire Crystal Dead End',
'Mire Hidden Shooters', '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 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 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 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 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 Crystal Right', 'Mire Crystal Mid', 'Mire Crystal Left',
'Mire Crystal Top', 'Mire Shooter Rupees', 'Mire Falling Foes', 'Mire Firesnake Skip', 'Mire Antechamber',
'Mire Boss'
]
tr_regions = [
@@ -276,8 +279,8 @@ tr_regions = [
gt_regions = [
'GT Lobby', 'GT Bob\'s Torch', 'GT Hope Room', 'GT Big Chest', 'GT Blocked Stairs', 'GT Bob\'s Room',
'GT Tile Room', 'GT Speed Torch', 'GT Pots n Blocks', 'GT Crystal Conveyor', 'GT Compass Room',
'GT Invisible Bridges', 'GT Invisible Catwalk', 'GT Conveyor Cross', 'GT Hookshot East Platform',
'GT Tile Room', 'GT Speed Torch', 'GT Speed Torch Upper', 'GT Pots n Blocks', 'GT Crystal Conveyor',
'GT Compass Room', 'GT Invisible Bridges', 'GT Invisible Catwalk', 'GT Conveyor Cross', 'GT Hookshot East Platform',
'GT Hookshot North Platform', 'GT Hookshot South Platform', 'GT Hookshot South Entry', 'GT Map Room',
'GT Double Switch Entry', 'GT Double Switch Switches', 'GT Double Switch Transition', 'GT Double Switch Key Spot',
'GT Double Switch Exit', 'GT Spike Crystals', 'GT Warp Maze - Left Section', 'GT Warp Maze - Mid Section',
@@ -380,3 +383,19 @@ dungeon_bigs = {
'Ganons Tower': 'Big Key (Ganons Tower)'
}
dungeon_hints = {
'Hyrule Castle': 'in Hyrule Castle',
'Eastern Palace': 'in Eastern Palace',
'Desert Palace': 'in Desert Palace',
'Tower of Hera': 'in Tower of Hera',
'Agahnims Tower': 'in Castle Tower',
'Palace of Darkness': 'in Palace of Darkness',
'Swamp Palace': 'in Swamp Palace)',
'Skull Woods': 'in Skull Woods',
'Thieves Town': 'in Thieves\' Town)',
'Ice Palace': 'in Ice Palace',
'Misery Mire': 'in Misery Mire',
'Turtle Rock': 'in Turtle Rock',
'Ganons Tower': 'in Ganon\'s Tower'
}
+6 -2
View File
@@ -276,6 +276,8 @@ def link_entrances(world, player):
if world.mode[player] == 'standard':
# must connect front of hyrule castle to do escape
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
elif world.doorShuffle[player] != 'vanilla':
lw_entrances.append('Hyrule Castle Entrance (South)')
else:
caves.append(tuple(random.sample(['Hyrule Castle Exit (South)', 'Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)'],3)))
lw_entrances.append('Hyrule Castle Entrance (South)')
@@ -312,6 +314,10 @@ def link_entrances(world, player):
if world.mode[player] == 'standard':
# rest of hyrule castle must be in light world
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
# in full, Sanc must be in light world, so must all of HC if door shuffle is on
elif world.doorShuffle[player] != 'vanilla':
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)', 'Hyrule Castle Exit (South)')], player)
# place old man, has limited options
# exit has to come from specific set of doors, the entrance is free to move about
@@ -2292,8 +2298,6 @@ Bomb_Shop_Multi_Cave_Doors = ['Hyrule Castle Entrance (South)',
'Death Mountain Return Cave (East)',
'Death Mountain Return Cave (West)',
'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Paradox Cave (Bottom)',
'Paradox Cave (Middle)',
'Paradox Cave (Top)',
+34 -2
View File
@@ -201,7 +201,8 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
else:
test_state = maximum_exploration_state
if (not single_player_placement or location.player == item_to_place.player)\
and location.can_fill(test_state, item_to_place, perform_access_check):
and location.can_fill(test_state, item_to_place, perform_access_check)\
and valid_key_placement(item_to_place, location, itempool, world):
spot_to_fill = location
break
elif item_to_place.smallkey or item_to_place.bigkey:
@@ -217,11 +218,42 @@ def fill_restrictive(world, base_state, locations, itempool, single_player_place
raise FillError('No more spots to place %s' % item_to_place)
world.push_item(spot_to_fill, item_to_place, False)
track_outside_keys(item_to_place, spot_to_fill, world)
locations.remove(spot_to_fill)
spot_to_fill.event = True
itempool.extend(unplaced_items)
def valid_key_placement(item, location, itempool, world):
if (not item.smallkey and not item.bigkey) or item.player != location.player or world.retro[item.player]:
return True
dungeon = location.parent_region.dungeon
if dungeon:
if dungeon.name not in item.name and (dungeon.name != 'Hyrule Castle' or 'Escape' not in item.name):
return True
key_logic = world.key_logic[item.player][dungeon.name]
unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player])
return key_logic.check_placement(unplaced_keys)
else:
inside_dungeon_item = ((item.smallkey and not world.keyshuffle[item.player])
or (item.bigkey and not world.bigkeyshuffle[item.player]))
return not inside_dungeon_item
def track_outside_keys(item, location, world):
if not item.smallkey:
return
item_dungeon = item.name.split('(')[1][:-1]
if item_dungeon == 'Escape':
item_dungeon = 'Hyrule Castle'
if location.player == item.player:
loc_dungeon = location.parent_region.dungeon
if loc_dungeon and loc_dungeon.name == item_dungeon:
return # this is an inside key
world.key_logic[item.player][item_dungeon].outside_keys += 1
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations:
@@ -236,7 +268,7 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
# fill in gtower locations with trash first
for player in range(1, world.players + 1):
if not gftower_trash or not world.ganonstower_vanilla[player]:
if not gftower_trash or not world.ganonstower_vanilla[player] or world.doorShuffle[player] == 'crossed':
continue
gftower_trash_count = (random.randint(15, 50) if world.goal[player] == 'triforcehunt' else random.randint(0, 15))
+138 -1537
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -0,0 +1,59 @@
# -*- mode: python -*-
block_cipher = None
console = True
def recurse_for_py_files(names_so_far):
returnvalue = []
for name in os.listdir(os.path.join(*names_so_far)):
if name != "__pycache__":
subdir_name = os.path.join(*names_so_far, name)
if os.path.isdir(subdir_name):
new_name_list = names_so_far + [name]
for filename in os.listdir(os.path.join(*new_name_list)):
base_file,file_extension = os.path.splitext(filename)
if file_extension == ".py":
new_name = ".".join(new_name_list+[base_file])
if not new_name in returnvalue:
returnvalue.append(new_name)
returnvalue.extend(recurse_for_py_files(new_name_list))
returnvalue.append("PIL._tkinter_finder") #Linux needs this
return returnvalue
hiddenimports = []
a = Analysis(['Gui.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
# https://stackoverflow.com/questions/17034434/how-to-remove-exclude-modules-and-files-from-pyinstaller
excluded_binaries = [
'VCRUNTIME140.dll',
'msvcp140.dll',
'mfc140u.dll']
a.binaries = TOC([x for x in a.binaries if x[0] not in excluded_binaries])
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Gui',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=console ) # <--- change this to True to enable command prompt when the app runs
+121 -100
View File
@@ -9,6 +9,8 @@ from EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive
from Items import ItemFactory
import classes.constants as CONST
#This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
#Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -58,7 +60,7 @@ difficulties = {
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt = ['Triforce Piece'] * 30,
triforce_pieces_required = 20,
retro = ['Small Key (Universal)'] * 17 + ['Rupees (20)'] * 10,
retro = ['Small Key (Universal)'] * 18 + ['Rupees (20)'] * 10,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 4,
progressive_shield_limit = 3,
@@ -85,7 +87,7 @@ difficulties = {
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt = ['Triforce Piece'] * 30,
triforce_pieces_required = 20,
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 3,
progressive_shield_limit = 2,
@@ -112,7 +114,7 @@ difficulties = {
timedother = ['Green Clock'] * 20 + ['Blue Clock'] * 10 + ['Red Clock'] * 10,
triforcehunt = ['Triforce Piece'] * 30,
triforce_pieces_required = 20,
retro = ['Small Key (Universal)'] * 12 + ['Rupees (5)'] * 15,
retro = ['Small Key (Universal)'] * 13 + ['Rupees (5)'] * 15,
extras = [normalfirst15extra, normalsecond15extra, normalthird10extra, normalfourth5extra, normalfinal25extra],
progressive_sword_limit = 2,
progressive_shield_limit = 1,
@@ -124,6 +126,58 @@ difficulties = {
),
}
def get_custom_array_key(item):
label_switcher = {
"silverarrow": "silversupgrade",
"blueboomerang": "boomerang",
"redboomerang": "redmerang",
"ocarina": "flute",
"bugcatchingnet": "bugnet",
"bookofmudora": "book",
"pegasusboots": "boots",
"titansmitts": "titansmitt",
"pieceofheart": "heartpiece",
"bossheartcontainer": "heartcontainer",
"sanctuaryheartcontainer": "sancheart",
"mastersword": "sword2",
"temperedsword": "sword3",
"goldensword": "sword4",
"blueshield": "shield1",
"redshield": "shield2",
"mirrorshield": "shield3",
"bluemail": "mail2",
"redmail": "mail3",
"progressivearmor": "progressivemail",
"splus12": "halfmagic",
"splus14": "quartermagic",
"singlearrow": "arrow1",
"singlebomb": "bomb1",
"triforcepiece": "triforcepieces"
}
key = item.lower()
trans = {
" ": "",
'(': "",
'/': "",
')': "",
'+': "",
"magic": "",
"caneof": "",
"upgrade": "splus",
"arrows": "arrow",
"arrowplus": "arrowsplus",
"bombs": "bomb",
"bombplus": "bombsplus",
"rupees": "rupee"
}
for check in trans:
repl = trans[check]
key = key.replace(check,repl)
if key in label_switcher:
key = label_switcher.get(key)
return key
def generate_itempool(world, player):
if (world.difficulty[player] not in ['normal', 'hard', 'expert'] or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'crystals']
or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']):
@@ -200,13 +254,21 @@ def generate_itempool(world, player):
world.get_location('Zelda Drop Off', player).event = True
world.get_location('Zelda Drop Off', player).locked = True
# set up item pool
if world.custom:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = make_custom_item_pool(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.customitemarray)
world.rupoor_cost = min(world.customitemarray[69], 9999)
world.rupoor_cost = min(world.customitemarray["rupoorcost"], 9999)
else:
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player])
(pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms) = get_pool_core(world.progressive, world.shuffle[player], world.difficulty[player], world.timer, world.goal[player], world.mode[player], world.swords[player], world.retro[player], world.doorShuffle[player])
if player in world.pool_adjustment.keys():
amt = world.pool_adjustment[player]
if amt < 0:
for i in range(0, amt):
pool.remove('Rupees (20)')
elif amt > 0:
for i in range(0, amt):
pool.append('Rupees (20)')
for item in precollected_items:
world.push_precollected(ItemFactory(item, player))
@@ -259,9 +321,9 @@ def generate_itempool(world, player):
# logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
# 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 world.customitemarray[30] == 0):
if world.difficulty[player] in ['normal', 'hard'] and not (world.custom and world.customitemarray["heartcontainer"] == 0):
[item for item in items if item.name == 'Boss Heart Container'][0].advancement = True
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray[29] < 4):
elif world.difficulty[player] in ['expert'] and not (world.custom and world.customitemarray["heartpiece"] < 4):
adv_heart_pieces = [item for item in items if item.name == 'Piece of Heart'][0:4]
for hp in adv_heart_pieces:
hp.advancement = True
@@ -345,6 +407,7 @@ def set_up_take_anys(world, player):
world.initialize_regions()
def create_dynamic_shop_locations(world, player):
for shop in world.shops:
if shop.region.player == player:
@@ -397,8 +460,10 @@ def set_up_shops(world, player):
if world.retro[player]:
rss = world.get_region('Red Shield Shop', player).shop
if not rss.locked:
rss.custom = True
rss.add_inventory(2, 'Single Arrow', 80)
for shop in random.sample([s for s in world.shops if s.custom and not s.locked and s.region.player == player], 5):
for shop in random.sample([s for s in world.shops 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)
@@ -406,7 +471,7 @@ def set_up_shops(world, player):
rss.locked = True
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro):
def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, retro, door_shuffle):
pool = []
placed_items = {}
precollected_items = []
@@ -525,8 +590,11 @@ def get_pool_core(progressive, shuffle, difficulty, timer, goal, mode, swords, r
pool = [item.replace('Arrow Upgrade (+10)','Rupees (5)') for item in pool]
pool.extend(diff.retro)
if mode == 'standard':
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)')
if door_shuffle == 'vanilla':
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)')
else:
pool.extend(['Small Key (Universal)'])
else:
pool.extend(['Small Key (Universal)'])
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
@@ -544,80 +612,31 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
placed_items[loc] = item
# Correct for insanely oversized item counts and take initial steps to handle undersized pools.
for x in range(0, 66):
if customitemarray[x] > total_items_to_place:
customitemarray[x] = total_items_to_place
if customitemarray[68] > total_items_to_place:
customitemarray[68] = total_items_to_place
itemtotal = 0
for x in range(0, 66):
itemtotal = itemtotal + customitemarray[x]
itemtotal = itemtotal + customitemarray[68]
itemtotal = itemtotal + customitemarray[70]
# Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors
for x in [*range(0, 66 + 1), 68, 69]:
key = CONST.CUSTOMITEMS[x]
if customitemarray[key] > total_items_to_place:
customitemarray[key] = total_items_to_place
pool.extend(['Bow'] * customitemarray[0])
pool.extend(['Silver Arrows']* customitemarray[1])
pool.extend(['Blue Boomerang'] * customitemarray[2])
pool.extend(['Red Boomerang'] * customitemarray[3])
pool.extend(['Hookshot'] * customitemarray[4])
pool.extend(['Mushroom'] * customitemarray[5])
pool.extend(['Magic Powder'] * customitemarray[6])
pool.extend(['Fire Rod'] * customitemarray[7])
pool.extend(['Ice Rod'] * customitemarray[8])
pool.extend(['Bombos'] * customitemarray[9])
pool.extend(['Ether'] * customitemarray[10])
pool.extend(['Quake'] * customitemarray[11])
pool.extend(['Lamp'] * customitemarray[12])
pool.extend(['Hammer'] * customitemarray[13])
pool.extend(['Shovel'] * customitemarray[14])
pool.extend(['Ocarina'] * customitemarray[15])
pool.extend(['Bug Catching Net'] * customitemarray[16])
pool.extend(['Book of Mudora'] * customitemarray[17])
pool.extend(['Cane of Somaria'] * customitemarray[19])
pool.extend(['Cane of Byrna'] * customitemarray[20])
pool.extend(['Cape'] * customitemarray[21])
pool.extend(['Pegasus Boots'] * customitemarray[23])
pool.extend(['Power Glove'] * customitemarray[24])
pool.extend(['Titans Mitts'] * customitemarray[25])
pool.extend(['Progressive Glove'] * customitemarray[26])
pool.extend(['Flippers'] * customitemarray[27])
pool.extend(['Piece of Heart'] * customitemarray[29])
pool.extend(['Boss Heart Container'] * customitemarray[30])
pool.extend(['Sanctuary Heart Container'] * customitemarray[31])
pool.extend(['Master Sword'] * customitemarray[33])
pool.extend(['Tempered Sword'] * customitemarray[34])
pool.extend(['Golden Sword'] * customitemarray[35])
pool.extend(['Blue Shield'] * customitemarray[37])
pool.extend(['Red Shield'] * customitemarray[38])
pool.extend(['Mirror Shield'] * customitemarray[39])
pool.extend(['Progressive Shield'] * customitemarray[40])
pool.extend(['Blue Mail'] * customitemarray[41])
pool.extend(['Red Mail'] * customitemarray[42])
pool.extend(['Progressive Armor'] * customitemarray[43])
pool.extend(['Magic Upgrade (1/2)'] * customitemarray[44])
pool.extend(['Magic Upgrade (1/4)'] * customitemarray[45])
pool.extend(['Bomb Upgrade (+5)'] * customitemarray[46])
pool.extend(['Bomb Upgrade (+10)'] * customitemarray[47])
pool.extend(['Arrow Upgrade (+5)'] * customitemarray[48])
pool.extend(['Arrow Upgrade (+10)'] * customitemarray[49])
pool.extend(['Single Arrow'] * customitemarray[50])
pool.extend(['Arrows (10)'] * customitemarray[51])
pool.extend(['Single Bomb'] * customitemarray[52])
pool.extend(['Bombs (3)'] * customitemarray[53])
pool.extend(['Rupee (1)'] * customitemarray[54])
pool.extend(['Rupees (5)'] * customitemarray[55])
pool.extend(['Rupees (20)'] * customitemarray[56])
pool.extend(['Rupees (50)'] * customitemarray[57])
pool.extend(['Rupees (100)'] * customitemarray[58])
pool.extend(['Rupees (300)'] * customitemarray[59])
pool.extend(['Rupoor'] * customitemarray[60])
pool.extend(['Blue Clock'] * customitemarray[61])
pool.extend(['Green Clock'] * customitemarray[62])
pool.extend(['Red Clock'] * customitemarray[63])
pool.extend(['Progressive Bow'] * customitemarray[64])
pool.extend(['Bombs (10)'] * customitemarray[65])
pool.extend(['Triforce Piece'] * customitemarray[66])
pool.extend(['Triforce'] * customitemarray[68])
# Triforce
if customitemarray["triforce"] > total_items_to_place:
customitemarray["triforce"] = total_items_to_place
itemtotal = 0
# Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors
for x in [*range(0, 66 + 1), 68, 69]:
key = CONST.CUSTOMITEMS[x]
itemtotal = itemtotal + customitemarray[key]
# Triforce
itemtotal = itemtotal + customitemarray["triforce"]
# Generic Keys
itemtotal = itemtotal + customitemarray["generickeys"]
customitems = [
"Bow", "Silver Arrows", "Blue Boomerang", "Red Boomerang", "Hookshot", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Catching Net", "Book of Mudora", "Cane of Somaria", "Cane of Byrna", "Cape", "Pegasus Boots", "Power Glove", "Titans Mitts", "Progressive Glove", "Flippers", "Piece of Heart", "Boss Heart Container", "Sanctuary Heart Container", "Master Sword", "Tempered Sword", "Golden Sword", "Blue Shield", "Red Shield", "Mirror Shield", "Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)", "Magic Upgrade (1/4)", "Bomb Upgrade (+5)", "Bomb Upgrade (+10)", "Arrow Upgrade (+5)", "Arrow Upgrade (+10)", "Single Arrow", "Arrows (10)", "Single Bomb", "Bombs (3)", "Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)", "Rupoor", "Blue Clock", "Green Clock", "Red Clock", "Progressive Bow", "Bombs (10)", "Triforce Piece", "Triforce"
]
for customitem in customitems:
pool.extend([customitem] * customitemarray[get_custom_array_key(customitem)])
diff = difficulties[difficulty]
@@ -627,17 +646,17 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
# all bottles, since only one bottle is available
if diff.same_bottle:
thisbottle = random.choice(diff.bottles)
for _ in range(customitemarray[18]):
for _ in range(customitemarray["bottle"]):
if not diff.same_bottle:
thisbottle = random.choice(diff.bottles)
pool.append(thisbottle)
if customitemarray[66] > 0 or customitemarray[67] > 0:
treasure_hunt_count = max(min(customitemarray[67], 99), 1) #To display, count must be between 1 and 99.
if customitemarray["triforcepieces"] > 0 or customitemarray["triforcepiecesgoal"] > 0:
treasure_hunt_count = max(min(customitemarray["triforcepiecesgoal"], 99), 1) #To display, count must be between 1 and 99.
treasure_hunt_icon = 'Triforce Piece'
# Ensure game is always possible to complete here, force sufficient pieces if the player is unwilling.
if (customitemarray[66] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray[68] == 0):
extrapieces = treasure_hunt_count - customitemarray[66]
if (customitemarray["triforcepieces"] < treasure_hunt_count) and (goal == 'triforcehunt') and (customitemarray["triforce"] == 0):
extrapieces = treasure_hunt_count - customitemarray["triforcepieces"]
pool.extend(['Triforce Piece'] * extrapieces)
itemtotal = itemtotal + extrapieces
@@ -656,28 +675,30 @@ def make_custom_item_pool(progressive, shuffle, difficulty, timer, goal, mode, s
if retro:
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[70] - 1), 0))
pool.extend(['Small Key (Universal)'] * max((customitemarray["generickeys"] - 1), 0))
else:
pool.extend(['Small Key (Universal)'] * customitemarray[70])
pool.extend(['Small Key (Universal)'] * customitemarray["generickeys"])
else:
pool.extend(['Small Key (Universal)'] * customitemarray[70])
pool.extend(['Small Key (Universal)'] * customitemarray["generickeys"])
pool.extend(['Fighter Sword'] * customitemarray[32])
pool.extend(['Progressive Sword'] * customitemarray[36])
pool.extend(['Fighter Sword'] * customitemarray["sword1"])
pool.extend(['Progressive Sword'] * customitemarray["progressivesword"])
if shuffle == 'insanity_legacy':
place_item('Link\'s House', 'Magic Mirror')
place_item('Sanctuary', 'Moon Pearl')
pool.extend(['Magic Mirror'] * max((customitemarray[22] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray[28] - 1), 0))
pool.extend(['Magic Mirror'] * max((customitemarray["mirror"] -1 ), 0))
pool.extend(['Moon Pearl'] * max((customitemarray["pearl"] - 1), 0))
else:
pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28])
pool.extend(['Magic Mirror'] * customitemarray["mirror"])
pool.extend(['Moon Pearl'] * customitemarray["pearl"])
if retro:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
nothings = total_items_to_place - itemtotal
print("Placing " + str(nothings) + " Nothings")
pool.extend(['Nothing'] * nothings)
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, lamps_needed_for_dark_rooms)
+240 -21
View File
@@ -21,6 +21,7 @@ class KeyLayout(object):
self.max_drops = None
self.all_chest_locations = {}
self.big_key_special = False
self.all_locations = set()
# bk special?
# bk required? True if big chests or big doors exists
@@ -44,6 +45,14 @@ class KeyLogic(object):
self.bk_chests = set()
self.logic_min = {}
self.logic_max = {}
self.placement_rules = []
self.outside_keys = 0
def check_placement(self, unplaced_keys):
for rule in self.placement_rules:
if not rule.is_satisfiable(self.outside_keys, unplaced_keys):
return False
return True
class DoorRules(object):
@@ -59,6 +68,38 @@ class DoorRules(object):
self.small_location = None
class PlacementRule(object):
def __init__(self):
self.door_reference = None
self.small_key = None
self.bk_conditional_set = None # the location that means
self.needed_keys_w_bk = None
self.needed_keys_wo_bk = None
self.check_locations_w_bk = None
self.check_locations_wo_bk = None
def is_satisfiable(self, outside_keys, unplaced_keys):
bk_blocked = False
if self.bk_conditional_set:
for loc in self.bk_conditional_set:
if loc.item and loc.item.bigkey:
bk_blocked = True
break
available_keys = outside_keys
empty_chests = 0
check_locations = self.check_locations_wo_bk if bk_blocked else self.check_locations_w_bk
threshold = self.needed_keys_wo_bk if bk_blocked else self.needed_keys_w_bk
for loc in check_locations:
if not loc.item:
empty_chests += 1
elif loc.item and loc.item.name == self.small_key:
available_keys += 1
place_able_keys = min(empty_chests, unplaced_keys)
available_keys += place_able_keys
return available_keys >= threshold
class KeyCounter(object):
def __init__(self, max_chests):
@@ -106,8 +147,10 @@ def analyze_dungeon(key_layout, world, player):
key_layout.key_counters = create_key_counters(key_layout, world, player)
key_logic = key_layout.key_logic
find_bk_locked_sections(key_layout, world)
find_bk_locked_sections(key_layout, world, player)
key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations))
if world.retro[player] and world.mode[player] != 'standard':
return
original_key_counter = find_counter({}, False, key_layout)
queue = deque([(None, original_key_counter)])
@@ -147,13 +190,15 @@ def analyze_dungeon(key_layout, world, player):
if not child.bigKey and child not in doors_completed:
best_counter = find_best_counter(child, odd_counter, key_counter, key_layout, world, player, False, empty_flag)
rule = create_rule(best_counter, key_counter, key_layout, world, player)
if not rule.is_valid:
logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name)
# todo: seems to be caused by best_counter not opening the big key door when that's logically required. Re-evaluate usage of this
# if not rule.is_valid:
# logging.getLogger('').warning('Key logic for door %s requires too many chests. Seed may be beatable anyway.', child.name)
if smallest_rule is None or rule.small_key_num < smallest_rule:
smallest_rule = rule.small_key_num
check_for_self_lock_key(rule, child, best_counter, key_layout, world, player)
bk_restricted_rules(rule, child, odd_counter, empty_flag, key_counter, key_layout, world, player)
key_logic.door_rules[child.name] = rule
create_placement_rule(key_layout, child, odd_counter, key_counter, world, player)
doors_completed.add(child)
next_counter = find_next_counter(child, key_counter, key_layout)
ctr_id = cid(next_counter, key_layout)
@@ -176,6 +221,65 @@ def analyze_dungeon(key_layout, world, player):
rule.small_key_num, rule.alternate_small_key = rule.alternate_small_key, rule.small_key_num
def create_placement_rule(key_layout, door, odd_ctr, current_ctr, world, player):
key_logic = key_layout.key_logic
worst_ctr = find_worst_counter(door, odd_ctr, current_ctr, key_layout, False)
sm_num = worst_ctr.used_keys + 1
accessible_loc = set()
accessible_loc.update(worst_ctr.free_locations)
accessible_loc.update(worst_ctr.key_only_locations)
worst_ctr_wo_bk, post_ctr, alt_num = find_worst_counter_wo_bk(sm_num, accessible_loc, door, odd_ctr, current_ctr, key_layout)
blocked_loc = key_layout.all_locations.difference(accessible_loc)
if len(blocked_loc) > 0:
rule = PlacementRule()
rule.door_reference = door
rule.small_key = key_logic.small_key_name
rule.needed_keys_w_bk = sm_num
placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player)
rule.check_locations_w_bk = accessible_loc
if worst_ctr_wo_bk:
accessible_wo_bk, post_set = set(), set()
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations)
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations)
post_set.update(post_ctr.free_locations)
post_set.update(post_ctr.key_only_locations)
blocked_wo_bk = post_set.difference(accessible_wo_bk)
if len(blocked_wo_bk) > 0:
rule.bk_conditional_set = blocked_wo_bk
rule.needed_keys_wo_bk = alt_num
# can this self lock a key if bk not avail? I'm thinking no.
# placement_self_lock_adjustment(rule, key_layout, ???, worst_ctr_wo_bk, world, player)
rule.check_locations_wo_bk = accessible_wo_bk
key_logic.placement_rules.append(rule)
if worst_ctr_wo_bk:
check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num)
def check_bk_restriction_needed(key_layout, worst_ctr_wo_bk, post_ctr, alt_num):
avail_keys = len(worst_ctr_wo_bk.key_only_locations)
place_able_keys = min(key_layout.max_chests, len(worst_ctr_wo_bk.free_locations))
if avail_keys + place_able_keys < alt_num:
accessible_wo_bk, post_set = set(), set()
accessible_wo_bk.update(worst_ctr_wo_bk.free_locations)
accessible_wo_bk.update(worst_ctr_wo_bk.key_only_locations)
post_set.update(post_ctr.free_locations)
post_set.update(post_ctr.key_only_locations)
key_layout.key_logic.bk_restricted.update(post_set.difference(accessible_wo_bk))
def placement_self_lock_adjustment(rule, key_layout, blocked_loc, worst_ctr, world, player):
if len(blocked_loc) == 1 and world.accessibility[player] != 'locations':
max_ctr = find_max_counter(key_layout)
blocked_others = set(max_ctr.other_locations).difference(set(worst_ctr.other_locations))
important_found = False
for loc in blocked_others:
if important_location(loc, world, player):
important_found = True
break
if not important_found:
rule.needed_keys_w_bk -= 1
def count_key_drops(sector):
cnt = 0
@@ -200,16 +304,18 @@ def queue_sorter_2(queue_item):
return 1 if door.bigKey else 0
def find_bk_locked_sections(key_layout, world):
def find_bk_locked_sections(key_layout, world, player):
if key_layout.big_key_special:
return
key_counters = key_layout.key_counters
key_logic = key_layout.key_logic
bk_key_not_required = set()
big_chest_allowed_big_key = world.accessibility != 'locations'
big_chest_allowed_big_key = world.accessibility[player] != 'locations'
for counter in key_counters.values():
key_layout.all_chest_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.free_locations)
key_layout.all_locations.update(counter.key_only_locations)
if counter.big_key_opened and counter.important_location:
big_chest_allowed_big_key = False
if not counter.big_key_opened:
@@ -240,6 +346,28 @@ def relative_empty_counter(odd_counter, key_counter):
return True
def relative_empty_counter_2(odd_counter, key_counter):
if len(set(odd_counter.key_only_locations).difference(key_counter.key_only_locations)) > 0:
return False
if len(set(odd_counter.free_locations).difference(key_counter.free_locations)) > 0:
return False
for child in odd_counter.child_doors:
if unique_child_door_2(child, key_counter):
return False
return True
def progressive_ctr(new_counter, last_counter):
if len(set(new_counter.key_only_locations).difference(last_counter.key_only_locations)) > 0:
return True
if len(set(new_counter.free_locations).difference(last_counter.free_locations)) > 0:
return True
for child in new_counter.child_doors:
if unique_child_door_2(child, last_counter):
return True
return False
def unique_child_door(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False
@@ -250,6 +378,14 @@ def unique_child_door(child, key_counter):
return True
def unique_child_door_2(child, key_counter):
if child in key_counter.child_doors or child.dest in key_counter.child_doors:
return False
if child in key_counter.open_doors or child.dest in key_counter.child_doors:
return False
return True
def find_best_counter(door, odd_counter, key_counter, key_layout, world, player, skip_bk, empty_flag): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
@@ -279,7 +415,34 @@ def find_best_counter(door, odd_counter, key_counter, key_layout, world, player,
return last_counter
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
def find_worst_counter(door, odd_counter, key_counter, key_layout, skip_bk): # try to waste as many keys as possible?
ignored_doors = {door, door.dest} if door is not None else {}
finished = False
opened_doors = dict(key_counter.open_doors)
bk_opened = key_counter.big_key_opened
# new_counter = key_counter
last_counter = key_counter
while not finished:
door_set = find_potential_open_doors(last_counter, ignored_doors, key_layout, skip_bk, 0)
if door_set is None or len(door_set) == 0:
finished = True
continue
for new_door in door_set:
proposed_doors = {**opened_doors, **dict.fromkeys([new_door, new_door.dest])}
bk_open = bk_opened or new_door.bigKey
new_counter = find_counter(proposed_doors, bk_open, key_layout)
bk_open = new_counter.big_key_opened
if not new_door.bigKey and progressive_ctr(new_counter, last_counter) and relative_empty_counter_2(odd_counter, new_counter):
ignored_doors.add(new_door)
else:
last_counter = new_counter
opened_doors = proposed_doors
bk_opened = bk_open
# this means the new_door invalidates the door / leads to the same stuff
return last_counter
def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk, reserve=1):
small_doors = []
big_doors = []
for other in key_counter.child_doors:
@@ -292,7 +455,7 @@ def find_potential_open_doors(key_counter, ignored_doors, key_layout, skip_bk):
if key_layout.big_key_special:
big_key_available = key_counter.big_key_opened
else:
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(1) > 0
big_key_available = len(key_counter.free_locations) - key_counter.used_smalls_loc(reserve) > 0
if len(small_doors) == 0 and (not skip_bk and (len(big_doors) == 0 or not big_key_available)):
return None
return small_doors + big_doors
@@ -366,7 +529,7 @@ def create_rule(key_counter, prev_counter, key_layout, world, player):
def check_for_self_lock_key(rule, door, parent_counter, key_layout, world, player):
if world.accessibility != 'locations':
if world.accessibility[player] != 'locations':
counter = find_inverted_counter(door, parent_counter, key_layout, world, player)
if not self_lock_possible(counter):
return
@@ -488,6 +651,27 @@ def bk_restricted_rules(rule, door, odd_counter, empty_flag, key_counter, key_la
# key_layout.key_logic.bk_restricted.update(unique_loc)
def find_worst_counter_wo_bk(small_key_num, accessible_set, door, odd_ctr, key_counter, key_layout):
if key_counter.big_key_opened:
return None, None, None
worst_counter = find_worst_counter(door, odd_ctr, key_counter, key_layout, True)
bk_rule_num = worst_counter.used_keys + 1
bk_access_set = set()
bk_access_set.update(worst_counter.free_locations)
bk_access_set.update(worst_counter.key_only_locations)
if bk_rule_num == small_key_num and len(bk_access_set ^ accessible_set) == 0:
return None, None, None
door_open = find_next_counter(door, worst_counter, key_layout)
ignored_doors = dict_intersection(worst_counter.child_doors, door_open.child_doors)
dest_ignored = []
for door in ignored_doors.keys():
if door.dest not in ignored_doors:
dest_ignored.append(door.dest)
ignored_doors = {**ignored_doors, **dict.fromkeys(dest_ignored)}
post_counter = open_some_counter(door_open, key_layout, ignored_doors.keys())
return worst_counter, post_counter, bk_rule_num
def open_a_door(door, child_state, flat_proposal):
if door.bigKey:
child_state.big_key_opened = True
@@ -521,7 +705,7 @@ def unique_doors(doors):
def count_unique_sm_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and d.dest not in unique_d_set and not d.bigKey:
if d not in unique_d_set and (d.dest not in unique_d_set or d.type == DoorType.SpiralStairs) and not d.bigKey:
unique_d_set.add(d)
return len(unique_d_set)
@@ -534,7 +718,8 @@ def count_unique_small_doors(key_counter, proposal):
if door in proposal and door not in counted:
cnt += 1
counted.add(door)
counted.add(door.dest)
if door.type != DoorType.SpiralStairs:
counted.add(door.dest)
return cnt
@@ -806,7 +991,8 @@ def reduce_rules(small_rules, collected, collected_alt):
# Soft lock stuff
def validate_key_layout(key_layout, world, player):
if world.retro[player]: # retro is all good - don't care how the doors are laid out
# 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'):
return True
flat_proposal = key_layout.flat_prop
state = ExplorationState(dungeon=key_layout.sector.name)
@@ -815,7 +1001,7 @@ def validate_key_layout(key_layout, world, player):
for region in key_layout.start_regions:
state.visit_region(region, key_checks=True)
state.add_all_doors_check_keys(region, flat_proposal, world, player)
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, None, world, player)
return validate_key_layout_sub_loop(key_layout, state, {}, flat_proposal, None, 0, world, player)
def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposal, prev_state, prev_avail, world, player):
@@ -831,6 +1017,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa
available_big_locations = cnt_avail_big_locations(ttl_locations, state, world, player)
if invalid_self_locking_key(state, prev_state, prev_avail, world, player):
return False
# todo: allow more key shuffles - refine placement rules
# if (not smalls_avail or available_small_locations == 0) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
if (not smalls_avail or not enough_small_locations(state, available_small_locations)) and (state.big_key_opened or num_bigs == 0 or available_big_locations == 0):
return False
else:
@@ -882,15 +1070,6 @@ def invalid_self_locking_key(state, prev_state, prev_avail, world, player):
return prev_avail - 1 == 0
# does not allow dest doors
def count_unique_sm_doors(doors):
unique_d_set = set()
for d in doors:
if d not in unique_d_set and d.dest not in unique_d_set and not d.bigKey:
unique_d_set.add(d)
return len(unique_d_set)
def enough_small_locations(state, avail_small_loc):
unique_d_set = set()
for exp_door in state.small_doors:
@@ -1232,3 +1411,43 @@ def val_rule(rule, skn, allow=False, loc=None, askn=None, setCheck=None):
assert len(setCheck) == len(rule.alternate_big_key_loc)
for loc in rule.alternate_big_key_loc:
assert loc.name in setCheck
# Soft lock stuff
def validate_key_placement(key_layout, world, player):
if world.retro[player] 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
dungeon = world.get_dungeon(key_layout.sector.name, player)
smallkey_name = 'Small Key (%s)' % (key_layout.sector.name if key_layout.sector.name != 'Hyrule Castle' else 'Escape')
if world.keyshuffle[player]:
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)
big_key_outside = dungeon.big_key not in (l.item for l in max_counter.free_locations)
for counter in key_layout.key_counters.values():
if len(counter.child_doors) == 0:
continue
big_found = any(i.item == dungeon.big_key for i in counter.free_locations if "- Big Chest" not in i.name) or big_key_outside
if counter.big_key_opened and not big_found:
continue # Can't get to this state
found_locations = set(i for i in counter.free_locations if big_found or "- Big Chest" not in i.name)
found_keys = sum(1 for i in found_locations if i.item is not None and i.item.name == smallkey_name and i.item.player == player) + \
len(counter.key_only_locations) + keys_outside
can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \
found_keys > counter.used_keys and any(not d.bigKey for d in counter.child_doors)
if not can_progress:
missing_locations = set(max_counter.free_locations.keys()).difference(found_locations)
missing_items = [l for l in missing_locations if l.item is None or (l.item.name != smallkey_name and l.item != dungeon.big_key) or "- Boss" in l.name]
# missing_key_only = set(max_counter.key_only_locations.keys()).difference(counter.key_only_locations.keys()) # do freestanding keys matter for locations?
if len(missing_items) > 0: # world.accessibility[player]=='locations' and (len(missing_locations)>0 or len(missing_key_only) > 0):
logging.getLogger('').error("Keylock - can't open locations: ")
for i in missing_locations:
logging.getLogger('').error(i)
return False
return True
+16 -4
View File
@@ -10,6 +10,7 @@ import zlib
from BaseClasses import World, CollectionState, Item, Region, Location, Shop
from Items import ItemFactory
from KeyDoorShuffle import validate_key_placement
from Regions import create_regions, create_shops, mark_light_world_regions, create_dungeon_regions
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
from EntranceShuffle import link_entrances, link_inverted_entrances
@@ -23,7 +24,7 @@ from Fill import distribute_items_cutoff, distribute_items_staleness, distribute
from ItemList import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names
__version__ = '0.0.12pre'
__version__ = '0.0.18.2d'
def main(args, seed=None):
@@ -56,6 +57,8 @@ def main(args, seed=None):
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.experimental = args.experimental.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)}
@@ -132,6 +135,11 @@ def main(args, seed=None):
else:
fill_dungeons(world)
for player in range(1, world.players+1):
for key_layout in world.key_layout[player].values():
if not validate_key_placement(key_layout, world, player):
raise RuntimeError("Keylock detected: %s (Player %d)" % (key_layout.sector.name, player))
logger.info('Fill the world.')
if args.algorithm == 'flood':
@@ -177,9 +185,13 @@ def main(args, seed=None):
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer and (args.enemizercli or not args.jsonout):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
if os.path.exists(args.enemizercli):
patch_enemizer(world, player, rom, args.rom, args.enemizercli, args.shufflepots[player], sprite_random_on_hit)
if not args.jsonout:
rom = LocalRom.fromJsonRom(rom, args.rom, 0x400000)
else:
logging.warning("EnemizerCLI not found at:" + args.enemizercli)
logging.warning("No Enemizer options will be applied until this is resolved.")
if args.race:
patch_race_rom(rom)
+2 -1
View File
@@ -148,6 +148,7 @@ def roll_settings(weights):
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
door_shuffle = get_choice('door_shuffle')
ret.door_shuffle = door_shuffle if door_shuffle != 'none' else 'vanilla'
ret.experimental = get_choice('experimental') == 'on'
goal = get_choice('goals')
ret.goal = {'ganon': 'ganon',
@@ -156,7 +157,7 @@ def roll_settings(weights):
'pedestal': 'pedestal',
'triforce-hunt': 'triforcehunt'
}[goal]
ret.openpyramid = goal == 'fast_ganon'
ret.openpyramid = goal == 'fast_ganon' if ret.shuffle in ['vanilla', 'dungeonsfull', 'dungeonssimple'] else False
ret.crystals_gt = get_choice('tower_open')
-4
View File
@@ -36,10 +36,6 @@ Doors are shuffled between dungeons as well.
Doors are not shuffled.
### Experimental
Used for development testing. This will be removed in a future version. Use at your own risk. Might play like a plando.
## Map/Compass/Small Key/Big Key shuffle (aka Keysanity)
These settings allow dungeon specific items to be distributed anywhere in the world and not just in their native dungeon.
+20 -15
View File
@@ -277,8 +277,9 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'Eastern Boss', 'Eastern Palace', ['Eastern Palace - Boss', 'Eastern Palace - Prize'], ['Eastern Boss SE']),
# Desert Palace
# note for later: pots in desert keep out the bunny - logically (though not practically b/c of dungeon bunny revival)
create_dungeon_region(player, 'Desert Main Lobby', 'Desert Palace', None, ['Desert Palace Exit (South)','Desert Main Lobby NW Edge', 'Desert Main Lobby N Edge', 'Desert Main Lobby NE Edge', 'Desert Main Lobby E Edge']),
create_dungeon_region(player, 'Desert Main Lobby', 'Desert Palace', None, ['Desert Palace Exit (South)', 'Desert Main Lobby N Edge', 'Desert Main Lobby Left Path', 'Desert Main Lobby Right Path']),
create_dungeon_region(player, 'Desert Left Alcove', 'Desert Palace', None, ['Desert Main Lobby NW Edge', 'Desert Left Alcove Path']),
create_dungeon_region(player, 'Desert Right Alcove', 'Desert Palace', None, ['Desert Main Lobby NE Edge', 'Desert Main Lobby E Edge', 'Desert Right Alcove Path']),
create_dungeon_region(player, 'Desert Dead End', 'Desert Palace', None, ['Desert Dead End Edge']),
create_dungeon_region(player, 'Desert East Lobby', 'Desert Palace', None, ['Desert East Lobby WS', 'Desert Palace Exit (East)']),
create_dungeon_region(player, 'Desert East Wing', 'Desert Palace', None, ['Desert East Wing ES', 'Desert East Wing Key Door EN', 'Desert East Wing W Edge', 'Desert East Wing N Edge']),
@@ -344,7 +345,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'PoD Left Cage', 'Palace of Darkness', None, ['PoD Left Cage SW', 'PoD Left Cage Down Stairs']),
create_dungeon_region(player, 'PoD Middle Cage', 'Palace of Darkness', None, ['PoD Middle Cage S', 'PoD Middle Cage SE', 'PoD Middle Cage N', 'PoD Middle Cage Down Stairs']),
create_dungeon_region(player, 'PoD Shooter Room', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['PoD Shooter Room Up Stairs']),
create_dungeon_region(player, 'PoD Pit Room', 'Palace of Darkness', None, ['PoD Pit Room S', 'PoD Pit Room NW', 'PoD Pit Room NE', 'PoD Pit Room Freefall', 'PoD Pit Room Bomb Hole']),
create_dungeon_region(player, 'PoD Pit Room', 'Palace of Darkness', None, ['PoD Pit Room S', 'PoD Pit Room NW', 'PoD Pit Room Bomb Hole', 'PoD Pit Room Block Path N']),
create_dungeon_region(player, 'PoD Pit Room Blocked', 'Palace of Darkness', None, ['PoD Pit Room NE', 'PoD Pit Room Freefall', 'PoD Pit Room Block Path S']),
create_dungeon_region(player, 'PoD Arena Main', 'Palace of Darkness', None, ['PoD Arena Main SW', 'PoD Arena Main Crystal Path', 'PoD Arena Main Orange Barrier', 'PoD Arena Bonk Path']),
create_dungeon_region(player, 'PoD Arena North', 'Palace of Darkness', None, ['PoD Arena Main NW', 'PoD Arena Main NE', 'PoD Arena North Drop Down']),
create_dungeon_region(player, 'PoD Arena Crystal', 'Palace of Darkness', None, ['PoD Arena Crystals E', 'PoD Arena Crystal Path']),
@@ -360,7 +362,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'PoD Stalfos Basement', 'Palace of Darkness', ['Palace of Darkness - Stalfos Basement'], ['PoD Stalfos Basement Warp']),
create_dungeon_region(player, 'PoD Basement Ledge', 'Palace of Darkness', None, ['PoD Basement Ledge Drop Down', 'PoD Basement Ledge Up Stairs']),
create_dungeon_region(player, 'PoD Big Key Landing', 'Palace of Darkness', ['Palace of Darkness - Big Key Chest'], ['PoD Big Key Landing Down Stairs', 'PoD Big Key Landing Hole']),
create_dungeon_region(player, 'PoD Falling Bridge', 'Palace of Darkness', None, ['PoD Falling Bridge SW', 'PoD Falling Bridge WN', 'PoD Falling Bridge EN']),
create_dungeon_region(player, 'PoD Falling Bridge Ledge', 'Palace of Darkness', None, ['PoD Falling Bridge WN', 'PoD Falling Bridge EN', 'PoD Falling Bridge Path S']),
create_dungeon_region(player, 'PoD Falling Bridge', 'Palace of Darkness', None, ['PoD Falling Bridge SW', 'PoD Falling Bridge Path N']),
create_dungeon_region(player, 'PoD Dark Maze', 'Palace of Darkness', ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom'], ['PoD Dark Maze EN', 'PoD Dark Maze E']),
create_dungeon_region(player, 'PoD Big Chest Balcony', 'Palace of Darkness', ['Palace of Darkness - Big Chest'], ['PoD Big Chest Balcony W']),
create_dungeon_region(player, 'PoD Compass Room', 'Palace of Darkness', ['Palace of Darkness - Compass Chest'], ['PoD Compass Room SE', 'PoD Compass Room WN', 'PoD Compass Room W Down Stairs', 'PoD Compass Room E Down Stairs']),
@@ -544,7 +547,8 @@ def create_dungeon_regions(world, player):
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']),
create_dungeon_region(player, 'Mire Hidden Shooters', 'Misery Mire', None, ['Mire Hidden Shooters SE', 'Mire Hidden Shooters WS', 'Mire Hidden Shooters ES', 'Mire Hidden Shooters NE']),
create_dungeon_region(player, 'Mire Hidden Shooters', 'Misery Mire', None, ['Mire Hidden Shooters SE', 'Mire Hidden Shooters WS', 'Mire Hidden Shooters ES', 'Mire Hidden Shooters Block Path N']),
create_dungeon_region(player, 'Mire Hidden Shooters Blocked', 'Misery Mire', None, ['Mire Hidden Shooters NE', 'Mire Hidden Shooters Block Path S']),
create_dungeon_region(player, 'Mire Cross', 'Misery Mire', None, ['Mire Cross ES', 'Mire Cross SW']),
create_dungeon_region(player, 'Mire Minibridge', 'Misery Mire', None, ['Mire Minibridge SE', 'Mire Minibridge NE']),
create_dungeon_region(player, 'Mire BK Door Room', 'Misery Mire', None, ['Mire BK Door Room EN', 'Mire BK Door Room N']),
@@ -633,7 +637,8 @@ def create_dungeon_regions(world, player):
create_dungeon_region(player, 'GT Blocked Stairs', 'Ganon\'s Tower', None, ['GT Blocked Stairs Down Stairs', 'GT Blocked Stairs Block Path']),
create_dungeon_region(player, 'GT Bob\'s Room', 'Ganon\'s Tower', ['Ganons Tower - Bob\'s Chest'], ['GT Bob\'s Room SE', 'GT Bob\'s Room Hole']),
create_dungeon_region(player, 'GT Tile Room', 'Ganon\'s Tower', ['Ganons Tower - Tile Room'], ['GT Tile Room WN', 'GT Tile Room EN']),
create_dungeon_region(player, 'GT Speed Torch', 'Ganon\'s Tower', None, ['GT Speed Torch WN', 'GT Speed Torch NE', 'GT Speed Torch WS', 'GT Speed Torch SE']),
create_dungeon_region(player, 'GT Speed Torch', 'Ganon\'s Tower', None, ['GT Speed Torch WS', 'GT Speed Torch SE', 'GT Speed Torch North Path']),
create_dungeon_region(player, 'GT Speed Torch Upper', 'Ganon\'s Tower', None, ['GT Speed Torch WN', 'GT Speed Torch NE', 'GT Speed Torch South Path']),
create_dungeon_region(player, 'GT Pots n Blocks', 'Ganon\'s Tower', None, ['GT Pots n Blocks ES']),
create_dungeon_region(player, 'GT Crystal Conveyor', 'Ganon\'s Tower', None, ['GT Crystal Conveyor NE', 'GT Crystal Conveyor WN']),
create_dungeon_region(player, 'GT Compass Room', 'Ganon\'s Tower', ['Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right'],
@@ -826,15 +831,15 @@ def create_shops(world, player):
_basic_shop_defaults = [('Red Potion', 150), ('Small Heart', 10), ('Bombs (10)', 50)]
_dark_world_shop_defaults = [('Red Potion', 150), ('Blue Shield', 50), ('Bombs (10)', 50)]
shop_table = {
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, True, False, _basic_shop_defaults),
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, True, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, True, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, True, False, _basic_shop_defaults),
'Cave Shop (Dark Death Mountain)': (0x0112, ShopType.Shop, 0xC1, False, False, _basic_shop_defaults),
'Red Shield Shop': (0x0110, ShopType.Shop, 0xC1, False, False, [('Red Shield', 500), ('Bee', 10), ('Arrows (10)', 30)]),
'Dark Lake Hylia Shop': (0x010F, ShopType.Shop, 0xC1, False, False, _dark_world_shop_defaults),
'Dark World Lumberjack Shop': (0x010F, ShopType.Shop, 0xC1, False, False, _dark_world_shop_defaults),
'Village of Outcasts Shop': (0x010F, ShopType.Shop, 0xC1, False, False, _dark_world_shop_defaults),
'Dark World Potion Shop': (0x010F, ShopType.Shop, 0xC1, False, False, _dark_world_shop_defaults),
'Light World Death Mountain Shop': (0x00FF, ShopType.Shop, 0xA0, False, False, _basic_shop_defaults),
'Kakariko Shop': (0x011F, ShopType.Shop, 0xA0, False, False, _basic_shop_defaults),
'Cave Shop (Lake Hylia)': (0x0112, ShopType.Shop, 0xA0, False, False, _basic_shop_defaults),
'Potion Shop': (0x0109, ShopType.Shop, 0xFF, False, True, [('Red Potion', 120), ('Green Potion', 60), ('Blue Potion', 160)]),
'Capacity Upgrade': (0x0115, ShopType.UpgradeShop, 0x04, True, True, [('Bomb Upgrade (+5)', 100, 7), ('Arrow Upgrade (+5)', 100, 7)])
}
+23 -18
View File
@@ -22,7 +22,7 @@ from EntranceShuffle import door_addresses, exit_ids
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = 'c06e14396839bc443a6918e736f1e5a7'
RANDOMIZERBASEHASH = '5e01caffabb4509a0987ef2f2f0bcd56'
class JsonRom(object):
@@ -591,7 +591,7 @@ def patch_rom(world, rom, player, team, enemized):
patch_shuffled_dark_sanc(world, rom, player)
# patch doors
dr_flags = DROptions.Eternal_Mini_Bosses
dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' or not world.experimental[player] else DROptions.Town_Portal
if world.doorShuffle[player] == 'crossed':
rom.write_byte(0x139004, 2)
rom.write_byte(0x151f1, 2)
@@ -618,6 +618,8 @@ def patch_rom(world, rom, player, team, enemized):
if builder.pre_open_stonewall.name == 'Desert Wall Slide NW':
dr_flags |= DROptions.Open_Desert_Wall
rom.write_byte(0x139006, dr_flags.value)
if dr_flags & DROptions.Town_Portal and world.mode[player] == 'inverted':
rom.write_byte(0x139008, 1)
# fix skull woods exit, if not fixed during exit patching
if world.fix_skullwoods_exit[player] and world.shuffle[player] == 'vanilla':
@@ -979,8 +981,8 @@ def patch_rom(world, rom, player, team, enemized):
startingstate = CollectionState(world)
if startingstate.has('Bow', player):
equip[0x340] = 1
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
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]:
equip[0x38E] |= 0x80
if startingstate.has('Silver Arrows', player):
@@ -1155,9 +1157,11 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if world.clock_mode != 'off':
if world.clock_mode != 'off' or world.dungeon_counters[player] == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla':
elif world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup
else:
rom.write_byte(0x18003C, 0x00)
@@ -1676,10 +1680,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':
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
del locations_to_hint[hint_count:]
for location in locations_to_hint:
if location == 'Swamp Left':
@@ -1733,20 +1740,17 @@ def write_strings(rom, world, player, team):
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
while hint_count > 0:
this_item = items_to_hint.pop(0)
this_location = world.find_items_not_key_only(this_item, player)
random.shuffle(this_location)
#This looks dumb but prevents hints for Skull Woods Pinball Room's key safely with any item pool.
if this_location:
if this_location[0].name == 'Skull Woods - Pinball Room':
this_location.pop(0)
if this_location:
this_hint = this_location[0].item.hint_text + ' 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 Doorshufle.
# Adding a hint for the Thieves' Town Attic location in Crossed door shuffle.
if world.doorShuffle[player] in ['crossed']:
attic_hint = world.get_location("Thieves' Town - Attic", player).parent_region.dungeon.name
this_hint = 'A cracked floor can be found in ' + attic_hint + '.'
@@ -2300,7 +2304,7 @@ HintLocations = ['telepathic_tile_eastern_palace',
'telepathic_tile_castle_tower',
'telepathic_tile_ice_large_room',
'telepathic_tile_turtle_rock',
'telepathic_tile_ice_entrace',
'telepathic_tile_ice_entrance',
'telepathic_tile_ice_stalfos_knights_room',
'telepathic_tile_tower_of_hera_entrance',
'telepathic_tile_south_east_darkworld_cave',
@@ -2313,15 +2317,16 @@ HintLocations = ['telepathic_tile_eastern_palace',
InconvenientLocations = ['Spike Cave',
'Sahasrahla',
'Purple Chest',
'Swamp Left',
'Mire Left',
'Tower of Hera - Big Key Chest',
'Eastern Palace - Big Key Chest',
'Thieves\' Town - Big Chest',
'Ice Palace - Big Chest',
'Ganons Tower - Big Chest',
'Magic Bat']
InconvenientDungeonLocations = ['Swamp Left',
'Mire Left',
'Eastern Palace - Big Key Chest',
'Thieves\' Town - Big Chest',
'Ice Palace - Big Chest',
'Ganons Tower - Big Chest']
InconvenientVanillaLocations = ['Graveyard Cave',
'Mimic Cave']
+5 -1
View File
@@ -251,6 +251,9 @@ class Room(object):
self.doorList = []
self.modified = False
def kind(self, door):
return self.doorList[door.doorListPos][1]
def door(self, pos, kind):
self.doorList.append((pos, kind))
return self
@@ -299,10 +302,11 @@ class Room(object):
class PairedDoor(object):
def __init__(self, door_a, door_b):
def __init__(self, door_a, door_b, original=False):
self.door_a = door_a
self.door_b = door_b
self.pair = True
self.original = original
def address_a(self, world, player):
d = world.check_for_door(self.door_a, player)
+261 -127
View File
@@ -1,7 +1,8 @@
import collections
import logging
from BaseClasses import CollectionState
from BaseClasses import CollectionState, RegionType, DoorType
from Regions import key_only_locations
from RoomData import DoorKind
from collections import deque
def set_rules(world, player):
@@ -168,6 +169,7 @@ def global_rules(world, player):
set_defeat_dungeon_boss_rule(world.get_location('Tower of Hera - Prize', player))
set_rule(world.get_entrance('Tower Altar NW', player), lambda state: state.has_sword(player))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 1', player))
set_rule(world.get_entrance('PoD Arena Bonk Path', player), lambda state: state.has_Boots(player))
set_rule(world.get_entrance('PoD Mimics 1 NW', player), lambda state: state.can_shoot_arrows(player))
@@ -302,7 +304,18 @@ def global_rules(world, 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 can_kill_most_things to gauntlet
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))
set_rule(world.get_entrance('GT Gauntlet 3 NW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 3 SW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 4 NW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 4 SW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 5 NW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Gauntlet 5 WS', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Wizzrobes 1 SW', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Wizzrobes 2 SE', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Wizzrobes 2 NE', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('GT Lanmolas 2 ES', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_entrance('GT Lanmolas 2 NW', player), lambda state: world.get_region('GT Lanmolas 2', player).dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_entrance('GT Torch Cross ES', player), lambda state: state.has_fire_source(player))
@@ -688,63 +701,59 @@ def no_glitches_rules(world, player):
if (not world.dark_world_light_cone and check_is_dark_world(world.get_region(region, player))) or (not world.light_world_light_cone and not check_is_dark_world(world.get_region(region, player))):
add_lamp_requirement(spot, player)
add_conditional_lamp('TR Dark Ride Up Stairs', 'TR Dark Ride', 'Entrance')
add_conditional_lamp('TR Dark Ride SW', 'TR Dark Ride', 'Entrance')
add_conditional_lamp('Mire Dark Shooters Up Stairs', 'Mire Dark Shooters', 'Entrance')
add_conditional_lamp('Mire Dark Shooters SW', 'Mire Dark Shooters', 'Entrance')
add_conditional_lamp('Mire Dark Shooters SE', 'Mire Dark Shooters', 'Entrance')
add_conditional_lamp('Mire Key Rupees NE', 'Mire Key Rupees', 'Entrance')
add_conditional_lamp('Mire Block X NW', 'Mire Block X', 'Entrance')
add_conditional_lamp('Mire Block X WS', 'Mire Block X', 'Entrance')
add_conditional_lamp('Mire Tall Dark and Roomy ES', 'Mire Tall Dark and Roomy', 'Entrance')
add_conditional_lamp('Mire Tall Dark and Roomy WS', 'Mire Tall Dark and Roomy', 'Entrance')
add_conditional_lamp('Mire Tall Dark and Roomy WN', 'Mire Tall Dark and Roomy', 'Entrance')
add_conditional_lamp('Mire Crystal Right ES', 'Mire Crystal Right', 'Entrance')
add_conditional_lamp('Mire Crystal Mid NW', 'Mire Crystal Mid', 'Entrance')
add_conditional_lamp('Mire Crystal Left WS', 'Mire Crystal Left', 'Entrance')
add_conditional_lamp('Mire Crystal Top SW', 'Mire Crystal Top', 'Entrance')
add_conditional_lamp('Mire Shooter Rupees EN', 'Mire Shooter Rupees', 'Entrance')
add_conditional_lamp('PoD Dark Alley NE', 'PoD Dark Alley', 'Entrance')
add_conditional_lamp('PoD Callback WS', 'PoD Callback', 'Entrance')
add_conditional_lamp('PoD Callback Warp', 'PoD Callback', 'Entrance')
add_conditional_lamp('PoD Turtle Party ES', 'PoD Turtle Party', 'Entrance')
add_conditional_lamp('PoD Turtle Party NW', 'PoD Turtle Party', 'Entrance')
add_conditional_lamp('PoD Lonely Turtle SW', 'PoD Lonely Turtle', 'Entrance')
add_conditional_lamp('PoD Lonely Turtle EN', 'PoD Lonely Turtle', 'Entrance')
add_conditional_lamp('PoD Dark Pegs Up Ladder', 'PoD Dark Pegs', 'Entrance')
add_conditional_lamp('PoD Dark Pegs WN', 'PoD Dark Pegs', 'Entrance')
add_conditional_lamp('PoD Dark Basement W Up Stairs', 'PoD Dark Basement', 'Entrance')
add_conditional_lamp('PoD Dark Basement E Up Stairs', 'PoD Dark Basement', 'Entrance')
add_conditional_lamp('PoD Dark Maze EN', 'PoD Dark Maze', 'Entrance')
add_conditional_lamp('PoD Dark Maze E', 'PoD Dark Maze', 'Entrance')
add_conditional_lamp('Palace of Darkness - Dark Basement - Left', 'PoD Dark Basement', 'Location')
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'PoD Dark Basement', 'Location')
add_conditional_lamp('Palace of Darkness - Dark Maze - Top', 'PoD Dark Maze', 'Location')
add_conditional_lamp('Palace of Darkness - Dark Maze - Bottom', 'PoD Dark Maze', 'Location')
add_conditional_lamp('Eastern Dark Square NW', 'Eastern Dark Square', 'Entrance')
add_conditional_lamp('Eastern Dark Square Key Door WN', 'Eastern Dark Square', 'Entrance')
add_conditional_lamp('Eastern Dark Square EN', 'Eastern Dark Square', 'Entrance')
add_conditional_lamp('Eastern Dark Pots WN', 'Eastern Dark Pots', 'Entrance')
add_conditional_lamp('Eastern Darkness S', 'Eastern Darkness', 'Entrance')
add_conditional_lamp('Eastern Darkness Up Stairs', 'Eastern Darkness', 'Entrance')
add_conditional_lamp('Eastern Darkness NE', 'Eastern Darkness', 'Entrance')
add_conditional_lamp('Eastern Rupees SE', 'Eastern Rupees', 'Entrance')
add_conditional_lamp('Eastern Palace - Dark Square Pot Key', 'Eastern Dark Square', 'Location')
add_conditional_lamp('Eastern Palace - Dark Eyegore Key Drop', 'Eastern Darkness', 'Location')
add_conditional_lamp('Tower Lone Statue Down Stairs', 'Tower Lone Statue', 'Entrance')
add_conditional_lamp('Tower Lone Statue WN', 'Tower Lone Statue', 'Entrance')
add_conditional_lamp('Tower Dark Maze EN', 'Tower Dark Maze', 'Entrance')
add_conditional_lamp('Tower Dark Maze ES', 'Tower Dark Maze', 'Entrance')
add_conditional_lamp('Tower Dark Chargers WS', 'Tower Dark Chargers', 'Entrance')
add_conditional_lamp('Tower Dark Chargers Up Stairs', 'Tower Dark Chargers', 'Entrance')
add_conditional_lamp('Tower Dual Statues Down Stairs', 'Tower Dual Statues', 'Entrance')
add_conditional_lamp('Tower Dual Statues WS', 'Tower Dual Statues', 'Entrance')
add_conditional_lamp('Tower Dark Pits ES', 'Tower Dark Pits', 'Entrance')
add_conditional_lamp('Tower Dark Pits EN', 'Tower Dark Pits', 'Entrance')
add_conditional_lamp('Tower Dark Archers WN', 'Tower Dark Archers', 'Entrance')
add_conditional_lamp('Tower Dark Archers Up Stairs', 'Tower Dark Archers', 'Entrance')
add_conditional_lamp('Castle Tower - Dark Maze', 'Tower Dark Maze', 'Location')
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Tower Dark Archers', 'Location')
dark_rooms = {
'TR Dark Ride': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride SW'], 'locations': []},
'Mire Dark Shooters': {'sewer': False, 'entrances': ['Mire Dark Shooters Up Stairs', 'Mire Dark Shooters SW', 'Mire Dark Shooters SE'], 'locations': []},
'Mire Key Rupees': {'sewer': False, 'entrances': ['Mire Key Rupees NE'], 'locations': []},
'Mire Block X': {'sewer': False, 'entrances': ['Mire Block X NW', 'Mire Block X WS'], 'locations': []},
'Mire Tall Dark and Roomy': {'sewer': False, 'entrances': ['Mire Tall Dark and Roomy ES', 'Mire Tall Dark and Roomy WS', 'Mire Tall Dark and Roomy WN'], 'locations': []},
'Mire Crystal Right': {'sewer': False, 'entrances': ['Mire Crystal Right ES'], 'locations': []},
'Mire Crystal Mid': {'sewer': False, 'entrances': ['Mire Crystal Mid NW'], 'locations': []},
'Mire Crystal Left': {'sewer': False, 'entrances': ['Mire Crystal Left WS'], 'locations': []},
'Mire Crystal Top': {'sewer': False, 'entrances': ['Mire Crystal Top SW'], 'locations': []},
'Mire Shooter Rupees': {'sewer': False, 'entrances': ['Mire Shooter Rupees EN'], 'locations': []},
'PoD Dark Alley': {'sewer': False, 'entrances': ['PoD Dark Alley NE'], 'locations': []},
'PoD Callback': {'sewer': False, 'entrances': ['PoD Callback WS', 'PoD Callback Warp'], 'locations': []},
'PoD Turtle Party': {'sewer': False, 'entrances': ['PoD Turtle Party ES', 'PoD Turtle Party NW'], 'locations': []},
'PoD Lonely Turtle': {'sewer': False, 'entrances': ['PoD Lonely Turtle SW', 'PoD Lonely Turtle EN'], 'locations': []},
'PoD Dark Pegs': {'sewer': False, 'entrances': ['PoD Dark Pegs Up Ladder', 'PoD Dark Pegs WN'], 'locations': []},
'PoD Dark Basement': {'sewer': False, 'entrances': ['PoD Dark Basement W Up Stairs', 'PoD Dark Basement E Up Stairs'], 'locations': ['Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right']},
'PoD Dark Maze': {'sewer': False, 'entrances': ['PoD Dark Maze EN', 'PoD Dark Maze E'], 'locations': ['Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom']},
'Eastern Dark Square': {'sewer': False, 'entrances': ['Eastern Dark Square NW', 'Eastern Dark Square Key Door WN', 'Eastern Dark Square EN'], 'locations': []},
'Eastern Dark Pots': {'sewer': False, 'entrances': ['Eastern Dark Pots WN'], 'locations': ['Eastern Palace - Dark Square Pot Key']},
'Eastern Darkness': {'sewer': False, 'entrances': ['Eastern Darkness S', 'Eastern Darkness Up Stairs', 'Eastern Darkness NE'], 'locations': ['Eastern Palace - Dark Eyegore Key Drop']},
'Eastern Rupees': {'sewer': False, 'entrances': ['Eastern Rupees SE'], 'locations': []},
'Tower Lone Statue': {'sewer': False, 'entrances': ['Tower Lone Statue Down Stairs', 'Tower Lone Statue WN'], 'locations': []},
'Tower Dark Maze': {'sewer': False, 'entrances': ['Tower Dark Maze EN', 'Tower Dark Maze ES'], 'locations': ['Castle Tower - Dark Maze']},
'Tower Dark Chargers': {'sewer': False, 'entrances': ['Tower Dark Chargers WS', 'Tower Dark Chargers Up Stairs'], 'locations': []},
'Tower Dual Statues': {'sewer': False, 'entrances': ['Tower Dual Statues Down Stairs', 'Tower Dual Statues WS'], 'locations': []},
'Tower Dark Pits': {'sewer': False, 'entrances': ['Tower Dark Pits ES', 'Tower Dark Pits EN'], 'locations': []},
'Tower Dark Archers': {'sewer': False, 'entrances': ['Tower Dark Archers WN', 'Tower Dark Archers Up Stairs'], 'locations': ['Castle Tower - Dark Archer Key Drop']},
'Sewers Dark Cross': {'sewer': True, 'entrances': ['Sewers Dark Cross Key Door N', 'Sewers Dark Cross South Stairs'], 'locations': ['Sewers - Dark Cross']},
'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 Dark Cross Key Door 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']},
}
dark_debug_set = set()
for region, info in dark_rooms.items():
is_dark = False
if not world.sewer_light_cone[player]:
is_dark = True
elif world.doorShuffle[player] != 'crossed' and not info['sewer']:
is_dark = True
elif world.doorShuffle[player] == 'crossed':
sewer_builder = world.dungeon_layouts[player]['Hyrule Castle']
is_dark = region not in sewer_builder.master_sector.region_set()
if is_dark:
dark_debug_set.add(region)
for ent in info['entrances']:
add_conditional_lamp(ent, region, 'Entrance')
for loc in info['locations']:
add_conditional_lamp(loc, region, 'Location')
logging.getLogger('').debug('Non Dark Regions: ' + ', '.join(set(dark_rooms.keys()).difference(dark_debug_set)))
add_conditional_lamp('Old Man', 'Old Man Cave', 'Location')
add_conditional_lamp('Old Man Cave Exit (East)', 'Old Man Cave', 'Entrance')
add_conditional_lamp('Death Mountain Return Cave Exit (East)', 'Death Mountain Return Cave', 'Entrance')
@@ -752,19 +761,6 @@ def no_glitches_rules(world, player):
add_conditional_lamp('Old Man House Front to Back', 'Old Man House', 'Entrance')
add_conditional_lamp('Old Man House Back to Front', 'Old Man House', 'Entrance')
if not world.sewer_light_cone[player]:
add_lamp_requirement(world.get_location('Sewers - Dark Cross', player), player)
add_lamp_requirement(world.get_entrance('Sewers Behind Tapestry S', player), player)
add_lamp_requirement(world.get_entrance('Sewers Behind Tapestry Down Stairs', player), player)
add_lamp_requirement(world.get_entrance('Sewers Rope Room Up Stairs', player), player)
add_lamp_requirement(world.get_entrance('Sewers Rope Room North Stairs', player), player)
add_lamp_requirement(world.get_entrance('Sewers Dark Cross South Stairs', player), player)
add_lamp_requirement(world.get_entrance('Sewers Dark Cross Key Door N', player), player)
add_lamp_requirement(world.get_entrance('Sewers Dark Cross Key Door S', player), player)
add_lamp_requirement(world.get_entrance('Sewers Water W', player), player)
add_lamp_requirement(world.get_entrance('Sewers Key Rat E', player), player)
add_lamp_requirement(world.get_entrance('Sewers Key Rat Key Door N', player), player)
def open_rules(world, player):
# softlock protection as you can reach the sewers small key door with a guard drop key
@@ -795,10 +791,42 @@ def swordless_rules(world, player):
set_rule(world.get_location('Bombos Tablet', player), lambda state: state.has('Book of Mudora', player) and state.has('Hammer', player))
std_kill_rooms = {
'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S'],
'Hyrule Dungeon Armory Boomerang': ['Hyrule Dungeon Armory Boomerang WS'],
'Eastern Stalfos Spawn': ['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'],
'Desert Compass Room': ['Desert Compass NW'],
'Desert Four Statues': ['Desert Four Statues NW', 'Desert Four Statues ES'],
'Hera Beetles': ['Hera Beetles WS'],
'Tower Gold Knights': ['Tower Gold Knights SW', 'Tower Gold Knights EN'],
'Tower Dark Archers': ['Tower Dark Archers WN'],
'Tower Red Spears': ['Tower Red Spears WN'],
'Tower Red Guards': ['Tower Red Guards EN', 'Tower Red Guards SW'],
'Tower Circle of Pots': ['Tower Circle of Pots NW'],
'PoD Turtle Party': ['PoD Turtle Party ES', 'PoD Turtle Party NW'], # todo: hammer req. in main rules
'Thieves Basement Block': ['Thieves Basement Block WN'],
'Ice Stalfos Hint': ['Ice Stalfos Hint SE'],
'Ice Pengator Trap': ['Ice Pengator Trap NE'],
'Mire 2': ['Mire 2 NE'],
'Mire Cross': ['Mire Cross ES'],
'TR Twin Pokeys': ['TR Twin Pokeys EN', 'TR Twin Pokeys SW'],
'GT Petting Zoo': ['GT Petting Zoo SE'],
'GT DMs Room': ['GT DMs Room SW'],
'GT Gauntlet 1': ['GT Gauntlet 1 WN'],
'GT Gauntlet 2': ['GT Gauntlet 2 EN', 'GT Gauntlet 2 SW'],
'GT Gauntlet 3': ['GT Gauntlet 3 NW', 'GT Gauntlet 3 SW'],
'GT Gauntlet 4': ['GT Gauntlet 4 NW', 'GT Gauntlet 4 SW'],
'GT Gauntlet 5': ['GT Gauntlet 5 NW', 'GT Gauntlet 5 WS'],
'GT Wizzrobes 1': ['GT Wizzrobes 1 SW'],
'GT Wizzrobes 2': ['GT Wizzrobes 2 SE', 'GT Wizzrobes 2 NE']
} # all trap rooms?
def standard_rules(world, player):
# these are because of rails
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.has('Zelda Delivered', player))
if world.shuffle[player] != 'vanilla':
set_rule(world.get_entrance('Hyrule Castle Exit (East)', player), lambda state: state.has('Zelda Delivered', player))
set_rule(world.get_entrance('Hyrule Castle Exit (West)', player), lambda state: state.has('Zelda Delivered', player))
# too restrictive for crossed?
def uncle_item_rule(item):
@@ -815,21 +843,21 @@ def standard_rules(world, player):
add_rule(world.get_location(location, player), lambda state: state.can_kill_most_things(player))
add_rule(world.get_location('Secret Passage', player), lambda state: state.can_kill_most_things(player))
# todo: in crossed these chest/key drops are not necessarily present
add_rule(world.get_location('Hyrule Castle - Map Chest', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Sewers - Dark Cross', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), lambda state: state.can_kill_most_things(player))
escape_builder = world.dungeon_layouts[player]['Hyrule Castle']
for region in escape_builder.master_sector.regions:
for loc in region.locations:
add_rule(loc, lambda state: state.can_kill_most_things(player))
if region.name in std_kill_rooms:
for ent in std_kill_rooms[region.name]:
add_rule(world.get_entrance(ent, player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Map Guard Key Drop', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Key Rat Key Drop', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_entrance('Hyrule Dungeon Armory S', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Hyrule Castle - Big Key Drop', player), lambda state: state.can_kill_most_things(player))
set_rule(world.get_location('Zelda Pickup', player), lambda state: state.has('Big Key (Escape)', player))
set_rule(world.get_entrance('Hyrule Castle Throne Room N', player), lambda state: state.has('Zelda Herself', player))
set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player))
def check_rule_list(state, r_list):
return True if len(r_list) <= 0 else r_list[0](state) and check_rule_list(state, r_list[1:])
rule_list, debug_path = find_rules_for_zelda_delivery(world, player)
set_rule(world.get_location('Zelda Drop Off', player), lambda state: state.has('Zelda Herself', player) and check_rule_list(state, rule_list))
for location in ['Mushroom', 'Bottle Merchant', 'Flute Spot', 'Sunken Treasure', 'Purple Chest']:
add_rule(world.get_location(location, player), lambda state: state.has('Zelda Delivered', player))
@@ -853,6 +881,31 @@ def standard_rules(world, player):
add_rule(world.get_entrance(entrance, player), lambda state: state.has('Zelda Delivered', player))
def find_rules_for_zelda_delivery(world, player):
# path rules for backtracking
start_region = world.get_region('Hyrule Dungeon Cellblock', player)
queue = deque([(start_region, [], [])])
visited = {start_region}
blank_state = CollectionState(world)
while len(queue) > 0:
region, path_rules, path = queue.popleft()
for ext in region.exits:
connect = ext.connected_region
if connect and connect.type == RegionType.Dungeon and connect not in visited:
rule = ext.access_rule
rule_list = list(path_rules)
next_path = list(path)
if not rule(blank_state):
rule_list.append(rule)
next_path.append(ext.name)
if connect.name == 'Sanctuary':
return rule_list, next_path
else:
visited.add(connect)
queue.append((connect, rule_list, next_path))
raise Exception('No path to Sanctuary found')
def set_big_bomb_rules(world, player):
# this is a mess
bombshop_entrance = world.get_region('Big Bomb Shop', player).entrances[0]
@@ -1132,14 +1185,13 @@ def set_inverted_big_bomb_rules(world, player):
'Hyrule Castle Entrance (East)',
'Inverted Ganons Tower',
'Cave 45',
'Checkerboard Cave']
'Checkerboard Cave',
'Inverted Big Bomb Shop']
LW_DM_entrances = ['Old Man Cave (East)',
'Old Man House (Bottom)',
'Old Man House (Top)',
'Death Mountain Return Cave (East)',
'Spectacle Rock Cave Peak',
'Spectacle Rock Cave',
'Spectacle Rock Cave (Bottom)',
'Tower of Hera',
'Death Mountain Return Cave (West)',
'Paradox Cave (Top)',
@@ -1159,7 +1211,7 @@ def set_inverted_big_bomb_rules(world, player):
'Chest Game',
'Dark World Hammer Peg Cave',
'Red Shield Shop',
'Dark Sanctuary Hint',
'Inverted Dark Sanctuary',
'Fortune Teller (Dark)',
'Dark World Shop',
'Dark World Lumberjack Shop',
@@ -1169,7 +1221,7 @@ def set_inverted_big_bomb_rules(world, player):
Southern_DW_entrances = ['Hype Cave',
'Bonk Fairy (Dark)',
'Archery Game',
'Inverted Big Bomb Shop',
'Inverted Links House',
'Dark Lake Hylia Shop',
'Swamp Palace']
Isolated_DW_entrances = ['Spike Cave',
@@ -1200,7 +1252,7 @@ def set_inverted_big_bomb_rules(world, player):
set_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.can_reach('East Dark World', 'Region', player) and state.can_reach('Inverted Big Bomb Shop', 'Region', player) and state.has('Crystal 5', player) and state.has('Crystal 6', player))
#crossing peg bridge starting from the southern dark world
# crossing peg bridge starting from the southern dark world
def cross_peg_bridge(state):
return state.has('Hammer', player)
@@ -1261,23 +1313,18 @@ def set_bunny_rules(world, player):
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave',
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)']
# todo: bunny impassable caves
# sewers drop may or may not be - maybe just new terminology
# desert pots are impassible by bunny - need rules for those transitions
# skull woods drops tend to soft lock bunny
# tr too - dark ride, chest gap, entrance gap, pots in lazy eyes, etc
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins']
def path_to_access_rule(path, entrance):
return lambda state: state.can_reach(entrance) and all(rule(state) for rule in path)
return lambda state: state.can_reach(entrance) and all(rule_func(state) for rule_func in path)
def options_to_access_rule(options):
return lambda state: any(rule(state) for rule in options)
return lambda state: any(rule_func(state) for rule_func in options)
def get_rule_to_add(region):
if not region.is_light_world:
def get_rule_to_add(start_region):
if not start_region.is_light_world:
return lambda state: state.has_Pearl(player)
# in this case we are mixed region.
# we collect possible options.
@@ -1290,8 +1337,8 @@ def set_bunny_rules(world, player):
# for each such entrance a new option is added that consist of:
# a) being able to reach it, and
# b) being able to access all entrances from there to `region`
seen = set([region])
queue = collections.deque([(region, [])])
seen = {start_region}
queue = deque([(start_region, [])])
while queue:
(current, path) = queue.popleft()
for entrance in current.entrances:
@@ -1301,7 +1348,7 @@ def set_bunny_rules(world, player):
new_path = path + [entrance.access_rule]
seen.add(new_region)
if not new_region.is_light_world:
continue # we don't care about pure dark world entrances
continue # we don't care about pure dark world entrances
if new_region.is_dark_world:
queue.append((new_region, new_path))
else:
@@ -1315,13 +1362,25 @@ def set_bunny_rules(world, player):
if not region.is_dark_world:
continue
rule = get_rule_to_add(region)
for exit in region.exits:
add_rule(exit, rule)
for ext in region.exits:
add_rule(ext, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if paradox_shop.is_dark_world:
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
for ent_name in bunny_impassible_doors:
bunny_exit = world.get_entrance(ent_name, player)
if bunny_exit.parent_region.is_dark_world:
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors]
doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked]
for door in doors_to_check:
room = world.get_room(door.roomIndex, player)
if door.entrance.parent_region.is_dark_world and room.kind(door) in [DoorKind.Dashable, DoorKind.Bombable, DoorKind.Hidden]:
add_rule(door.entrance, get_rule_to_add(door.entrance.parent_region))
# Add requirements for all locations that are actually in the dark world, except those available to the bunny
for location in world.get_locations():
if location.player == player and location.parent_region.is_dark_world:
@@ -1331,29 +1390,26 @@ def set_bunny_rules(world, player):
add_rule(location, get_rule_to_add(location.parent_region))
def set_inverted_bunny_rules(world, player):
# regions for the exits of multi-entrace caves/drops that bunny cannot pass
# Note spiral cave may be technically passible, but it would be too absurd to require since OHKO mode is a thing.
bunny_impassable_caves = ['Bumper Cave', 'Two Brothers House', 'Hookshot Cave',
'Pyramid', 'Spiral Cave (Top)', 'Fairy Ascension Cave (Drop)', 'The Sky']
# todo: bunny impassable caves
# sewers drop may or may not be - maybe just new terminology
# desert pots are impassible by bunny - need rules for those transitions
# skull woods drops tend to soft lock bunny
# tr too - dark ride, chest gap, entrance gap, pots in lazy eyes, etc
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree', 'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid', 'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins', 'Bombos Tablet', 'Ether Tablet', 'Purple Chest']
bunny_accessible_locations = ['Link\'s Uncle', 'Sahasrahla', 'Sick Kid', 'Lost Woods Hideout', 'Lumberjack Tree',
'Checkerboard Cave', 'Potion Shop', 'Spectacle Rock Cave', 'Pyramid',
'Hype Cave - Generous Guy', 'Peg Cave', 'Bumper Cave Ledge', 'Dark Blacksmith Ruins',
'Bombos Tablet', 'Ether Tablet', 'Purple Chest']
def path_to_access_rule(path, entrance):
return lambda state: state.can_reach(entrance) and all(rule(state) for rule in path)
return lambda state: state.can_reach(entrance) and all(rule_func(state) for rule_func in path)
def options_to_access_rule(options):
return lambda state: any(rule(state) for rule in options)
return lambda state: any(rule_func(state) for rule_func in options)
def get_rule_to_add(region):
if not region.is_dark_world:
def get_rule_to_add(start_region):
if not start_region.is_dark_world:
return lambda state: state.has_Pearl(player)
# in this case we are mixed region.
# we collect possible options.
@@ -1366,8 +1422,8 @@ def set_inverted_bunny_rules(world, player):
# for each such entrance a new option is added that consist of:
# a) being able to reach it, and
# b) being able to access all entrances from there to `region`
seen = set([region])
queue = collections.deque([(region, [])])
seen = {start_region}
queue = deque([(start_region, [])])
while queue:
(current, path) = queue.popleft()
for entrance in current.entrances:
@@ -1377,7 +1433,7 @@ def set_inverted_bunny_rules(world, player):
new_path = path + [entrance.access_rule]
seen.add(new_region)
if not new_region.is_dark_world:
continue # we don't care about pure light world entrances
continue # we don't care about pure light world entrances
if new_region.is_light_world:
queue.append((new_region, new_path))
else:
@@ -1391,13 +1447,25 @@ def set_inverted_bunny_rules(world, player):
if not region.is_light_world:
continue
rule = get_rule_to_add(region)
for exit in region.exits:
add_rule(exit, rule)
for ext in region.exits:
add_rule(ext, rule)
paradox_shop = world.get_region('Light World Death Mountain Shop', player)
if paradox_shop.is_light_world:
add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop))
for ent_name in bunny_impassible_doors:
bunny_exit = world.get_entrance(ent_name, player)
if bunny_exit.parent_region.is_light_world:
add_rule(bunny_exit, get_rule_to_add(bunny_exit.parent_region))
doors_to_check = [x for x in world.doors if x.player == player and x not in bunny_impassible_doors]
doors_to_check = [x for x in doors_to_check if x.type in [DoorType.Normal, DoorType.Interior] and not x.blocked]
for door in doors_to_check:
room = world.get_room(door.roomIndex, player)
if door.entrance.parent_region.is_light_world and room.kind(door) in [DoorKind.Dashable, DoorKind.Bombable, DoorKind.Hidden]:
add_rule(door.entrance, get_rule_to_add(door.entrance.parent_region))
# Add requirements for all locations that are actually in the light world, except those available to the bunny
for location in world.get_locations():
if location.player == player and location.parent_region.is_light_world:
@@ -1408,6 +1476,72 @@ def set_inverted_bunny_rules(world, player):
add_rule(location, get_rule_to_add(location.parent_region))
bunny_impassible_doors = {
'Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES', 'Sewers Secret Room Push Block', 'Sewers Pull Switch S',
'Eastern Lobby N', 'Eastern Courtyard Ledge W', 'Eastern Courtyard Ledge E', 'Eastern Pot Switch SE',
'Eastern Map Balcony Hook Path', 'Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW',
'Eastern Hint Tile Push Block', 'Eastern Darkness S', 'Eastern Darkness NE', 'Eastern Darkness Up Stairs',
'Eastern Attic Start WS', 'Eastern Single Eyegore NE', 'Eastern Duo Eyegores NE', 'Desert Main Lobby Left Path',
'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NW',
'Desert West Lobby NW', 'Desert Back Lobby NW', 'Desert Four Statues NW', 'Desert Four Statues ES',
'Desert Beamos Hall WS', 'Desert Beamos Hall NE', 'Desert Wall Slide NW', 'Hera Lobby Down Stairs',
'Hera Lobby Key Stairs', 'Hera Lobby Up Stairs', 'Hera Tile Room EN', 'Hera Tridorm SE', 'Hera Beetles WS',
'Hera 4F Down Stairs', 'Tower Gold Knights SW', 'Tower Dark Maze EN', 'Tower Dark Pits ES', 'Tower Dark Archers WN',
'Tower Red Spears WN', 'Tower Red Guards EN', 'Tower Red Guards SW', 'Tower Circle of Pots NW', 'Tower Altar NW',
'PoD Left Cage SW', 'PoD Middle Cage SE', 'PoD Pit Room Bomb Hole', 'PoD Pit Room Block Path N',
'PoD Pit Room Block Path S', 'PoD Stalfos Basement Warp', 'PoD Arena Main SW', 'PoD Arena Main Crystal Path',
'PoD Arena Bonk Path', 'PoD Arena Crystal Path', 'PoD Sexy Statue NW', 'PoD Map Balcony Drop Down',
'PoD Mimics 1 NW', 'PoD Warp Hint Warp', 'PoD Falling Bridge Path N', 'PoD Falling Bridge Path S',
'PoD Mimics 2 NW', 'PoD Bow Statue Down Ladder', 'PoD Dark Pegs Up Ladder', 'PoD Dark Pegs WN',
'PoD Turtle Party ES', 'PoD Turtle Party NW', 'PoD Callback Warp', 'Swamp Lobby Moat', 'Swamp Entrance Moat',
'Swamp Trench 1 Approach Swim Depart', 'Swamp Trench 1 Approach Key', 'Swamp Trench 1 Key Approach',
'Swamp Trench 1 Key Ledge Depart', 'Swamp Trench 1 Departure Approach', 'Swamp Trench 1 Departure Key',
'Swamp Hub Hook Path', 'Swamp Compass Donut Push Block',
'Swamp Shortcut Blue Barrier', 'Swamp Trench 2 Pots Blue Barrier', 'Swamp Trench 2 Pots Wet',
'Swamp Trench 2 Departure Wet', 'Swamp West Shallows Push Blocks', 'Swamp West Ledge Hook Path',
'Swamp Barrier Ledge Hook Path', 'Swamp Attic Left Pit', 'Swamp Attic Right Pit', 'Swamp Push Statue NW',
'Swamp Push Statue NE', 'Swamp Drain Right Switch', 'Swamp Waterway NE', 'Swamp Waterway N', 'Swamp Waterway NW',
'Skull Pot Circle WN', 'Skull Pot Circle Star Path', 'Skull Pull Switch S', 'Skull Big Chest N',
'Skull Big Chest Hookpath', 'Skull 2 East Lobby NW', 'Skull Back Drop Star Path', 'Skull 2 West Lobby NW',
'Skull 3 Lobby EN', 'Skull Star Pits SW', 'Skull Star Pits ES', 'Skull Torch Room WN', 'Skull Vines NW',
'Thieves Conveyor Maze EN', 'Thieves Triple Bypass EN', 'Thieves Triple Bypass SE', 'Thieves Triple Bypass WN',
'Thieves Hellway Blue Barrier', 'Thieves Hellway Crystal Blue Barrier', 'Thieves Attic ES',
'Thieves Basement Block Path', 'Thieves Blocked Entry Path', 'Thieves Conveyor Bridge Block Path',
'Thieves Conveyor Block Path', 'Ice Lobby WS', 'Ice Cross Left Push Block', 'Ice Cross Bottom Push Block Left',
'Ice Cross Bottom Push Block Right', 'Ice Cross Right Push Block Top', 'Ice Cross Right Push Block Bottom',
'Ice Cross Top Push Block Bottom', 'Ice Cross Top Push Block Right', 'Ice Bomb Drop Hole', 'Ice Pengator Switch WS',
'Ice Pengator Switch ES', 'Ice Big Key Push Block', 'Ice Stalfos Hint SE', 'Ice Bomb Jump EN',
'Ice Pengator Trap NE', 'Ice Hammer Block ES', 'Ice Tongue Pull WS', 'Ice Freezors Bomb Hole', 'Ice Tall Hint WS',
'Ice Hookshot Ledge Path', 'Ice Hookshot Balcony Path', 'Ice Many Pots SW', 'Ice Many Pots WS',
'Ice Crystal Right Blue Hole', 'Ice Crystal Left Blue Barrier', 'Ice Big Chest Landing Push Blocks',
'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 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 Hidden Shooters Block Path S',
'Mire Hidden Shooters Block Path N', '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 Torches NW',
'TR Pokey 2 EN', 'TR Pokey 2 ES', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap',
'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs',
'TR Dark Ride SW', 'TR Crystal Maze Forwards Path', 'TR Crystal Maze Blue Path', 'TR Crystal Maze Cane Path',
'TR Final Abyss South Stairs', 'TR Final Abyss NW', 'GT Hope Room EN', 'GT Blocked Stairs Block Path',
'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path',
'GT Crystal Conveyor NE', 'GT Crystal Conveyor WN', 'GT Conveyor Cross EN', 'GT Conveyor Cross WN',
'GT Hookshot East-North Path', 'GT Hookshot East-South Path', 'GT Hookshot North-East Path',
'GT Hookshot North-South Path', 'GT Hookshot South-East Path', 'GT Hookshot South-North Path',
'GT Hookshot Platform Blue Barrier', 'GT Hookshot Entry Blue Barrier', 'GT Double Switch Blue Path',
'GT Double Switch Key Blue Path', 'GT Double Switch Blue Barrier', 'GT Double Switch Transition Blue',
'GT Firesnake Room Hook Path', 'GT Falling Bridge WN', 'GT Falling Bridge WS', 'GT Ice Armos NE', 'GT Ice Armos WS',
'GT Crystal Paths SW', 'GT Mimics 1 NW', 'GT Mimics 1 ES', 'GT Mimics 2 WS', 'GT Mimics 2 NE',
'GT Hidden Spikes EN', 'GT Cannonball Bridge SE', 'GT Gauntlet 1 WN', 'GT Gauntlet 2 EN', 'GT Gauntlet 2 SW',
'GT Gauntlet 3 NW', 'GT Gauntlet 3 SW', 'GT Gauntlet 4 NW', 'GT Gauntlet 4 SW', 'GT Gauntlet 5 NW',
'GT Gauntlet 5 WS', 'GT Lanmolas 2 ES', 'GT Lanmolas 2 NW', 'GT Wizzrobes 1 SW', 'GT Wizzrobes 2 SE',
'GT Wizzrobes 2 NE', 'GT Torch Cross ES', 'GT Falling Torches NE', 'GT Moldorm Gap', 'GT Validation Block Path'
}
def add_key_logic_rules(world, player):
key_logic = world.key_logic[player]
for d_name, d_logic in key_logic.items():
+1 -1
View File
@@ -1651,7 +1651,7 @@ class TextTable(object):
text['telepathic_tile_castle_tower'] = CompressedTextMapper.convert("{NOBORDER}\nYou can reflect Agahnim's energy with Sword, Bug-net or Hammer.")
text['telepathic_tile_ice_large_room'] = CompressedTextMapper.convert("{NOBORDER}\nAll right stop collaborate and listen\nIce is back with my brand new invention")
text['telepathic_tile_turtle_rock'] = CompressedTextMapper.convert("{NOBORDER}\nYou shall not pass… without the red cane")
text['telepathic_tile_ice_entrace'] = CompressedTextMapper.convert("{NOBORDER}\nYou can use Fire Rod or Bombos to pass.")
text['telepathic_tile_ice_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nYou can use Fire Rod or Bombos to pass.")
text['telepathic_tile_ice_stalfos_knights_room'] = CompressedTextMapper.convert("{NOBORDER}\nKnock 'em down and then bomb them dead.")
text['telepathic_tile_tower_of_hera_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nThis is a bad place, with a guy who will make you fall…\n\n\na lot.")
text['houlihan_room'] = CompressedTextMapper.convert("Randomizer tournament winners\n{HARP}\n ~~~2018~~~\nS: Andy\n\n ~~~2017~~~\nA: ajneb174\nS: ajneb174")
+6 -4
View File
@@ -34,6 +34,8 @@ def is_bundled():
return getattr(sys, 'frozen', False)
def local_path(path):
return path
if local_path.cached_path is not None:
return os.path.join(local_path.cached_path, path)
@@ -188,16 +190,16 @@ def read_entrance_data(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan)
for ent, offset in entrance_offsets.items():
# print(ent)
str = ent
string = ent
for dp, data in entrance_data.items():
byte_array = []
address, size = data
for i in range(0, size):
byte_array.append(old_rom_data[address+(offset*size)+i])
bytes = ', '.join('0x{:02x}'.format(x) for x in byte_array)
str += '\t'+bytes
some_bytes = ', '.join('0x{:02x}'.format(x) for x in byte_array)
string += '\t'+some_bytes
# print("%s: %s" % (dp, bytes))
print(str)
print(string)
def print_wiki_doors(d_regions, world, player):
+2
View File
@@ -27,6 +27,8 @@ DRMode:
dw 0
DRFlags:
dw 0
DRScroll:
db 0
; Vert 0,6,0 Horz 2,0,8
org $279010
+9
View File
@@ -63,6 +63,8 @@ org $0DFA53
jsl.l LampCheckOverride
org $028046 ; <- 10046 - Bank02.asm : 217 (JSL EnableForceBlank) (Start of Module_LoadFile)
jsl.l OnFileLoadOverride
org $07A93F ; < 3A93F - Bank07.asm 6548 (LDA $8A : AND.b #$40 - Mirror checks)
jsl.l MirrorCheckOverride
org $05ef47
Sprite_HeartContainer_Override: ;sprite_heart_upgrades.asm : 96-100 (LDA $040C : CMP.b #$1A : BNE .not_in_ganons_tower)
@@ -70,6 +72,13 @@ jsl GtBossHeartCheckOverride : bcs .not_in_ganons_tower
nop : stz $0dd0, X : rts
.not_in_ganons_tower
org $2081f2
jsl MirrorCheckOverride2
org $20825c
jsl MirrorCheckOverride2
; These two, if enabled together, have implications for vanilla BK doors in IP/Hera/Mire
; IPBJ is common enough to consider not doing this. Mire is not a concern for vanilla - maybe glitched modes
; Hera BK door back can be seen with Pot clipping - likely useful for no logic seeds
+13
View File
@@ -37,4 +37,17 @@ OnFileLoadOverride:
jsl OnFileLoad ; what I wrote over
lda DRFlags : and #$80 : beq + ;flag is off
lda $7ef086 : ora #$80 : sta $7ef086
+ lda DRFlags : and #$02 : beq +
lda $7ef353 : bne +
lda #$01 : sta $7ef353
+ rtl
MirrorCheckOverride:
lda DRFlags : and #$02 : beq ++
lda $7ef353 : cmp #$01 : beq +
++ lda $8A : and #$40 ; what I wrote over
rtl
+ lda DRScroll : rtl
MirrorCheckOverride2:
lda $7ef353 : and #$02 : rtl
+21
View File
@@ -0,0 +1,21 @@
import subprocess
import os
import shutil
DEST_DIRECTORY = '.'
if os.path.isdir("upx"):
upx_string = "--upx-dir=upx"
else:
upx_string = ""
if os.path.isdir("build"):
shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller DungeonRandomizer.spec ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)
+21
View File
@@ -0,0 +1,21 @@
import subprocess
import os
import shutil
DEST_DIRECTORY = '.'
if os.path.isdir("upx"):
upx_string = "--upx-dir=upx"
else:
upx_string = ""
if os.path.isdir("build"):
shutil.rmtree("build")
subprocess.run(" ".join(["pyinstaller Gui.spec ",
upx_string,
"-y ",
"--onefile ",
f"--distpath {DEST_DIRECTORY} ",
]),
shell=True)
+347
View File
@@ -0,0 +1,347 @@
from tkinter import filedialog, messagebox, Button, Canvas, Label, LabelFrame, Frame, PhotoImage, Scrollbar, Toplevel, ALL, NSEW, LEFT, BOTTOM, X, RIGHT, TOP, HORIZONTAL, EW, NS
from glob import glob
import json
import os
import random
import shutil
from urllib.parse import urlparse
from urllib.request import urlopen
import webbrowser
from GuiUtils import ToolTips, set_icon, BackgroundTaskProgress
from Rom import Sprite
from Utils import is_bundled, local_path, output_path, open_file
class SpriteSelector(object):
def __init__(self, parent, callback, adjuster=False):
if is_bundled():
self.deploy_icons()
self.parent = parent
self.window = Toplevel(parent)
self.window.geometry("800x650")
self.sections = []
self.callback = callback
self.adjuster = adjuster
self.window.wm_title("TAKE ANY ONE YOU WANT")
self.window['padx'] = 5
self.window['pady'] = 5
self.all_sprites = []
def open_official_sprite_listing(_evt):
webbrowser.open("http://alttpr.com/sprite_preview")
def open_unofficial_sprite_dir(_evt):
open_file(self.unofficial_sprite_dir)
def open_spritesomething_listing(_evt):
webbrowser.open("https://artheau.github.io/SpriteSomething/?mode=zelda3/link")
official_frametitle = Frame(self.window)
official_title_text = Label(official_frametitle, text="Official Sprites")
official_title_link = Label(official_frametitle, text="(open)", fg="blue", cursor="hand2")
official_title_text.pack(side=LEFT)
official_title_link.pack(side=LEFT)
official_title_link.bind("<Button-1>", open_official_sprite_listing)
unofficial_frametitle = Frame(self.window)
unofficial_title_text = Label(unofficial_frametitle, text="Unofficial Sprites")
unofficial_title_link = Label(unofficial_frametitle, text="(open)", fg="blue", cursor="hand2")
unofficial_title_text.pack(side=LEFT)
unofficial_title_link.pack(side=LEFT)
unofficial_title_link.bind("<Button-1>", open_unofficial_sprite_dir)
spritesomething_title_link = Label(unofficial_frametitle, text="(SpriteSomething)", fg="blue", cursor="hand2")
spritesomething_title_link.pack(side=LEFT)
spritesomething_title_link.bind("<Button-1>", open_spritesomething_listing)
self.icon_section(official_frametitle, self.official_sprite_dir+'/*', 'Official sprites not found. Click "Update official sprites" to download them.')
self.icon_section(unofficial_frametitle, self.unofficial_sprite_dir+'/*', 'Put sprites in the unofficial sprites folder (see open link above) to have them appear here.')
frame = Frame(self.window)
frame.pack(side=BOTTOM, fill=X, pady=5)
button = Button(frame, text="Browse for file...", command=self.browse_for_sprite)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Update official sprites", command=self.update_official_sprites)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
button.pack(side=LEFT, padx=(0, 5))
button = Button(frame, text="Random sprite", command=self.use_random_sprite)
button.pack(side=LEFT, padx=(0, 5))
if adjuster:
button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
button.pack(side=LEFT, padx=(0, 5))
set_icon(self.window)
self.window.focus()
def icon_section(self, frame_label, path, no_results_label):
frame = LabelFrame(self.window, labelwidget=frame_label, padx=5, pady=5)
canvas = Canvas(frame, borderwidth=0)
y_scrollbar = Scrollbar(frame, orient="vertical", command=canvas.yview)
y_scrollbar.pack(side="right", fill="y")
content_frame = Frame(canvas)
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4, 4), window=content_frame, anchor="nw")
canvas.configure(yscrollcommand=y_scrollbar.set)
def onFrameConfigure(canvas):
"""Reset the scroll region to encompass the inner frame"""
canvas.configure(scrollregion=canvas.bbox("all"))
content_frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
frame.pack(side=TOP, fill=X)
sprites = []
for file in glob(output_path(path)):
sprites.append(Sprite(file))
sprites.sort(key=lambda s: str.lower(s.name or "").strip())
i = 0
for sprite in sprites:
image = get_image_for_sprite(sprite)
if image is None:
continue
self.all_sprites.append(sprite)
button = Button(content_frame, image=image, command=lambda spr=sprite: self.select_sprite(spr))
ToolTips.register(button, sprite.name + ("\nBy: %s" % sprite.author_name if sprite.author_name else ""))
button.image = image
button.grid(row=i // 16, column=i % 16)
i += 1
if i == 0:
label = Label(content_frame, text=no_results_label)
label.pack()
def update_official_sprites(self):
# need to wrap in try catch. We don't want errors getting the json or downloading the files to break us.
self.window.destroy()
self.parent.update()
def work(task):
resultmessage = ""
successful = True
def finished():
task.close_window()
if successful:
messagebox.showinfo("Sprite Updater", resultmessage)
else:
messagebox.showerror("Sprite Updater", resultmessage)
SpriteSelector(self.parent, self.callback, self.adjuster)
try:
task.update_status("Downloading official sprites list")
with urlopen('https://alttpr.com/sprites') as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of official sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
try:
task.update_status("Determining needed sprites")
current_sprites = [os.path.basename(file) for file in glob(self.official_sprite_dir+'/*')]
official_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path)) for sprite in sprites_arr]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in official_sprites if filename not in current_sprites]
bundled_sprites = [os.path.basename(file) for file in glob(self.local_official_sprite_dir+'/*')]
# todo: eventually use the above list to avoid downloading any sprites that we already have cached in the bundle.
official_filenames = [filename for (_, filename) in official_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in official_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
task.queue_event(finished)
return
updated = 0
for (sprite_url, filename) in needed_sprites:
try:
task.update_status("Downloading needed sprite %g/%g" % (updated + 1, len(needed_sprites)))
target = os.path.join(self.official_sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
except Exception as e:
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
updated += 1
deleted = 0
for sprite in obsolete_sprites:
try:
task.update_status("Removing obsolete sprite %g/%g" % (deleted + 1, len(obsolete_sprites)))
os.remove(os.path.join(self.official_sprite_dir, sprite))
except Exception as e:
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (type(e).__name__, e)
successful = False
deleted += 1
if successful:
resultmessage = "official sprites updated successfully"
task.queue_event(finished)
BackgroundTaskProgress(self.parent, work, "Updating Sprites")
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
("ZSprite files", ".zspr"),
("Sprite files", ".spr"),
("Rom Files", (".sfc", ".smc")),
("All Files", "*")])
try:
self.callback(Sprite(sprite))
except Exception:
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None, False)
self.window.destroy()
def use_default_link_sprite(self):
self.callback(Sprite.default_link_sprite(), False)
self.window.destroy()
def use_random_sprite(self):
self.callback(random.choice(self.all_sprites) if self.all_sprites else None, True)
self.window.destroy()
def select_sprite(self, spritename):
self.callback(spritename, False)
self.window.destroy()
def deploy_icons(self):
if not os.path.exists(self.unofficial_sprite_dir):
os.makedirs(self.unofficial_sprite_dir)
if not os.path.exists(self.official_sprite_dir):
shutil.copytree(self.local_official_sprite_dir, self.official_sprite_dir)
@property
def official_sprite_dir(self):
if is_bundled():
return output_path("sprites/official")
return self.local_official_sprite_dir
@property
def local_official_sprite_dir(self):
return local_path("data/sprites/official")
@property
def unofficial_sprite_dir(self):
if is_bundled():
return output_path("sprites/unofficial")
return self.local_unofficial_sprite_dir
@property
def local_unofficial_sprite_dir(self):
return local_path("data/sprites/unofficial")
def get_image_for_sprite(sprite):
if not sprite.valid:
return None
height = 24
width = 16
def draw_sprite_into_gif(add_palette_color, set_pixel_color_index):
def drawsprite(spr, pal_as_colors, offset):
for y, row in enumerate(spr):
for x, pal_index in enumerate(row):
if pal_index:
color = pal_as_colors[pal_index - 1]
set_pixel_color_index(x + offset[0], y + offset[1], color)
add_palette_color(16, (40, 40, 40))
shadow = [
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0],
]
drawsprite(shadow, [16], (2, 17))
palettes = sprite.decode_palette()
for i in range(15):
add_palette_color(i + 1, palettes[0][i])
body = sprite.decode16(0x4C0)
drawsprite(body, list(range(1, 16)), (0, 8))
head = sprite.decode16(0x40)
drawsprite(head, list(range(1, 16)), (0, 0))
def make_gif(callback):
gif_header = b'GIF89a'
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
gif_gce = bytearray(8)
gif_gce[0] = 0x21 # start of extention blocked
gif_gce[1] = 0xF9 # identifies this as the Graphics Control extension
gif_gce[2] = 4 # we are suppling only the 4 four bytes
gif_gce[3] = 0x01 # this gif includes transparency
gif_gce[4] = gif_gce[5] = 0 # animation frrame delay (unused)
gif_gce[6] = 0 # transparent color is index 0
gif_gce[7] = 0 # end of gif_gce
gif_id = bytearray(10)
gif_id[0] = 0x2c
# byte 1,2 are image left. 3,4 are image top both are left as zerosuitsamus
gif_id[5] = width
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
unchunked_image_data = bytearray(height * (width + 1) + 1)
# we technically need a Clear code once every 125 bytes, but we do it at the start of every row for simplicity
for row in range(height):
unchunked_image_data[row * (width + 1)] = clear
unchunked_image_data[-1] = stop
def add_palette_color(index, color):
gif_gct[3 * index] = color[0]
gif_gct[3 * index + 1] = color[1]
gif_gct[3 * index + 2] = color[2]
def set_pixel_color_index(x, y, color):
unchunked_image_data[y * (width + 1) + x + 1] = color
callback(add_palette_color, set_pixel_color_index)
def chunk_image(img):
for i in range(0, len(img), 255):
chunk = img[i:i + 255]
yield bytes([len(chunk)])
yield chunk
gif_img = b''.join([gif_img_minimum_code_size] + list(chunk_image(unchunked_image_data)) + [b'\x00'])
gif = b''.join([gif_header, gif_lsd, gif_gct, gif_gce, gif_id, gif_img, b'\x3b'])
return gif
gif_data = make_gif(draw_sprite_into_gif)
image = PhotoImage(data=gif_data)
return image.zoom(2)
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "classes" package
+109
View File
@@ -0,0 +1,109 @@
CUSTOMITEMS = [
"bow", "progressivebow", "boomerang", "redmerang", "hookshot",
"mushroom", "powder", "firerod", "icerod", "bombos",
"ether", "quake", "lamp", "hammer", "shovel",
"flute", "bugnet", "book", "bottle", "somaria",
"byrna", "cape", "mirror", "boots", "powerglove",
"titansmitt", "progressiveglove", "flippers", "pearl", "heartpiece",
"heartcontainer", "sancheart", "sword1", "sword2", "sword3",
"sword4", "progressivesword", "shield1", "shield2", "shield3",
"progressiveshield", "mail2", "mail3", "progressivemail", "halfmagic",
"quartermagic", "bombsplus5", "bombsplus10", "arrowsplus5", "arrowsplus10",
"arrow1", "arrow10", "bomb1", "bomb3", "bomb10",
"rupee1", "rupee5", "rupee20", "rupee50", "rupee100",
"rupee300", "blueclock", "greenclock", "redclock", "silversupgrade",
"generickeys", "triforcepieces", "triforcepiecesgoal", "triforce", "rupoor",
"rupoorcost"
]
CANTSTARTWITH = [
"triforcepiecesgoal", "triforce", "rupoor",
"rupoorcost"
]
CUSTOMITEMLABELS = [
"Bow", "Progressive Bow", "Blue Boomerang", "Red Boomerang", "Hookshot",
"Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos",
"Ether", "Quake", "Lamp", "Hammer", "Shovel",
"Ocarina", "Bug Catching Net", "Book of Mudora", "Bottle", "Cane of Somaria",
"Cane of Byrna", "Magic Cape", "Magic Mirror", "Pegasus Boots", "Power Glove",
"Titans Mitts", "Progressive Glove", "Flippers", "Moon Pearl", "Piece of Heart",
"Boss Heart Container", "Sanctuary Heart Container", "Fighter Sword", "Master Sword", "Tempered Sword",
"Golden Sword", "Progressive Sword", "Blue Shield", "Red Shield", "Mirror Shield",
"Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)",
"Magic Upgrade (1/4)", "Bomb Upgrade (+5)", "Bomb Upgrade (+10)", "Arrow Upgrade (+5)", "Arrow Upgrade (+10)",
"Single Arrow", "Arrows (10)", "Single Bomb", "Bombs (3)", "Bombs (10)",
"Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)",
"Rupees (300)", "Blue Clock", "Green Clock", "Red Clock", "Silver Arrows",
"Small Key (Universal)", "Triforce Piece", "Triforce Piece Goal", "Triforce", "Rupoor",
"Rupoor Cost"
]
SETTINGSTOPROCESS = {
"randomizer": {
"item": {
"retro": "retro",
"worldstate": "mode",
"logiclevel": "logic",
"goal": "goal",
"crystals_gt": "crystals_gt",
"crystals_ganon": "crystals_ganon",
"weapons": "swords",
"itempool": "difficulty",
"itemfunction": "item_functionality",
"timer": "timer",
"progressives": "progressive",
"accessibility": "accessibility",
"sortingalgo": "algorithm"
},
"entrance": {
"openpyramid": "openpyramid",
"shuffleganon": "shuffleganon",
"entranceshuffle": "shuffle"
},
"enemizer": {
"potshuffle": "shufflepots",
"enemyshuffle": "shuffleenemies",
"bossshuffle": "shufflebosses",
"enemydamage": "enemy_damage",
"enemyhealth": "enemy_health"
},
"dungeon": {
"mapshuffle": "mapshuffle",
"compassshuffle": "compassshuffle",
"smallkeyshuffle": "keyshuffle",
"bigkeyshuffle": "bigkeyshuffle",
"dungeondoorshuffle": "door_shuffle",
"experimental": "experimental",
"dungeon_counters": "dungeon_counters"
},
"multiworld": {
"names": "names"
},
"gameoptions": {
"hints": "hints",
"nobgm": "disablemusic",
"quickswap": "quickswap",
"heartcolor": "heartcolor",
"heartbeep": "heartbeep",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes"
},
"generation": {
"spoiler": "create_spoiler",
"suppressrom": "suppress_rom",
"usestartinventory": "usestartinventory",
"usecustompool": "custom",
"saveonexit": "saveonexit"
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui" package
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui.about" package
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui.adjust" package
+113
View File
@@ -0,0 +1,113 @@
from tkinter import ttk, filedialog, messagebox, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT, X, BOTTOM
from AdjusterMain import adjust
from argparse import Namespace
from classes.SpriteSelector import SpriteSelector
import gui.widgets as widgets
import json
import logging
import os
def adjust_page(top, parent, settings):
# Adjust page
self = ttk.Frame(parent)
# Adjust options
self.widgets = {}
# Adjust option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["selectOptionsFrame"] = Frame(self)
self.frames["leftAdjustFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["rightAdjustFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["bottomAdjustFrame"] = Frame(self)
self.frames["selectOptionsFrame"].pack(fill=X)
self.frames["leftAdjustFrame"].pack(side=LEFT)
self.frames["rightAdjustFrame"].pack(side=RIGHT)
self.frames["bottomAdjustFrame"].pack(fill=X)
with open(os.path.join("resources","app","gui","adjust","overview","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
# Sprite Selection
self.spriteNameVar2 = StringVar()
spriteDialogFrame2 = Frame(self.frames["leftAdjustFrame"])
baseSpriteLabel2 = Label(spriteDialogFrame2, text='Sprite:')
spriteEntry2 = Label(spriteDialogFrame2, textvariable=self.spriteNameVar2)
self.sprite = None
def set_sprite(sprite_param, random_sprite=False):
if sprite_param is None or not sprite_param.valid:
self.sprite = None
self.spriteNameVar2.set('(unchanged)')
else:
self.sprite = sprite_param
self.spriteNameVar2.set(self.sprite.name)
top.randomSprite.set(random_sprite)
def SpriteSelectAdjuster():
SpriteSelector(parent, set_sprite, adjuster=True)
spriteSelectButton2 = Button(spriteDialogFrame2, text='...', command=SpriteSelectAdjuster)
baseSpriteLabel2.pack(side=LEFT)
spriteEntry2.pack(side=LEFT)
spriteSelectButton2.pack(side=LEFT)
spriteDialogFrame2.pack(anchor=E)
adjustRomFrame = Frame(self.frames["bottomAdjustFrame"])
adjustRomLabel = Label(adjustRomFrame, text='Rom to adjust: ')
self.romVar2 = StringVar(value=settings["rom"])
romEntry2 = Entry(adjustRomFrame, textvariable=self.romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
if rom:
settings["rom"] = rom
self.romVar2.set(rom)
romSelectButton2 = Button(adjustRomFrame, text='Select Rom', command=RomSelect2)
adjustRomLabel.pack(side=LEFT)
romEntry2.pack(side=LEFT, fill=X, expand=True)
romSelectButton2.pack(side=LEFT)
adjustRomFrame.pack(fill=X)
def adjustRom():
options = {
"heartbeep": "heartbeep",
"heartcolor": "heartcolor",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes",
"quickswap": "quickswap",
"nobgm": "disablemusic"
}
guiargs = Namespace()
for option in options:
arg = options[option]
setattr(guiargs, arg, self.widgets[option].storageVar.get())
guiargs.rom = self.romVar2.get()
guiargs.baserom = top.pages["randomizer"].pages["generation"].romVar.get()
guiargs.sprite = self.sprite
try:
adjust(args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
adjustButton = Button(self.frames["bottomAdjustFrame"], text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 0))
return self,settings
+147
View File
@@ -0,0 +1,147 @@
from tkinter import ttk, messagebox, StringVar, Button, Entry, Frame, Label, Spinbox, E, W, LEFT, RIGHT, X
from argparse import Namespace
from functools import partial
import logging
import os
import random
from CLI import parse_arguments, get_settings
from Main import main
from Utils import local_path, output_path, open_file
import classes.constants as CONST
import gui.widgets as widgets
def bottom_frame(self, parent, args=None):
# Bottom Frame
self = ttk.Frame(parent)
# Bottom Frame options
self.widgets = {}
seedCountFrame = Frame(self)
seedCountFrame.pack()
## Seed #
seedLabel = Label(self, text='Seed #')
savedSeed = parent.settings["seed"]
self.seedVar = StringVar(value=savedSeed)
def saveSeed(caller,_,mode):
savedSeed = self.seedVar.get()
parent.settings["seed"] = int(savedSeed) if savedSeed.isdigit() else None
self.seedVar.trace_add("write",saveSeed)
seedEntry = Entry(self, width=15, textvariable=self.seedVar)
seedLabel.pack(side=LEFT)
seedEntry.pack(side=LEFT)
## Number of Generation attempts
key = "generationcount"
self.widgets[key] = widgets.make_widget(
self,
"spinbox",
self,
"Count",
None,
None,
{"label": {"side": LEFT}, "spinbox": {"side": RIGHT}}
)
self.widgets[key].pack(side=LEFT)
def generateRom():
guiargs = create_guiargs(parent)
# get default values for missing parameters
for k,v in vars(parse_arguments(['--multi', str(guiargs.multi)])).items():
if k not in vars(guiargs):
setattr(guiargs, k, v)
elif type(v) is dict: # use same settings for every player
setattr(guiargs, k, {player: getattr(guiargs, k) for player in range(1, guiargs.multi + 1)})
try:
if guiargs.count is not None:
seed = guiargs.seed
for _ in range(guiargs.count):
main(seed=seed, args=guiargs)
seed = random.randint(0, 999999999)
else:
main(seed=guiargs.seed, args=guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while creating seed", message=str(e))
else:
messagebox.showinfo(title="Success", message="Rom patched successfully")
## Generate Button
generateButton = Button(self, text='Generate Patched Rom', command=generateRom)
generateButton.pack(side=LEFT)
def open_output():
if args and args.outputpath:
open_file(output_path(args.outputpath))
else:
open_file(output_path(parent.settings["outputpath"]))
openOutputButton = Button(self, text='Open Output Directory', command=open_output)
openOutputButton.pack(side=RIGHT)
## Documentation Button
if os.path.exists(local_path('README.html')):
def open_readme():
open_file(local_path('README.html'))
openReadmeButton = Button(self, text='Open Documentation', command=open_readme)
openReadmeButton.pack(side=RIGHT)
return self
def create_guiargs(parent):
guiargs = Namespace()
# set up settings to gather
# Page::Subpage::GUI-id::param-id
options = CONST.SETTINGSTOPROCESS
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
arg = options[mainpage][subpage][widget]
setattr(guiargs, arg, parent.pages[mainpage].pages[subpage].widgets[widget].storageVar.get())
guiargs.enemizercli = parent.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.get()
guiargs.multi = int(parent.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.get())
guiargs.rom = parent.pages["randomizer"].pages["generation"].romVar.get()
guiargs.custom = bool(parent.pages["randomizer"].pages["generation"].widgets["usecustompool"].storageVar.get())
guiargs.seed = int(parent.frames["bottom"].seedVar.get()) if parent.frames["bottom"].seedVar.get() else None
guiargs.count = int(parent.frames["bottom"].widgets["generationcount"].storageVar.get()) if parent.frames["bottom"].widgets["generationcount"].storageVar.get() != '1' else None
adjustargs = {
"nobgm": "disablemusic",
"quickswap": "quickswap",
"heartcolor": "heartcolor",
"heartbeep": "heartbeep",
"menuspeed": "fastmenu",
"owpalettes": "ow_palettes",
"uwpalettes": "uw_palettes"
}
for adjustarg in adjustargs:
internal = adjustargs[adjustarg]
setattr(guiargs,"adjust." + internal, parent.pages["adjust"].content.widgets[adjustarg].storageVar.get())
customitems = CONST.CUSTOMITEMS
guiargs.startinventory = []
guiargs.customitemarray = {}
guiargs.startinventoryarray = {}
for customitem in customitems:
if customitem not in ["triforcepiecesgoal", "triforce", "rupoor", "rupoorcost"]:
amount = int(parent.pages["startinventory"].content.startingWidgets[customitem].storageVar.get())
guiargs.startinventoryarray[customitem] = amount
for i in range(0, amount):
label = CONST.CUSTOMITEMLABELS[customitems.index(customitem)]
guiargs.startinventory.append(label)
guiargs.customitemarray[customitem] = int(parent.pages["custom"].content.customWidgets[customitem].storageVar.get())
guiargs.startinventory = ','.join(guiargs.startinventory)
guiargs.sprite = parent.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"]
guiargs.randomSprite = parent.randomSprite.get()
guiargs.outputpath = parent.outputPath.get()
return guiargs
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui.custom" package
+55
View File
@@ -0,0 +1,55 @@
from tkinter import ttk, Frame, N, LEFT, VERTICAL, Y
import gui.widgets as widgets
import json
import os
import classes.constants as CONST
def custom_page(top, parent):
# Custom Item Pool
self = ttk.Frame(parent)
def create_list_frame(parent, framename):
parent.frames[framename] = Frame(parent)
parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N)
parent.frames[framename].thisRow = 0
parent.frames[framename].thisCol = 0
def create_vertical_rule(num=1):
for i in range(0,num):
ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y)
def validation(P):
if str.isdigit(P) or P == "":
return True
else:
return False
vcmd=(self.register(validation), '%P')
# Custom Item Pool options
self.customWidgets = {}
# Custom Item Pool option sections
self.frames = {}
create_list_frame(self, "itemList1")
create_vertical_rule(2)
create_list_frame(self, "itemList2")
create_vertical_rule(2)
create_list_frame(self, "itemList3")
create_vertical_rule(2)
create_list_frame(self, "itemList4")
create_vertical_rule(2)
create_list_frame(self, "itemList5")
with open(os.path.join("resources", "app", "gui", "custom", "overview", "widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.customWidgets[key] = dictWidgets[key]
for i, key in enumerate(CONST.CUSTOMITEMS):
self.customWidgets[key].storageVar.set(top.settings["customitemarray"][i])
return self
+74
View File
@@ -0,0 +1,74 @@
from classes.SpriteSelector import SpriteSelector as spriteSelector
from gui.randomize.gameoptions import set_sprite
from Rom import Sprite, get_sprite_from_name
import classes.constants as CONST
def loadcliargs(gui, args, settings=None):
if args is not None:
# for k, v in vars(args).items():
# if type(v) is dict:
# setattr(args, k, v[1]) # only get values for player 1 for now
# load values from commandline args
# set up options to get
# Page::Subpage::GUI-id::param-id
options = CONST.SETTINGSTOPROCESS
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
arg = options[mainpage][subpage][widget]
gui.pages[mainpage].pages[subpage].widgets[widget].storageVar.set(args[arg])
if subpage == "gameoptions" and not widget == "hints":
hasSettings = settings is not None
hasWidget = ("adjust." + widget) in settings if hasSettings else None
if hasWidget is None:
gui.pages["adjust"].content.widgets[widget].storageVar.set(args[arg])
gui.pages["randomizer"].pages["enemizer"].enemizerCLIpathVar.set(args["enemizercli"])
gui.pages["randomizer"].pages["generation"].romVar.set(args["rom"])
if args["multi"]:
gui.pages["randomizer"].pages["multiworld"].widgets["worlds"].storageVar.set(str(args["multi"]))
if args["seed"]:
gui.frames["bottom"].seedVar.set(str(args["seed"]))
if args["count"]:
gui.frames["bottom"].widgets["generationcount"].storageVar.set(str(args["count"]))
gui.outputPath.set(args["outputpath"])
def sprite_setter(spriteObject):
gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteObject"] = spriteObject
if args["sprite"] is not None:
sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"])
set_sprite(sprite_obj, False, spriteSetter=sprite_setter,
spriteNameVar=gui.pages["randomizer"].pages["gameoptions"].widgets["sprite"]["spriteNameVar"],
randomSpriteVar=gui.randomSprite)
def sprite_setter_adj(spriteObject):
gui.pages["adjust"].content.sprite = spriteObject
if args["sprite"] is not None:
sprite_obj = args.sprite if isinstance(args["sprite"], Sprite) else get_sprite_from_name(args["sprite"])
set_sprite(sprite_obj, False, spriteSetter=sprite_setter_adj,
spriteNameVar=gui.pages["adjust"].content.spriteNameVar2,
randomSpriteVar=gui.randomSprite)
def loadadjustargs(gui, settings):
options = {
"adjust": {
"content": {
"nobgm": "adjust.nobgm",
"quickswap": "adjust.quickswap",
"heartcolor": "adjust.heartcolor",
"heartbeep": "adjust.heartbeep",
"menuspeed": "adjust.menuspeed",
"owpalettes": "adjust.owpalettes",
"uwpalettes": "adjust.uwpalettes"
}
}
}
for mainpage in options:
for subpage in options[mainpage]:
for widget in options[mainpage][subpage]:
key = options[mainpage][subpage][widget]
if key in settings:
gui.pages[mainpage].content.widgets[widget].storageVar.set(settings[key])
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui.randomize" package
+38
View File
@@ -0,0 +1,38 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def dungeon_page(parent):
# Dungeon Shuffle
self = ttk.Frame(parent)
# Dungeon Shuffle options
self.widgets = {}
# Dungeon Shuffle option sections
self.frames = {}
self.frames["keysanity"] = Frame(self)
self.frames["keysanity"].pack(anchor=W)
## Dungeon Item Shuffle
mscbLabel = Label(self.frames["keysanity"], text="Shuffle: ")
mscbLabel.pack(side=LEFT)
with open(os.path.join("resources","app","gui","randomize","dungeon","keysanity.json")) as keysanityItems:
myDict = json.load(keysanityItems)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["keysanity"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(side=LEFT)
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","dungeon","widgets.json")) as dungeonWidgets:
myDict = json.load(dungeonWidgets)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(anchor=W)
return self
+63
View File
@@ -0,0 +1,63 @@
import os
from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, LabelFrame, OptionMenu, N, E, W, LEFT, RIGHT, BOTTOM, X
import gui.widgets as widgets
import json
import os
import webbrowser
def enemizer_page(parent,settings):
def open_enemizer_download(_evt):
webbrowser.open("https://github.com/Bonta0/Enemizer/releases")
# Enemizer
self = ttk.Frame(parent)
# Enemizer options
self.widgets = {}
# Enemizer option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["selectOptionsFrame"] = Frame(self)
self.frames["leftEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["rightEnemizerFrame"] = Frame(self.frames["selectOptionsFrame"])
self.frames["bottomEnemizerFrame"] = Frame(self)
self.frames["selectOptionsFrame"].pack(fill=X)
self.frames["leftEnemizerFrame"].pack(side=LEFT)
self.frames["rightEnemizerFrame"].pack(side=RIGHT)
self.frames["bottomEnemizerFrame"].pack(fill=X)
with open(os.path.join("resources","app","gui","randomize","enemizer","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
## Enemizer CLI Path
enemizerPathFrame = Frame(self.frames["bottomEnemizerFrame"])
enemizerCLIlabel = Label(enemizerPathFrame, text="EnemizerCLI path: ")
enemizerCLIlabel.pack(side=LEFT)
enemizerURL = Label(enemizerPathFrame, text="(get online)", fg="blue", cursor="hand2")
enemizerURL.pack(side=LEFT)
enemizerURL.bind("<Button-1>", open_enemizer_download)
self.enemizerCLIpathVar = StringVar(value=settings["enemizercli"])
enemizerCLIpathEntry = Entry(enemizerPathFrame, textvariable=self.enemizerCLIpathVar)
enemizerCLIpathEntry.pack(side=LEFT, fill=X, expand=True)
def EnemizerSelectPath():
path = filedialog.askopenfilename(filetypes=[("EnemizerCLI executable", "*EnemizerCLI*")], initialdir=os.path.join("."))
if path:
self.enemizerCLIpathVar.set(path)
settings["enemizercli"] = path
enemizerCLIbrowseButton = Button(enemizerPathFrame, text='...', command=EnemizerSelectPath)
enemizerCLIbrowseButton.pack(side=LEFT)
enemizerPathFrame.pack(fill=X)
return self,settings
+29
View File
@@ -0,0 +1,29 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def entrando_page(parent):
# Entrance Randomizer
self = ttk.Frame(parent)
# Entrance Randomizer options
self.widgets = {}
# Entrance Randomizer option sections
self.frames = {}
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","entrando","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
return self
+78
View File
@@ -0,0 +1,78 @@
from tkinter import ttk, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
from functools import partial
import classes.SpriteSelector as spriteSelector
import gui.widgets as widgets
import json
import os
def gameoptions_page(top, parent):
# Game Options
self = ttk.Frame(parent)
# Game Options options
self.widgets = {}
# Game Options option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["leftRomOptionsFrame"] = Frame(self)
self.frames["rightRomOptionsFrame"] = Frame(self)
self.frames["leftRomOptionsFrame"].pack(side=LEFT)
self.frames["rightRomOptionsFrame"].pack(side=RIGHT)
with open(os.path.join("resources","app","gui","randomize","gameoptions","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
## Sprite selection
spriteDialogFrame = Frame(self.frames["leftRomOptionsFrame"])
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
self.widgets["sprite"] = {}
self.widgets["sprite"]["spriteObject"] = None
self.widgets["sprite"]["spriteNameVar"] = StringVar()
self.widgets["sprite"]["spriteNameVar"].set('(unchanged)')
spriteEntry = Label(spriteDialogFrame, textvariable=self.widgets["sprite"]["spriteNameVar"])
def sprite_setter(spriteObject):
self.widgets["sprite"]["spriteObject"] = spriteObject
def sprite_select():
spriteSelector.SpriteSelector(parent, partial(set_sprite, spriteSetter=sprite_setter,
spriteNameVar=self.widgets["sprite"]["spriteNameVar"],
randomSpriteVar=top.randomSprite))
spriteSelectButton = Button(spriteDialogFrame, text='...', command=sprite_select)
baseSpriteLabel.pack(side=LEFT)
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
spriteDialogFrame.pack(anchor=E)
return self
def set_sprite(sprite_param, random_sprite=False, spriteSetter=None, spriteNameVar=None, randomSpriteVar=None):
if sprite_param is None or not sprite_param.valid:
if spriteSetter:
spriteSetter(None)
if spriteNameVar is not None:
spriteNameVar.set('(unchanged)')
else:
if spriteSetter:
spriteSetter(sprite_param)
if spriteNameVar is not None:
spriteNameVar.set(sprite_param.name)
if randomSpriteVar:
randomSpriteVar.set(random_sprite)
+45
View File
@@ -0,0 +1,45 @@
import os
from tkinter import ttk, filedialog, IntVar, StringVar, Button, Checkbutton, Entry, Frame, Label, E, W, LEFT, RIGHT, X
import gui.widgets as widgets
import json
import os
def generation_page(parent,settings):
# Generation Setup
self = ttk.Frame(parent)
# Generation Setup options
self.widgets = {}
# Generation Setup option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
with open(os.path.join("resources","app","gui","randomize","generation","checkboxes.json")) as checkboxes:
myDict = json.load(checkboxes)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["checkboxes"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(anchor=W)
self.frames["baserom"] = Frame(self)
self.frames["baserom"].pack(anchor=W, fill=X)
## Locate base ROM
baseRomFrame = Frame(self.frames["baserom"])
baseRomLabel = Label(baseRomFrame, text='Base Rom: ')
self.romVar = StringVar()
romEntry = Entry(baseRomFrame, textvariable=self.romVar)
self.romVar.set(settings["rom"])
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")], initialdir=os.path.join("."))
self.romVar.set(rom)
romSelectButton = Button(baseRomFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, fill=X, expand=True)
romSelectButton.pack(side=LEFT)
baseRomFrame.pack(fill=X)
return self,settings
+35
View File
@@ -0,0 +1,35 @@
from tkinter import ttk, IntVar, StringVar, Checkbutton, Frame, Label, OptionMenu, E, W, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def item_page(parent):
# Item Randomizer
self = ttk.Frame(parent)
# Item Randomizer options
self.widgets = {}
# Item Randomizer option sections
self.frames = {}
self.frames["checkboxes"] = Frame(self)
self.frames["checkboxes"].pack(anchor=W)
self.frames["leftItemFrame"] = Frame(self)
self.frames["rightItemFrame"] = Frame(self)
self.frames["leftItemFrame"].pack(side=LEFT)
self.frames["rightItemFrame"].pack(side=RIGHT)
with open(os.path.join("resources","app","gui","randomize","item","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
packAttrs = {"anchor":E}
if self.widgets[key].type == "checkbox":
packAttrs["anchor"] = W
self.widgets[key].pack(packAttrs)
return self
+38
View File
@@ -0,0 +1,38 @@
from tkinter import ttk, StringVar, Entry, Frame, Label, Spinbox, N, E, W, X, LEFT, RIGHT
import gui.widgets as widgets
import json
import os
def multiworld_page(parent,settings):
# Multiworld
self = ttk.Frame(parent)
# Multiworld options
self.widgets = {}
# Multiworld option sections
self.frames = {}
self.frames["widgets"] = Frame(self)
self.frames["widgets"].pack(anchor=W, fill=X)
with open(os.path.join("resources","app","gui","randomize","multiworld","widgets.json")) as multiworldItems:
myDict = json.load(multiworldItems)
dictWidgets = widgets.make_widgets_from_dict(self, myDict, self.frames["widgets"])
for key in dictWidgets:
self.widgets[key] = dictWidgets[key]
self.widgets[key].pack(side=LEFT, anchor=N)
## List of Player Names
key = "names"
self.widgets[key] = Frame(self.frames["widgets"])
self.widgets[key].label = Label(self.widgets[key], text='Player names')
self.widgets[key].storageVar = StringVar(value=settings["names"])
def saveMultiNames(caller,_,mode):
settings["names"] = self.widgets[key].storageVar.get()
self.widgets[key].storageVar.trace_add("write",saveMultiNames)
self.widgets[key].textbox = Entry(self.widgets[key], textvariable=self.widgets[key].storageVar)
self.widgets[key].label.pack(side=LEFT)
self.widgets[key].textbox.pack(side=LEFT, fill=X, expand=True)
self.widgets[key].pack(anchor=N, fill=X, expand=True)
return self,settings
+1
View File
@@ -0,0 +1 @@
# do nothing, just exist to make "gui.startinventory" package
+63
View File
@@ -0,0 +1,63 @@
from tkinter import ttk, StringVar, Entry, Frame, Label, N, E, W, LEFT, RIGHT, X, VERTICAL, Y
import gui.widgets as widgets
import json
import os
import classes.constants as CONST
def startinventory_page(top,parent):
# Starting Inventory
self = ttk.Frame(parent)
def create_list_frame(parent, framename):
parent.frames[framename] = Frame(parent)
parent.frames[framename].pack(side=LEFT, padx=(0,0), anchor=N)
parent.frames[framename].thisRow = 0
parent.frames[framename].thisCol = 0
def create_vertical_rule(num=1):
for i in range(0,num):
ttk.Separator(self, orient=VERTICAL).pack(side=LEFT, anchor=N, fill=Y)
def validation(P):
if str.isdigit(P) or P == "":
return True
else:
return False
vcmd=(self.register(validation), '%P')
# Starting Inventory options
self.startingWidgets = {}
# Starting Inventory option sections
self.frames = {}
create_list_frame(self,"itemList1")
create_vertical_rule(2)
create_list_frame(self,"itemList2")
create_vertical_rule(2)
create_list_frame(self,"itemList3")
create_vertical_rule(2)
create_list_frame(self,"itemList4")
create_vertical_rule(2)
create_list_frame(self,"itemList5")
with open(os.path.join("resources","app","gui","custom","overview","widgets.json")) as widgetDefns:
myDict = json.load(widgetDefns)
for key in CONST.CANTSTARTWITH:
for num in range(1, 5 + 1):
thisList = "itemList" + str(num)
if key in myDict[thisList]:
del myDict[thisList][key]
for framename,theseWidgets in myDict.items():
dictWidgets = widgets.make_widgets_from_dict(self, theseWidgets, self.frames[framename])
for key in dictWidgets:
self.startingWidgets[key] = dictWidgets[key]
for key in CONST.CUSTOMITEMS:
if key not in CONST.CANTSTARTWITH:
val = 0
if key in top.settings["startinventoryarray"]:
val = top.settings["startinventoryarray"][key]
self.startingWidgets[key].storageVar.set(val)
return self
+145
View File
@@ -0,0 +1,145 @@
from tkinter import Checkbutton, Entry, Frame, IntVar, Label, OptionMenu, Spinbox, StringVar, RIGHT, X
class Empty():
pass
class mySpinbox(Spinbox):
def __init__(self, *args, **kwargs):
Spinbox.__init__(self, *args, **kwargs)
self.bind('<MouseWheel>', self.mouseWheel)
self.bind('<Button-4>', self.mouseWheel)
self.bind('<Button-5>', self.mouseWheel)
def mouseWheel(self, event):
if event.num == 5 or event.delta == -120:
self.invoke('buttondown')
elif event.num == 4 or event.delta == 120:
self.invoke('buttonup')
def make_checkbox(self, parent, label, storageVar, manager, managerAttrs):
self = Frame(parent, name="checkframe-" + label.lower())
self.storageVar = storageVar
self.checkbox = Checkbutton(self, text=label, variable=self.storageVar, name="checkbox-" + label.lower())
if managerAttrs is not None:
self.checkbox.pack(managerAttrs)
else:
self.checkbox.pack()
return self
def make_selectbox(self, parent, label, options, storageVar, manager, managerAttrs):
def change_storage(*args):
self.storageVar.set(options[self.labelVar.get()])
def change_selected(*args):
keys = options.keys()
vals = options.values()
keysList = list(keys)
valsList = list(vals)
self.labelVar.set(keysList[valsList.index(str(self.storageVar.get()))])
self = Frame(parent, name="selectframe-" + label.lower())
self.storageVar = storageVar
self.storageVar.trace_add("write",change_selected)
self.labelVar = StringVar()
self.labelVar.trace_add("write",change_storage)
self.label = Label(self, text=label)
if managerAttrs is not None and "label" in managerAttrs:
self.label.pack(managerAttrs["label"])
else:
self.label.pack()
self.selectbox = OptionMenu(self, self.labelVar, *options.keys())
self.selectbox.config(width=20)
self.labelVar.set(managerAttrs["default"] if "default" in managerAttrs else list(options.keys())[0])
if managerAttrs is not None and "selectbox" in managerAttrs:
self.selectbox.pack(managerAttrs["selectbox"])
else:
self.selectbox.pack()
return self
def make_spinbox(self, parent, label, storageVar, manager, managerAttrs):
self = Frame(parent, name="spinframe-" + label.lower())
self.storageVar = storageVar
self.label = Label(self, text=label)
if managerAttrs is not None and "label" in managerAttrs:
self.label.pack(managerAttrs["label"])
else:
self.label.pack()
fromNum = 1
toNum = 100
if "spinbox" in managerAttrs:
if "from" in managerAttrs:
fromNum = managerAttrs["spinbox"]["from"]
if "to" in managerAttrs:
toNum = managerAttrs["spinbox"]["to"]
self.spinbox = mySpinbox(self, from_=fromNum, to=toNum, width=5, textvariable=self.storageVar, name="spinbox-" + label.lower())
if managerAttrs is not None and "spinbox" in managerAttrs:
self.spinbox.pack(managerAttrs["spinbox"])
else:
self.spinbox.pack()
return self
def make_textbox(self, parent, label, storageVar, manager, managerAttrs):
widget = Empty()
widget.storageVar = storageVar
widget.label = Label(parent, text=label)
widget.textbox = Entry(parent, justify=RIGHT, textvariable=widget.storageVar, width=3)
if "default" in managerAttrs:
widget.storageVar.set(managerAttrs["default"])
# grid
if manager == "grid":
widget.label.grid(managerAttrs["label"] if managerAttrs is not None and "label" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol)
parent.thisCol += 1
widget.textbox.grid(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None, row=parent.thisRow, column=parent.thisCol)
parent.thisRow += 1
parent.thisCol = 0
# pack
elif manager == "pack":
widget.label.pack(managerAttrs["label"] if managerAttrs is not None and "label" in managerAttrs else None)
widget.textbox.pack(managerAttrs["textbox"] if managerAttrs is not None and "textbox" in managerAttrs else None)
return widget
def make_widget(self, type, parent, label, storageVar=None, manager=None, managerAttrs=dict(), options=None):
widget = None
if manager is None:
manager = "pack"
thisStorageVar = storageVar
if isinstance(storageVar,str):
if storageVar == "int" or storageVar == "integer":
thisStorageVar = IntVar()
elif storageVar == "str" or storageVar == "string":
thisStorageVar = StringVar()
if type == "checkbox":
if thisStorageVar is None:
thisStorageVar = IntVar()
widget = make_checkbox(self, parent, label, thisStorageVar, manager, managerAttrs)
elif type == "selectbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_selectbox(self, parent, label, options, thisStorageVar, manager, managerAttrs)
elif type == "spinbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_spinbox(self, parent, label, thisStorageVar, manager, managerAttrs)
elif type == "textbox":
if thisStorageVar is None:
thisStorageVar = StringVar()
widget = make_textbox(self, parent, label, thisStorageVar, manager, managerAttrs)
widget.type = type
return widget
def make_widget_from_dict(self, defn, parent):
type = defn["type"] if "type" in defn else None
label = defn["label"]["text"] if "label" in defn and "text" in defn["label"] else ""
manager = defn["manager"] if "manager" in defn else None
managerAttrs = defn["managerAttrs"] if "managerAttrs" in defn else None
options = defn["options"] if "options" in defn else None
widget = make_widget(self, type, parent, label, None, manager, managerAttrs, options)
return widget
def make_widgets_from_dict(self, defns, parent):
widgets = {}
for key,defn in defns.items():
widgets[key] = make_widget_from_dict(self, defn, parent)
return widgets
@@ -0,0 +1,124 @@
{
"checkboxes": {
"nobgm": {
"type": "checkbox",
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
},
"leftAdjustFrame": {
"heartcolor": {
"type": "selectbox",
"label": {
"text": "Heart Color"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Red": "red",
"Blue": "blue",
"Green": "green",
"Yellow": "yellow",
"Random": "random"
}
},
"heartbeep": {
"type": "selectbox",
"label": {
"text": "Heart Beep sound rate"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Double": "double",
"Normal": "normal",
"Half": "half",
"Quarter": "quarter",
"Off": "off"
}
}
},
"rightAdjustFrame": {
"menuspeed": {
"type": "selectbox",
"label": {
"text": "Menu Speed"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Instant": "instant",
"Quadruple": "quadruple",
"Triple": "triple",
"Double": "double",
"Normal": "normal",
"Half": "half"
}
},
"owpalettes": {
"type": "selectbox",
"label": {
"text": "Overworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
},
"uwpalettes": {
"type": "selectbox",
"label": {
"text": "Underworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
}
}
}
@@ -0,0 +1,935 @@
{
"itemList1": {
"bow": {
"type": "textbox",
"label": {
"text": "Bow"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivebow": {
"type": "textbox",
"label": {
"text": "Progressive Bow"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"boomerang": {
"type": "textbox",
"label": {
"text": "Blue Boomerang"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"redmerang": {
"type": "textbox",
"label": {
"text": "Red Boomerang"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"hookshot": {
"type": "textbox",
"label": {
"text": "Hookshot"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"mushroom": {
"type": "textbox",
"label": {
"text": "Mushroom"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"powder": {
"type": "textbox",
"label": {
"text": "Magic Powder"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"firerod": {
"type": "textbox",
"label": {
"text": "Fire Rod"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"icerod": {
"type": "textbox",
"label": {
"text": "Ice Rod"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bombos": {
"type": "textbox",
"label": {
"text": "Bombos"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"ether": {
"type": "textbox",
"label": {
"text": "Ether"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"quake": {
"type": "textbox",
"label": {
"text": "Quake"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"lamp": {
"type": "textbox",
"label": {
"text": "Lamp"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"hammer": {
"type": "textbox",
"label": {
"text": "Hammer"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"shovel": {
"type": "textbox",
"label": {
"text": "Shovel"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList2": {
"flute": {
"type": "textbox",
"label": {
"text": "Flute"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bugnet": {
"type": "textbox",
"label": {
"text": "Bug Net"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"book": {
"type": "textbox",
"label": {
"text": "Book"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"bottle": {
"type": "textbox",
"label": {
"text": "Bottle"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"somaria": {
"type": "textbox",
"label": {
"text": "Cane of Somaria"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"byrna": {
"type": "textbox",
"label": {
"text": "Cane of Byrna"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"cape": {
"type": "textbox",
"label": {
"text": "Magic Cape"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"mirror": {
"type": "textbox",
"label": {
"text": "Magic Mirror"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"boots": {
"type": "textbox",
"label": {
"text": "Pegasus Boots"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"powerglove": {
"type": "textbox",
"label": {
"text": "Power Glove"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"titansmitt": {
"type": "textbox",
"label": {
"text": "Titan's Mitt"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressiveglove": {
"type": "textbox",
"label": {
"text": "Progressive Glove"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"flippers": {
"type": "textbox",
"label": {
"text": "Flippers"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"pearl": {
"type": "textbox",
"label": {
"text": "Moon Pearl"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"heartpiece": {
"type": "textbox",
"label": {
"text": "Piece of Heart"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 24
}
}
},
"itemList3": {
"heartcontainer": {
"type": "textbox",
"label": {
"text": "Heart Container"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 10
}
},
"sancheart": {
"type": "textbox",
"label": {
"text": "Sanctuary Heart"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"sword1": {
"type": "textbox",
"label": {
"text": "Fighters' Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword2": {
"type": "textbox",
"label": {
"text": "Master Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword3": {
"type": "textbox",
"label": {
"text": "Tempered Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"sword4": {
"type": "textbox",
"label": {
"text": "Golden Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivesword": {
"type": "textbox",
"label": {
"text": "Progressive Sword"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"shield1": {
"type": "textbox",
"label": {
"text": "Fighters' Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"shield2": {
"type": "textbox",
"label": {
"text": "Fire Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"shield3": {
"type": "textbox",
"label": {
"text": "Mirror Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressiveshield": {
"type": "textbox",
"label": {
"text": "Progressive Shield"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 3
}
},
"mail2": {
"type": "textbox",
"label": {
"text": "Blue Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"mail3": {
"type": "textbox",
"label": {
"text": "Red Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"progressivemail": {
"type": "textbox",
"label": {
"text": "Progressive Mail"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"halfmagic": {
"type": "textbox",
"label": {
"text": "Half Magic"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList4": {
"quartermagic": {
"type": "textbox",
"label": {
"text": "Quarter Magic"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bombsplus5": {
"type": "textbox",
"label": {
"text": "Bomb Cap +5"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bombsplus10": {
"type": "textbox",
"label": {
"text": "Bomb Cap +10"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrowsplus5": {
"type": "textbox",
"label": {
"text": "Arrow Cap +5"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrowsplus10": {
"type": "textbox",
"label": {
"text": "Arrow Cap +10"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"arrow1": {
"type": "textbox",
"label": {
"text": "Arrow (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"arrow10": {
"type": "textbox",
"label": {
"text": "Arrow (10)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 12
}
},
"bomb1": {
"type": "textbox",
"label": {
"text": "Bomb (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"bomb3": {
"type": "textbox",
"label": {
"text": "Bomb (3)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 16
}
},
"bomb10": {
"type": "textbox",
"label": {
"text": "Bomb (10)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
},
"rupee1": {
"type": "textbox",
"label": {
"text": "Rupee (1)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 2
}
},
"rupee5": {
"type": "textbox",
"label": {
"text": "Rupee (5)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 4
}
},
"rupee20": {
"type": "textbox",
"label": {
"text": "Rupee (20)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 28
}
},
"rupee50": {
"type": "textbox",
"label": {
"text": "Rupee (50)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 7
}
},
"rupee100": {
"type": "textbox",
"label": {
"text": "Rupee (100)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 1
}
}
},
"itemList5": {
"rupee300": {
"type": "textbox",
"label": {
"text": "Rupee (300)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 5
}
},
"blueclock": {
"type": "textbox",
"label": {
"text": "Blue Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"greenclock": {
"type": "textbox",
"label": {
"text": "Green Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"redclock": {
"type": "textbox",
"label": {
"text": "Red Clock"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"silversupgrade": {
"type": "textbox",
"label": {
"text": "Silver Arrows Upgrade"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"generickeys": {
"type": "textbox",
"label": {
"text": "Generic Keys"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforcepieces": {
"type": "textbox",
"label": {
"text": "Triforce Pieces"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforcepiecesgoal": {
"type": "textbox",
"label": {
"text": "Triforce Pieces Goal"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"triforce": {
"type": "textbox",
"label": {
"text": "Triforce (win game)"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"rupoor": {
"type": "textbox",
"label": {
"text": "Rupoor"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 0
}
},
"rupoorcost": {
"type": "textbox",
"label": {
"text": "Rupoor Cost"
},
"manager": "grid",
"managerAttrs": {
"label": {
"sticky": "w"
},
"default": 10
}
}
}
}
@@ -0,0 +1,26 @@
{
"mapshuffle": {
"type": "checkbox",
"label": {
"text": "Maps"
}
},
"compassshuffle": {
"type": "checkbox",
"label": {
"text": "Compasses"
}
},
"smallkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Small Keys"
}
},
"bigkeyshuffle": {
"type": "checkbox",
"label": {
"text": "Big Keys"
}
}
}
@@ -0,0 +1,48 @@
{
"dungeondoorshuffle": {
"type": "selectbox",
"label": {
"text": "Dungeon Door Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Basic"
},
"options": {
"Vanilla": "vanilla",
"Basic": "basic",
"Crossed": "crossed"
}
},
"experimental": {
"type": "checkbox",
"label": {
"text": "Enable Experimental Features"
}
},
"dungeon_counters": {
"type": "selectbox",
"label": {
"text": "Dungeon Chest Counters"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Off"
},
"options": {
"Off": "off",
"On": "on",
"On Compass Pickup": "pickup"
}
}
}
@@ -0,0 +1,93 @@
{
"checkboxes": {
"potshuffle": {
"type": "checkbox",
"label": {
"text": "Pot Shuffle"
}
}
},
"leftEnemizerFrame": {
"enemyshuffle": {
"type": "selectbox",
"label": {
"text": "Enemy Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
},
"bossshuffle": {
"type": "selectbox",
"label": {
"text": "Boss Shuffle"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "none",
"Basic": "basic",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
}
},
"rightEnemizerFrame": {
"enemydamage": {
"type": "selectbox",
"label": {
"text": "Enemy Damage"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Shuffled": "shuffled",
"Chaos": "chaos"
}
},
"enemyhealth": {
"type": "selectbox",
"label": {
"text": "Enemy Health"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Vanilla": "default",
"Easy": "easy",
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
}
}
}
@@ -0,0 +1,40 @@
{
"widgets": {
"openpyramid": {
"type": "checkbox",
"label": {
"text": "Pre-open Pyramid Hole"
}
},
"shuffleganon": {
"type": "checkbox",
"label": {
"text": "Include Ganon's Tower and Pyramid Hole in shuffle pool"
}
},
"entranceshuffle": {
"type": "selectbox",
"label": {
"text": "Entrance Shuffle"
},
"managerAttrs": {
"label": { "side": "left" },
"selectbox": { "side": "right" }
},
"options": {
"Vanilla": "vanilla",
"Simple": "simple",
"Restricted": "restricted",
"Full": "full",
"Crossed": "crossed",
"Insanity": "insanity",
"Restricted (Legacy)": "restricted_legacy",
"Full (Legacy)": "full_legacy",
"Madness (Legacy)": "madness_legacy",
"Insanity (Legacy)": "insanity_legacy",
"Dungeons + Full": "dungeonsfull",
"Dungeons + Simple": "dungeonssimple"
}
}
}
}
@@ -0,0 +1,131 @@
{
"checkboxes": {
"hints": {
"type": "checkbox",
"label": {
"text": "Include Helpful Hints"
},
"default": "true"
},
"nobgm": {
"type": "checkbox",
"label": {
"text": "Disable Music & MSU-1"
}
},
"quickswap": {
"type": "checkbox",
"label": {
"text": "L/R Quickswapping"
}
}
},
"leftRomOptionsFrame": {
"heartcolor": {
"type": "selectbox",
"label": {
"text": "Heart Color"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Red": "red",
"Blue": "blue",
"Green": "green",
"Yellow": "yellow",
"Random": "random"
}
},
"heartbeep": {
"type": "selectbox",
"label": {
"text": "Heart Beep sound rate"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Double": "double",
"Normal": "normal",
"Half": "half",
"Quarter": "quarter",
"Off": "off"
}
}
},
"rightRomOptionsFrame": {
"menuspeed": {
"type": "selectbox",
"label": {
"text": "Menu Speed"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Normal"
},
"options": {
"Instant": "instant",
"Quadruple": "quadruple",
"Triple": "triple",
"Double": "double",
"Normal": "normal",
"Half": "half"
}
},
"owpalettes": {
"type": "selectbox",
"label": {
"text": "Overworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
},
"uwpalettes": {
"type": "selectbox",
"label": {
"text": "Underworld Palettes"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Default": "default",
"Random": "random",
"Blackout": "blackout"
}
}
}
}
@@ -0,0 +1,45 @@
{
"spoiler": {
"type": "checkbox",
"label": {
"text": "Create Spoiler Log"
}
},
"suppressrom": {
"type": "checkbox",
"label": {
"text": "Do not create patched ROM"
}
},
"usestartinventory": {
"type": "checkbox",
"label": {
"text": "Use starting inventory"
}
},
"usecustompool": {
"type": "checkbox",
"label": {
"text": "Use custom item pool"
}
},
"saveonexit": {
"type": "selectbox",
"label": {
"text": "Save Settings on Exit"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Ask Me": "ask",
"Always": "always",
"Never": "never"
}
}
}
@@ -0,0 +1,266 @@
{
"checkboxes": {
"retro": {
"type": "checkbox",
"label": {
"text": "Retro mode (universal keys)"
}
}
},
"leftItemFrame": {
"worldstate": {
"type": "selectbox",
"label": {
"text": "World State"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Open"
},
"options": {
"Standard": "standard",
"Open": "open",
"Inverted": "inverted"
}
},
"logiclevel": {
"type": "selectbox",
"label": {
"text": "Logic Level"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"No Glitches": "noglitches",
"Minor Glitches": "minorglitches",
"No Logic": "nologic"
}
},
"goal": {
"type": "selectbox",
"label": {
"text": "Goal"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Defeat Ganon": "ganon",
"Master Sword Pedestal": "pedestal",
"All Dungeons": "dungeons",
"Triforce Hunt": "triforcehunt",
"Crystals": "crystals"
}
},
"crystals_gt": {
"type": "selectbox",
"label": {
"text": "Crystals to open GT"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"Random": "random"
}
},
"crystals_ganon": {
"type": "selectbox",
"label": {
"text": "Crystals to harm Ganon"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"Random": "random"
}
},
"weapons": {
"type": "selectbox",
"label": {
"text": "Weapons"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Randomized": "random",
"Assured": "assured",
"Swordless": "swordless",
"Vanilla": "vanilla"
}
}
},
"rightItemFrame": {
"itempool": {
"type": "selectbox",
"label": {
"text": "Item Pool"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
},
"itemfunction": {
"type": "selectbox",
"label": {
"text": "Item Functionality"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"Normal": "normal",
"Hard": "hard",
"Expert": "expert"
}
},
"timer": {
"type": "selectbox",
"label": {
"text": "Timer Setting"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"No Timer": "none",
"Stopwatch": "display",
"Timed": "timed",
"Timed OHKO": "timed-ohko",
"OHKO": "ohko",
"Timed Countdown": "timed-countdown"
}
},
"progressives": {
"type": "selectbox",
"label": {
"text": "Progressive Items"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"On": "on",
"Off": "off",
"Random": "random"
}
},
"accessibility": {
"type": "selectbox",
"label": {
"text": "Accessibility"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
}
},
"options": {
"100% Inventory": "items",
"100% Locations": "locations",
"Beatable": "none"
}
},
"sortingalgo": {
"type": "selectbox",
"label": {
"text": "Item Sorting"
},
"managerAttrs": {
"label": {
"side": "left"
},
"selectbox": {
"side": "right"
},
"default": "Balanced"
},
"options": {
"Freshness": "freshness",
"Flood": "flood",
"VT8.21": "vt21",
"VT8.22": "vt22",
"VT8.25": "vt25",
"VT8.26": "vt26",
"Balanced": "balanced"
}
}
}
}
@@ -0,0 +1,16 @@
{
"worlds": {
"type": "spinbox",
"label": {
"text": "Worlds"
},
"managerAttrs": {
"label": {
"side": "left"
},
"spinbox": {
"side": "right"
}
}
}
}