diff --git a/BaseClasses.py b/BaseClasses.py index fd0d0ed9..11bccc67 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,3 +1,4 @@ +import base64 import copy import json import logging @@ -134,7 +135,7 @@ class World(object): set_player_attr('keydropshuffle', False) set_player_attr('mixed_travel', 'prevent') set_player_attr('standardize_palettes', 'standardize') - set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}); + set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -1961,6 +1962,8 @@ class Spoiler(object): 'teams': self.world.teams, 'experimental': self.world.experimental, 'keydropshuffle': self.world.keydropshuffle, + 'shopsanity': self.world.shopsanity, + 'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)} } def to_json(self): @@ -1997,6 +2000,7 @@ class Spoiler(object): if len(self.hashes) > 0: for team in range(self.world.teams): outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team])) + outfile.write(f'Settings Code: {self.metadata["code"][player]}\n') outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Mode: %s\n' % self.metadata['mode'][player]) outfile.write('Retro: %s\n' % ('Yes' if self.metadata['retro'][player] else 'No')) @@ -2024,6 +2028,7 @@ class Spoiler(object): outfile.write('Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No')) outfile.write('Experimental: %s\n' % ('Yes' if self.metadata['experimental'][player] else 'No')) outfile.write('Key Drops shuffled: %s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No')) + outfile.write(f"Shopsanity: {'Yes' if self.metadata['shopsanity'][player] else 'No'}\n") if self.doors: outfile.write('\n\nDoors:\n\n') outfile.write('\n'.join( @@ -2153,4 +2158,110 @@ class Pot(object): self.y = y self.item = item self.room = room - self.flags = flags \ No newline at end of file + self.flags = flags + + +# byte 0: DDDE EEEE (DR, ER) +dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0} +er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8, + "full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6} + +# byte 1: LLLW WSSR (logic, mode, sword, retro) +logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owg": 3, "majorglitches": 4} +world_mode = {"open": 0, "standard": 1, "inverted": 2} +sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} + +# byte 2: GGGD DFFH (goal, diff, item_func, hints) +goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "crystals": 4} +diff_mode = {"normal": 0, "hard": 1, "expert": 2} +func_mode = {"normal": 0, "hard": 1, "expert": 2} + +# byte 3: SKMM PIII (shop, keydrop, mixed, palettes, intensity) +mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} +# intensity is 3 bits (reserves 4-7 levels) + +# byte 4: CCCC CTTX (crystals gt, ctr2, experimental) +counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} + +# byte 5: CCCC CPAA (crystals ganon, pyramid, access +access_mode = {"items": 0, "locations": 1, "none": 2} + +# byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies) +boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3} +enemy_mode = {"none": 0, "shuffled": 1, "random": 2} + +# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?) +e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} +e_dmg = {"default": 0, "shuffled": 1, "random": 2} + +class Settings(object): + + @staticmethod + def make_code(w, p): + code = bytes([ + (dr_mode[w.doorShuffle[p]] << 5) | er_mode[w.shuffle[p]], + + (logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3) + | (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0), + + (goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3) + | (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0), + + (0x80 if w.shopsanity[p] else 0) | (0x40 if w.keydropshuffle[p] else 0) + | (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0) + | (0 if w.intensity[p] == "random" else w.intensity[p]), + + ((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3) + | (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0), + + ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) + | (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]], + + (0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0) + | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) + | (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), + + (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)]) + return base64.b64encode(code, "+-".encode()).decode() + + @staticmethod + def adjust_args_from_code(code, player, args): + settings, p = base64.b64decode(code.encode(), "+-".encode()), player + + def r(d): + return {y: x for x, y in d.items()} + + args.shuffle[p] = r(er_mode)[settings[0] & 0x1F] + args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xE0) >> 5] + args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5] + args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3] + args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1] + args.difficulty[p] = r(diff_mode)[(settings[2] & 0x18) >> 3] + args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1] + args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5] + args.accessibility[p] = r(access_mode)[settings[5] & 0x3] + args.retro[p] = True if settings[1] & 0x01 else False + args.hints[p] = True if settings[2] & 0x01 else False + args.retro[p] = True if settings[1] & 0x01 else False + args.shopsanity[p] = True if settings[3] & 0x80 else False + args.keydropshuffle[p] = True if settings[3] & 0x40 else False + args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] + args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize" + intensity = settings[3] & 0x7 + args.intensity[p] = "random" if intensity == 0 else intensity + args.dungeon_counters[p] = r(counter_mode)[(settings[4] & 0x6) >> 1] + cgt = (settings[4] & 0xf8) >> 3 + args.crystals_gt[p] = "random" if cgt == 8 else cgt + args.experimental[p] = True if settings[4] & 0x1 else False + cgan = (settings[5] & 0xf8) >> 3 + args.crystals_ganon[p] = "random" if cgan == 8 else cgan + args.openpyramid[p] = True if settings[5] & 0x4 else False + args.bigkeyshuffle[p] = True if settings[6] & 0x80 else False + args.keyshuffle[p] = True if settings[6] & 0x40 else False + args.mapshuffle[p] = True if settings[6] & 0x20 else False + args.compassshuffle[p] = True if settings[6] & 0x10 else False + args.shufflebosses[p] = r(boss_mode)[(settings[6] & 0xc) >> 2] + args.shuffleenemies[p] = r(enemy_mode)[settings[6] & 0x3] + args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5] + args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3] + args.shufflepots[p] = True if settings[7] & 0x4 else False diff --git a/CLI.py b/CLI.py index bcfc2181..441375ed 100644 --- a/CLI.py +++ b/CLI.py @@ -93,7 +93,7 @@ def parse_cli(argv, no_defaults=False): 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', - 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes']: + 'remote_items', 'shopsanity', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -147,6 +147,7 @@ def parse_settings(): "mixed_travel": "prevent", "standardize_palettes": "standardize", + "code": "", "multi": 1, "names": "", diff --git a/DoorShuffle.py b/DoorShuffle.py index 5c05698f..c2085c50 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -89,7 +89,7 @@ def link_doors_main(world, player): world.get_portal('Desert East', player).destination = True if world.mode[player] == 'inverted': world.get_portal('Desert West', player).destination = True - if world.mode[player] == 'open': + else: world.get_portal('Skull 2 West', player).destination = True world.get_portal('Turtle Rock Lazy Eyes', player).destination = True world.get_portal('Turtle Rock Eye Bridge', player).destination = True diff --git a/ItemList.py b/ItemList.py index 5c42aa75..b1d44981 100644 --- a/ItemList.py +++ b/ItemList.py @@ -619,7 +619,7 @@ def todays_discounts(world, player): for location, shop_name, slot in chosen_locations: shop = world.get_region(shop_name, player).shop orig = location.item.price - shop.inventory[slot]['price'] = randomize_price(orig // 10) + shop.inventory[slot]['price'] = randomize_price(orig // 5) repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart', diff --git a/Main.py b/Main.py index 25b81153..0c1caa6f 100644 --- a/Main.py +++ b/Main.py @@ -8,7 +8,7 @@ import random import time import zlib -from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance +from BaseClasses import World, CollectionState, Item, Region, Location, Shop, Entrance, Settings from Items import ItemFactory from KeyDoorShuffle import validate_key_placement from PotShuffle import shuffle_pots @@ -28,6 +28,7 @@ from Utils import output_path, parse_player_names __version__ = '0.3.1.1-u' + class EnemizerError(RuntimeError): pass @@ -40,6 +41,10 @@ def main(args, seed=None, fish=None): start = time.perf_counter() # initialize the world + if args.code: + for player, code in args.code.items(): + if code: + Settings.adjust_args_from_code(code, player, args) world = World(args.multi, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords, args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm, args.accessibility, args.shuffleganon, args.retro, args.custom, args.customitemarray, args.hints) @@ -218,7 +223,7 @@ def main(args, seed=None, fish=None): customize_shops(world, player) balance_money_progression(world) - outfilebase = 'DR_%s' % (args.outputname if args.outputname else world.seed) + outfilebase = f'DR_{args.outputname if args.outputname else world.seed}' rom_names = [] jsonout = {} @@ -264,59 +269,12 @@ def main(args, seed=None, fish=None): if args.jsonout: jsonout[f'patch_t{team}_p{player}'] = rom.patches else: - mcsb_name = '' - if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): - mcsb_name = '-keysanity' - elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]].count(True) == 1: - mcsb_name = '-mapshuffle' if world.mapshuffle[player] else '-compassshuffle' if world.compassshuffle[player] else '-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle' - elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player], world.bigkeyshuffle[player]]): - mcsb_name = '-%s%s%s%sshuffle' % ( - 'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '', - 'S' if world.keyshuffle[player] else '', 'B' if world.bigkeyshuffle[player] else '') - outfilepname = f'_T{team+1}' if world.teams > 1 else '' if world.players > 1: outfilepname += f'_P{player}' if world.players > 1 or world.teams > 1: outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" if world.player_names[player][team] != 'Player %d' % player else '' - outfilestuffs = { - "logic": world.logic[player], # 0 - "difficulty": world.difficulty[player], # 1 - "difficulty_adjustments": world.difficulty_adjustments[player], # 2 - "mode": world.mode[player], # 3 - "goal": world.goal[player], # 4 - "timer": str(world.timer), # 5 - "shuffle": world.shuffle[player], # 6 - "doorShuffle": world.doorShuffle[player], # 7 - "algorithm": world.algorithm, # 8 - "mscb": mcsb_name, # 9 - "retro": world.retro[player], # A - "progressive": world.progressive, # B - "hints": 'True' if world.hints[player] else 'False' # C - } - # 0 1 2 3 4 5 6 7 8 9 A B C - outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s_%s-%s%s%s%s%s' % ( - # 0 1 2 3 4 5 6 7 8 9 A B C - # _noglitches_normal-normal-open-ganon-ohko_simple_basic-balanced-keysanity-retro-prog_swords-nohints - # _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity-retro - # _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity -prog_swords - # _noglitches_normal-normal-open-ganon _simple_basic-balanced-keysanity -nohints - outfilestuffs["logic"], # 0 - - outfilestuffs["difficulty"], # 1 - outfilestuffs["difficulty_adjustments"], # 2 - outfilestuffs["mode"], # 3 - outfilestuffs["goal"], # 4 - "" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5 - - outfilestuffs["shuffle"], # 6 - outfilestuffs["doorShuffle"], # 7 - outfilestuffs["algorithm"], # 8 - outfilestuffs["mscb"], # 9 - - "-retro" if outfilestuffs["retro"] == "True" else "", # A - "-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # B - "-nohints" if not outfilestuffs["hints"] == "True" else "")) if not args.outputname else '' # C + outfilesuffix = f'_{Settings.make_code(world, player)}' if not args.outputname else '' rom.write_to_file(output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')) if world.players > 1: diff --git a/Mystery.py b/Mystery.py index e0a19467..77ab630a 100644 --- a/Mystery.py +++ b/Mystery.py @@ -156,6 +156,11 @@ def roll_settings(weights): if ret.dungeon_counters == 'default': ret.dungeon_counters = 'pickup' if ret.door_shuffle != 'vanilla' or ret.compassshuffle == 'on' else 'off' + ret.shopsanity = get_choice('shopsanity') == 'on' + ret.keydropshuffle = get_choice('keydropshuffle') == 'on' + ret.mixed_travel = get_choice('mixed_travel') if 'mixed_travel' in weights else 'prevent' + ret.standardize_palettes = get_choice('standardize_palettes') if 'standardize_palettes' in weights else 'standardize' + goal = get_choice('goals') ret.goal = {'ganon': 'ganon', 'fast_ganon': 'crystals', @@ -173,6 +178,7 @@ def roll_settings(weights): if ret.mode == 'retro': ret.mode = 'open' ret.retro = True + ret.retro = get_choice('retro') == 'on' # this overrides world_state if used ret.hints = get_choice('hints') == 'on' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5bac936d..33db309a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,7 @@ ## Shopsanity --shopsanity added. This adds 32 shop locations (9 more in retro) to the general and location pool. + Multi-world supported. Thanks go to Pepper and CaitSith2 for figuring out several items related to this major feature. Shop locations: @@ -32,35 +33,24 @@ Item Pool changes: To accommodate the new locations, new items are added to the * 1 - +5 Bomb Capacity * 1 - +5 Arrow Capacity -1. Initially, 1 of each type of potion refill is shuffled to the shops. (the Capacity Fairy is excluded from this). -This ensures that potions can be bought somewhere. +1. Initially, 1 of each type of potion refill is shuffled to the shops. (the Capacity Fairy is excluded from this, see step 4). This ensures that potions can be bought somewhere. 2. The rest of the shop pool is shuffled with the rest of the item pool. -3. At this time, only Ten Bombs, Ten Arrows, Capacity upgrades, and Small Hearts can appear outside of shops. Any other -shop items are replaced with rupees of various amounts. This is because of two reasons: First, potion refills and the -Bee are indistinguishable from Bottles with that item in them. Receiving those items without a bottle or empty bottle is -essentially a nothing item but looks like a bottle. Second, the non-progressive Shields interact fine with Progressive -Shields but are usually also a nothing item most of the time. -4. The Capacity Fairy cannot sell Potion Refills because the graphics are incompatible. 300 Rupees will replace any -potion refill that ends up there. -5. For capacity upgrades, if any shop sells capacity upgrades, then it will sell all seven. Otherwise, if plain bombs or -arrows are sold somewhere, then the other six capacity upgrades will be purchasable first at those locations and then -replaced by the underlying ammo. If no suitable spot is found, then no more capacity upgrades will not be available for -that seed. (There is always one somewhere in the pool.) -6. Any shop item that is originally sold by shops can be bought indefinitely but only the first purchase counts toward -total checks on the credits screen & item counter. All other items can be bought only once. +3. At this time, only Ten Bombs, Ten Arrows, Capacity upgrades, and Small Hearts can appear outside of shops. Any other shop items are replaced with rupees of various amounts. This is because of two reasons: First, potion refills and the Bee are indistinguishable from Bottles with that item in them. Receiving those items without a bottle or empty bottle is essentially a nothing item but looks like a bottle. Second, the non-progressive Shields interact fine with Progressive Shields but are usually also a nothing item most of the time. +4. The Capacity Fairy cannot sell Potion Refills because the graphics are incompatible. 300 Rupees will replace any potion refill that ends up there. +5. For capacity upgrades, if any shop sells capacity upgrades, then it will sell all seven of that type. Otherwise, if plain bombs or arrows are sold somewhere, then the other six capacity upgrades will be purchasable first at those locations and then replaced by the underlying ammo. If no suitable spot is found, then no more capacity upgrades will not be available for that seed. (There is always one somewhere in the pool.) +6. Any shop item that is originally sold by shops can be bought indefinitely but only the first purchase counts toward total checks on the credits screen & item counter. All other items can be bought only once. All items in the item pool may appear in shops. #### Pricing Guide -All prices range approx. from half the base price to the base price in increments of 5, the exact price is chosen -randomly within the range. +All prices range approx. from half the base price to the base price in increments of 5, the exact price is chosen randomly within the range. | Category | Items | Base Price | Typical Range | | ----------------- | ------- |:----------:|:-------------:| | Major Progression | Hammer, Hookshot, Mirror, Ocarina, Boots, Somaria, Fire Rod, Ice Rod | 250 | 125-250 | | Moon Pearl | 200 | 100-200 -| | Lamp, Progressive Bow, Glove, Sword | 150 | 75-150 +| | Lamp, Progressive Bows, Gloves, & Swords | 150 | 75-150 | Medallions | Bombos, Ether, Quake | 100 | 50-100 | Safety/Fetch | Cape, Mushroom, Shovel, Powder, Bug Net, Byrna, Progressive Armor & Shields, Half Magic | 50 | 25-50 | Bottles | Empty Bottle or Bee Bottle | 50 | 25-50 @@ -88,40 +78,27 @@ In addition, 4-7 items are steeply discounted at random. #### Rupee Balancing Algorithm -To prevent needed to grind for rupees to buy things in Sphere 1 and later, a money balancing algorithm has been -developed to counteract the need for rupees. Basic logic: it assumes you buy nothing until you are blocked by a shop, -a check that requires money or blocked by Kiki. Then you must have enough to make all purchases. If not, any free rupees -encountered may be swapped with higher denominations that have not been encountered. Ammo may also be swapped, -if necessary. +To prevent needed to grind for rupees to buy things in Sphere 1 and later, a money balancing algorithm has been developed to counteract the need for rupees. Basic logic: it assumes you buy nothing until you are blocked by a shop, a check that requires money, or blocked by Kiki. Then you must have enough to make all purchases. If not, any free rupees encountered may be swapped with higher denominations that have not been encountered. Ammo may also be swapped, if necessary. -(Checks that require money: Bottle Merchant, King Zora, Digging Game, Chest Game, Blacksmith, anything blocked by Kiki -e.g. all of Palace of Darkness when ER is vanilla) +(Checks that require money: Bottle Merchant, King Zora, Digging Game, Chest Game, Blacksmith, anything blocked by Kiki e.g. all of Palace of Darkness when ER is vanilla) -The Houlihan room is not in logic but the five dungeon rooms that provide rupees are. Pots with rupees, the arrow game, -and all other gambling games are not counted for determining income. +The Houlihan room is not in logic but the five dungeon rooms that provide rupees are. Pots with rupees, the arrow game, and all other gambling games are not counted for determining income. -Currently this is applied to seeds without shopsanity on so early money is slightly more likely if progression is on -a check that requires money. +Currently this is applied to seeds without shopsanity on so early money is slightly more likely if progression is on a check that requires money even if Shopsanity is not turned on. #### Retro and Shopsanity 9 new locations are added. -The four "Take Any" caves are converted into "Take Both" caves. Those and the old man cave are included in the shuffle. -The sword is returned to the pool, and the 4 heart containers and 4 blue potion refills are also added to the general -item pool. All items found in the retro caves are free to take once. Potion refills will disappear after use. +The four "Take Any" caves are converted into "Take Both" caves. Those and the old man cave are included in the shuffle. The sword is returned to the pool, and the 4 heart containers and 4 blue potion refills are also added to the general item pool. All items found in the retro caves are free to take once. Potion refills will disappear after use. Arrow Capacity upgrades are now replaced by Rupees wherever it might end up. -The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item -(represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured -that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys -retain their original base price, but may be discounted. +The Ten Arrows and 5 randomly selected Small Hearts or Blue Shields are replaced by the quiver item (represented by the Single Arrow in game.) 5 Red Potion refills are replaced by the Universal small key. It is assured that at least one shop sells Universal Small Keys. The quiver may thus not be found in shops. The quiver and small keys retain their original base price, but may be discounted. ##### Misc Notes -The location counter now - +The location counter both experimental and the credits now reflects the total and current checks made. Original retro for example is 221 while shopsanity by itself is 248. Keydropshuffle+sanity+retro can reach up to 290. ## In-Room Staircases/Ladders @@ -130,6 +107,21 @@ any N/S connections. (those that appear to go up one floor are North connection Big thanks to Catobat for doing all the hard work. +## Enemizer change + +The attic/maiden sequence is now active and required when Blind is the boss of Theives' Town even when bosses are shuffled. + +## Settings code + +File names have changed with a settings code instead of listing major settings chosen. Mystery games omit this for obvious reasons. Also found in the spoiler. + +Added to CLI only now. + +## Mystery fixes + +The Mystery.py file has been updated for those who like to use that for generating games. Supports keydropshuffle, +shopsanity, and other settings that have been included. + # Bug Fixes * 0.3.1.1-u @@ -138,6 +130,7 @@ Big thanks to Catobat for doing all the hard work. * Fix for Ice Jelly room when going backward and enemizer is on * Fix for inverted - don't start as a bunny in Dark Sanctuary * Fix for non-ER Inverted with Lobby shuffle. Aga Tower's exit works properly now. + * Minor fix to Standard generation * 0.3.0.1-u * Problem with lobbies on re-rolls corrected * Potential playthrough problem addressed diff --git a/Rom.py b/Rom.py index 290d9d01..0f998312 100644 --- a/Rom.py +++ b/Rom.py @@ -310,6 +310,13 @@ def patch_enemizer(world, player, rom, baserom_path, enemizercli, random_sprite_ for patch in json.load(f): rom.write_bytes(patch["address"], patch["patchData"]) + if world.get_dungeon("Thieves Town", player).boss.enemizer_name == "Blind": + rom.write_byte(0x04DE81, 0x6) # maiden spawn + # restore blind spawn code + rom.write_bytes(0xEA081, [0xaf, 0xcc, 0xf3, 0x7e, 0xc9, 0x6, 0xf0, 0x24, + 0xad, 0x3, 0x4, 0x29, 0x20, 0xf0, 0x1d]) + rom.write_byte(0x200101, 0) # Do not close boss room door on entry. + if random_sprite_on_hit: _populate_sprite_table() sprites = list(_sprite_table.values()) @@ -1537,8 +1544,7 @@ def write_custom_shops(rom, world, player): if item is None: break if world.shopsanity[player] or shop.type == ShopType.TakeAny: - slot = 0 if shop.type == ShopType.TakeAny else index - rom.write_byte(0x186560 + shop.sram_address + slot, 1) + rom.write_byte(0x186560 + shop.sram_address + index, 1) item_id = ItemFactory(item['item'], player).code price = int16_as_bytes(item['price']) replace = ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF diff --git a/mystery_example.yml b/mystery_example.yml new file mode 100644 index 00000000..cb075090 --- /dev/null +++ b/mystery_example.yml @@ -0,0 +1,125 @@ + description: Example door rando weights + door_shuffle: + vanilla: 2 + basic: 1 + crossed: 1 + intensity: + 1: 1 + 2: 1 + 3: 4 + keydropshuffle: + on: 1 + off: 1 + shopsanity: + on: 1 + off: 1 + pot_shuffle: + on: 1 + off: 3 + entrance_shuffle: + none: 15 + dungeonssimple: 3 + dungeonsfull: 2 + simple: 2 + restricted: 2 + full: 2 + crossed: 3 + insanity: 1 + world_state: + standard: 1 + open: 1 + inverted: 1 + retro: 0 + retro: + on: 1 + off: 4 + goals: + ganon: 2 + fast_ganon: 2 + dungeons: 1 + pedestal: 2 + triforce-hunt: 0 + dungeon_items: + standard: 10 + mc: 3 + mcs: 2 + full: 5 + experimental: + on: 1 + off: 0 + glitches_required: + none: 1 + no_logic: 0 + accessibility: + items: 1 + locations: 0 + none: 0 + tower_open: + "0": 1 + "1": 2 + "2": 3 + "3": 4 + "4": 4 + "5": 3 + "6": 2 + "7": 1 + random: 1 + ganon_open: + "0": 1 + "1": 2 + "2": 3 + "3": 4 + "4": 4 + "5": 3 + "6": 2 + "7": 1 + random: 1 + boss_shuffle: + none: 3 + simple: 1 + full: 1 + random: 1 + enemy_shuffle: + none: 3 + shuffled: 1 + random: 0 + hints: + on: 1 + off: 0 + weapons: + randomized: 2 + assured: 1 + vanilla: 1 + swordless: 0 + item_pool: + normal: 1 + hard: 0 + expert: 0 + item_functionality: + normal: 1 + hard: 0 + expert: 0 + enemy_damage: + default: 3 + shuffled: 1 + random: 0 + enemy_health: + default: 6 + easy: 1 + hard: 1 + expert: 0 + rom: + quickswap: + on: 1 + off: 0 + heartcolor: + red: 1 + blue: 1 + green: 1 + yellow: 1 + heartbeep: + double: 0 + normal: 0 + half: 0 + quarter: 1 + off: 0 diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 44bf9fc7..7859a9e4 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -359,5 +359,6 @@ "never" ] }, - "outputname": {} + "outputname": {}, + "code": {} }