diff --git a/BaseClasses.py b/BaseClasses.py index f0177fbd..5030b161 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -142,6 +142,7 @@ class World(object): set_player_attr('compassshuffle', False) set_player_attr('keyshuffle', 'none') set_player_attr('bigkeyshuffle', False) + set_player_attr('prizeshuffle', 'none') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('flute_mode', False) @@ -473,7 +474,8 @@ class World(object): def push_precollected(self, item): item.world = self - if ((item.smallkey and self.keyshuffle[item.player] != 'none') + if ((item.prize and self.prizeshuffle[item.player] != 'none') + or (item.smallkey and self.keyshuffle[item.player] != 'none') or (item.bigkey and self.bigkeyshuffle[item.player])): item.advancement = True self.precollected_items.append(item) @@ -1607,7 +1609,8 @@ class Region(object): inside_dungeon_item = ((item.smallkey and self.world.keyshuffle[item.player] == 'none') or (item.bigkey and not self.world.bigkeyshuffle[item.player]) or (item.map and not self.world.mapshuffle[item.player]) - or (item.compass and not self.world.compassshuffle[item.player])) + or (item.compass and not self.world.compassshuffle[item.player]) + or (item.prize and self.world.prizeshuffle[item.player] == 'dungeon')) # not all small keys to escape must be in escape # sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)' if inside_dungeon_item: @@ -1840,6 +1843,7 @@ class Dungeon(object): def __init__(self, name, regions, big_key, small_keys, dungeon_items, player, dungeon_id): self.name = name self.regions = regions + self.prize = None self.big_key = big_key self.small_keys = small_keys self.dungeon_items = dungeon_items @@ -1864,10 +1868,13 @@ class Dungeon(object): @property def all_items(self): - return self.dungeon_items + self.keys + return self.dungeon_items + self.keys + ([self.prize] if self.prize else []) def is_dungeon_item(self, item): - return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] + if item.prize: + return item.player == self.player and self.prize is None and self.name not in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower'] + else: + return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items] def count_dungeon_item(self): return len(self.dungeon_items) + 1 if self.big_key_required else 0 + self.key_number @@ -2621,7 +2628,7 @@ class Boss(object): class Location(object): - def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None, + def __init__(self, player, name='', address=None, prize=False, hint_text=None, parent=None, forced_item=None, player_address=None, note=None): self.name = name self.parent_region = parent @@ -2635,7 +2642,7 @@ class Location(object): self.forced_item = None self.item = None self.event = False - self.crystal = crystal + self.prize = prize self.address = address self.player_address = player_address self.spot_type = 'Location' @@ -2643,14 +2650,14 @@ class Location(object): self.recursion_count = 0 self.staleness_count = 0 self.locked = False - self.real = not crystal + self.real = not prize self.always_allow = lambda item, state: False self.access_rule = lambda state: True self.verbose_rule = None self.item_rule = lambda item: True self.player = player self.skip = False - self.type = LocationType.Normal if not crystal else LocationType.Prize + self.type = LocationType.Normal if not prize else LocationType.Prize self.pot = None self.drop = None self.note = note @@ -2738,8 +2745,8 @@ class Item(object): self.player = player @property - def crystal(self): - return self.type == 'Crystal' + def prize(self): + return self.type == 'Prize' @property def smallkey(self): @@ -2767,11 +2774,33 @@ class Item(object): return item_dungeon def is_inside_dungeon_item(self, world): - return ((self.smallkey and world.keyshuffle[self.player] == 'none') + return ((self.prize and world.prizeshuffle[self.player] in ['none', 'dungeon']) + or (self.smallkey and world.keyshuffle[self.player] == 'none') or (self.bigkey and not world.bigkeyshuffle[self.player]) or (self.compass and not world.compassshuffle[self.player]) or (self.map and not world.mapshuffle[self.player])) + def get_map_location(self): + if self.location: + if self.location.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + return self.location + else: + def explore_region(region): + explored_regions.append(region.name) + for ent in region.entrances: + if ent.parent_region is not None: + if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]: + return ent + elif ent.parent_region.name not in explored_regions: + ret = explore_region(ent.parent_region) + if ret: + return ret + return None + explored_regions = list() + return explore_region(self.location.parent_region) + + return None + def __str__(self): return str(self.__unicode__()) @@ -2983,6 +3012,7 @@ class Spoiler(object): 'compassshuffle': self.world.compassshuffle, 'keyshuffle': self.world.keyshuffle, 'bigkeyshuffle': self.world.bigkeyshuffle, + 'prizeshuffle': self.world.prizeshuffle, 'boss_shuffle': self.world.boss_shuffle, 'enemy_shuffle': self.world.enemy_shuffle, 'enemy_health': self.world.enemy_health, @@ -3023,6 +3053,13 @@ class Spoiler(object): self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] + self.prizes = OrderedDict() + for player in range(1, self.world.players + 1): + player_name = '' if self.world.players == 1 else str(' (Player ' + str(player) + ')') + for dungeon in self.world.dungeons: + if dungeon.player == player and dungeon.prize: + self.prizes[dungeon.name + player_name] = dungeon.prize.name + self.bottles = OrderedDict() if self.world.players == 1: self.bottles['Waterfall Bottle'] = self.world.bottle_refills[1][0] @@ -3033,7 +3070,7 @@ class Spoiler(object): self.bottles[f'Pyramid Bottle ({self.world.get_player_names(player)})'] = self.world.bottle_refills[player][1] def include_item(item): - return 'all' in self.settings or ('items' in self.settings and not item.crystal) or ('prizes' in self.settings and item.crystal) + return 'all' in self.settings or ('items' in self.settings and not item.prize) or ('prizes' in self.settings and item.prize) self.locations = OrderedDict() listed_locations = set() @@ -3214,6 +3251,7 @@ class Spoiler(object): outfile.write('Compass Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['compassshuffle'][player])) outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % yn(self.metadata['bigkeyshuffle'][player])) + outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) outfile.write('Key Logic Algorithm:'.ljust(line_width) + '%s\n' % self.metadata['key_logic'][player]) outfile.write('\n') outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player]) @@ -3282,7 +3320,10 @@ class Spoiler(object): outfile.write(str('Crystals Required for GT' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]))) if self.world.crystals_ganon_orig[player] == 'random': outfile.write(str('Crystals Required for Ganon' + player_name + ':').ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]))) - + outfile.write('\n\nPrizes:\n\n') + for dungeon, prize in self.prizes.items(): + outfile.write(str(dungeon + ':').ljust(line_width) + '%s\n' % prize) + if 'all' in self.settings or 'misc' in self.settings: outfile.write('\n\nBottle Refills:\n\n') for fairy, bottle in self.bottles.items(): @@ -3539,7 +3580,7 @@ dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0, "partitioned": 3, 'paired': 4 er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, 'lite': 8, 'lean': 9, "dungeonsfull": 7, "dungeonssimple": 6, "swapped": 10, "district": 11} -# byte 1: LLLW WSS? (logic, mode, sword) +# byte 1: LLLW WSSB (logic, mode, sword, bombbag) logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4, "hybridglitches": 5} world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} @@ -3550,12 +3591,12 @@ goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'cryst diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} -# byte 3: SDMM PIII (shop, decouple doors, mixed, palettes, intensity) +# byte 3: SDMM PIII (shop, decouple doors, mixed travel, palettes, intensity) # keydrop now has it's own byte mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2} # intensity is 3 bits (reserves 4-7 levels) -# new byte 4: TDDD PPPP (tavern shuffle, drop, pottery) +# byte 4: TDDD PPPP (tavern shuffle, drop, pottery) # dropshuffle reserves 2 bits, pottery needs 4) drop_shuffle_mode = {'none': 0, 'keys': 1, 'underworld': 2} pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'cavekeys': 6, 'reduced': 7, @@ -3564,17 +3605,17 @@ pottery_mode = {'none': 0, 'keys': 2, 'lottery': 3, 'dungeon': 4, 'cave': 5, 'ca # byte 5: SCCC CTTX (self-loop doors, crystals gt, ctr2, experimental) counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3} -# byte 6: ?CCC CPAA (crystals ganon, pyramid, access +# byte 6: LCCC CPAA (shuffle links, crystals ganon, pyramid, access access_mode = {"items": 0, "locations": 1, "none": 2} -# byte 7: B?MC DDEE (big, ?, maps, compass, door_type, enemies) +# byte 7: B?MC DDPP (big, ?, maps, compass, door_type, prize shuffle) door_type_mode = {'original': 0, 'big': 1, 'all': 2, 'chaos': 3} -enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} +prizeshuffle_mode = {'none': 0, 'dungeon': 1, 'wild': 3} -# byte 8: HHHD DPBS (enemy_health, enemy_dmg, potshuffle, bomb logic, shuffle links) -# potshuffle decprecated, now unused +# byte 8: HHHD DPEE (enemy_health, enemy_dmg, potshuffle, enemies) e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4} e_dmg = {"default": 0, "shuffled": 1, "random": 2} +enemy_mode = {"none": 0, "shuffled": 1, "chaos": 2, "random": 2, "legacy": 3} # byte 9: RRAA ABBB (restrict boss mode, algorithm, boss shuffle) rb_mode = {"none": 0, "mapcompass": 1, "dungeon": 2} @@ -3593,9 +3634,9 @@ flutespot_mode = {"vanilla": 0, "balanced": 1, "random": 2} # byte 13: FBBB TTSS (flute_mode, bow_mode, take_any, small_key_mode) flute_mode = {'normal': 0, 'active': 1} -keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # reserved 8 modes? +bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # reserved 8 modes? take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} -bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} +keyshuffle_mode = {'none': 0, 'off': 0, 'wild': 1, 'on': 1, 'universal': 2} # additions # byte 14: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) @@ -3617,7 +3658,7 @@ class Settings(object): (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), + | (sword_mode[w.swords[p]] << 1) | (0x1 if w.bombbag[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), @@ -3633,15 +3674,15 @@ class Settings(object): | ((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) + (0x80 if w.shufflelinks[p] else 0) | ((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3) | (0x4 if w.is_pyramid_open(p) else 0) | access_mode[w.accessibility[p]], (0x80 if w.bigkeyshuffle[p] else 0) | (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0) - | (door_type_mode[w.door_type_mode[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]), + | (door_type_mode[w.door_type_mode[p]] << 2) | prizeshuffle_mode[w.prizeshuffle[p]], - (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0) - | (0x2 if w.bombbag[p] else 0) | (1 if w.shufflelinks[p] else 0), + (e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) + | (0x4 if w.potshuffle[p] else 0) | (enemy_mode[w.enemy_shuffle[p]]), (rb_mode[w.restrict_boss_items[p]] << 6) | (algo_mode[w.algorithm] << 3) | (boss_mode[w.boss_shuffle[p]]), @@ -3675,15 +3716,19 @@ class Settings(object): 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.bombbag[p] = True if settings[1] & 0x1 else False + 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[6] & 0x3] # args.retro[p] = True if settings[1] & 0x01 else False args.hints[p] = True if settings[2] & 0x01 else False + args.shopsanity[p] = True if settings[3] & 0x80 else False args.decoupledoors[p] = True if settings[3] & 0x40 else False args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4] @@ -3701,22 +3746,21 @@ class Settings(object): args.crystals_gt[p] = "random" if cgt == 8 else cgt args.experimental[p] = True if settings[5] & 0x1 else False + args.shufflelinks[p] = True if settings[6] & 0x80 else False cgan = (settings[6] & 0x78) >> 3 args.crystals_ganon[p] = "random" if cgan == 8 else cgan args.openpyramid[p] = True if settings[6] & 0x4 else False args.bigkeyshuffle[p] = True if settings[7] & 0x80 else False - # args.keyshuffle[p] = True if settings[7] & 0x40 else False args.mapshuffle[p] = True if settings[7] & 0x20 else False args.compassshuffle[p] = True if settings[7] & 0x10 else False args.door_type_mode[p] = r(door_type_mode)[(settings[7] & 0xc) >> 2] - args.shuffleenemies[p] = r(enemy_mode)[settings[7] & 0x3] + args.prizeshuffle[p] = r(prizeshuffle_mode)[settings[7] & 0x3] args.enemy_health[p] = r(e_health)[(settings[8] & 0xE0) >> 5] args.enemy_damage[p] = r(e_dmg)[(settings[8] & 0x18) >> 3] args.shufflepots[p] = True if settings[8] & 0x4 else False - args.bombbag[p] = True if settings[8] & 0x2 else False - args.shufflelinks[p] = True if settings[8] & 0x1 else False + args.shuffleenemies[p] = r(enemy_mode)[settings[8] & 0x3] if len(settings) > 9: args.restrict_boss_items[p] = r(rb_mode)[(settings[9] & 0xC0) >> 6] diff --git a/CLI.py b/CLI.py index c9603654..c8763902 100644 --- a/CLI.py +++ b/CLI.py @@ -135,7 +135,7 @@ def parse_cli(argv, no_defaults=False): 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'openpyramid', - 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory', + 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_max_difference', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', @@ -223,6 +223,7 @@ def parse_settings(): "compassshuffle": False, "keyshuffle": "none", "bigkeyshuffle": False, + "prizeshuffle": "none", "keysanity": False, "door_shuffle": "vanilla", "intensity": 3, diff --git a/DoorShuffle.py b/DoorShuffle.py index 19483bd9..aac435ef 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -1629,7 +1629,7 @@ 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': + if not location.event and '- Boss' not in location.name and not location.prize and location.name != 'Sanctuary': if location.type == LocationType.Pot and location.pot: hint_text = ('under a block' if location.pot.flags & PotFlags.Block else 'in a pot') location.hint_text = f'{hint_text} {dungeon_hints[name]}' diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 5f34fbf2..39cc374a 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -11,7 +11,7 @@ from typing import List from BaseClasses import DoorType, Direction, CrystalBarrier, RegionType, Polarity, PolSlot, flooded_keys, Sector from BaseClasses import Hook, hook_from_door, Door -from Regions import dungeon_events, flooded_keys_reverse +from Regions import location_events, flooded_keys_reverse from Dungeons import split_region_starts from RoomData import DoorKind @@ -884,19 +884,19 @@ class ExplorationState(object): if key_checks and location not in self.found_locations: if location.forced_item and 'Small Key' in location.item.name: self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: + if location.name not in location_events and not location.prize: self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) if not bk_flag and (not location.forced_item or 'Big Key' in location.item.name): self.bk_found.add(location) - if location.name in dungeon_events and location.name not in self.events: + if location.name in location_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) - if '- Prize' in location.name: + if location.prize: self.prize_received = True def flooded_key_check(self, location): @@ -1096,7 +1096,7 @@ def count_locations_exclude_big_chest(locations, world, player): def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + return loc.name in location_events or loc.prize def reserved_location(loc, world, player): @@ -1557,13 +1557,13 @@ def define_sector_features(sectors): for sector in sectors: for region in sector.regions: for loc in region.locations: - if '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2']: + if loc.prize or loc.name in ['Agahnim 1', 'Agahnim 2']: pass elif loc.forced_item and 'Small Key' in loc.item.name: sector.key_only_locations += 1 elif loc.forced_item and loc.forced_item.bigkey: sector.bk_provided = True - elif loc.name not in dungeon_events and not loc.forced_item: + elif loc.name not in location_events and not loc.forced_item: sector.chest_locations += 1 sector.chest_location_set.add(loc.name) if '- Big Chest' in loc.name or loc.name in ["Hyrule Castle - Zelda's Chest", @@ -1773,6 +1773,8 @@ def build_orig_location_set(dungeon_map): def requested_dungeon_items(world, player): num = 0 + if world.prizeshuffle[player] == 'dungeon': + num += 1 if not world.bigkeyshuffle[player]: num += 1 if not world.compassshuffle[player]: @@ -4055,7 +4057,7 @@ def calc_door_equation(door, sector, look_for_entrance, sewers_flag=None): crystal_barrier = CrystalBarrier.Either # todo: backtracking from double switch with orange on-- for loc in region.locations: - if loc.name in dungeon_events: + if loc.name in location_events: found_events.add(loc.name) for d in event_doors: if loc.name == d.req_event: diff --git a/Dungeons.py b/Dungeons.py index 7ffe518c..53818b7f 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -34,16 +34,16 @@ def create_dungeons(world, player): world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] -dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], - 'Desert Palace - Prize': [0x1559B, 0x1559C, 0x1559D, 0x1559E], - 'Tower of Hera - Prize': [0x155C5, 0x1107A, 0x10B8C], - 'Palace of Darkness - Prize': [0x155B8], - 'Swamp Palace - Prize': [0x155B7], - 'Thieves\' Town - Prize': [0x155C6], - 'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B], - 'Ice Palace - Prize': [0x155BF], - 'Misery Mire - Prize': [0x155B9], - 'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} +dungeon_music_addresses = {'Eastern Palace': [0x1559A], + 'Desert Palace': [0x1559B, 0x1559C, 0x1559D, 0x1559E], + 'Tower of Hera': [0x155C5, 0x1107A, 0x10B8C], + 'Palace of Darkness': [0x155B8], + 'Swamp Palace': [0x155B7], + 'Thieves Town': [0x155C6], + 'Skull Woods': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B], + 'Ice Palace': [0x155BF], + 'Misery Mire': [0x155B9], + 'Turtle Rock': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} hyrule_castle_regions = [ 'Hyrule Castle Lobby', 'Hyrule Castle West Lobby', 'Hyrule Castle East Lobby', 'Hyrule Castle East Hall', @@ -270,7 +270,7 @@ flexible_starts = { class DungeonInfo: - def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, midx): + def __init__(self, free, keys, bk, map, compass, bk_drop, drops, prize, dungeon_idx, extra_map_idx): # todo reduce static maps ideas: prize, bk_name, sm_name, cmp_name, map_name): self.free_items = free self.key_num = keys @@ -281,23 +281,24 @@ class DungeonInfo: self.key_drops = drops self.prize = prize - self.map_index = midx + self.dungeon_index = dungeon_idx + self.extra_map_index = extra_map_idx dungeon_table = { - 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None, 0xc), - 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, 'Eastern Palace - Prize', 0x0), - 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, 'Desert Palace - Prize', 0x2), - 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, 'Tower of Hera - Prize', 0x1), - 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None, 0xb), - 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, 'Palace of Darkness - Prize', 0x3), - 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, 'Swamp Palace - Prize', 0x9), - 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, 'Skull Woods - Prize', 0x4), - 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, "Thieves' Town - Prize", 0x6), - 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, 'Ice Palace - Prize', 0x8), - 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, 'Misery Mire - Prize', 0x7), - 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, 'Turtle Rock - Prize', 0x5), - 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None, 0xa), + 'Hyrule Castle': DungeonInfo(6, 1, False, True, False, True, 3, None, 0x00, 0x00), + 'Eastern Palace': DungeonInfo(3, 0, True, True, True, False, 2, [0x1209D, 0x53F3A, 0x53F3B, 0x180052, 0x180070, 0x186FE2], 0x04, None), + 'Desert Palace': DungeonInfo(2, 1, True, True, True, False, 3, [0x1209E, 0x53F3C, 0x53F3D, 0x180053, 0x180072, 0x186FE3], 0x06, 0x12), + 'Tower of Hera': DungeonInfo(2, 1, True, True, True, False, 0, [0x120A5, 0x53F4A, 0x53F4B, 0x18005A, 0x180071, 0x186FEA], 0x14, None), + 'Agahnims Tower': DungeonInfo(0, 2, False, False, False, False, 2, None, 0x08, None), + 'Palace of Darkness': DungeonInfo(5, 6, True, True, True, False, 0, [0x120A1, 0x53F22, 0x53F43, 0x180056, 0x180073, 0x186FE6], 0x0C, None), + 'Swamp Palace': DungeonInfo(6, 1, True, True, True, False, 5, [0x120A0, 0x53F40, 0x53F41, 0x180055, 0x180079, 0x186FE5], 0x0A, None), + 'Skull Woods': DungeonInfo(2, 3, True, True, True, False, 2, [0x120A3, 0x53F46, 0x53F47, 0x180058, 0x180074, 0x186FE8], 0x10, 0x20), + 'Thieves Town': DungeonInfo(4, 1, True, True, True, False, 2, [0x120A6, 0x53F4C, 0x53F4D, 0x18005B, 0x180076, 0x186FEB], 0x16, None), + 'Ice Palace': DungeonInfo(3, 2, True, True, True, False, 4, [0x120A4, 0x53F48, 0x53F49, 0x180059, 0x180078, 0x186FE9], 0x12, None), + 'Misery Mire': DungeonInfo(2, 3, True, True, True, False, 3, [0x120A2, 0x53F44, 0x53F45, 0x180057, 0x180077, 0x186FE7], 0x0E, None), + 'Turtle Rock': DungeonInfo(5, 4, True, True, True, False, 2, [0x120A7, 0x53F4E, 0x53F4F, 0x18005C, 0x180075, 0x186FEC], 0x18, 0x3E), + 'Ganons Tower': DungeonInfo(20, 4, True, True, True, False, 4, None, 0x1A, None), } diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index c86ca335..943609ce 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -316,7 +316,7 @@ Library: near books Potion Shop: near potions Spike Cave: beyond spikes Mimic Cave: in a cave of mimicry -Chest Game: as a prize +Chest Game: as a game reward Chicken House: near poultry Aginah's Cave: with Aginah Ice Rod Cave: in a frozen cave diff --git a/EntranceShuffle.py b/EntranceShuffle.py index e49aa18e..56ff285f 100644 --- a/EntranceShuffle.py +++ b/EntranceShuffle.py @@ -2675,11 +2675,16 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Desert Palace Entrance (North)': (0x0e1, 0xba0), 'Desert Palace Entrance (East)': (0x191, 0xca0), 'Eastern Palace': (0xf31, 0x620), 'Tower of Hera': (0x8D0, 0x080), 'Hyrule Castle Entrance (South)': (0x820, 0x730), 'Hyrule Castle Entrance (West)': (0x740, 0x5D0), - 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), 'Inverted Pyramid Entrance': (0x6C0, 0x5D0), + 'Hyrule Castle Entrance (East)': (0x8f0, 0x5D0), 'Agahnims Tower': (0x820, 0x5D0), - 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x240, 0x280), - 'Skull Woods Second Section Door (East)': (0x1a0, 0x240), - 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), 'Skull Woods Final Section': (0x082, 0x0b0), + 'Thieves Town': (0x1d0, 0x780), 'Skull Woods First Section Door': (0x2e0, 0x280), + 'Skull Woods Second Section Door (East)': (0x200, 0x240), + 'Skull Woods Second Section Door (West)': (0x0c0, 0x1c0), + 'Skull Woods Final Section': (0x082, 0x0b0), + 'Skull Woods First Section Hole (West)': (0x200, 0x2b0), + 'Skull Woods First Section Hole (East)': (0x340, 0x2e0), + 'Skull Woods First Section Hole (North)': (0x320, 0x1e0), + 'Skull Woods Second Section Hole': (0x0f0, 0x0b0), 'Ice Palace': (0xca0, 0xda0), 'Misery Mire': (0x100, 0xca0), 'Palace of Darkness': (0xf40, 0x620), 'Swamp Palace': (0x759, 0xED0), @@ -2687,16 +2692,23 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Dark Death Mountain Ledge (West)': (0xb80, 0x180), 'Dark Death Mountain Ledge (East)': (0xc80, 0x180), 'Turtle Rock Isolated Ledge Entrance': (0xc00, 0x240), + 'Hyrule Castle Secret Entrance Drop': (0x9D0, 0x680), 'Hyrule Castle Secret Entrance Stairs': (0x8D0, 0x700), + 'Kakariko Well Drop': (0x030, 0x680), 'Kakariko Well Cave': (0x060, 0x680), - 'Bat Cave Cave': (0x540, 0x8f0), + 'Bat Cave Drop': (0x520, 0x8f0), + 'Bat Cave Cave': (0x560, 0x940), 'Elder House (East)': (0x2b0, 0x6a0), 'Elder House (West)': (0x230, 0x6a0), + 'North Fairy Cave Drop': (0xa40, 0x500), 'North Fairy Cave': (0xa80, 0x440), + 'Lost Woods Hideout Drop': (0x290, 0x200), 'Lost Woods Hideout Stump': (0x240, 0x280), - 'Lumberjack Tree Cave': (0x4e0, 0x004), + 'Lumberjack Tree Tree': (0x4e0, 0x140), + 'Lumberjack Tree Cave': (0x560, 0x004), 'Two Brothers House (East)': (0x200, 0x0b60), 'Two Brothers House (West)': (0x180, 0x0b60), + 'Sanctuary Grave': (0x820, 0x4c0), 'Sanctuary': (0x720, 0x4a0), 'Old Man Cave (West)': (0x580, 0x2c0), 'Old Man Cave (East)': (0x620, 0x2c0), @@ -2721,17 +2733,13 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Hookshot Cave': (0xc80, 0x0c0), 'Hookshot Cave Back Entrance': (0xcf0, 0x004), 'Ganons Tower': (0x8D0, 0x080), + 'Pyramid Hole': (0x820, 0x680), + 'Inverted Pyramid Hole': (0x820, 0x680), 'Pyramid Entrance': (0x640, 0x7c0), - 'Skull Woods First Section Hole (West)': None, - 'Skull Woods First Section Hole (East)': None, - 'Skull Woods First Section Hole (North)': None, - 'Skull Woods Second Section Hole': None, - 'Pyramid Hole': None, - 'Inverted Pyramid Hole': None, + 'Inverted Pyramid Entrance': (0x6C0, 0x5D0), 'Waterfall of Wishing': (0xe80, 0x280), 'Dam': (0x759, 0xED0), 'Blinds Hideout': (0x190, 0x6c0), - 'Hyrule Castle Secret Entrance Drop': None, 'Bonk Fairy (Light)': (0x740, 0xa80), 'Lake Hylia Fairy': (0xd40, 0x9f0), 'Light Hype Fairy': (0x940, 0xc80), @@ -2743,11 +2751,8 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Sahasrahlas Hut': (0xcf0, 0x6c0), 'Lake Hylia Shop': (0xbc0, 0xc00), 'Capacity Upgrade': (0xca0, 0xda0), - 'Kakariko Well Drop': None, 'Blacksmiths Hut': (0x4a0, 0x880), - 'Bat Cave Drop': None, 'Sick Kids House': (0x220, 0x880), - 'North Fairy Cave Drop': None, 'Lost Woods Gamble': (0x240, 0x080), 'Fortune Teller (Light)': (0x2c0, 0x4c0), 'Snitch Lady (East)': (0x310, 0x7a0), @@ -2756,8 +2761,6 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Tavern (Front)': (0x270, 0x980), 'Light World Bomb Hut': (0x070, 0x980), 'Kakariko Shop': (0x170, 0x980), - 'Lost Woods Hideout Drop': None, - 'Lumberjack Tree Tree': None, 'Cave 45': (0x440, 0xca0), 'Graveyard Cave': (0x8f0, 0x430), 'Checkerboard Cave': (0x260, 0xc00), 'Mini Moldorm Cave': (0xa40, 0xe80), @@ -2769,7 +2772,6 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Bonk Rock Cave': (0x5f0, 0x460), 'Library': (0x270, 0xaa0), 'Potion Shop': (0xc80, 0x4c0), - 'Sanctuary Grave': None, 'Hookshot Fairy': (0xd00, 0x180), 'Pyramid Fairy': (0x740, 0x740), 'East Dark World Hint': (0xf60, 0xb00), @@ -2798,6 +2800,6 @@ ow_prize_table = {'Links House': (0x8b1, 0xb2d), 'Mimic Cave': (0xc80, 0x180), 'Big Bomb Shop': (0x8b1, 0xb2d), 'Dark Lake Hylia Shop': (0xa40, 0xc40), - 'Lumberjack House': (0x4e0, 0x0d0), + 'Lumberjack House': (0x580, 0x100), 'Lake Hylia Fortune Teller': (0xa40, 0xc40), 'Kakariko Gamble Game': (0x2f0, 0xaf0)} diff --git a/Fill.py b/Fill.py index cc6141e8..5727e7c2 100644 --- a/Fill.py +++ b/Fill.py @@ -14,14 +14,19 @@ from source.item.FillUtil import filter_special_locations, valid_pot_items def get_dungeon_item_pool(world): - return [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] + dungeon_items = [item for dungeon in world.dungeons for item in dungeon.all_items if item.location is None] + for player in range(1, world.players+1): + if world.prizeshuffle[player] != 'none': + dungeon_items.extend(ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)) + + return dungeon_items def promote_dungeon_items(world): world.itempool += get_dungeon_item_pool(world) for item in world.get_items(): - if item.smallkey or item.bigkey: + if item.smallkey or item.bigkey or item.prize: item.advancement = True elif item.map or item.compass: item.priority = True @@ -38,36 +43,62 @@ def fill_dungeons_restrictive(world, shuffled_locations): # with shuffled dungeon items they are distributed as part of the normal item pool for item in world.get_items(): - if ((item.smallkey and world.keyshuffle[item.player] != 'none') + if ((item.prize and world.prizeshuffle[item.player] != 'none') + or (item.smallkey and world.keyshuffle[item.player] != 'none') or (item.bigkey and world.bigkeyshuffle[item.player])): item.advancement = True elif (item.map and world.mapshuffle[item.player]) or (item.compass and world.compassshuffle[item.player]): item.priority = True dungeon_items = [item for item in get_dungeon_item_pool(world) if item.is_inside_dungeon_item(world)] - bigs, smalls, others = [], [], [] + bigs, smalls, prizes, others = [], [], [], [] for i in dungeon_items: - (bigs if i.bigkey else smalls if i.smallkey else others).append(i) + (bigs if i.bigkey else smalls if i.smallkey else prizes if i.prize else others).append(i) unplaced_smalls = list(smalls) for i in world.itempool: if i.smallkey and world.keyshuffle[i.player] != 'none': unplaced_smalls.append(i) - def fill(base_state, items, key_pool): - fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) + def fill(base_state, items, locations, key_pool=None): + fill_restrictive(world, base_state, locations, items, key_pool, True) all_state_base = world.get_all_state() big_state_base = all_state_base.copy() - for x in smalls + others: + for x in smalls + prizes + others: big_state_base.collect(x, True) - fill(big_state_base, bigs, unplaced_smalls) + fill(big_state_base, bigs, shuffled_locations, unplaced_smalls) random.shuffle(shuffled_locations) small_state_base = all_state_base.copy() - for x in others: + for x in prizes + others: small_state_base.collect(x, True) - fill(small_state_base, smalls, unplaced_smalls) + fill(small_state_base, smalls, shuffled_locations, unplaced_smalls) + + prizes_copy = prizes.copy() + for attempt in range(15): + try: + random.shuffle(prizes) + random.shuffle(shuffled_locations) + prize_state_base = all_state_base.copy() + for x in others: + prize_state_base.collect(x, True) + fill(prize_state_base, prizes, shuffled_locations) + except FillError as e: + logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, 14 - attempt) + prizes = prizes_copy.copy() + for dungeon in world.dungeons: + if world.prizeshuffle[dungeon.player] == 'dungeon': + dungeon.prize = None + for prize in prizes: + if prize.location: + prize.location.item = None + prize.location = None + continue + break + else: + raise FillError(f'Unable to place dungeon prizes {", ".join(list(map(lambda d: d.hint_text, prize_locs)))}') + random.shuffle(shuffled_locations) - fill(all_state_base, others, None) + fill(all_state_base, others, shuffled_locations) def fill_restrictive(world, base_state, locations, itempool, key_pool=None, single_player_placement=False, @@ -141,7 +172,7 @@ def fill_restrictive(world, base_state, locations, itempool, key_pool=None, sing def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_placement, perform_access_check, key_pool, world): - if item_to_place.smallkey or item_to_place.bigkey: # a better test to see if a key can go there + if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: # a better test to see if a key can go there location.item = item_to_place location.event = True if item_to_place.smallkey: @@ -155,9 +186,9 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl test_state.sweep_for_events() if location.can_fill(test_state, item_to_place, perform_access_check): if valid_key_placement(item_to_place, location, key_pool, test_state, world): - if item_to_place.crystal or valid_dungeon_placement(item_to_place, location, world): + if item_to_place.prize or valid_dungeon_placement(item_to_place, location, world): return location - if item_to_place.smallkey or item_to_place.bigkey: + if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize: location.item = None location.event = False if item_to_place.smallkey: @@ -181,8 +212,8 @@ def valid_key_placement(item, location, key_pool, collection_state, world): key_logic = world.key_logic[item.player][dungeon.name] unplaced_keys = len([x for x in key_pool if x.name == key_logic.small_key_name and x.player == item.player]) prize_loc = None - if key_logic.prize_location: - prize_loc = world.get_location(key_logic.prize_location, location.player) + if key_logic.prize_location and dungeon.prize and dungeon.prize.location and dungeon.prize.location.player == item.player: + prize_loc = dungeon.prize.location cr_count = world.crystals_needed_for_gt[location.player] wild_keys = world.keyshuffle[item.player] != 'none' if wild_keys: @@ -193,7 +224,7 @@ def valid_key_placement(item, location, key_pool, collection_state, world): self_locking_keys = sum(1 for d, rule in key_logic.door_rules.items() if rule.allow_small and rule.small_location.item and rule.small_location.item.name == key_logic.small_key_name) return key_logic.check_placement(unplaced_keys, wild_keys, reached_keys, self_locking_keys, - location if item.bigkey else None, prize_loc, cr_count) + location if item.bigkey else None, prize_loc, cr_count) else: return not item.is_inside_dungeon_item(world) @@ -228,16 +259,19 @@ def track_outside_keys(item, location, world): def track_dungeon_items(item, location, world): - if location.parent_region.dungeon and not item.crystal: + if location.parent_region.dungeon and (not item.prize or world.prizeshuffle[item.player] == 'dungeon'): layout = world.dungeon_layouts[location.player][location.parent_region.dungeon.name] if is_dungeon_item(item, world) and item.player == location.player: layout.dungeon_items -= 1 else: layout.free_items -= 1 + if item.prize: + location.parent_region.dungeon.prize = item def is_dungeon_item(item, world): - return ((item.smallkey and world.keyshuffle[item.player] == 'none') + return ((item.prize and world.prizeshuffle[item.player] == 'none') + or (item.smallkey and world.keyshuffle[item.player] == 'none') or (item.bigkey and not world.bigkeyshuffle[item.player]) or (item.compass and not world.compassshuffle[item.player]) or (item.map and not world.mapshuffle[item.player])) @@ -250,8 +284,8 @@ def recovery_placement(item_to_place, locations, world, state, base_state, itemp return last_ditch_placement(item_to_place, locations, world, state, base_state, itempool, key_pool, single_player_placement) elif world.algorithm == 'vanilla_fill': - if item_to_place.type == 'Crystal': - possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + if item_to_place.prize: + possible_swaps = [x for x in state.locations_checked if x.item.prize] return try_possible_swaps(possible_swaps, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) else: @@ -310,11 +344,15 @@ def last_ditch_placement(item_to_place, locations, world, state, base_state, ite return 3 return 4 - if item_to_place.type == 'Crystal': - possible_swaps = [x for x in state.locations_checked if x.item.type == 'Crystal'] + # TODO: Verify correctness in using item player in multiworld situations + if item_to_place.prize and world.prizeshuffle[item_to_place.player] == 'none': + possible_swaps = [x for x in state.locations_checked if x.item.prize] else: + ignored_types = ['Event'] + if world.prizeshuffle[item_to_place.player] == 'none': + ignored_types.append('Prize') possible_swaps = [x for x in state.locations_checked - if x.item.type not in ['Event', 'Crystal'] and not x.forced_item and not x.locked] + if x.item.type not in ignored_types and not x.forced_item and not x.locked] swap_locations = sorted(possible_swaps, key=location_preference) return try_possible_swaps(swap_locations, item_to_place, locations, world, base_state, itempool, key_pool, single_player_placement) diff --git a/ItemList.py b/ItemList.py index 107b4b46..08563aba 100644 --- a/ItemList.py +++ b/ItemList.py @@ -5,7 +5,7 @@ import RaceRandom as random from BaseClasses import LocationType, Region, RegionType, Shop, ShopType, Location, CollectionState, PotItem from EntranceShuffle import connect_entrance -from Regions import shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location +from Regions import location_events, shop_to_location_table, retro_shops, shop_table_by_location, valid_pot_location from Fill import FillError, fill_restrictive, get_dungeon_item_pool, track_dungeon_items, track_outside_keys from PotShuffle import vanilla_pots from Tables import bonk_prize_lookup @@ -240,41 +240,9 @@ def generate_itempool(world, player): old_man.forced_item = old_man.item old_man.skip = True - event_items = { - 'Agahnim 1': 'Beat Agahnim 1', - 'Agahnim 2': 'Beat Agahnim 2', - 'Eastern Palace - Boss Kill': 'Beat Boss', - 'Desert Palace - Boss Kill': 'Beat Boss', - 'Tower of Hera - Boss Kill': 'Beat Boss', - 'Palace of Darkness - Boss Kill': 'Beat Boss', - 'Swamp Palace - Boss Kill': 'Beat Boss', - 'Skull Woods - Boss Kill': 'Beat Boss', - 'Thieves\' Town - Boss Kill': 'Beat Boss', - 'Ice Palace - Boss Kill': 'Beat Boss', - 'Misery Mire - Boss Kill': 'Beat Boss', - 'Turtle Rock - Boss Kill': 'Beat Boss', - 'Lost Old Man': 'Escort Old Man', - 'Old Man Drop Off': 'Return Old Man', - 'Floodgate': 'Open Floodgate', - 'Big Bomb': 'Pick Up Big Bomb', - 'Pyramid Crack': 'Detonate Big Bomb', - 'Frog': 'Get Frog', - 'Missing Smith': 'Return Smith', - 'Dark Blacksmith Ruins': 'Pick Up Purple Chest', - 'Middle Aged Man': 'Deliver Purple Chest', - 'Trench 1 Switch': 'Trench 1 Filled', - 'Trench 2 Switch': 'Trench 2 Filled', - 'Swamp Drain': 'Drained Swamp', - 'Turtle Medallion Pad': 'Turtle Opened', - 'Attic Cracked Floor': 'Shining Light', - 'Suspicious Maiden': 'Maiden Rescued', - 'Revealing Light': 'Maiden Unmasked', - 'Ice Block Drop': 'Convenient Block', - 'Skull Star Tile': 'Hidden Pits' - } - - for loc, item in event_items.items(): - set_event_item(loc, item) + for loc, item in location_events.items(): + if item: + set_event_item(loc, item) if world.mode[player] == 'standard': set_event_item('Zelda Pickup', 'Zelda Herself') @@ -375,7 +343,8 @@ def generate_itempool(world, player): world.treasure_hunt_icon[player] = 'Triforce Piece' world.itempool.extend([item for item in get_dungeon_item_pool(world) if item.player == player - and ((item.smallkey and world.keyshuffle[player] != 'none') + and ((item.prize and world.prizeshuffle[player] == 'wild') + or (item.smallkey and world.keyshuffle[player] != 'none') or (item.bigkey and world.bigkeyshuffle[player]) or (item.map and world.mapshuffle[player]) or (item.compass and world.compassshuffle[player]))]) @@ -486,7 +455,7 @@ def generate_itempool(world, player): world.itempool = [beemizer(item) for item in world.itempool] # increase pool if not enough items - ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if world.prizeshuffle[player] != 'none' or not x.prize) pool_size = count_player_dungeon_item_pool(world, player) pool_size += sum(1 for x in world.itempool if x.player == player) @@ -725,9 +694,12 @@ def create_dynamic_bonkdrop_locations(world, player): def fill_prizes(world, attempts=15): + from Items import prize_item_table all_state = world.get_all_state(keys=True) for player in range(1, world.players + 1): - crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player) + if world.prizeshuffle[player] != 'none': + continue + crystals = ItemFactory(list(prize_item_table.keys()), player) crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player), world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player), world.get_location('Misery Mire - Prize', player)] @@ -742,6 +714,8 @@ def fill_prizes(world, attempts=15): random.shuffle(prizepool) random.shuffle(prize_locs) fill_restrictive(world, all_state, prize_locs, prizepool, single_player_placement=True) + for prize_loc in crystal_locations: + prize_loc.parent_region.dungeon.prize = prize_loc.item except FillError as e: logging.getLogger('').info("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt - 1) for location in empty_crystal_locations: @@ -1419,9 +1393,11 @@ def make_customizer_pool(world, player): target_amount = max(amount, len(dungeon.small_keys)) additional_amount = target_amount - len(dungeon.small_keys) dungeon.small_keys.extend([d_item] * additional_amount) - elif item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass'): + elif (item_name.startswith('Big Key') or item_name.startswith('Map') or item_name.startswith('Compass') + or item_name.startswith('Crystal') or item_name.endswith('Pendant')): d_item = ItemFactory(item_name, player) - if ((d_item.bigkey and not world.bigkeyshuffle[player]) + if ((d_item.prize and world.prizeshuffle[player] in ['none', 'dungeon']) + or (d_item.bigkey and not world.bigkeyshuffle[player]) or (d_item.compass and not world.compassshuffle[player]) or (d_item.map and not world.mapshuffle[player])): d_name = d_item.dungeon @@ -1566,11 +1542,11 @@ def set_default_triforce(goal, custom_goal, custom_total): def fill_specific_items(world): if world.customizer: + from Items import prize_item_table placements = world.customizer.get_placements() dungeon_pool = get_dungeon_item_pool(world) prize_pool = [] - prize_set = {'Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', - 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'} + prize_set = set(prize_item_table.keys()) for p in range(1, world.players + 1): prize_pool.extend(prize_set) if placements: @@ -1672,7 +1648,8 @@ def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_ def is_dungeon_item(item, world, player): - return ((item.startswith('Small Key') and world.keyshuffle[player] == 'none') + return (((item.startswith('Crystal') or item.endswith('Pendant')) and world.prizeshuffle[player] in ['none', 'dungeon']) + or (item.startswith('Small Key') and world.keyshuffle[player] == 'none') or (item.startswith('Big Key') and not world.bigkeyshuffle[player]) or (item.startswith('Compass') and not world.compassshuffle[player]) or (item.startswith('Map') and not world.mapshuffle[player])) diff --git a/Items.py b/Items.py index 8a4f9c14..d383b987 100644 --- a/Items.py +++ b/Items.py @@ -60,19 +60,19 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'A better copy\nof your sword\nfor your time', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'), 'Progressive Glove': (True, False, None, 0x61, 150, 'A way to lift\nheavier things', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'), 'Silver Arrows': (True, False, None, 0x58, 100, 'Do you fancy\nsilver tipped\narrows?', 'and the ganonsbane', 'ganon-killing kid', 'ganon doom for sale', 'fungus for pork', 'archer boy shines again', 'the Silver Arrows'), - 'Green Pendant': (True, False, 'Crystal', [0x04, 0x38, 0x62, 0x00, 0x69, 0x37, 0x08], 999, None, None, None, None, None, None, None), - 'Blue Pendant': (True, False, 'Crystal', [0x02, 0x34, 0x60, 0x00, 0x69, 0x39, 0x09], 999, None, None, None, None, None, None, None), - 'Red Pendant': (True, False, 'Crystal', [0x01, 0x32, 0x60, 0x00, 0x69, 0x38, 0x0a], 999, None, None, None, None, None, None, None), + 'Green Pendant': (True, False, 'Prize', 0x37, 250, 'A pendant that\nsome old guy\nwill never see', 'and the green pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Blue Pendant': (True, False, 'Prize', 0x39, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), + 'Red Pendant': (True, False, 'Prize', 0x38, 150, 'A pendant that you\'ll never get', 'and the pendant', 'pendant kid', 'pendant for sale', 'fungus for pendant', 'pendant boy dabs again', 'a Prize'), 'Triforce': (True, False, None, 0x6A, 777, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'), 'Power Star': (True, False, None, 0x6B, 100, 'A small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'), 'Triforce Piece': (True, False, None, 0x6C, 100, 'A small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce piece'), - 'Crystal 1': (True, False, 'Crystal', [0x02, 0x34, 0x64, 0x40, 0x7F, 0x20, 0x01], 999, None, None, None, None, None, None, None), - 'Crystal 2': (True, False, 'Crystal', [0x10, 0x34, 0x64, 0x40, 0x79, 0x20, 0x02], 999, None, None, None, None, None, None, None), - 'Crystal 3': (True, False, 'Crystal', [0x40, 0x34, 0x64, 0x40, 0x6C, 0x20, 0x03], 999, None, None, None, None, None, None, None), - 'Crystal 4': (True, False, 'Crystal', [0x20, 0x34, 0x64, 0x40, 0x6D, 0x20, 0x04], 999, None, None, None, None, None, None, None), - 'Crystal 5': (True, False, 'Crystal', [0x04, 0x32, 0x64, 0x40, 0x6E, 0x20, 0x05], 999, None, None, None, None, None, None, None), - 'Crystal 6': (True, False, 'Crystal', [0x01, 0x32, 0x64, 0x40, 0x6F, 0x20, 0x06], 999, None, None, None, None, None, None, None), - 'Crystal 7': (True, False, 'Crystal', [0x08, 0x34, 0x64, 0x40, 0x7C, 0x20, 0x07], 999, None, None, None, None, None, None, None), + 'Crystal 1': (True, False, 'Prize', 0xB1, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 2': (True, False, 'Prize', 0xB4, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 3': (True, False, 'Prize', 0xB6, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 4': (True, False, 'Prize', 0xB5, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 5': (True, False, 'Prize', 0xB2, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 6': (True, False, 'Prize', 0xB0, 250, 'A crystal that\nunlocks a bomb', 'and the red crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), + 'Crystal 7': (True, False, 'Prize', 0xB3, 200, 'A pretty nice,\nshiny crystal', 'and the crystal', 'crystal kid', 'crystal for sale', 'fungus for crystal', 'crystal boy sparkles again', 'a Prize'), 'Single Arrow': (False, False, None, 0x43, 3, 'A lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'), 'Arrows (10)': (False, False, None, 0x44, 30, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack', 'stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again', 'ten arrows'), 'Arrow Upgrade (+10)': (False, False, None, 0x54, 100, 'Increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'), @@ -203,3 +203,16 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'You have\nchosen the\narche 'Farmable Bombs': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), 'Farmable Rupees': (True, False, 'Event', 999, None, None, None, None, None, None, None, None), } + +prize_item_table = { + 'Green Pendant': [0x04, 0x38, 0x62, 0x00, 0x69, 0x08], + 'Blue Pendant': [0x02, 0x34, 0x60, 0x00, 0x69, 0x09], + 'Red Pendant': [0x01, 0x32, 0x60, 0x00, 0x69, 0x0a], + 'Crystal 1': [0x02, 0x34, 0x64, 0x40, 0x7F, 0x01], + 'Crystal 2': [0x10, 0x34, 0x64, 0x40, 0x79, 0x02], + 'Crystal 3': [0x40, 0x34, 0x64, 0x40, 0x6C, 0x03], + 'Crystal 4': [0x20, 0x34, 0x64, 0x40, 0x6D, 0x04], + 'Crystal 5': [0x04, 0x32, 0x64, 0x40, 0x6E, 0x05], + 'Crystal 6': [0x01, 0x32, 0x64, 0x40, 0x6F, 0x06], + 'Crystal 7': [0x08, 0x34, 0x64, 0x40, 0x7C, 0x07] +} diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index a5ca9cad..a451200e 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -3,7 +3,7 @@ import logging from collections import defaultdict, deque from BaseClasses import DoorType, dungeon_keys, KeyRuleType, RegionType -from Regions import dungeon_events +from Regions import location_events from Dungeons import dungeon_keys, dungeon_bigs, dungeon_table from DungeonGenerator import ExplorationState, get_special_big_key_doors, count_locations_exclude_big_chest, prize_or_event from DungeonGenerator import reserved_location, blind_boss_unavail @@ -1116,7 +1116,7 @@ def location_is_bk_locked(loc, key_logic): # todo: verfiy this code is defunct # def prize_or_event(loc): -# return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] +# return loc.name in location_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] # # # def reserved_location(loc, world, player): @@ -1504,7 +1504,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk) # prize door should not be opened if the boss is reachable - but not reached yet allow_for_prize_lock = (key_layout.prize_can_lock and - not any(x for x in state.found_locations if '- Prize' in x.name)) + not any(x for x in state.found_locations if x.prize)) prize_done = not key_layout.prize_relevant or state.prize_doors_opened or allow_for_prize_lock if smalls_done and bk_done and prize_done: return False @@ -1623,7 +1623,7 @@ def determine_prize_lock(key_layout, world, player): elif len(state.small_doors) > 0: open_a_door(state.small_doors[0].door, state, flat_proposal, world, player) expand_key_state(state, flat_proposal, world, player) - if any(x for x in state.found_locations if '- Prize' in x.name): + if any(x for x in state.found_locations if x.prize): key_layout.prize_can_lock = True @@ -1776,7 +1776,7 @@ def create_key_counter(state, key_layout, world, player): key_counter.key_only_locations[loc] = None elif loc.forced_item and loc.item.name == key_layout.key_logic.bk_name: key_counter.other_locations[loc] = None - elif loc.name not in dungeon_events: + elif loc.name not in location_events: key_counter.free_locations[loc] = None else: key_counter.other_locations[loc] = None @@ -1785,7 +1785,7 @@ def create_key_counter(state, key_layout, world, player): key_counter.big_key_opened = state.big_key_opened if len(state.prize_door_set) > 0 and state.prize_doors_opened: key_counter.prize_doors_opened = True - if any(x for x in key_counter.important_locations if '- Prize' in x.name): + if any(x for x in key_counter.important_locations if x.prize): key_counter.prize_received = True return key_counter @@ -1805,7 +1805,7 @@ def imp_locations_factory(world, player): def important_location(loc, world, player): - return '- Prize' in loc.name or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key()) + return loc.prize or loc.name in imp_locations_factory(world, player) or (loc.forced_big_key()) def create_odd_key_counter(door, parent_counter, key_layout, world, player): @@ -2135,9 +2135,9 @@ def validate_key_placement(key_layout, world, player): 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 if key_layout.prize_relevant: - found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) + found_prize = any(x for x in counter.important_locations if x.prize) if not found_prize and dungeon_table[key_layout.sector.name].prize: - prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) + prize_loc = dungeon_table[key_layout.sector.name].prize.location if key_layout.prize_relevant == 'BigBomb': found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6'] elif key_layout.prize_relevant == 'GT': diff --git a/Main.py b/Main.py index 7c1b9aea..e18c114a 100644 --- a/Main.py +++ b/Main.py @@ -460,6 +460,7 @@ def init_world(args, fish): world.compassshuffle = args.compassshuffle.copy() world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() + world.prizeshuffle = args.prizeshuffle.copy() world.bombbag = args.bombbag.copy() world.flute_mode = args.flute_mode.copy() world.bow_mode = args.bow_mode.copy() @@ -555,6 +556,7 @@ def copy_world(world): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.prizeshuffle = world.prizeshuffle.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() @@ -751,6 +753,7 @@ def copy_world_premature(world, player): ret.compassshuffle = world.compassshuffle.copy() ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() + ret.prizeshuffle = world.prizeshuffle.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() @@ -889,7 +892,7 @@ def copy_dynamic_regions_and_locations(world, ret): for location in world.dynamic_locations: new_reg = ret.get_region(location.parent_region.name, location.parent_region.player) - new_loc = Location(location.player, location.name, location.address, location.crystal, location.hint_text, new_reg) + new_loc = Location(location.player, location.name, location.address, location.prize, location.hint_text, new_reg) new_loc.type = location.type new_reg.locations.append(new_loc) diff --git a/MultiClient.py b/MultiClient.py index 3723599f..d28d401f 100644 --- a/MultiClient.py +++ b/MultiClient.py @@ -340,6 +340,16 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), 'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400), 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Validation Chest': (0x4d, 0x10)} +location_table_boss = {'Eastern Palace - Prize': 0x2000, + 'Desert Palace - Prize': 0x1000, + 'Tower of Hera - Prize': 0x0020, + 'Palace of Darkness - Prize': 0x0200, + 'Thieves Town - Prize': 0x0010, + 'Skull Woods - Prize': 0x0080, + 'Swamp Palace - Prize': 0x0400, + 'Ice Palace - Prize': 0x0040, + 'Misery Mire - Prize': 0x0100, + 'Turtle Rock - Prize': 0x0008} location_table_npc = {'Mushroom': 0x1000, 'King Zora': 0x2, 'Sahasrahla': 0x10, @@ -949,6 +959,14 @@ async def track_locations(ctx : Context, roomid, roomdata): if roomdata & mask != 0: new_check(location) + if not all([location in ctx.locations_checked for location in location_table_boss.keys()]): + boss_data = await snes_read(ctx, SAVEDATA_START + 0x472, 2) + if boss_data is not None: + boss_value = boss_data[0] | (boss_data[1] << 8) + for location, mask in location_table_boss.items(): + if boss_value & mask != 0 and location not in ctx.locations_checked: + new_check(location) + ow_begin = 0x82 ow_end = 0 ow_unchecked = {} diff --git a/Regions.py b/Regions.py index 6101d1ba..4b835c4a 100644 --- a/Regions.py +++ b/Regions.py @@ -1124,8 +1124,8 @@ def _create_region(player, name, type, hint='Hyrule', locations=None, exits=None ko_hint = key_drop_data[location][2] ret.locations.append(Location(player, location, None, False, ko_hint, ret, key_drop_data[location][3])) else: - address, player_address, crystal, hint_text = location_table[location] - ret.locations.append(Location(player, location, address, crystal, hint_text, ret, None, player_address)) + address, player_address, prize, hint_text = location_table[location] + ret.locations.append(Location(player, location, address, prize, hint_text, ret, None, player_address)) return ret def mark_light_dark_world_regions(world, player): @@ -1240,14 +1240,14 @@ def adjust_locations(world, player): # player address? it is in the shop table index += 1 setup_enemy_locations(world, player) + # disable forced prize locations + if world.prizeshuffle[player] != 'none': + for l in [name for name, data in location_table.items() if data[2]]: + location = world.get_location_unsafe(l, player) + if location: + location.prize = False # unreal events: - for l in ['Ganon', 'Agahnim 1', 'Agahnim 2', 'Frog', 'Missing Smith', 'Dark Blacksmith Ruins', 'Middle Aged Man', - 'Floodgate', 'Trench 1 Switch', 'Trench 2 Switch', 'Swamp Drain', 'Turtle Medallion Pad', - 'Attic Cracked Floor', 'Suspicious Maiden', 'Revealing Light', 'Big Bomb', 'Pyramid Crack', - 'Ice Block Drop', 'Lost Old Man', 'Old Man Drop Off', 'Zelda Pickup', 'Zelda Drop Off', 'Skull Star Tile', - 'Eastern Palace - Boss Kill', 'Desert Palace - Boss Kill', 'Tower of Hera - Boss Kill', - 'Palace of Darkness - Boss Kill', 'Swamp Palace - Boss Kill', 'Skull Woods - Boss Kill', - 'Thieves\' Town - Boss Kill', 'Ice Palace - Boss Kill', 'Misery Mire - Boss Kill', 'Turtle Rock - Boss Kill']: + for l in ['Ganon', 'Zelda Pickup', 'Zelda Drop Off'] + list(location_events): location = world.get_location_unsafe(l, player) if location: location.type = LocationType.Logical @@ -1397,18 +1397,42 @@ shop_table_by_location_id = {0x400000+cnt: x for cnt, x in enumerate(flat_normal shop_table_by_location_id = {**shop_table_by_location_id, **{0x400020+cnt: x for cnt, x in enumerate(flat_retro_shops)}} shop_table_by_location = {y: x for x, y in shop_table_by_location_id.items()} -dungeon_events = [ - 'Trench 1 Switch', - 'Trench 2 Switch', - 'Swamp Drain', - 'Attic Cracked Floor', - 'Suspicious Maiden', - 'Revealing Light', - 'Ice Block Drop', - 'Skull Star Tile', - 'Zelda Pickup', - 'Zelda Drop Off' -] + +location_events = { + 'Agahnim 1': 'Beat Agahnim 1', + 'Agahnim 2': 'Beat Agahnim 2', + 'Eastern Palace - Boss Kill': 'Beat Boss', + 'Desert Palace - Boss Kill': 'Beat Boss', + 'Tower of Hera - Boss Kill': 'Beat Boss', + 'Palace of Darkness - Boss Kill': 'Beat Boss', + 'Swamp Palace - Boss Kill': 'Beat Boss', + 'Skull Woods - Boss Kill': 'Beat Boss', + 'Thieves\' Town - Boss Kill': 'Beat Boss', + 'Ice Palace - Boss Kill': 'Beat Boss', + 'Misery Mire - Boss Kill': 'Beat Boss', + 'Turtle Rock - Boss Kill': 'Beat Boss', + 'Lost Old Man': 'Escort Old Man', + 'Old Man Drop Off': 'Return Old Man', + 'Floodgate': 'Open Floodgate', + 'Big Bomb': 'Pick Up Big Bomb', + 'Pyramid Crack': 'Detonate Big Bomb', + 'Frog': 'Get Frog', + 'Missing Smith': 'Return Smith', + 'Dark Blacksmith Ruins': 'Pick Up Purple Chest', + 'Middle Aged Man': 'Deliver Purple Chest', + 'Trench 1 Switch': 'Trench 1 Filled', + 'Trench 2 Switch': 'Trench 2 Filled', + 'Swamp Drain': 'Drained Swamp', + 'Turtle Medallion Pad': 'Turtle Opened', + 'Attic Cracked Floor': 'Shining Light', + 'Suspicious Maiden': 'Maiden Rescued', + 'Revealing Light': 'Maiden Unmasked', + 'Ice Block Drop': 'Convenient Block', + 'Skull Star Tile': 'Hidden Pits', + 'Zelda Pickup': None, + 'Zelda Drop Off': None +} + flooded_keys_reverse = { 'Swamp Palace - Trench 1 Pot Key': 'Trench 1 Switch', @@ -1524,7 +1548,7 @@ location_table = {'Mushroom': (0x180013, 0x186df8, False, 'in the woods'), 'Pyramid Fairy - Right': (0xe983, 0x186c17, False, 'near a fairy'), 'Brewery': (0xe9ec, 0x186c80, False, 'alone in a home'), 'C-Shaped House': (0xe9ef, 0x186c83, False, 'alone in a home'), - 'Chest Game': (0xeda8, 0x186e2b, False, 'as a prize'), + 'Chest Game': (0xeda8, 0x186e2b, False, 'as a game reward'), 'Bumper Cave Ledge': (0x180146, 0x186e15, False, 'on a ledge'), 'Mire Shed - Left': (0xea73, 0x186d07, False, 'near sparks'), 'Mire Shed - Right': (0xea76, 0x186d0a, False, 'near sparks'), @@ -1665,16 +1689,16 @@ location_table = {'Mushroom': (0x180013, 0x186df8, False, 'in the woods'), 'Skull Star Tile': (None, None, False, None), 'Zelda Pickup': (None, None, False, None), 'Zelda Drop Off': (None, None, False, None), - 'Eastern Palace - Prize': ([0x1209D, 0x53E76, 0x53E77, 0x180052, 0x180070, 0xC6FE, 0x186FE2], None, True, 'Eastern Palace'), - 'Desert Palace - Prize': ([0x1209E, 0x53E7A, 0x53E7B, 0x180053, 0x180072, 0xC6FF, 0x186FE3], None, True, 'Desert Palace'), - 'Tower of Hera - Prize': ([0x120A5, 0x53E78, 0x53E79, 0x18005A, 0x180071, 0xC706, 0x186FEA], None, True, 'Tower of Hera'), - 'Palace of Darkness - Prize': ([0x120A1, 0x53E7C, 0x53E7D, 0x180056, 0x180073, 0xC702, 0x186FE6], None, True, 'Palace of Darkness'), - 'Swamp Palace - Prize': ([0x120A0, 0x53E88, 0x53E89, 0x180055, 0x180079, 0xC701, 0x186FE5], None, True, 'Swamp Palace'), - 'Thieves\' Town - Prize': ([0x120A6, 0x53E82, 0x53E83, 0x18005B, 0x180076, 0xC707, 0x186FEB], None, True, 'Thieves Town'), - 'Skull Woods - Prize': ([0x120A3, 0x53E7E, 0x53E7F, 0x180058, 0x180074, 0xC704, 0x186FE8], None, True, 'Skull Woods'), - 'Ice Palace - Prize': ([0x120A4, 0x53E86, 0x53E87, 0x180059, 0x180078, 0xC705, 0x186FE9], None, True, 'Ice Palace'), - 'Misery Mire - Prize': ([0x120A2, 0x53E84, 0x53E85, 0x180057, 0x180077, 0xC703, 0x186FE7], None, True, 'Misery Mire'), - 'Turtle Rock - Prize': ([0x120A7, 0x53E80, 0x53E81, 0x18005C, 0x180075, 0xC708, 0x186FEC], None, True, 'Turtle Rock'), + 'Eastern Palace - Prize': (0xC6FE, 0x186E2C, True, 'with the Armos'), + 'Desert Palace - Prize': (0xC6FF, 0x186E2D, True, 'with Lanmolas'), + 'Tower of Hera - Prize': (0xC706, 0x186E2E, True, 'with Moldorm'), + 'Palace of Darkness - Prize': (0xC702, 0x186E2F, True, 'with Helmasaur King'), + 'Swamp Palace - Prize': (0xC701, 0x186E30, True, 'with Arrghus'), + 'Skull Woods - Prize': (0xC704, 0x186E31, True, 'with Mothula'), + 'Thieves\' Town - Prize': (0xC707, 0x186E32, True, 'with Blind'), + 'Ice Palace - Prize': (0xC705, 0x186E33, True, 'with Kholdstare'), + 'Misery Mire - Prize': (0xC703, 0x186E34, True, 'with Vitreous'), + 'Turtle Rock - Prize': (0xC708, 0x186E35, True, 'with Trinexx'), 'Kakariko Shop - Left': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Middle': (None, None, False, 'for sale in Kakariko'), 'Kakariko Shop - Right': (None, None, False, 'for sale in Kakariko'), diff --git a/Rom.py b/Rom.py index db416798..467c8522 100644 --- a/Rom.py +++ b/Rom.py @@ -29,7 +29,7 @@ from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths from Text import LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts from Text import Lumberjacks_texts, SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc -from Items import ItemFactory +from Items import ItemFactory, prize_item_table from EntranceShuffle import door_addresses, exit_ids, ow_prize_table from OverworldShuffle import default_flute_connections, flute_data from InitialSram import InitialSram @@ -43,7 +43,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'ddc8fd3a8c7265b8748c0df4382f8f0e' +RANDOMIZERBASEHASH = 'd0d67e62722fc7c2192c1f3b1cd8284b' class JsonRom(object): @@ -462,32 +462,31 @@ def patch_rom(world, rom, player, team, is_mystery=False): if location.address is None or (type(location.address) is int and location.address >= 0x400000): continue - if not location.crystal: - if location.item is not None: - # Keys in their native dungeon should use the original item code for keys - itemid = handle_native_dungeon(location, itemid) - if world.remote_items[player]: - itemid = list(location_table.keys()).index(location.name) + 1 - assert itemid < 0x100 - rom.write_byte(location.player_address, 0xFF) - elif location.item.player != player: - if location.player_address is not None: - rom.write_byte(location.player_address, location.item.player) - else: - itemid = 0x5A - rom.write_byte(location.address, itemid) - else: + if location.item is not None: + # Keys in their native dungeon should use the original item code for keys + itemid = handle_native_dungeon(location, itemid) + if world.remote_items[player]: + itemid = list(location_table.keys()).index(location.name) + 1 + assert itemid < 0x100 + rom.write_byte(location.player_address, 0xFF) + elif location.item.player != player: + if location.player_address is not None: + rom.write_byte(location.player_address, location.item.player) + else: + itemid = 0x5A + rom.write_byte(location.address, itemid) + for dungeon in [d for d in world.dungeons if d.player == player]: + if dungeon.prize: # crystals - for address, value in zip(location.address, itemid): + for address, value in zip(dungeon_table[dungeon.name].prize, prize_item_table[dungeon.prize.name]): rom.write_byte(address, value) # patch music - music_addresses = dungeon_music_addresses[location.name] if world.mapshuffle[player]: music = random.choice([0x11, 0x16]) else: - music = 0x11 if 'Pendant' in location.item.name else 0x16 - for music_address in music_addresses: + music = 0x11 if 'Pendant' in dungeon.prize.name else 0x16 + for music_address in dungeon_music_addresses[dungeon.name]: rom.write_byte(music_address, music) if world.mapshuffle[player]: @@ -724,7 +723,8 @@ def patch_rom(world, rom, player, team, is_mystery=False): or (l.type == LocationType.Drop and not l.forced_item) or (l.type == LocationType.Normal and not l.forced_item) or (l.type == LocationType.Bonk and not l.forced_item) - or (l.type == LocationType.Shop and world.shopsanity[player]))] + or (l.type == LocationType.Shop and world.shopsanity[player]) + or (l.type == LocationType.Prize and not l.prize))] valid_loc_by_dungeon = valid_dungeon_locations(valid_locations) # fix hc big key problems (map and compass too) @@ -1077,7 +1077,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): 0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel ]) - # item GFX changes + # item property changes + if world.prizeshuffle[player] != 'none': + # allows prizes to contribute to collection rate + write_int16s(rom, snes_to_pc(0x22C06E), [0x01]*3) # pendants + write_int16s(rom, snes_to_pc(0x22C160), [0x81]*7) # crystals if world.bombbag[player]: rom.write_byte(snes_to_pc(0x22C8A4), 0xE0) # use new bomb bag gfx rom.write_byte(snes_to_pc(0x22BD52), 0x02) @@ -1207,6 +1211,8 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp rom.write_byte(0x180174, 0x01 if world.fix_fake_world[player] else 0x00) rom.write_byte(0x18017E, 0x01) # Fairy fountains only trade in bottles + # fix for allowing prize itemgets being able to update the HUD (buying prizes in shops and updating rupees) + rom.write_bytes(snes_to_pc(0x0799FB), [0x80, 0x11, 0xEA]) # Starting equipment if world.pseudoboots[player]: @@ -1262,94 +1268,152 @@ def patch_rom(world, rom, player, team, is_mystery=False): # Bitfield - enable text box to show with free roaming items # - # ---o bmcs + # --po bmcs + # p - enabled for non-prize crystals # o - enabled for outside dungeon items # b - enabled for inside big keys # m - enabled for inside maps # c - enabled for inside compasses # s - enabled for inside small keys - rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) + rom.write_byte(0x18016A, 0x10 | ((0x20 if world.prizeshuffle[player] == 'wild' else 0x00) + | (0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.compassshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] else 0x00) | (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld # compasses showing dungeon count - compass_mode = 0x00 + compass_mode = 0x80 if world.compassshuffle[player] else 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': - compass_mode = 0x00 # Currently must be off if timer is on, because they use same HUD location - rom.write_byte(0x18003C, 0x00) + pass elif world.dungeon_counters[player] == 'on': - compass_mode = 0x02 # always on + compass_mode |= 0x02 # always on elif (world.compassshuffle[player] or world.doorShuffle[player] != 'vanilla' or world.dropshuffle[player] != 'none' or world.dungeon_counters[player] == 'pickup' or world.pottery[player] not in ['none', 'cave']): - compass_mode = 0x01 # show on pickup - if (world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default') or world.owMixed[player]: - compass_mode |= 0x80 # turn on locating dungeons - if world.overworld_map[player] == 'compass': - compass_mode |= 0x20 # show icon if compass is collected, 0x00 for maps - if world.compassshuffle[player]: - compass_mode |= 0x40 # dungeon item that enables icon is wild - elif world.overworld_map[player] == 'map' or world.owMixed[player]: - if world.mapshuffle[player]: - compass_mode |= 0x40 # dungeon item that enables icon is wild - - if world.shuffle[player] != 'vanilla' and world.overworld_map[player] != 'default': - x_map_position_generic = [0x3c0, 0xbc0, 0x7c0, 0x1c0, 0x5c0, 0xdc0, 0x7c0, 0xbc0, 0x9c0, 0x3c0] - for idx, x_map in enumerate(x_map_position_generic): - rom.write_bytes(0x53df6+idx*2, int16_as_bytes(x_map)) - rom.write_bytes(0x53e16+idx*2, int16_as_bytes(0xFC0)) - elif world.overworld_map[player] == 'default': - # disable HC/AT/GT icons - if not world.owMixed[player]: - rom.write_bytes(0x53E8A, int16_as_bytes(0xFF00)) # GT - rom.write_bytes(0x53E8C, int16_as_bytes(0xFF00)) # AT - rom.write_bytes(0x53E8E, int16_as_bytes(0xFF00)) # HC - for dungeon, portal_list in dungeon_portals.items(): - ow_map_index = dungeon_table[dungeon].map_index - if world.shuffle[player] != 'vanilla' and world.overworld_map[player] == 'default': - vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', - 'Desert Palace': 'Desert Palace Entrance (North)', - 'Skull Woods': 'Skull Woods Final Section' - } - entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon - entrance = world.get_entrance(entrance_name, player) + compass_mode |= 0x01 # show on pickup + if world.overworld_map[player] == 'map': + compass_mode |= 0x10 # show icon if map is collected + elif world.overworld_map[player] == 'compass': + compass_mode |= 0x20 # show icon if compass is collected + if world.prizeshuffle[player] == 'wild': + compass_mode |= 0x40 # show icon if boss is defeated, hide if collected + rom.write_byte(0x18003C, compass_mode) + + def get_entrance_coords(ent): + if type(ent) is Location: + from OverworldShuffle import OWTileRegions + if ent.name == 'Hobo': + coords = (0xb80, 0xb80) + elif ent.name == 'Master Sword Pedestal': + coords = (0x06d, 0x070) else: - if world.shuffle[player] != 'vanilla': - if len(portal_list) == 1: - portal_idx = 0 - else: - if world.doorShuffle[player] not in ['vanilla', 'basic']: - # the random choice excludes sanctuary - portal_idx = next((i for i, elem in enumerate(portal_list) - if world.get_portal(elem, player).chosen), random.choice([1, 2, 3])) - else: - portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3}[dungeon] + owid = OWTileRegions[ent.parent_region.name] + if owid == 0x81: + coords = (0x220, 0xf40) else: - if dungeon in ['Hyrule Castle', 'Agahnims Tower', 'Ganons Tower']: - portal_idx = -1 - elif len(portal_list) == 1: - portal_idx = 0 - else: - portal_idx = {'Desert Palace': 1, 'Skull Woods': 3, 'Turtle Rock': 0}[dungeon] - portal = world.get_portal(portal_list[0 if portal_idx == -1 else portal_idx], player) - entrance = portal.find_portal_entrance() - world_indicator = 0x01 if entrance.parent_region.type == RegionType.DarkWorld else 0x00 - coords = ow_prize_table[entrance.name] - # figure out compass entrances and what world (light/dark) - if world.overworld_map[player] != 'default' or world.owMixed[player]: - rom.write_bytes(0x53E36+ow_map_index*2, int16_as_bytes(coords[0])) - rom.write_bytes(0x53E56+ow_map_index*2, int16_as_bytes(coords[1])) - rom.write_byte(0x53EA6+ow_map_index, world_indicator) + owid = owid % 0x40 + coords = (0x200 * (owid % 0x08) + 0x100, 0x200 * int(owid / 0x08) + 0x100) + if owid in [0x00, 0x03, 0x05, 0x18, 0x1b, 0x1e, 0x30, 0x35]: + coords = (coords[0] + 0x100, coords[1] + 0x100) + else: + coords = ow_prize_table[ent.name] + coords = ((0x8000 if ent.parent_region.type == RegionType.DarkWorld else 0x0000) | coords[0], coords[1]) + return coords + if world.overworld_map[player] == 'default': + # disable HC/AT/GT icons + if not world.owMixed[player]: + write_int16(rom, snes_to_pc(0x0ABF52)+0x1A, 0x0000) # GT + write_int16(rom, snes_to_pc(0x0ABF52)+0x08, 0x0000) # AT + write_int16(rom, snes_to_pc(0x0ABF52)+0x00, 0x0000) # HC + for dungeon, portal_list in dungeon_portals.items(): + dungeon_index = dungeon_table[dungeon].dungeon_index + extra_map_index = dungeon_table[dungeon].extra_map_index + map_index = max(0, dungeon_index - 2) + + # write out dislocated coords + if map_index >= 0x02 and map_index < 0x18 and (world.overworld_map[player] != 'default' or world.prizeshuffle[player] == 'wild'): + owid_map = [0x1E, 0x30, 0xFF, 0x7B, 0x5E, 0x70, 0x40, 0x75, 0x03, 0x58, 0x47] + x_map_position_generic = [0x03c0, 0x0740, 0xff00, 0x03c0, 0x01c0, 0x0bc0, 0x05c0, 0x09c0, 0x0ac0, 0x07c0, 0x0dc0] + y_map_position_generic = [0xff00, 0xff00, 0xff00, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0x0fc0, 0xff00, 0x0fc0, 0x0fc0] + world_indicator = 0x0000 + idx = int((map_index-2)/2) + owid = owid_map[idx] + if owid != 0xFF: + if (owid < 0x40) == (world.is_tile_swapped(owid, player)): + world_indicator = 0x8000 + write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+4, world_indicator | x_map_position_generic[idx]) + write_int16(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+6, y_map_position_generic[idx]) + + # write out icon coord data + if world.prizeshuffle[player] == 'wild' and dungeon_table[dungeon].prize: + dungeon_obj = world.get_dungeon(dungeon, player) + entrance = dungeon_obj.prize.get_map_location() + coords = get_entrance_coords(entrance) + # prize location + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) + if world.shuffle[player] == 'vanilla' or world.overworld_map[player] == 'default': + # TODO: I think this is logically the same as some of the vanilla stuff below + vanilla_entrances = { 'Hyrule Castle': 'Hyrule Castle Entrance (South)', + 'Desert Palace': 'Desert Palace Entrance (North)', + 'Skull Woods': 'Skull Woods Final Section' } + entrance_name = vanilla_entrances[dungeon] if dungeon in vanilla_entrances else dungeon + if world.is_atgt_swapped(player): + swap_entrances = { 'Agahnims Tower': 'Ganons Tower', + 'Ganons Tower': 'Agahnims Tower' } + entrance_name = swap_entrances[dungeon] if dungeon in swap_entrances else entrance_name + entrance = world.get_entrance(entrance_name, player) + else: + if len(portal_list) == 1: + portal_idx = 0 + else: + vanilla_portal_idx = {'Hyrule Castle': 0, 'Desert Palace': 0, 'Skull Woods': 3, 'Turtle Rock': 3} + extra_map_offsets = {'Hyrule Castle': 0, 'Desert Palace': 0x12, 'Skull Woods': 0x20, 'Turtle Rock': 0x3E} + portal_idx = vanilla_portal_idx[dungeon] + offset = 0 + if (world.overworld_map[player] != 'default' and world.shuffle[player] not in ['vanilla', 'dungeonssimple'] + and (dungeon != 'Skull Woods' or world.shuffle[player] in ['district', 'insanity'])): + for i, elem in enumerate(portal_list): + if i != portal_idx and (elem != 'Sanctuary' or world.shuffle[player] in ['district', 'insanity']): + portal = world.get_portal(elem, player) + entrance = portal.find_portal_entrance() + coords = get_entrance_coords(entrance) + write_int16s(rom, snes_to_pc(0x8ABECA+extra_map_offsets[dungeon]+offset), coords) + offset += 4 + portal = world.get_portal(portal_list[portal_idx], player) + entrance = portal.find_portal_entrance() + coords = get_entrance_coords(entrance) + + # figure out compass entrances and what world (light/dark) + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6), coords) + if world.prizeshuffle[player] != 'wild' and dungeon_table[dungeon].prize: + # prize location + write_int16s(rom, snes_to_pc(0x0ABE2E)+(map_index*6)+8, coords) + + # Map reveals + reveal_bytes = { + "Hyrule Castle": 0xC000, + "Eastern Palace": 0x2000, + "Desert Palace": 0x1000, + "Tower of Hera": 0x0020, + "Agahnims Tower": 0x800, + "Palace of Darkness": 0x0200, + "Thieves Town": 0x0010, + "Skull Woods": 0x0080, + "Swamp Palace": 0x0400, + "Ice Palace": 0x0040, + "Misery Mire": 0x0100, + "Turtle Rock": 0x0008, + "Ganons Tower": 0x0004 + } # in crossed doors - flip the compass exists flags if world.doorShuffle[player] not in ['vanilla', 'basic']: + compass_exists = 0x0000 for dungeon, portal_list in dungeon_portals.items(): - ow_map_index = dungeon_table[dungeon].map_index - exists_flag = any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass') - rom.write_byte(0x53E96+ow_map_index, 0x1 if exists_flag else 0x0) - - rom.write_byte(0x18003C, compass_mode) + dungeon_index = dungeon_table[dungeon].dungeon_index + if any(x for x in world.get_dungeon(dungeon, player).dungeon_items if x.type == 'Compass'): + compass_exists |= reveal_bytes.get(dungeon, 0x0000) + write_int16(rom, snes_to_pc(0x0ABF6E), compass_exists) # Bitfield - enable free items to show up in menu # @@ -1360,34 +1424,17 @@ def patch_rom(world, rom, player, team, is_mystery=False): # b - Big Key # a - Small Key # - enable_menu_map_check = world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla' + enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] == 'wild' rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] == 'wild' else 0x00) | (0x02 if world.bigkeyshuffle[player] else 0x00) | (0x04 if world.mapshuffle[player] or enable_menu_map_check else 0x00) | (0x08 if world.compassshuffle[player] else 0x00) # free roaming items in menu | (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon - # Map reveals - reveal_bytes = { - "Eastern Palace": 0x2000, - "Desert Palace": 0x1000, - "Tower of Hera": 0x0020, - "Palace of Darkness": 0x0200, - "Thieves Town": 0x0010, - "Skull Woods": 0x0080, - "Swamp Palace": 0x0400, - "Ice Palace": 0x0040, - "Misery Mire'": 0x0100, - "Turtle Rock": 0x0008, - } - def get_reveal_bytes(itemName): - locations = world.find_items(itemName, player) - if len(locations) < 1: - return 0x0000 - location = locations[0] - if location.parent_region and location.parent_region.dungeon: - return reveal_bytes.get(location.parent_region.dungeon.name, 0x0000) + for dungeon in world.dungeons: + if dungeon.player == player and dungeon.prize and dungeon.prize.name == itemName: + return reveal_bytes.get(dungeon.name, 0x0000) return 0x0000 write_int16(rom, 0x18017A, get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal @@ -2160,6 +2207,8 @@ def write_strings(rom, world, player, team): items_to_hint.extend(SmallKeys) if world.bigkeyshuffle[player]: items_to_hint.extend(BigKeys) + if world.prizeshuffle[player] == 'wild': + items_to_hint.extend(Prizes) random.shuffle(items_to_hint) hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district', 'swapped'] else 8 hint_count += 2 if world.doorShuffle[player] not in ['vanilla', 'basic'] else 0 @@ -2273,10 +2322,14 @@ def write_strings(rom, world, player, team): crystal5 = world.find_items('Crystal 5', player)[0] crystal6 = world.find_items('Crystal 6', player)[0] - tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5.hint_text, crystal6.hint_text) - greenpendant = world.find_items('Green Pendant', player)[0] - tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text + if world.prizeshuffle[player] == 'none': + (crystal5, crystal6, greenpendant) = tuple([x.parent_region.dungeon.name for x in [crystal5, crystal6, greenpendant]]) + tt['bomb_shop'] = 'Big Bomb?\nMy supply is blocked until you clear %s and %s.' % (crystal5, crystal6) + tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant + else: + tt['bomb_shop'] = 'Big Bomb?\nThe crystals can be found %s and %s.' % (crystal5.hint_text, crystal6.hint_text) + tt['sahasrahla_bring_courage'] = 'My family heirloom can be found %s' % greenpendant.hint_text tt['sign_ganons_tower'] = ('You need %d crystal to enter.' if world.crystals_needed_for_gt[player] == 1 else 'You need %d crystals to enter.') % world.crystals_needed_for_gt[player] @@ -2966,6 +3019,18 @@ BigKeys = ['Big Key (Eastern Palace)', 'Big Key (Ganons Tower)' ] +Prizes = ['Green Pendant', + 'Blue Pendant', + 'Red Pendant', + 'Crystal 1', + 'Crystal 2', + 'Crystal 3', + 'Crystal 4', + 'Crystal 5', + 'Crystal 6', + 'Crystal 7' + ] + hash_alphabet = [ "Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", diff --git a/data/base2current.bps b/data/base2current.bps index f62d66ee..789a90f9 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index fc102f1b..eef2cccc 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -387,6 +387,13 @@ "action": "store_true", "type": "bool" }, + "prizeshuffle": { + "choices": [ + "none", + "dungeon", + "wild" + ] + }, "keysanity": { "action": "store_true", "type": "bool", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index e98c9228..80f479e8 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -350,6 +350,7 @@ "compassshuffle": [ "Compasses are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "keyshuffle": [ "Small Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], "bigkeyshuffle": [ "Big Keys are no longer restricted to their dungeons, but can be anywhere. (default: %(default)s)" ], + "prizeshuffle": [ "Prizes are no longer restricted to the bosses, but can be anywhere. (default: %(default)s)" ], "shopsanity": ["Shop contents are shuffle in the main item pool and other items can take their place. (default: %(default)s)"], "dropshuffle": [ "Controls how enemies drop items (default: %(default)s)", "None: Enemies drops prize packs or keys as normal", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index 27bafd29..a9541a0b 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -59,6 +59,10 @@ "randomizer.dungeon.smallkeyshuffle.wild": "Randomized", "randomizer.dungeon.smallkeyshuffle.universal": "Universal", "randomizer.dungeon.bigkeyshuffle": "Big Keys", + "randomizer.dungeon.prizeshuffle": "Prizes", + "randomizer.dungeon.prizeshuffle.none": "On Boss", + "randomizer.dungeon.prizeshuffle.dungeon": "In Dungeon", + "randomizer.dungeon.prizeshuffle.wild": "Randomized", "randomizer.dungeon.decoupledoors": "Decouple Doors", "randomizer.dungeon.door_self_loops": "Allow Self-Looping Spiral Stairs", diff --git a/resources/app/gui/randomize/dungeon/widgets.json b/resources/app/gui/randomize/dungeon/widgets.json index b02dfa6f..268fee35 100644 --- a/resources/app/gui/randomize/dungeon/widgets.json +++ b/resources/app/gui/randomize/dungeon/widgets.json @@ -1,5 +1,17 @@ { "widgets": { + "prizeshuffle": { + "type": "selectbox", + "default": "default", + "options": [ + "none", + "dungeon", + "wild" + ], + "config": { + "padx": [20,0] + } + }, "key_logic_algorithm": { "type": "selectbox", "default": "default", diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index c67d760f..33fd8f87 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -138,6 +138,7 @@ class CustomSettings(object): args.experimental[p] = get_setting(settings['experimental'], args.experimental[p]) args.collection_rate[p] = get_setting(settings['collection_rate'], args.collection_rate[p]) args.openpyramid[p] = get_setting(settings['openpyramid'], args.openpyramid[p]) + args.prizeshuffle[p] = get_setting(settings['prizeshuffle'], args.prizeshuffle[p]) args.bigkeyshuffle[p] = get_setting(settings['bigkeyshuffle'], args.bigkeyshuffle[p]) args.keyshuffle[p] = get_setting(settings['keyshuffle'], args.keyshuffle[p]) args.mapshuffle[p] = get_setting(settings['mapshuffle'], args.mapshuffle[p]) @@ -320,6 +321,7 @@ class CustomSettings(object): settings_dict[p]['experimental'] = world.experimental[p] settings_dict[p]['collection_rate'] = world.collection_rate[p] settings_dict[p]['openpyramid'] = world.open_pyramid[p] + settings_dict[p]['prizeshuffle'] = world.prizeshuffle[p] settings_dict[p]['bigkeyshuffle'] = world.bigkeyshuffle[p] settings_dict[p]['keyshuffle'] = world.keyshuffle[p] settings_dict[p]['mapshuffle'] = world.mapshuffle[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 41d54fd8..4d170746 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -111,6 +111,7 @@ SETTINGSTOPROCESS = { "mapshuffle": "mapshuffle", "compassshuffle": "compassshuffle", "bigkeyshuffle": "bigkeyshuffle", + "prizeshuffle": "prizeshuffle", "key_logic_algorithm": "key_logic_algorithm", "dungeondoorshuffle": "door_shuffle", "dungeonintensity": "intensity", diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 84139157..7f0efec5 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -5,7 +5,7 @@ import time from BaseClasses import CrystalBarrier, DoorType, Hook, RegionType, Sector from BaseClasses import hook_from_door, flooded_keys -from Regions import dungeon_events, flooded_keys_reverse +from Regions import location_events, flooded_keys_reverse def pre_validate(builder, entrance_region_names, split_dungeon, world, player): @@ -556,7 +556,7 @@ class ExplorationState(object): if key_checks and location not in self.found_locations: if location.forced_item and 'Small Key' in location.item.name: self.key_locations += 1 - if location.name not in dungeon_events and '- Prize' not in location.name and location.name not in ['Agahnim 1', 'Agahnim 2']: + if location.name not in location_events and not ('- Prize' in location.name and location.prize) and location.name not in ['Agahnim 1', 'Agahnim 2']: self.ttl_locations += 1 if location not in self.found_locations: self.found_locations.append(location) @@ -568,13 +568,13 @@ class ExplorationState(object): else: self.bk_found.add(location) self.re_add_big_key_doors() - if location.name in dungeon_events and location.name not in self.events: + if location.name in location_events and location.name not in self.events: if self.flooded_key_check(location): self.perform_event(location.name, key_region) if location.name in flooded_keys_reverse.keys() and self.location_found( flooded_keys_reverse[location.name]): self.perform_event(flooded_keys_reverse[location.name], key_region) - if '- Prize' in location.name: + if location.prize: self.prize_received = True def flooded_key_check(self, location): @@ -837,7 +837,7 @@ def count_locations_exclude_big_chest(locations, world, player): def prize_or_event(loc): - return loc.name in dungeon_events or '- Prize' in loc.name or loc.name in ['Agahnim 1', 'Agahnim 2'] + return loc.name in location_events or loc.prize def reserved_location(loc, world, player): diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py index 1e8a8092..f49e7e0b 100644 --- a/source/item/FillUtil.py +++ b/source/item/FillUtil.py @@ -59,6 +59,8 @@ def create_item_pool_config(world): if info.prize: d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon config.reserved_locations[player].add(f'{d_name} - Boss') + if world.prizeshuffle[player] != 'none': + config.reserved_locations[player].add(f'{d_name} - Prize') for dungeon in world.dungeons: if world.restrict_boss_items[dungeon.player] != 'none': for item in dungeon.all_items: @@ -118,6 +120,9 @@ def create_item_pool_config(world): LocationGroup('bkgt').locs(mode_grouping['GT Trash'])] for loc_name in mode_grouping['Big Chests'] + mode_grouping['Heart Containers']: config.reserved_locations[player].add(loc_name) + if world.prizeshuffle[player] != 'none': + for loc_name in mode_grouping['Prizes']: + config.reserved_locations[player].add(loc_name) elif world.algorithm == 'major_only': config.location_groups = [ LocationGroup('MajorItems'), @@ -127,6 +132,8 @@ def create_item_pool_config(world): init_set = mode_grouping['Overworld Major'] + mode_grouping['Big Chests'] + mode_grouping['Heart Containers'] for player in range(1, world.players + 1): groups = LocationGroup('Major').locs(init_set) + if world.prizeshuffle[player] != 'none': + groups.locations.extend(mode_grouping['Prizes']) if world.bigkeyshuffle[player]: groups.locations.extend(mode_grouping['Big Keys']) if world.dropshuffle[player] != 'none': @@ -251,21 +258,33 @@ def location_prefilled(location, world, player): def previously_reserved(location, world, player): - if '- Boss' in location.name: + if '- Boss' in location.name or '- Prize' in location.name: if world.restrict_boss_items[player] == 'mapcompass' and (not world.compassshuffle[player] or not world.mapshuffle[player]): return True if world.restrict_boss_items[player] == 'dungeon' and (not world.compassshuffle[player] or not world.mapshuffle[player] or not world.bigkeyshuffle[player] - or world.keyshuffle[player] == 'none'): + or world.keyshuffle[player] == 'none' + or world.prizeshuffle[player] in ['none', 'dungeon']): return True return False def massage_item_pool(world): player_pool = defaultdict(list) + dungeon_pool = defaultdict(list) + for dungeon in world.dungeons: + if dungeon_table[dungeon.name].prize: + dungeon_pool[dungeon.player].append(dungeon) + for player in dungeon_pool: + dungeons = list(dungeon_pool[player]) + random.shuffle(dungeons) + dungeon_pool[player] = dungeons for item in world.itempool: + if item.prize: + dungeon = dungeon_pool[item.player].pop() + dungeon.prize = item player_pool[item.player].append(item) for dungeon in world.dungeons: for item in dungeon.all_items: @@ -273,7 +292,7 @@ def massage_item_pool(world): player_pool[item.player].append(item) player_locations = defaultdict(list) for player in player_pool: - player_locations[player] = [x for x in world.get_unfilled_locations(player) if '- Prize' not in x.name] + player_locations[player] = [x for x in world.get_unfilled_locations(player) if not x.prize] discrepancy = len(player_pool[player]) - len(player_locations[player]) if discrepancy: trash_options = [x for x in player_pool[player] if x.name in trash_items] @@ -342,6 +361,8 @@ def determine_major_items(world, player): major_item_set = set(major_items) if world.progressive == 'off': pass # now what? + if world.prizeshuffle[player] not in ['none', 'dungeon']: + major_item_set.update({x for x, y in item_table.items() if y[2] == 'Prize'}) if world.bigkeyshuffle[player]: major_item_set.update({x for x, y in item_table.items() if y[2] == 'BigKey'}) if world.keyshuffle[player] != 'none': @@ -687,6 +708,11 @@ mode_grouping = { 'Graveyard Cave', 'Kakariko Well - Top', "Blind's Hideout - Top", 'Bonk Rock Cave', "Aginah's Cave", 'Chest Game', 'Digging Game', 'Mire Shed - Left', 'Mimic Cave' ], + 'Prizes': [ + 'Eastern Palace - Prize', 'Desert Palace - Prize', 'Tower of Hera - Prize', + 'Palace of Darkness - Prize', 'Swamp Palace - Prize', 'Skull Woods - Prize', + "Thieves' Town - Prize", 'Ice Palace - Prize', 'Misery Mire - Prize', 'Turtle Rock - Prize', + ], 'Big Keys': [ 'Eastern Palace - Big Key Chest', 'Ganons Tower - Big Key Chest', 'Desert Palace - Big Key Chest', 'Tower of Hera - Big Key Chest', 'Palace of Darkness - Big Key Chest', diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index c37ea0a3..77f6dd2e 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -106,6 +106,7 @@ def roll_settings(weights): if 'u' in dungeon_items: ret.keyshuffle = 'universal' ret.bigkeyshuffle = get_choice_bool('bigkey_shuffle') if 'bigkey_shuffle' in weights else 'b' in dungeon_items + ret.prizeshuffle = get_choice('prize_shuffle') ret.accessibility = get_choice('accessibility') ret.restrict_boss_items = get_choice('restrict_boss_items')