Implement Key-sanity
Still need to add documentation for this mode.
This commit is contained in:
parent
623e6637ff
commit
f2ea4b6442
|
@ -6,7 +6,7 @@ from collections import OrderedDict
|
|||
|
||||
class World(object):
|
||||
|
||||
def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu):
|
||||
def __init__(self, shuffle, logic, mode, difficulty, goal, algorithm, place_dungeon_items, check_beatable_only, shuffle_ganon, quickswap, fastmenu, keysanity):
|
||||
self.shuffle = shuffle
|
||||
self.logic = logic
|
||||
self.mode = mode
|
||||
|
@ -49,8 +49,13 @@ class World(object):
|
|||
self.can_access_trock_eyebridge = None
|
||||
self.quickswap = quickswap
|
||||
self.fastmenu = fastmenu
|
||||
self.keysanity = keysanity
|
||||
self.spoiler = Spoiler(self)
|
||||
|
||||
def intialize_regions(self):
|
||||
for region in self.regions:
|
||||
region.world = self
|
||||
|
||||
def get_region(self, regionname):
|
||||
if isinstance(regionname, Region):
|
||||
return regionname
|
||||
|
@ -221,7 +226,8 @@ class World(object):
|
|||
algorithm = ['freshness', 'flood', 'vt21', 'vt22', 'vt25', 'vt26'].index(self.algorithm)
|
||||
beatableonly = 1 if self.check_beatable_only else 0
|
||||
shuffleganon = 1 if self.shuffle_ganon else 0
|
||||
return logic | (beatableonly << 1) | (dungeonitems << 2) | (shuffleganon << 3) | (goal << 4) | (shuffle << 7) | (difficulty << 11) | (algorithm << 13) | (mode << 16)
|
||||
keysanity = 1 if self.keysanity else 0
|
||||
return logic | (beatableonly << 1) | (dungeonitems << 2) | (shuffleganon << 3) | (goal << 4) | (shuffle << 7) | (difficulty << 11) | (algorithm << 13) | (mode << 16) | (keysanity << 18)
|
||||
|
||||
|
||||
class CollectionState(object):
|
||||
|
@ -448,6 +454,7 @@ class Region(object):
|
|||
self.exits = []
|
||||
self.locations = []
|
||||
self.dungeon = None
|
||||
self.world = None
|
||||
self.spot_type = 'Region'
|
||||
self.hint_text = 'Hyrule'
|
||||
self.recursion_count = 0
|
||||
|
@ -457,16 +464,17 @@ class Region(object):
|
|||
if state.can_reach(entrance):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def can_fill(self, item):
|
||||
if item.key or item.map or item.compass:
|
||||
is_dungeon_item = item.key or item.map or item.compass
|
||||
sewer_hack = self.world.mode == 'standard' and item.name == 'Small Key (Escape)'
|
||||
if sewer_hack or (is_dungeon_item and not self.world.keysanity):
|
||||
if self.dungeon and self.dungeon.is_dungeon_item(item):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
@ -509,6 +517,7 @@ class Entrance(object):
|
|||
def __unicode__(self):
|
||||
return '%s' % self.name
|
||||
|
||||
|
||||
class Dungeon(object):
|
||||
|
||||
def __init__(self, name, regions, big_key, small_keys, dungeon_items):
|
||||
|
@ -517,24 +526,25 @@ class Dungeon(object):
|
|||
self.big_key = big_key
|
||||
self.small_keys = small_keys
|
||||
self.dungeon_items = dungeon_items
|
||||
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
return self.small_keys + ([self.big_key] if self.big_key else [])
|
||||
|
||||
|
||||
@property
|
||||
def all_items(self):
|
||||
return self.dungeon_items+self.keys
|
||||
|
||||
return self.dungeon_items + self.keys
|
||||
|
||||
def is_dungeon_item(self, item):
|
||||
return item.name in [dungeon_item.name for dungeon_item in self.all_items]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__unicode__())
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s' % self.name
|
||||
|
||||
|
||||
class Location(object):
|
||||
|
||||
def __init__(self, name='', address=None, crystal=False, hint_text=None, parent=None):
|
||||
|
@ -554,7 +564,7 @@ class Location(object):
|
|||
|
||||
def item_rule(self, item):
|
||||
return True
|
||||
|
||||
|
||||
def can_fill(self, item):
|
||||
return self.parent_region.can_fill(item) and self.item_rule(item)
|
||||
|
||||
|
@ -570,7 +580,7 @@ class Location(object):
|
|||
return '%s' % self.name
|
||||
|
||||
|
||||
class Item(object):
|
||||
class Item(object):
|
||||
|
||||
def __init__(self, name='', advancement=False, priority=False, type=None, code=None, altar_hint=None, altar_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None):
|
||||
self.name = name
|
||||
|
@ -585,20 +595,20 @@ class Item(object):
|
|||
self.fluteboy_credit_text = fluteboy_credit
|
||||
self.code = code
|
||||
self.location = None
|
||||
|
||||
@property
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self.type == 'SmallKey' or self.type == 'BigKey'
|
||||
|
||||
@property
|
||||
|
||||
@property
|
||||
def crystal(self):
|
||||
return self.type == 'Crystal'
|
||||
|
||||
@property
|
||||
|
||||
@property
|
||||
def map(self):
|
||||
return self.type == 'Map'
|
||||
|
||||
@property
|
||||
|
||||
@property
|
||||
def compass(self):
|
||||
return self.type == 'Compass'
|
||||
|
||||
|
@ -642,7 +652,8 @@ class Spoiler(object):
|
|||
'completeable': not self.world.check_beatable_only,
|
||||
'dungeonitems': self.world.place_dungeon_items,
|
||||
'quickswap': self.world.quickswap,
|
||||
'fastmenu': self.world.fastmenu}
|
||||
'fastmenu': self.world.fastmenu,
|
||||
'keysanity': self.world.keysanity}
|
||||
|
||||
def to_json(self):
|
||||
self.parse_data()
|
||||
|
@ -666,7 +677,8 @@ class Spoiler(object):
|
|||
outfile.write('All Locations Accessible: %s\n' % ('Yes' if self.metadata['completeable'] else 'No, some locations may be unreachable'))
|
||||
outfile.write('Maps and Compasses in Dungeons: %s\n' % ('Yes' if self.metadata['dungeonitems'] else 'No'))
|
||||
outfile.write('L\\R Quickswap enabled: %s\n' % ('Yes' if self.metadata['quickswap'] else 'No'))
|
||||
outfile.write('Fastmenu enabled: %s' % ('Yes' if self.metadata['fastmenu'] else 'No'))
|
||||
outfile.write('Fastmenu enabled: %s\n' % ('Yes' if self.metadata['fastmenu'] else 'No'))
|
||||
outfile.write('Keysanity enabled: %s' % ('Yes' if self.metadata['keysanity'] else 'No'))
|
||||
if self.entrances:
|
||||
outfile.write('\n\nEntrances:\n\n')
|
||||
outfile.write('\n'.join(['%s %s %s' % (entry['entrance'], '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', entry['exit']) for entry in self.entrances]))
|
||||
|
|
|
@ -126,6 +126,10 @@ if __name__ == '__main__':
|
|||
''', type=int)
|
||||
parser.add_argument('--fastmenu', help='Enable instant menu', action='store_true')
|
||||
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
|
||||
parser.add_argument('--keysanity', help='''\
|
||||
Keys (and other dungeon items) are no longer restricted to
|
||||
their dungeons, but can be anywhere
|
||||
''', action='store_true')
|
||||
parser.add_argument('--nodungeonitems', help='''\
|
||||
Remove Maps and Compasses from Itempool, replacing them by
|
||||
empty slots.
|
||||
|
|
5
Gui.py
5
Gui.py
|
@ -21,6 +21,8 @@ def guiMain(args=None):
|
|||
quickSwapCheckbutton = Checkbutton(checkBoxFrame, text="Enabled L/R Item quickswapping", variable=quickSwapVar)
|
||||
fastMenuVar = IntVar()
|
||||
fastMenuCheckbutton = Checkbutton(checkBoxFrame, text="Enable instant menu", variable=fastMenuVar)
|
||||
keysanityVar = IntVar()
|
||||
keysanityCheckbutton = Checkbutton(checkBoxFrame, text="Keysanity (keys anywhere)", variable=keysanityVar)
|
||||
dungeonItemsVar = IntVar()
|
||||
dungeonItemsCheckbutton = Checkbutton(checkBoxFrame, text="Place Dungeon Items (Compasses/Maps)", onvalue=0, offvalue=1, variable=dungeonItemsVar)
|
||||
beatableOnlyVar = IntVar()
|
||||
|
@ -32,6 +34,7 @@ def guiMain(args=None):
|
|||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||
quickSwapCheckbutton.pack(expand=True, anchor=W)
|
||||
fastMenuCheckbutton.pack(expand=True, anchor=W)
|
||||
keysanityCheckbutton.pack(expand=True, anchor=W)
|
||||
dungeonItemsCheckbutton.pack(expand=True, anchor=W)
|
||||
beatableOnlyCheckbutton.pack(expand=True, anchor=W)
|
||||
shuffleGanonCheckbutton.pack(expand=True, anchor=W)
|
||||
|
@ -160,6 +163,7 @@ def guiMain(args=None):
|
|||
guiargs.heartbeep = heartbeepVar.get()
|
||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
||||
guiargs.keysanity = bool(keysanityVar.get())
|
||||
guiargs.nodungeonitems = bool(dungeonItemsVar.get())
|
||||
guiargs.beatableonly = bool(beatableOnlyVar.get())
|
||||
guiargs.fastmenu = bool(fastMenuVar.get())
|
||||
|
@ -198,6 +202,7 @@ def guiMain(args=None):
|
|||
# load values from commandline args
|
||||
createSpoilerVar.set(int(args.create_spoiler))
|
||||
suppressRomVar.set(int(args.suppress_rom))
|
||||
keysanityVar.set(args.keysanity)
|
||||
if args.nodungeonitems:
|
||||
dungeonItemsVar.set(int(not args.nodungeonitems))
|
||||
beatableOnlyVar.set(int(args.beatableonly))
|
||||
|
|
11
Main.py
11
Main.py
|
@ -28,7 +28,7 @@ def main(args, seed=None):
|
|||
start = time.clock()
|
||||
|
||||
# initialize the world
|
||||
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu)
|
||||
world = World(args.shuffle, args.logic, args.mode, args.difficulty, args.goal, args.algorithm, not args.nodungeonitems, args.beatableonly, args.shuffleganon, args.quickswap, args.fastmenu, args.keysanity)
|
||||
logger = logging.getLogger('')
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
|
@ -91,7 +91,7 @@ def main(args, seed=None):
|
|||
else:
|
||||
sprite = None
|
||||
|
||||
outfilebase = 'ER_%s_%s-%s-%s_%s-%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, world.shuffle, world.algorithm, "-fastmenu" if world.fastmenu else "","-quickswap" if world.quickswap else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed)
|
||||
outfilebase = 'ER_%s_%s-%s-%s_%s-%s%s%s%s%s_%s' % (world.logic, world.difficulty, world.mode, world.goal, world.shuffle, world.algorithm, "-keysanity" if world.keysanity else "", "-fastmenu" if world.fastmenu else "","-quickswap" if world.quickswap else "", "-shuffleganon" if world.shuffle_ganon else "", world.seed)
|
||||
|
||||
if not args.suppress_rom:
|
||||
if args.jsonout:
|
||||
|
@ -202,7 +202,7 @@ def generate_itempool(world):
|
|||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu)
|
||||
ret = World(world.shuffle, world.logic, world.mode, world.difficulty, world.goal, world.algorithm, world.place_dungeon_items, world.check_beatable_only, world.shuffle_ganon, world.quickswap, world.fastmenu, world.keysanity)
|
||||
ret.required_medallions = list(world.required_medallions)
|
||||
ret.swamp_patch_required = world.swamp_patch_required
|
||||
ret.ganon_at_pyramid = world.ganon_at_pyramid
|
||||
|
@ -256,14 +256,15 @@ def create_playthrough(world):
|
|||
raise RuntimeError('Cannot beat game. Something went terribly wrong here!')
|
||||
|
||||
# get locations containing progress items
|
||||
prog_locations = [location for location in world.get_locations() if location.item is not None and location.item.advancement]
|
||||
prog_locations = [location for location in world.get_locations() if location.item is not None and (location.item.advancement or (location.item.key and world.keysanity))]
|
||||
|
||||
collection_spheres = []
|
||||
state = CollectionState(world)
|
||||
sphere_candidates = list(prog_locations)
|
||||
logging.getLogger('').debug('Building up collection spheres.')
|
||||
while sphere_candidates:
|
||||
state.sweep_for_events(key_only=True)
|
||||
if not world.keysanity:
|
||||
state.sweep_for_events(key_only=True)
|
||||
|
||||
sphere = []
|
||||
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
||||
|
|
|
@ -29,7 +29,7 @@ def main(args, seed=None):
|
|||
start = time.clock()
|
||||
|
||||
# initialize the world
|
||||
world = World('vanilla', 'noglitches', 'standard', 'normal', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu)
|
||||
world = World('vanilla', 'noglitches', 'standard', 'normal', 'ganon', 'freshness', False, False, False, args.quickswap, args.fastmenu, False)
|
||||
logger = logging.getLogger('')
|
||||
|
||||
hasher = hashlib.md5()
|
||||
|
|
|
@ -262,6 +262,8 @@ def create_regions(world):
|
|||
create_region('Bottom of Pyramid', None, ['Pyramid Exit']),
|
||||
create_region('Pyramid Ledge', None, ['Pyramid Entrance', 'Pyramid Drop'])
|
||||
]
|
||||
|
||||
world.intialize_regions()
|
||||
|
||||
|
||||
def create_region(name, locations=None, exits=None):
|
||||
|
|
25
Rom.py
25
Rom.py
|
@ -64,7 +64,7 @@ class LocalRom(object):
|
|||
patchedmd5 = hashlib.md5()
|
||||
patchedmd5.update(self.buffer)
|
||||
if not RANDOMIZERBASEHASH == patchedmd5.hexdigest():
|
||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')
|
||||
|
||||
def write_crc(self):
|
||||
# this does not seem to work
|
||||
|
@ -97,9 +97,15 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
|
|||
|
||||
# patch music
|
||||
music_addresses = dungeon_music_addresses[location.name]
|
||||
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
||||
if world.keysanity:
|
||||
music = random.choice([0x11, 0x16])
|
||||
else:
|
||||
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
||||
for music_address in music_addresses:
|
||||
rom.write_byte(music_address, music)
|
||||
|
||||
if world.keysanity:
|
||||
rom.write_byte(0x155C9, random.choice([0x11, 0x16])) #Randomize GT music too in keysanity mode
|
||||
|
||||
# patch entrances
|
||||
for region in world.regions:
|
||||
|
@ -308,9 +314,18 @@ def patch_rom(world, rom, hashtable, beep='normal', sprite=None):
|
|||
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat
|
||||
elif world.goal in ['crystals']:
|
||||
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
|
||||
rom.write_byte(0x18016A, 0x00) # disable free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x00) # disable maps showing crystals on overworld
|
||||
rom.write_byte(0x18003C, 0x00) # disable compasses showing dungeon count
|
||||
rom.write_byte(0x18016A, 0x01 if world.keysanity else 0x00) # free roaming item text boxes
|
||||
rom.write_byte(0x18003B, 0x01 if world.keysanity else 0x00) # maps showing crystals on overworld
|
||||
|
||||
# compasses showing dungeon count
|
||||
if world.clock_mode != 'off':
|
||||
rom.write_byte(0x18003C, 0x00) #Currently must be off if timer is on, because they use same HUD location
|
||||
elif world.keysanity:
|
||||
rom.write_byte(0x18003C, 0x01) #show on pickup
|
||||
else:
|
||||
rom.write_byte(0x18003C, 0x00)
|
||||
|
||||
rom.write_byte(0x180045, 0x01 if world.keysanity else 0x00) # free roaming items in menu
|
||||
digging_game_rng = random.randint(1, 30) # set rng for digging game
|
||||
rom.write_byte(0x180020, digging_game_rng)
|
||||
rom.write_byte(0xEFD95, digging_game_rng)
|
||||
|
|
Loading…
Reference in New Issue