diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index f9ac15fa..907ddf87 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,6 @@ weights/ /output/ /enemizer/ -base2current.json - resources/user/* !resources/user/.gitkeep diff --git a/BaseClasses.py b/BaseClasses.py index 99af09b9..65f7e789 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -21,6 +21,7 @@ from Tables import ( spiral_offset_table, ) from Utils import int16_as_bytes +from Versions import DRVersion, GKVersion, ORVersion class World(object): @@ -79,6 +80,8 @@ class World(object): self.dark_rooms = {} self.damage_challenge = {} self.shuffle_damage_table = {} + self.bosses_ganon = {} + self.bosshunt_include_agas = {} self.ganon_item = {} self.ganon_item_orig = {} self.custom = custom @@ -154,6 +157,8 @@ class World(object): set_player_attr('keyshuffle', 'none') set_player_attr('bigkeyshuffle', 'none') set_player_attr('prizeshuffle', 'none') + set_player_attr('showloot', 'never') + set_player_attr('showmap', 'map') set_player_attr('restrict_boss_items', 'none') set_player_attr('bombbag', False) set_player_attr('flute_mode', 'normal') @@ -171,6 +176,8 @@ class World(object): set_player_attr('escape_assist', []) set_player_attr('crystals_needed_for_ganon', 7) set_player_attr('crystals_needed_for_gt', 7) + set_player_attr('bosses_ganon', 8) + set_player_attr('bosshunt_include_agas', False) set_player_attr('ganon_item', 'silver') set_player_attr('crystals_ganon_orig', {}) set_player_attr('crystals_gt_orig', {}) @@ -359,7 +366,7 @@ class World(object): else: if self.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'district']: return False - elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt']: + elif self.goal[player] in ['crystals', 'trinity', 'ganonhunt', 'bosshunt']: return True else: return False @@ -3077,12 +3084,9 @@ class Spoiler(object): self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)]) def parse_meta(self): - from Main import __version__ as ERVersion - from OverworldShuffle import __version__ as ORVersion - self.startinventory = list(map(str, self.world.precollected_items)) - self.metadata = {'version': ERVersion, - 'versions': {'Door':ERVersion, 'Overworld':ORVersion}, + self.metadata = {'version': GKVersion, + 'versions': {'Door': DRVersion, 'Overworld': ORVersion}, 'logic': self.world.logic, 'mode': self.world.mode, 'bombbag': self.world.bombbag, @@ -3121,6 +3125,8 @@ class Spoiler(object): 'beemizer': self.world.beemizer, 'gt_crystals': self.world.crystals_needed_for_gt, 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'ganon_bosses': self.world.bosses_ganon, + 'bosshunt_include_agas': self.world.bosshunt_include_agas, 'ganon_item': self.world.ganon_item, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, @@ -3131,6 +3137,8 @@ class Spoiler(object): 'keyshuffle': self.world.keyshuffle, 'bigkeyshuffle': self.world.bigkeyshuffle, 'prizeshuffle': self.world.prizeshuffle, + 'showloot': self.world.showloot, + 'showmap': self.world.showmap, 'boss_shuffle': self.world.boss_shuffle, 'enemy_shuffle': self.world.enemy_shuffle, 'enemy_health': self.world.enemy_health, @@ -3295,7 +3303,7 @@ class Spoiler(object): self.parse_meta() with open(filename, 'w') as outfile: line_width = 35 - outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed)) + outfile.write('ALttP GwaaKiwi Randomizer - Seed: %s\n\n' % (self.world.seed)) for k,v in self.metadata["versions"].items(): outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) for player in range(1, self.world.players + 1): @@ -3310,7 +3318,7 @@ class Spoiler(object): self.parse_meta() with open(filename, 'w') as outfile: line_width = 35 - outfile.write('ALttP Overworld Randomizer - Seed: %s\n\n' % (self.world.seed)) + outfile.write('ALttP GwaaKiwi Randomizer - Seed: %s\n\n' % (self.world.seed)) for k,v in self.metadata["versions"].items(): outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v) if self.metadata['user_notes']: @@ -3341,6 +3349,10 @@ class Spoiler(object): if custom['ganongoal'] and 'requirements' in custom['ganongoal']: outfile.write('Ganon Requirement:'.ljust(line_width) + 'custom\n') outfile.write(' %s\n' % custom['ganongoal']['goaltext']) + elif self.metadata['goal'][player] == 'bosshunt': + outfile.write('Ganon Requirement:'.ljust(line_width) + '%s bosses%s\n' % + (str(self.world.bosses_ganon[player]), + ' (including both Agahnims)' if self.world.bosshunt_include_agas[player] else '')) else: outfile.write('Ganon Requirement:'.ljust(line_width) + '%s crystals\n' % str(self.world.crystals_ganon_orig[player])) if custom['pedgoal'] and 'requirements' in custom['pedgoal']: @@ -3395,6 +3407,8 @@ class Spoiler(object): outfile.write('Small Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['keyshuffle'][player]) outfile.write('Big Key Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['bigkeyshuffle'][player]) outfile.write('Prize Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['prizeshuffle'][player]) + outfile.write('Show Value of Checks:'.ljust(line_width) + '%s\n' % self.metadata['showloot'][player]) + outfile.write('Show Map:'.ljust(line_width) + '%s\n' % self.metadata['showmap'][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]) @@ -3748,8 +3762,9 @@ world_mode = {"open": 0, "standard": 1, "inverted": 2} sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3} # byte 2: GGGD DFFH (goal, diff, item_func, hints) -goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, 'crystals': 4, 'trinity': 5, - 'ganonhunt': 6, 'completionist': 7, 'sanctuary': 1} +goal_mode = {'ganon': 0, 'pedestal': 1, 'dungeons': 2, 'triforcehunt': 3, + 'crystals': 4, 'trinity': 5, 'ganonhunt': 6, 'completionist': 7, + 'sanctuary': 1, 'bosshunt': 6} diff_mode = {"normal": 0, "hard": 1, "expert": 2} func_mode = {"normal": 0, "hard": 1, "expert": 2} diff --git a/CLI.py b/CLI.py index 21f21a6a..809a90c6 100644 --- a/CLI.py +++ b/CLI.py @@ -106,7 +106,7 @@ def parse_cli(argv, no_defaults=False): ret = parser.parse_args(argv) if ret.keysanity: - ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = 'wild' * 4 + ret.mapshuffle, ret.compassshuffle, ret.keyshuffle, ret.bigkeyshuffle = ['wild'] * 4 if ret.keydropshuffle: ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle @@ -133,8 +133,8 @@ def parse_cli(argv, no_defaults=False): for name in ['logic', 'mode', 'swords', 'goal', 'difficulty', 'item_functionality', 'ow_shuffle', 'ow_terrain', 'ow_crossed', 'ow_keepsimilar', 'ow_mixed', 'ow_whirlpool', 'ow_fluteshuffle', 'flute_mode', 'bow_mode', 'take_any', 'boots_hint', 'shuffle_followers', - 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', - 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', + 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'bosses_ganon', 'bosshunt_include_agas', 'ganon_item', 'openpyramid', + 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'showloot', 'showmap', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', @@ -177,6 +177,8 @@ def parse_settings(): "goal": "ganon", "crystals_gt": "7", "crystals_ganon": "7", + "bosses_ganon": "8", + "bosshunt_include_agas": False, "ganon_item": "silver", "swords": "random", "flute_mode": "normal", @@ -234,6 +236,8 @@ def parse_settings(): "keyshuffle": "none", "bigkeyshuffle": "none", "prizeshuffle": "none", + "showloot": "never", + "showmap": "map", "keysanity": False, "door_shuffle": "vanilla", "intensity": 3, diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 474d8b56..616bfb8d 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -2340,7 +2340,7 @@ def parallel_full_neutralization(dungeon_map, polarized_sectors, global_pole): increment_depth = True current_depth = last_depth + 1 if increment_depth else last_depth finished = all([(x.polarity()+sum_polarity(solution_list[x])).is_neutral() for x in builders]) - logging.getLogger('').info(f'-Balanced solution found in {time.process_time()-start}') + logging.getLogger('').debug(f'-Balanced solution found in {time.process_time()-start}') for builder, sectors in solution_list.items(): for sector in sectors: assign_sector(sector, builder, polarized_sectors, global_pole) diff --git a/DungeonRandomizer.py b/DungeonRandomizer.py index 1c3f82e9..6a168bd7 100755 --- a/DungeonRandomizer.py +++ b/DungeonRandomizer.py @@ -11,7 +11,7 @@ import RaceRandom as random import source.classes.diags as diagnostics from CLI import get_args_priority, parse_cli from Fill import FillError -from Main import EnemizerError, __version__, main +from Main import EnemizerError, main from Rom import get_sprite_from_name from source.classes.BabelFish import BabelFish from Utils import close_console, is_bundled @@ -80,7 +80,7 @@ def start(): break except (FillError, EnemizerError, Exception, RuntimeError) as err: failures.append((err, seed)) - logger.warning('%s: %s', fish.translate("cli","cli","generation.failed"), err) + logger.exception('Attempt %d - %s: %s', trynum, fish.translate("cli","cli","generation.failed"), err) logger.info('') seed = random.randint(0, 999999999) diff --git a/Dungeons.py b/Dungeons.py index 8fc54156..a3f2e6c1 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -13,7 +13,19 @@ def create_dungeons(world, player): dungeon.world = world return dungeon - ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], [ItemFactory('Map (Escape)', player)]) + hc_dungeon_items = ['Map (Escape)'] + at_dungeon_items = [] + + if world.showloot[player] == 'compass': + if world.dropshuffle[player] == 'underworld' or world.pottery[player] in ['dungeon', 'reduced', 'clustered', 'nonempty', 'lottery']: + hc_dungeon_items.append('Compass (Escape)') + at_dungeon_items.append('Compass (Agahnims Tower)') + elif world.compassshuffle[player] == 'wild': + hc_dungeon_items.append('Compass (Escape)') + if world.keyshuffle[player] == 'wild': + at_dungeon_items.append('Compass (Agahnims Tower)') + + ES = make_dungeon('Hyrule Castle', 1, None, hyrule_castle_regions, None, [ItemFactory('Small Key (Escape)', player)], ItemFactory(hc_dungeon_items, player)) EP = make_dungeon('Eastern Palace', 2, 'Armos Knights', eastern_regions, ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player)) DP = make_dungeon('Desert Palace', 3, 'Lanmolas', desert_regions, ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player)) ToH = make_dungeon('Tower of Hera', 10, 'Moldorm', hera_regions, ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player)) @@ -24,7 +36,7 @@ def create_dungeons(world, player): IP = make_dungeon('Ice Palace', 9, 'Kholdstare', ice_regions, ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player)) MM = make_dungeon('Misery Mire', 7, 'Vitreous', mire_regions, ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player)) TR = make_dungeon('Turtle Rock', 12, 'Trinexx', tr_regions, ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) - AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) + AT = make_dungeon('Agahnims Tower', 4, 'Agahnim', tower_regions, None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), ItemFactory(at_dungeon_items, player)) GT = make_dungeon('Ganons Tower', 13, 'Agahnim2', gt_regions, ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) GT.bosses['bottom'] = BossFactory('Armos Knights', player) diff --git a/ER_hint_reference.txt b/ER_hint_reference.txt index 943609ce..9b759706 100644 --- a/ER_hint_reference.txt +++ b/ER_hint_reference.txt @@ -1,361 +1,361 @@ -Hint description: - -Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations: - -4 hints for inconvenient entrances. -4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints). -3 hints for inconvenient item locations. -5 hints for valuable items. -4 junk hints. - -In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead: - -5 hints for inconvenient item locations. -8 hints for valuable items. -7 junk hints. - -In the simple, restricted shuffles, these are the ratios: - -2 hints for inconvenient entrances. -1 hint for an inconvenient dungeon entrance. -4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints). -3 hints for inconvenient item locations. -5 hints for valuable items. -5 junk hints. - -These hints will use the following format: - -Entrance hints go "[Entrance on overworld] leads to [interior]". - -Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass). - -The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots. - -Hint Locations: - -Eastern Palace room before Big Chest -Desert Palace bonk torch room -Tower of Hera entrance room -Tower of Hera Big Chest room -Castle Tower after dark rooms -Palace of Darkness before Bow section -Swamp Palace entryway -Thieves' Town upstairs -Ice Palace entrance -Ice Palace after first drop -Ice Palace tall ice floor room -Misery Mire cutscene room -Turtle Rock entrance -Spectacle Rock cave -Spiky Hint cave -PoD Bdlg NPC -Near PoD Storyteller (bug near bomb wall) -Dark Sanctuary Storyteller (long room with tables) -Near Mire Storyteller (feather duster in winding cave) -SE DW Storyteller (owl in winding cave) - -Inconvenient entrance list: - -Skull Woods Final -Ice Palace -Misery Mire -Turtle Rock -Ganon's Tower -Mimic Ledge -SW DM Foothills Cave (mirror from upper Bumper ledge) -Hammer Pegs (near purple chest) -Super Bomb cracked wall - -Inconvenient location list: - -Swamp left (two chests) -Mire left (two chests) -Hera basement -Eastern Palace Big Key chest (protected by anti-fairies) -Thieves' Town Big Chest -Ice Palace Big Chest -Ganon's Tower Big Chest -Purple Chest -Spike Cave -Magic Bat -Sahasrahla (Green Pendant) - -In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list: - -Graveyard Cave -Mimic Cave - -Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses. - -While the exact verbage of location names and item names can be found in the source code, here's a copy for reference: - -Overworld Entrance naming: - -Links House: The hero's old residence -Turtle Rock: Turtle Rock Main -Misery Mire: Misery Mire -Ice Palace: Ice Palace -Skull Woods Final Section: The back of Skull Woods -Death Mountain Return Cave (West): The SW DM Foothills Cave -Mimic Cave: Mimic Ledge -Hammer Peg Cave: The rows of pegs -Pyramid Fairy: The crack on the pyramid -Eastern Palace: Eastern Palace -Elder House (East): Elder House -Elder House (West): Elder House -Two Brothers House (East): Eastern Quarreling Brothers' house -Old Man Cave (West): The lower DM entrance -Hyrule Castle Entrance (South): The ground level castle door -Thieves Town: Thieves' Town -Bumper Cave (Bottom): The lower Bumper Cave -Swamp Palace: Swamp Palace -Dark Death Mountain Ledge (West): The East dark DM connector ledge -Dark Death Mountain Ledge (East): The East dark DM connector ledge -Superbunny Cave (Top): The summit of dark DM cave -Superbunny Cave (Bottom): The base of east dark DM -Hookshot Cave: The rock on dark DM -Desert Palace Entrance (South): The book sealed passage -Tower of Hera: The Tower of Hera -Two Brothers House (West): The door near the race game -Old Man Cave (East): The SW-most cave on west DM -Old Man House (Bottom): A cave with a door on west DM -Old Man House (Top): The eastmost cave on west DM -Death Mountain Return Cave (East): The westmost cave on west DM -Spectacle Rock Cave Peak: The highest cave on west DM -Spectacle Rock Cave: The right ledge on west DM -Spectacle Rock Cave (Bottom): The left ledge on west DM -Paradox Cave (Bottom): The right paired cave on east DM -Paradox Cave (Middle): The southmost cave on east DM -Paradox Cave (Top): The east DM summit cave -Fairy Ascension Cave (Bottom): The east DM cave behind rocks -Fairy Ascension Cave (Top): The central ledge on east DM -Spiral Cave: The left ledge on east DM -Spiral Cave (Bottom): The SWmost cave on east DM -Palace of Darkness: Palace of Darkness -Hyrule Castle Entrance (West): The left castle door -Hyrule Castle Entrance (East): The right castle door -Agahnims Tower: The sealed castle door -Desert Palace Entrance (West): The westmost building in the desert -Desert Palace Entrance (North): The northmost cave in the desert -Blinds Hideout: Blind's old house -Lake Hylia Fairy: A cave NE of Lake Hylia -Light Hype Fairy: The cave south of your house -Desert Fairy: The cave near the desert -Chicken House: The chicken lady's house -Tavern North: A backdoor -Aginahs Cave: The open desert cave -Sahasrahlas Hut: The house near armos -Lake Hylia Shop: The cave NW Lake Hylia -Blacksmiths Hut: The old smithery -Sick Kids House: The central house in Kakariko -Lost Woods Gamble: A tree trunk door -Fortune Teller (Light): A building NE of Kakariko -Snitch Lady (East): A house guarded by a snitch -Snitch Lady (West): A house guarded by a snitch -Bush Covered House: A house with an uncut lawn -Tavern (Front): A building with a backdoor -Light World Bomb Hut: A Kakariko building with no door -Kakariko Shop: The old Kakariko shop -Mini Moldorm Cave: The cave south of Lake Hylia -Long Fairy Cave: The eastmost portal cave -Good Bee Cave: The open cave SE Lake Hylia -20 Rupee Cave: The rock SE Lake Hylia -50 Rupee Cave: The rock near the desert -Ice Rod Cave: The sealed cave SE Lake Hylia -Library: The old library -Potion Shop: The witch's building -Dam: The old dam -Lumberjack House: The lumberjack house -Lake Hylia Fortune Teller: The building NW Lake Hylia -Kakariko Gamble Game: The old Kakariko gambling den -Waterfall of Wishing: Going behind the waterfall -Capacity Upgrade: The cave on the island -Bonk Rock Cave: The rock pile near Sanctuary -Graveyard Cave: The graveyard ledge -Checkerboard Cave: The NE desert ledge -Cave 45: The ledge south of haunted grove -Kings Grave: The northeastmost grave -Bonk Fairy (Light): The rock pile near your home -Hookshot Fairy: A cave on east DM -Bonk Fairy (Dark): The rock pile near the old bomb shop -Dark Sanctuary Hint: The dark sanctuary cave -Dark Lake Hylia Fairy: The cave NE dark Lake Hylia -C-Shaped House: The NE house in Village of Outcasts -Big Bomb Shop: The old bomb shop -Dark Death Mountain Fairy: The SW cave on dark DM -Dark Lake Hylia Shop: The building NW dark Lake Hylia -Dark World Shop: The hammer sealed building -Red Shield Shop: The fenced in building -Mire Shed: The western hut in the mire -East Dark World Hint: The dark cave near the eastmost portal -Mire Hint: The cave east of the mire -Spike Cave: The ledge cave on west dark DM -Palace of Darkness Hint: The building south of Kiki -Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia -Dark Death Mountain Shop: The base of east dark DM -Dark Potion Shop: The building near the catfish -Archery Game: The old archery game -Dark Lumberjack Shop: The northmost Dark World building -Hype Cave: The cave south of the old bomb shop -Brewery: The Village of Outcasts building with no door -Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia -Chest Game: The westmost building in the Village of Outcasts -Mire Fairy: The eastern hut in the mire -Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia -Fortune Teller (Dark): The building NE the Village of Outcasts -Sanctuary: Sanctuary -Lumberjack Tree Cave: The cave Behind Lumberjacks -Lost Woods Hideout Stump: The stump in Lost Woods -North Fairy Cave: The cave East of Graveyard -Bat Cave Cave: The cave in eastern Kakariko -Kakariko Well Cave: The cave in northern Kakariko -Hyrule Castle Secret Entrance Stairs: The tunnel near the castle -Skull Woods First Section Door: The southeastmost skull -Skull Woods Second Section Door (East): The central open skull -Skull Woods Second Section Door (West): The westmost open skull -Desert Palace Entrance (East): The eastern building in the desert -Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM -Bumper Cave (Top): The upper Bumper Cave -Hookshot Cave Back Entrance: The stairs on the floating island - -Destination Entrance Naming: - -Hyrule Castle: Hyrule Castle (all three entrances) -Eastern Palace: Eastern Palace -Desert Palace: Desert Palace (all four entrances, including final) -Tower of Hera: Tower of Hera -Palace of Darkness: Palace of Darkness -Swamp Palace: Swamp Palace -Skull Woods: Skull Woods (any entrance including final) -Thieves' Town: Thieves' Town -Ice Palace: Ice Palace -Misery Mire: Misery Mire -Turtle Rock: Turtle Rock (all four entrances) -Ganon's Tower: Ganon's Tower -Castle Tower: Agahnim's Tower -A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave -A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout -Sahasrahla: Sahasrahla -A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy -A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave -A common shop: Any shop that sells bombs by default -The rare shop: The shop that sells the Red Shield by default -The potion shop: Potion Shop -The bomb shop: Bomb Shop -A fortune teller: Any of the three fortune tellers -A house with a chest: Chicken Lady's house, C-House, Brewery -A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave -A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave -The dam: Watergate -The sick kid: Sick Kid -The library: Library -Mimic Cave: Mimic Cave -Spike Cave: Spike Cave -A game of 16 chests: VoO chest game (for the item) -A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint -A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots) -A game of chance: Gambling game (just for cash, no items) -A game of skill: Archery minigame -The queen of fairies: Capacity Upgrade Fairy -A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit -A restock room: The Kakariko bomb/arrow restock room -The tavern: The Kakariko tavern -The grass man: The Kakariko man with many beds -A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee -Fairies deep in a cave: Hookshot Fairy - -Location naming reference: - -Mushroom: in the woods -Master Sword Pedestal: at the pedestal -Bottle Merchant: with a merchant -Stumpy: with tree boy -Flute Spot: underground -Digging Game: underground -Lake Hylia Island: on an island -Floating Island: on an island -Bumper Cave Ledge: on a ledge -Spectacle Rock: atop a rock -Maze Race: at the race -Desert Ledge: in the desert -Pyramid: on the pyramid -Catfish: with a catfish -Ether Tablet: at a monument -Bombos Tablet: at a monument -Hobo: with the hobo -Zora's Ledge: near Zora -King Zora: at a high price -Sunken Treasure: underwater -Floodgate Chest: in the dam -Blacksmith: with the smith -Purple Chest: from a box -Old Man: with the old man -Link's Uncle: with your uncle -Secret Passage: near your uncle -Kakariko Well (5 items): in a well -Lost Woods Hideout: near a thief -Lumberjack Tree: in a hole -Magic Bat: with the bat -Paradox Cave (7 items): in a cave with seven chests -Blind's Hideout (5 items): in a basement -Mini Moldorm Cave (5 items): near Moldorms -Hype Cave (4 back chests): near a bat-like man -Hype Cave - Generous Guy: with a bat-like man -Hookshot Cave (4 items): across pits -Sahasrahla's Hut (chests in back): near the elder -Sahasrahla: with the elder -Waterfall Fairy (2 items): near a fairy -Pyramid Fairy (2 items): near a fairy -Mire Shed (2 items): near sparks -Superbunny Cave (2 items): in a connection -Spiral Cave: in spiral cave -Kakariko Tavern: in the bar -Link's House: in your home -Sick Kid: with the sick -Library: near books -Potion Shop: near potions -Spike Cave: beyond spikes -Mimic Cave: in a cave of mimicry -Chest Game: as a game reward -Chicken House: near poultry -Aginah's Cave: with Aginah -Ice Rod Cave: in a frozen cave -Brewery: alone in a home -C-Shaped House: alone in a home -Spectacle Rock Cave: alone in a cave -King's Tomb: alone in a cave -Cave 45: alone in a cave -Graveyard Cave: alone in a cave -Checkerboard Cave: alone in a cave -Bonk Rock Cave: alone in a cave -Peg Cave: alone in a cave -Sanctuary: in Sanctuary -Hyrule Castle - Boomerang Chest: in Hyrule Castle -Hyrule Castle - Map Chest: in Hyrule Castle -Hyrule Castle - Zelda's Chest: in Hyrule Castle -Sewers - Dark Cross: in the sewers -Sewers - Secret Room (3 items): in the sewers -Eastern Palace - Boss: with the Armos -Eastern Palace (otherwise, 5 items): in Eastern Palace -Desert Palace - Boss: with Lanmolas -Desert Palace (otherwise, 5 items): in Desert Palace -Tower of Hera - Boss: with Moldorm -Tower of Hera (otherwise, 5 items): in Tower of Hera -Castle Tower (2 items): in Castle Tower -Palace of Darkness - Boss: with Helmasaur King -Palace of Darkness (otherwise, 13 items): in Palace of Darkness -Swamp Palace - Boss: with Arrghus -Swamp Palace (otherwise, 9 items): in Swamp Palace -Skull Woods - Bridge Room: near Mothula -Skull Woods - Boss: with Mothula -Skull Woods (otherwise, 6 items): in Skull Woods -Thieves' Town - Boss: with Blind -Thieves' Town (otherwise, 7 items): in Thieves' Town -Ice Palace - Boss: with Kholdstare -Ice Palace (otherwise, 7 items): in Ice Palace -Misery Mire - Boss: with Vitreous -Misery Mire (otherwise, 7 items): in Misery Mire -Turtle Rock - Boss: with Trinexx -Turtle Rock (otherwise, 11 items): in Turtle Rock -Ganons Tower (after climb, 4 items): atop Ganon's Tower +Hint description: + +Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations: + +4 hints for inconvenient entrances. +4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints). +3 hints for inconvenient item locations. +5 hints for valuable items. +4 junk hints. + +In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead: + +5 hints for inconvenient item locations. +8 hints for valuable items. +7 junk hints. + +In the simple, restricted shuffles, these are the ratios: + +2 hints for inconvenient entrances. +1 hint for an inconvenient dungeon entrance. +4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints). +3 hints for inconvenient item locations. +5 hints for valuable items. +5 junk hints. + +These hints will use the following format: + +Entrance hints go "[Entrance on overworld] leads to [interior]". + +Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass). + +The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots. + +Hint Locations: + +Eastern Palace room before Big Chest +Desert Palace bonk torch room +Tower of Hera entrance room +Tower of Hera Big Chest room +Castle Tower after dark rooms +Palace of Darkness before Bow section +Swamp Palace entryway +Thieves' Town upstairs +Ice Palace entrance +Ice Palace after first drop +Ice Palace tall ice floor room +Misery Mire cutscene room +Turtle Rock entrance +Spectacle Rock cave +Spiky Hint cave +PoD Bdlg NPC +Near PoD Storyteller (bug near bomb wall) +Dark Sanctuary Storyteller (long room with tables) +Near Mire Storyteller (feather duster in winding cave) +SE DW Storyteller (owl in winding cave) + +Inconvenient entrance list: + +Skull Woods Final +Ice Palace +Misery Mire +Turtle Rock +Ganon's Tower +Mimic Ledge +SW DM Foothills Cave (mirror from upper Bumper ledge) +Hammer Pegs (near purple chest) +Super Bomb cracked wall + +Inconvenient location list: + +Swamp left (two chests) +Mire left (two chests) +Hera basement +Eastern Palace Big Key chest (protected by anti-fairies) +Thieves' Town Big Chest +Ice Palace Big Chest +Ganon's Tower Big Chest +Purple Chest +Spike Cave +Magic Bat +Sahasrahla (Green Pendant) + +In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list: + +Graveyard Cave +Mimic Cave + +Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses. + +While the exact verbage of location names and item names can be found in the source code, here's a copy for reference: + +Overworld Entrance naming: + +Links House: The hero's old residence +Turtle Rock: Turtle Rock Main +Misery Mire: Misery Mire +Ice Palace: Ice Palace +Skull Woods Final Section: The back of Skull Woods +Death Mountain Return Cave (West): The SW DM Foothills Cave +Mimic Cave: Mimic Ledge +Hammer Peg Cave: The rows of pegs +Pyramid Fairy: The crack on the pyramid +Eastern Palace: Eastern Palace +Elder House (East): Elder House +Elder House (West): Elder House +Two Brothers House (East): Eastern Quarreling Brothers' house +Old Man Cave (West): The lower DM entrance +Hyrule Castle Entrance (South): The ground level castle door +Thieves Town: Thieves' Town +Bumper Cave (Bottom): The lower Bumper Cave +Swamp Palace: Swamp Palace +Dark Death Mountain Ledge (West): The East dark DM connector ledge +Dark Death Mountain Ledge (East): The East dark DM connector ledge +Superbunny Cave (Top): The summit of dark DM cave +Superbunny Cave (Bottom): The base of east dark DM +Hookshot Cave: The rock on dark DM +Desert Palace Entrance (South): The book sealed passage +Tower of Hera: The Tower of Hera +Two Brothers House (West): The door near the race game +Old Man Cave (East): The SW-most cave on west DM +Old Man House (Bottom): A cave with a door on west DM +Old Man House (Top): The eastmost cave on west DM +Death Mountain Return Cave (East): The westmost cave on west DM +Spectacle Rock Cave Peak: The highest cave on west DM +Spectacle Rock Cave: The right ledge on west DM +Spectacle Rock Cave (Bottom): The left ledge on west DM +Paradox Cave (Bottom): The right paired cave on east DM +Paradox Cave (Middle): The southmost cave on east DM +Paradox Cave (Top): The east DM summit cave +Fairy Ascension Cave (Bottom): The east DM cave behind rocks +Fairy Ascension Cave (Top): The central ledge on east DM +Spiral Cave: The left ledge on east DM +Spiral Cave (Bottom): The SWmost cave on east DM +Palace of Darkness: Palace of Darkness +Hyrule Castle Entrance (West): The left castle door +Hyrule Castle Entrance (East): The right castle door +Agahnims Tower: The sealed castle door +Desert Palace Entrance (West): The westmost building in the desert +Desert Palace Entrance (North): The northmost cave in the desert +Blinds Hideout: Blind's old house +Lake Hylia Fairy: A cave NE of Lake Hylia +Light Hype Fairy: The cave south of your house +Desert Fairy: The cave near the desert +Chicken House: The chicken lady's house +Tavern North: A backdoor +Aginahs Cave: The open desert cave +Sahasrahlas Hut: The house near armos +Lake Hylia Shop: The cave NW Lake Hylia +Blacksmiths Hut: The old smithery +Sick Kids House: The central house in Kakariko +Lost Woods Gamble: A tree trunk door +Fortune Teller (Light): A building NE of Kakariko +Snitch Lady (East): A house guarded by a snitch +Snitch Lady (West): A house guarded by a snitch +Bush Covered House: A house with an uncut lawn +Tavern (Front): A building with a backdoor +Light World Bomb Hut: A Kakariko building with no door +Kakariko Shop: The old Kakariko shop +Mini Moldorm Cave: The cave south of Lake Hylia +Long Fairy Cave: The eastmost portal cave +Good Bee Cave: The open cave SE Lake Hylia +20 Rupee Cave: The rock SE Lake Hylia +50 Rupee Cave: The rock near the desert +Ice Rod Cave: The sealed cave SE Lake Hylia +Library: The old library +Potion Shop: The witch's building +Dam: The old dam +Lumberjack House: The lumberjack house +Lake Hylia Fortune Teller: The building NW Lake Hylia +Kakariko Gamble Game: The old Kakariko gambling den +Waterfall of Wishing: Going behind the waterfall +Capacity Upgrade: The cave on the island +Bonk Rock Cave: The rock pile near Sanctuary +Graveyard Cave: The graveyard ledge +Checkerboard Cave: The NE desert ledge +Cave 45: The ledge south of haunted grove +Kings Grave: The northeastmost grave +Bonk Fairy (Light): The rock pile near your home +Hookshot Fairy: A cave on east DM +Bonk Fairy (Dark): The rock pile near the old bomb shop +Dark Sanctuary Hint: The dark sanctuary cave +Dark Lake Hylia Fairy: The cave NE dark Lake Hylia +C-Shaped House: The NE house in Village of Outcasts +Big Bomb Shop: The old bomb shop +Dark Death Mountain Fairy: The SW cave on dark DM +Dark Lake Hylia Shop: The building NW dark Lake Hylia +Dark World Shop: The hammer sealed building +Red Shield Shop: The fenced in building +Mire Shed: The western hut in the mire +East Dark World Hint: The dark cave near the eastmost portal +Mire Hint: The cave east of the mire +Spike Cave: The ledge cave on west dark DM +Palace of Darkness Hint: The building south of Kiki +Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia +Dark Death Mountain Shop: The base of east dark DM +Dark Potion Shop: The building near the catfish +Archery Game: The old archery game +Dark Lumberjack Shop: The northmost Dark World building +Hype Cave: The cave south of the old bomb shop +Brewery: The Village of Outcasts building with no door +Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia +Chest Game: The westmost building in the Village of Outcasts +Mire Fairy: The eastern hut in the mire +Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia +Fortune Teller (Dark): The building NE the Village of Outcasts +Sanctuary: Sanctuary +Lumberjack Tree Cave: The cave Behind Lumberjacks +Lost Woods Hideout Stump: The stump in Lost Woods +North Fairy Cave: The cave East of Graveyard +Bat Cave Cave: The cave in eastern Kakariko +Kakariko Well Cave: The cave in northern Kakariko +Hyrule Castle Secret Entrance Stairs: The tunnel near the castle +Skull Woods First Section Door: The southeastmost skull +Skull Woods Second Section Door (East): The central open skull +Skull Woods Second Section Door (West): The westmost open skull +Desert Palace Entrance (East): The eastern building in the desert +Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM +Bumper Cave (Top): The upper Bumper Cave +Hookshot Cave Back Entrance: The stairs on the floating island + +Destination Entrance Naming: + +Hyrule Castle: Hyrule Castle (all three entrances) +Eastern Palace: Eastern Palace +Desert Palace: Desert Palace (all four entrances, including final) +Tower of Hera: Tower of Hera +Palace of Darkness: Palace of Darkness +Swamp Palace: Swamp Palace +Skull Woods: Skull Woods (any entrance including final) +Thieves' Town: Thieves' Town +Ice Palace: Ice Palace +Misery Mire: Misery Mire +Turtle Rock: Turtle Rock (all four entrances) +Ganon's Tower: Ganon's Tower +Castle Tower: Agahnim's Tower +A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave +A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout +Sahasrahla: Sahasrahla +A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy +A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave +A common shop: Any shop that sells bombs by default +The rare shop: The shop that sells the Red Shield by default +The potion shop: Potion Shop +The bomb shop: Bomb Shop +A fortune teller: Any of the three fortune tellers +A house with a chest: Chicken Lady's house, C-House, Brewery +A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave +A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave +The dam: Watergate +The sick kid: Sick Kid +The library: Library +Mimic Cave: Mimic Cave +Spike Cave: Spike Cave +A game of 16 chests: VoO chest game (for the item) +A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint +A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots) +A game of chance: Gambling game (just for cash, no items) +A game of skill: Archery minigame +The queen of fairies: Capacity Upgrade Fairy +A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit +A restock room: The Kakariko bomb/arrow restock room +The tavern: The Kakariko tavern +The grass man: The Kakariko man with many beds +A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee +Fairies deep in a cave: Hookshot Fairy + +Location naming reference: + +Mushroom: in the woods +Master Sword Pedestal: at the pedestal +Bottle Merchant: with a merchant +Stumpy: with tree boy +Flute Spot: underground +Digging Game: underground +Lake Hylia Island: on an island +Floating Island: on an island +Bumper Cave Ledge: on a ledge +Spectacle Rock: atop a rock +Maze Race: at the race +Desert Ledge: in the desert +Pyramid: on the pyramid +Catfish: with a catfish +Ether Tablet: at a monument +Bombos Tablet: at a monument +Hobo: with the hobo +Zora's Ledge: near Zora +King Zora: at a high price +Sunken Treasure: underwater +Floodgate Chest: in the dam +Blacksmith: with the smith +Purple Chest: from a box +Old Man: with the old man +Link's Uncle: with your uncle +Secret Passage: near your uncle +Kakariko Well (5 items): in a well +Lost Woods Hideout: near a thief +Lumberjack Tree: in a hole +Magic Bat: with the bat +Paradox Cave (7 items): in a cave with seven chests +Blind's Hideout (5 items): in a basement +Mini Moldorm Cave (5 items): near Moldorms +Hype Cave (4 back chests): near a bat-like man +Hype Cave - Generous Guy: with a bat-like man +Hookshot Cave (4 items): across pits +Sahasrahla's Hut (chests in back): near the elder +Sahasrahla: with the elder +Waterfall Fairy (2 items): near a fairy +Pyramid Fairy (2 items): near a fairy +Mire Shed (2 items): near sparks +Superbunny Cave (2 items): in a connection +Spiral Cave: in spiral cave +Kakariko Tavern: in the bar +Link's House: in your home +Sick Kid: with the sick +Library: near books +Potion Shop: near potions +Spike Cave: beyond spikes +Mimic Cave: in a cave of mimicry +Chest Game: as a game reward +Chicken House: near poultry +Aginah's Cave: with Aginah +Ice Rod Cave: in a frozen cave +Brewery: alone in a home +C-Shaped House: alone in a home +Spectacle Rock Cave: alone in a cave +King's Tomb: alone in a cave +Cave 45: alone in a cave +Graveyard Cave: alone in a cave +Checkerboard Cave: alone in a cave +Bonk Rock Cave: alone in a cave +Peg Cave: alone in a cave +Sanctuary: in Sanctuary +Hyrule Castle - Boomerang Chest: in Hyrule Castle +Hyrule Castle - Map Chest: in Hyrule Castle +Hyrule Castle - Zelda's Chest: in Hyrule Castle +Sewers - Dark Cross: in the sewers +Sewers - Secret Room (3 items): in the sewers +Eastern Palace - Boss: with the Armos +Eastern Palace (otherwise, 5 items): in Eastern Palace +Desert Palace - Boss: with Lanmolas +Desert Palace (otherwise, 5 items): in Desert Palace +Tower of Hera - Boss: with Moldorm +Tower of Hera (otherwise, 5 items): in Tower of Hera +Castle Tower (2 items): in Castle Tower +Palace of Darkness - Boss: with Helmasaur King +Palace of Darkness (otherwise, 13 items): in Palace of Darkness +Swamp Palace - Boss: with Arrghus +Swamp Palace (otherwise, 9 items): in Swamp Palace +Skull Woods - Bridge Room: near Mothula +Skull Woods - Boss: with Mothula +Skull Woods (otherwise, 6 items): in Skull Woods +Thieves' Town - Boss: with Blind +Thieves' Town (otherwise, 7 items): in Thieves' Town +Ice Palace - Boss: with Kholdstare +Ice Palace (otherwise, 7 items): in Ice Palace +Misery Mire - Boss: with Vitreous +Misery Mire (otherwise, 7 items): in Misery Mire +Turtle Rock - Boss: with Trinexx +Turtle Rock (otherwise, 11 items): in Turtle Rock +Ganons Tower (after climb, 4 items): atop Ganon's Tower Ganon's Tower (otherwise, 23 items): in Ganon's Tower \ No newline at end of file diff --git a/Fill.py b/Fill.py index 106f51f9..0643e509 100644 --- a/Fill.py +++ b/Fill.py @@ -799,7 +799,7 @@ def sell_potions(world, player): if shop.region.name in shop_to_location_table and shop.region.name != 'Capacity Upgrade': loc_choices += [world.get_location(loc, player) for loc in shop_to_location_table[shop.region.name]] locations = [loc for loc in loc_choices if not loc.item] - for potion in ['Green Potion', 'Blue Potion', 'Red Potion']: + for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']: location = random.choice(filter_locations(ItemFactory(potion, player), locations, world, potion=True)) locations.remove(location) p_item = next((item for item in world.itempool if item.name == potion and item.player == player), None) @@ -1293,4 +1293,4 @@ def set_prize_drops(world, player): # saved fish prize world.prizes[player]['fish'] = prizes.pop() - world.prizes[player]['enemies'] = prizes \ No newline at end of file + world.prizes[player]['enemies'] = prizes diff --git a/Gui.py b/Gui.py index 8c9146bd..758c0bb3 100755 --- a/Gui.py +++ b/Gui.py @@ -22,8 +22,6 @@ from tkinter import ( from CLI import get_args_priority from DungeonRandomizer import parse_cli from GuiUtils import set_icon -from Main import __version__ as ESVersion -from OverworldShuffle import __version__ as ORVersion from source.classes.BabelFish import BabelFish from source.classes.Empty import Empty from source.gui.adjust.overview import adjust_page @@ -40,13 +38,14 @@ from source.gui.randomize.generation import generation_page from source.gui.randomize.item import item_page from source.gui.randomize.overworld import overworld_page from source.gui.startinventory.overview import startinventory_page +from Versions import DRVersion, GKVersion, ORVersion def check_python_version(fish): import sys version = sys.version_info if version.major < 3 or version.minor < 7: - messagebox.showinfo("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion), fish.translate("cli","cli","old.python.version") % sys.version) + messagebox.showinfo("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion), fish.translate("cli","cli","old.python.version") % sys.version) # Save settings to file @@ -95,7 +94,7 @@ def guiMain(args=None): mainWindow = Tk() self = mainWindow - mainWindow.wm_title("Overworld Shuffle %s (DR %s)" % (ORVersion, ESVersion)) + mainWindow.wm_title("GwaaKiwi Randomizer %s (OR %s, DR %s)" % (GKVersion, ORVersion, DRVersion)) mainWindow.protocol("WM_DELETE_WINDOW", guiExit) # intercept when user clicks the X # set program icon diff --git a/ItemList.py b/ItemList.py index 21acfcbb..cf76538f 100644 --- a/ItemList.py +++ b/ItemList.py @@ -236,12 +236,14 @@ def get_custom_array_key(item): def generate_itempool(world, player): if (world.difficulty[player] not in ['normal', 'hard', 'expert'] - or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', 'triforcehunt', 'trinity', 'crystals', - 'ganonhunt', 'completionist', 'sanctuary'] + or world.goal[player] not in ['ganon', 'pedestal', 'dungeons', + 'triforcehunt', 'trinity', 'crystals', + 'ganonhunt', 'completionist', 'sanctuary', + 'bosshunt'] or world.mode[player] not in ['open', 'standard', 'inverted'] or world.timer not in ['none', 'display', 'timed', 'timed-ohko', 'ohko', 'timed-countdown'] or world.progressive not in ['on', 'off', 'random']): - raise NotImplementedError('Not supported yet') + raise NotImplementedError('Not supported yet') if world.timer in ['ohko', 'timed-ohko']: world.can_take_damage[player] = False @@ -379,7 +381,7 @@ def generate_itempool(world, player): items = ItemFactory(pool, player) if world.shopsanity[player]: - for potion in ['Green Potion', 'Blue Potion', 'Red Potion']: + for potion in ['Green Potion', 'Blue Potion', 'Red Potion', 'Bee']: p_item = next(item for item in items if item.name == potion and item.player == player) p_item.priority = True # don't beemize one of each potion @@ -1125,7 +1127,7 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt precollected_items.append('Pegasus Boots') pool.remove('Pegasus Boots') pool.extend(['Rupees (20)']) - + if want_progressives(): pool.extend(progressivegloves) else: @@ -1520,7 +1522,7 @@ def make_customizer_pool(world, player): guaranteed_items.append('Ocarina (Activated)') missing_items = [] if world.shopsanity[player]: - guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion']) + guaranteed_items.extend(['Blue Potion', 'Green Potion', 'Red Potion', 'Bee']) if world.keyshuffle[player] == 'universal': guaranteed_items.append('Small Key (Universal)') for item in guaranteed_items: diff --git a/Main.py b/Main.py index d633f16b..4c68d0a1 100644 --- a/Main.py +++ b/Main.py @@ -74,6 +74,7 @@ from Rom import ( ) from RoomData import create_rooms from Rules import set_rules +from source.classes.BabelFish import BabelFish from source.classes.CustomSettings import CustomSettings from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies @@ -92,12 +93,7 @@ from UnderworldGlitchRules import ( create_hmg_entrances_regions, ) from Utils import output_path, parse_player_names - -version_number = '1.5.0' -version_branch = '-u' -__version__ = f'{version_number}{version_branch}' - -from source.classes.BabelFish import BabelFish +from Versions import DRVersion, GKVersion, ORVersion class EnemizerError(RuntimeError): @@ -133,7 +129,7 @@ def random_ganon_item(sword_mode): def main(args, seed=None, fish=None): check_python_version() - + if args.print_template_yaml: return export_yaml(args, fish) @@ -175,16 +171,14 @@ def main(args, seed=None, fish=None): world.rom_seeds = {player: random.randint(0, 999999999) for player in range(1, world.players + 1)} world.finish_init() - from OverworldShuffle import __version__ as ORVersion logger.info( - world.fish.translate("cli","cli","app.title") + "\n", - ORVersion, - "%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed, - Settings.make_code(world, 1) if world.players == 1 else '' + world.fish.translate("cli","cli","app.title") + "\n", + GKVersion, + "%s (%s)" % (world.seed, str(args.outputname)) if str(args.outputname).startswith('M') else world.seed, ) - for k,v in {"DR":__version__,"OR":ORVersion}.items(): - logger.info((k + ' Version:').ljust(16) + '%s' % v) + for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items(): + logger.info((k + ' Version:').ljust(16) + '%s' % v) parsed_names = parse_player_names(args.names, world.players, args.teams) world.teams = len(parsed_names) @@ -195,7 +189,7 @@ def main(args, seed=None, fish=None): world.player_names[player].append(name) logger.info('') - outfilebase = f'OR_{args.outputname if args.outputname else world.seed}' + outfilebase = f'GK_{args.outputname if args.outputname else world.seed}' for player in range(1, world.players + 1): world.difficulty_requirements[player] = difficulties[world.difficulty[player]] @@ -474,15 +468,13 @@ def export_yaml(args, fish): if args.seed and int(args.seed) > 0: world.seed = int(args.seed) - from OverworldShuffle import __version__ as ORVersion logger.info( world.fish.translate("cli","cli","app.title") + "\n", - ORVersion, + GKVersion, "(%s)" % outfilebase, - Settings.make_code(world, 1) if world.players == 1 else '' ) - for k,v in {"DR":__version__,"OR":ORVersion}.items(): + for k,v in {"GK": GKVersion, "OR": ORVersion, "DR": DRVersion}.items(): logger.info((k + ' Version:').ljust(16) + '%s' % v) for player in range(1, world.players + 1): @@ -523,12 +515,16 @@ def init_world(args, fish): world.keyshuffle = args.keyshuffle.copy() world.bigkeyshuffle = args.bigkeyshuffle.copy() world.prizeshuffle = args.prizeshuffle.copy() + world.showloot = args.showloot.copy() + world.showmap = args.showmap.copy() world.bombbag = args.bombbag.copy() world.flute_mode = args.flute_mode.copy() world.bow_mode = args.bow_mode.copy() world.crystals_ganon_orig = args.crystals_ganon.copy() world.crystals_gt_orig = args.crystals_gt.copy() world.ganon_item_orig = args.ganon_item.copy() + world.bosses_ganon = {player: int(args.bosses_ganon[player]) for player in range(1, world.players + 1)} + world.bosshunt_include_agas = args.bosshunt_include_agas.copy() world.owTerrain = args.ow_terrain.copy() world.owKeepSimilar = args.ow_keepsimilar.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy() @@ -576,7 +572,7 @@ def init_world(args, fish): world.money_balance = args.money_balance.copy() # custom settings - these haven't been promoted to full settings yet - in_progress_settings = ['force_enemy', 'free_lamp_cone'] + in_progress_settings = ['force_enemy'] for player in range(1, world.players + 1): for setting in in_progress_settings: if world.customizer and world.customizer.has_setting(player, setting): @@ -838,12 +834,16 @@ def copy_world(world): ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy() + ret.showloot = world.showloot.copy() + ret.showmap = world.showmap.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() ret.free_lamp_cone = world.free_lamp_cone.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() + ret.bosses_ganon = world.bosses_ganon.copy() + ret.bosshunt_include_agas = world.bosshunt_include_agas.copy() ret.ganon_item = world.ganon_item.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() ret.crystals_gt_orig = world.crystals_gt_orig.copy() @@ -1065,12 +1065,16 @@ def copy_world_premature(world, player, create_flute_exits=True): ret.keyshuffle = world.keyshuffle.copy() ret.bigkeyshuffle = world.bigkeyshuffle.copy() ret.prizeshuffle = world.prizeshuffle.copy() + ret.showloot = world.showloot.copy() + ret.showmap = world.showmap.copy() ret.bombbag = world.bombbag.copy() ret.flute_mode = world.flute_mode.copy() ret.bow_mode = world.bow_mode.copy() ret.free_lamp_cone = world.free_lamp_cone.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.copy() + ret.bosses_ganon = world.bosses_ganon.copy() + ret.bosshunt_include_agas = world.bosshunt_include_agas.copy() ret.ganon_item = world.ganon_item.copy() ret.crystals_ganon_orig = world.crystals_ganon_orig.copy() ret.crystals_gt_orig = world.crystals_gt_orig.copy() diff --git a/OverworldShuffle.py b/OverworldShuffle.py index d8f663a4..72b79b9d 100644 --- a/OverworldShuffle.py +++ b/OverworldShuffle.py @@ -27,12 +27,6 @@ from Regions import mark_light_dark_world_regions from source.overworld.EntranceShuffle2 import connect_simple from Utils import bidict -version_number = '0.6.1.7' -# branch indicator is intentionally different across branches -version_branch = '' - -__version__ = '%s%s' % (version_number, version_branch) - parallel_links_new = None # needs to be globally available, reset every new generation/player def link_overworld(world, player): diff --git a/Plando.py b/Plando.py index 41e4d5e9..13a41bda 100755 --- a/Plando.py +++ b/Plando.py @@ -29,7 +29,7 @@ from source.overworld.EntranceShuffle2 import ( link_entrances_new, ) -__version__ = '0.2-dev' +PlandoVersion = '0.2-dev' def main(args): start_time = time.process_time() @@ -47,7 +47,7 @@ def main(args): random.seed(world.seed) - logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', __version__, args.plando) + logger.info('ALttP Plandomizer Version %s - Seed: %s\n\n', PlandoVersion, args.plando) world.difficulty_requirements[1] = difficulties[world.difficulty[1]] diff --git a/Rom.py b/Rom.py index 16aedc3f..95fe46f3 100644 --- a/Rom.py +++ b/Rom.py @@ -81,9 +81,10 @@ from Text import ( text_addresses, ) from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc +from Versions import DRVersion, GKVersion, ORVersion JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '76dc2d00e5dd5b925ad01574b327d364' +RANDOMIZERBASEHASH = '2647cc28bca3675152576dd1f5ea0bab' class JsonRom(object): @@ -201,11 +202,12 @@ class LocalRom(object): with open(local_path('data/base2current.bps'), 'rb') as stream: bps.apply.apply_to_bytearrays(bps.io.read_bps(stream), orig_buffer, self.buffer) - # verify md5 - patchedmd5 = hashlib.md5() - patchedmd5.update(self.buffer) - if RANDOMIZERBASEHASH != patchedmd5.hexdigest(): - raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.') + if not os.getenv("SKIP_BASEROM_CHECK", False): + # verify md5 + patchedmd5 = hashlib.md5() + patchedmd5.update(self.buffer) + if RANDOMIZERBASEHASH != patchedmd5.hexdigest(): + raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.') def write_crc(self): crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF @@ -1225,7 +1227,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon[player] == 'Triforce Piece' else [0x0D, 0x28]) if world.goal[player] in ['triforcehunt', 'trinity', 'ganonhunt']: rom.write_bytes(0x180167, int16_as_bytes(world.treasure_hunt_count[player])) - rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled) + rom.write_byte(0x180194, 1) # Must turn in triforce pieces (instant win not enabled) rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed @@ -1254,7 +1256,6 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): rom.write_bytes(0x18016E, [0x04, 0x08, 0x10]) # Set spike cave and MM spike room Cape usage rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest - rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest if world.is_pyramid_open(player): rom.initial_sram.pre_open_pyramid_hole() rom.write_byte(0x18008F, 0x01 if world.is_atgt_swapped(player) else 0x00) # AT/GT swapped @@ -1300,6 +1301,8 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # 08: Goal items collected (ie. Triforce Pieces) # 09: Max collection rate # 0A: Custom goal + # 0B: Reserved for Bingo + # 0C: All bosses (prize bosses + aga1 + aga2) def get_goal_bytes(type): goal_bytes = [] @@ -1354,6 +1357,11 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): ganon_goal += [0x02, world.crystals_needed_for_ganon[player]] elif world.goal[player] in ['ganonhunt']: ganon_goal += [0x88] # triforce pieces + elif world.goal[player] in ['bosshunt']: + if world.bosshunt_include_agas[player]: + ganon_goal += [0x0C, world.bosses_ganon[player]] # total bosses + else: + ganon_goal += [0x05, world.bosses_ganon[player]] # prize bosses elif world.goal[player] in ['completionist']: ganon_goal += [0x81, 0x82, 0x06, 0x07, 0x89] # AD and max collection rate else: @@ -1455,6 +1463,60 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): or world.dropshuffle[player] != 'none' or world.pottery[player] not in ['none', 'cave']): rom.write_byte(0x18003A, 0x01) # show key counts on map pickup + loot_source = 0x09 + if world.prizeshuffle[player] != 'none': + loot_source |= 0x10 + if world.pottery[player] not in ['none', 'cave']: + loot_source |= 0x02 + if world.dropshuffle[player] != 'none': + loot_source |= 0x04 + rom.write_byte(0x1CFF10, loot_source) + + if world.showloot[player] == 'never': + rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + elif world.showloot[player] == 'presence': + rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + elif world.showloot[player] == 'compass': + rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00]) + rom.write_byte(0x1CFF11, 0x01) + elif world.showloot[player] == 'always': + rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00]) + rom.write_byte(0x1CFF11, 0x00) + + if world.showmap[player] == 'visited': + rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05]) + elif world.showmap[player] == 'map': + rom.write_bytes(0x1CFF00, [0x01, 0x05, 0x00, 0x05]) + elif world.showmap[player] == 'always': + rom.write_bytes(0x1CFF00, [0x05, 0x00, 0x00, 0x00]) + + loot_icons = 0x1CF900 + if world.bombbag[player]: + rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major + + triforce_piece_ids = [0x6B, 0x6C] + if world.treasure_hunt_count[player] > 20: + for triforce_piece_id in triforce_piece_ids: + rom.write_byte(loot_icons + triforce_piece_id, 0x04) + + crystal_ids = [0x20, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6] + if world.goal[player] in ['ganon', 'dungeons', 'crystals', 'trinity']: + crystal_category = 0x0D + else: + crystal_category = 0x06 + for crystal_id in crystal_ids: + rom.write_byte(loot_icons + crystal_id, crystal_category) + + pendant_ids = [0x37, 0x38, 0x39] + if world.goal[player] in ['pedestal', 'dungeons', 'trinity']: + pendant_category = 0x0C + else: + pendant_category = 0x06 + for pendant_id in pendant_ids: + rom.write_byte(loot_icons + pendant_id, pendant_category) + # compasses showing dungeon count compass_mode = 0x80 if world.compassshuffle[player] not in ['none', 'nearby'] else 0x00 if world.clock_mode != 'none' or world.dungeon_counters[player] == 'off': @@ -1601,12 +1663,28 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # b - Big Key # a - Small Key # + dungeon_items_menu = 0x00 + + if world.doorShuffle[player] not in ['vanilla', 'basic']: + dungeon_items_menu |= 0x0F + + if world.keyshuffle[player] not in ['none', 'universal']: + dungeon_items_menu |= 0x01 + + if world.bigkeyshuffle[player] != 'none': + dungeon_items_menu |= 0x02 + enable_menu_map_check = (world.overworld_map[player] != 'default' and world.shuffle[player] != 'vanilla') or world.prizeshuffle[player] not in ['none', 'dungeon', 'nearby'] - rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] not in ['none', 'universal'] else 0x00) - | (0x02 if world.bigkeyshuffle[player] != 'none' else 0x00) - | (0x04 if world.mapshuffle[player] != 'none' or enable_menu_map_check else 0x00) - | (0x08 if world.compassshuffle[player] != 'none' else 0x00) # free roaming items in menu - | (0x10 if world.logic[player] == 'nologic' else 0))) # boss icon + if world.mapshuffle[player] != 'none' or enable_menu_map_check: + dungeon_items_menu |= 0x04 + + if world.compassshuffle[player] != 'none': + dungeon_items_menu |= 0x08 + + if world.logic[player] == 'nologic' or world.goal[player] == 'bosshunt': + dungeon_items_menu |= 0x10 + + rom.write_byte(0x180045, dungeon_items_menu) def get_reveal_bytes(itemName): if world.prizeshuffle[player] != 'wild': @@ -1815,30 +1893,29 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None): # set rom name # 21 bytes - from Main import __version__ - from OverworldShuffle import __version__ as ORVersion if rom_header: if len(rom_header) > 21: raise Exception('ROM header too long. Max 21 bytes, found %d bytes.' % len(rom_header)) - elif '|' in rom_header: - gen, seedstring = rom_header.split('|', 1) - gen = f'{gen:<3}' - seedstring = f'{int(seedstring):09}' if seedstring.isdigit() else seedstring[:9] - rom.name = bytearray(f'OR{gen}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] - elif len(rom_header) <= 9: - seedstring = f'{int(rom_header):09}' if rom_header.isdigit() else rom_header - rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] - else: - rom.name = bytearray(rom_header, 'utf8')[:21] - else: - seedstring = f'{world.seed:09}' if isinstance(world.seed, int) else world.seed - rom.name = bytearray(f'OR{__version__.split("-")[0].replace(".","")[0:3]}_{team+1}_{player}_{seedstring}\0', 'utf8')[:21] + if world.players > 1 and len(rom_header) <= 12: + rom.name = bytearray(f"GK_{team + 1}_{player}_{rom_header}", 'utf8') + elif len(rom_header) <= 18: + rom.name = bytearray(f"GK_{rom_header}", 'utf8') + else: + rom.name = bytearray(rom_header, 'utf8') + else: + if world.players > 1: + rom.name = bytearray(f'GK_{team + 1}_{player}_{world.seed}', 'utf8') + else: + rom.name = bytearray(f'GK_{world.seed}', 'utf8') + + rom.name = rom.name[:21] rom.name.extend([0] * (21 - len(rom.name))) rom.write_bytes(0x7FC0, rom.name) - rom.write_bytes(0x138010, bytearray(__version__, 'utf8')) + rom.write_bytes(0x138010, bytearray(DRVersion, 'utf8')) rom.write_bytes(0x150010, bytearray(ORVersion, 'utf8')) + rom.write_bytes(0x1CEEF0, bytearray(GKVersion, 'utf8')) # set player names for p in range(1, min(world.players, 255) + 1): @@ -2712,12 +2789,18 @@ def write_strings(rom, world, player, team): tt['murahdahla'] = "Hello @. I\nam Murahdahla, brother of\nSahasrahla and Aginah. Behold the power of\ninvisibility.\n\n\n\n… … …\n\nWait! You can see me? I knew I should have\nhidden in a hollow tree. If you bring\n%d triforce pieces, I can reassemble it." % int(world.treasure_hunt_count[player]) elif world.goal[player] == 'ganonhunt': tt['sign_ganon'] = 'Go find the Triforce pieces to beat Ganon' + elif world.goal[player] == 'bosshunt': + bosshunt_count = '%d guardian%s of %sdungeons' % \ + (world.bosses_ganon[player], + '' if world.bosses_ganon[player] == 1 else 's', + '' if world.bosshunt_include_agas[player] else 'prize ') + tt['sign_ganon'] = 'To beat Ganon you must defeat %s.' % bosshunt_count elif world.goal[player] == 'completionist': tt['sign_ganon'] = 'Ganon only respects those who have done everything' tt['ganon_fall_in'] = Ganon1_texts[random.randint(0, len(Ganon1_texts) - 1)] tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!' tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!' - + def get_custom_goal_text(type): goal_text = world.custom_goals[player][type]['goaltext'] placeholder_count = goal_text.count('%d') diff --git a/Rules.py b/Rules.py index 67cc06bb..8707151e 100644 --- a/Rules.py +++ b/Rules.py @@ -90,6 +90,15 @@ def set_rules(world, player): add_rule(world.get_location('Ganon', player), lambda state: state.has_crystals(world.crystals_needed_for_ganon[player], player)) elif world.goal[player] == 'ganonhunt': add_rule(world.get_location('Ganon', player), lambda state: state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= int(state.world.treasure_hunt_count[player])) + elif world.goal[player] == 'bosshunt': + if world.bosshunt_include_agas[player]: + add_rule(world.get_location('Ganon', player), lambda state: + state.item_count('Beat Agahnim 1', player) + + state.item_count('Beat Agahnim 2', player) + + state.item_count('Beat Boss', player) >= world.bosses_ganon[player]) + else: + add_rule(world.get_location('Ganon', player), lambda state: + state.item_count('Beat Boss', player) >= world.bosses_ganon[player]) elif world.goal[player] == 'completionist': add_rule(world.get_location('Ganon', player), lambda state: state.everything(player)) diff --git a/Text.py b/Text.py index 7e753328..ea46a307 100644 --- a/Text.py +++ b/Text.py @@ -1781,7 +1781,7 @@ class TextTable(object): text['mastersword_pedestal_translated'] = CompressedTextMapper.convert("A test of strength: If you have 3 pendants, I'm yours.") text['telepathic_tile_spectacle_rock'] = CompressedTextMapper.convert("{NOBORDER}\n{NOBORDER}\nUse the Mirror, or the Hookshot and Hammer, to get to Tower of Hera!") text['telepathic_tile_swamp_entrance'] = CompressedTextMapper.convert("{NOBORDER}\nDrain the floodgate to raise the water here!") - text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hate's bright light.") + text['telepathic_tile_thieves_town_upstairs'] = CompressedTextMapper.convert("{NOBORDER}\nBlind hates bright light.") text['telepathic_tile_misery_mire'] = CompressedTextMapper.convert("{NOBORDER}\nLighting 4 torches will open your way forward!") text['hylian_text_2'] = CompressedTextMapper.convert("%%^= %==%\n ^ =%^=\n==%= ^^%^") text['desert_entry_translated'] = CompressedTextMapper.convert("Kneel before this stone, and magic will move around you.") @@ -2015,7 +2015,7 @@ class TextTable(object): text['thief_desert_rupee_cave'] = CompressedTextMapper.convert("So you, like, busted down my door, and are being a jerk by talking to me? Normally I would be angry and make you pay for it, but I bet you're just going to break all my pots and steal my 50 rupees.") text['thief_ice_rupee_cave'] = CompressedTextMapper.convert("I'm a rupee pot farmer. One day I will take over the world with my skillz. Have you met my brother in the desert? He's way richer than I am.") text['telepathic_tile_south_east_darkworld_cave'] = CompressedTextMapper.convert("~~ dev cave ~~\n no farming\n required") - text['cukeman'] = CompressedTextMapper.convert("Did you hear that Veetorp beat ajneb174 in a 1 on 1 race at AGDQ?") + text['cukeman'] = CompressedTextMapper.convert("Trans rights!") text['cukeman_2'] = CompressedTextMapper.convert("You found Shabadoo, huh?\nNiiiiice.") text['potion_shop_no_cash'] = CompressedTextMapper.convert("Yo! I'm not running a charity here.") text['kakariko_powdered_chicken'] = CompressedTextMapper.convert("Smallhacker…\n\n\nWas hiding, you found me!\n\n\nOkay, you can leave now.") diff --git a/Utils.py b/Utils.py index d7436309..1f3663e4 100644 --- a/Utils.py +++ b/Utils.py @@ -8,6 +8,7 @@ import urllib.parse import urllib.request import xml.etree.ElementTree as ET from collections import defaultdict +from hashlib import md5 from itertools import count from math import factorial from pathlib import Path @@ -102,31 +103,13 @@ def close_console(): pass -def make_new_base2current(old_rom='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', new_rom='working.sfc'): - import hashlib - import json - from collections import OrderedDict - with open(old_rom, 'rb') as stream: - old_rom_data = bytearray(stream.read()) +def get_new_romhash(new_rom='working.sfc'): with open(new_rom, 'rb') as stream: new_rom_data = bytearray(stream.read()) - # extend to 2 mb - old_rom_data.extend(bytearray([0x00] * (2097152 - len(old_rom_data)))) - out_data = OrderedDict() - for idx, old in enumerate(old_rom_data): - new = new_rom_data[idx] - if old != new: - out_data[idx] = [int(new)] - for offset in reversed(list(out_data.keys())): - if offset - 1 in out_data: - out_data[offset-1].extend(out_data.pop(offset)) - with open('data/base2current.json', 'wt') as outfile: - json.dump([{key: value} for key, value in out_data.items()], outfile, separators=(",", ":")) - - basemd5 = hashlib.md5() + basemd5 = md5() basemd5.update(new_rom_data) - return "New Rom Hash: " + basemd5.hexdigest() + return basemd5.hexdigest() def kth_combination(k, l, r): @@ -768,18 +751,18 @@ class bidict(dict): super(bidict, self).__init__(*args, **kwargs) self.inverse = {} for key, value in self.items(): - self.inverse.setdefault(value,[]).append(key) + self.inverse.setdefault(value,[]).append(key) def __setitem__(self, key, value): if key in self: - self.inverse[self[key]].remove(key) + self.inverse[self[key]].remove(key) super(bidict, self).__setitem__(key, value) - self.inverse.setdefault(value,[]).append(key) + self.inverse.setdefault(value,[]).append(key) def __delitem__(self, key): value = self[key] self.inverse.setdefault(value,[]).remove(key) - if value in self.inverse and not self.inverse[value]: + if value in self.inverse and not self.inverse[value]: del self.inverse[value] super(bidict, self).__delitem__(key) @@ -787,12 +770,11 @@ class bidict(dict): class HexInt(int): pass def hex_representer(dumper, data): - import yaml return yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#0{4}x}") if __name__ == '__main__': - print(make_new_base2current()) + print("New Rom Hash:", get_new_romhash()) # read_entrance_data(old_rom=sys.argv[1]) # room_palette_data(old_rom=sys.argv[1]) # extract_data_from_us_rom(sys.argv[1]) diff --git a/Versions.py b/Versions.py new file mode 100644 index 00000000..639ac093 --- /dev/null +++ b/Versions.py @@ -0,0 +1,3 @@ +GKVersion = '1.0.0' +ORVersion = '0.6.1.7' +DRVersion = '1.5.0-u' diff --git a/build-app_version.py b/build-app_version.py deleted file mode 100644 index d1686b89..00000000 --- a/build-app_version.py +++ /dev/null @@ -1,6 +0,0 @@ -import os - -from OverworldShuffle import __version__ as OWVersion - -with(open(os.path.join("resources","app","meta","manifests","app_version.txt"),"w+")) as f: - f.write(OWVersion) diff --git a/data/base2current.bps b/data/base2current.bps index e840822a..79683ba6 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/pyproject.toml b/pyproject.toml index 5d0023a2..872ff602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "alttpr-python" -version = "0.2.0" +version = "1.0.0" description = "Python ALttP Randomizer" readme = "README.md" requires-python = ">=3.7" diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 9b26e24f..591bc892 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -72,6 +72,7 @@ "trinity", "crystals", "ganonhunt", + "bosshunt", "completionist", "sanctuary" ] @@ -299,6 +300,27 @@ "random" ] }, + "bosses_ganon": { + "choices": [ + "12", + "11", + "10", + "9", + "8", + "7", + "6", + "5", + "4", + "3", + "2", + "1", + "0" + ] + }, + "bosshunt_include_agas": { + "action": "store_true", + "type": "bool" + }, "crystals_gt": { "choices": [ "7", @@ -431,6 +453,21 @@ "wild" ] }, + "showloot": { + "choices": [ + "never", + "presence", + "compass", + "always" + ] + }, + "showmap": { + "choices": [ + "visited", + "map", + "always" + ] + }, "keysanity": { "action": "store_true", "type": "bool", diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index c8b427b6..fa4f015e 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -2,7 +2,7 @@ "cli": { "yes": "Yes", "no": "No", - "app.title": "ALttP Overworld Randomizer Version %s : --seed %s --code %s", + "app.title": "ALttP GwaaKiwi Randomizer Version %s : --seed %s", "version": "Version", "seed": "Seed", "player": "Player", diff --git a/source/classes/appversion.py b/source/classes/appversion.py deleted file mode 100644 index 527d8212..00000000 --- a/source/classes/appversion.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from OverworldShuffle import __version__ - -OWR_VERSION = __version__ - -def write_appversion(): - APP_VERSION = OWR_VERSION - if "-" in APP_VERSION: - APP_VERSION = APP_VERSION[:APP_VERSION.find("-")] - APP_VERSION_FILE = os.path.join(".","resources","app","meta","manifests","app_version.txt") - with open(APP_VERSION_FILE,"w") as f: - f.seek(0) - f.truncate() - f.write(APP_VERSION) - -if __name__ == "__main__": - write_appversion() diff --git a/source/classes/diags.py b/source/classes/diags.py index 80791519..d971fc95 100644 --- a/source/classes/diags.py +++ b/source/classes/diags.py @@ -9,58 +9,51 @@ except ModuleNotFoundError as e: pass import datetime -from Main import __version__ +from Versions import DRVersion, GKVersion, ORVersion -DR_VERSION = __version__ - -from OverworldShuffle import __version__ - -OWR_VERSION = __version__ - -PROJECT_NAME = "ALttP Overworld Randomizer" def diagpad(str): - return str.ljust(len(f"{PROJECT_NAME} Version") + 5,'.') + return str.ljust(40, '.') def output(): - lines = [ - f"{PROJECT_NAME} Diagnostics", - "=================================", - diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19], - diagpad("ALttP Door Randomizer Version") + DR_VERSION, - diagpad(f"{PROJECT_NAME} Version") + OWR_VERSION, - diagpad("Python Version") + platform.python_version() - ] - lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release())) - if hasattr(sys, "executable"): - lines.append(diagpad("Executable") + sys.executable) - lines.append(diagpad("Build Date") + platform.python_build()[1]) - lines.append(diagpad("Compiler") + platform.python_compiler()) - if hasattr(sys, "api_version"): - lines.append(diagpad("Python API") + str(sys.api_version)) - if hasattr(os, "sep"): - lines.append(diagpad("Filepath Separator") + os.sep) - if hasattr(os, "pathsep"): - lines.append(diagpad("Path Env Separator") + os.pathsep) - lines.append("") - lines.append("Packages") - lines.append("--------") - ''' - #this breaks when run from the .exe - reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) - installed_packages = [r.decode() for r in reqs.split()] - for pkg in installed_packages: - pkg = pkg.split("==") - lines.append(diagpad(pkg[0]) + pkg[1]) - ''' - installed_packages = [] - installed_packages = [str(d) for d in pkg_resources.working_set] #this doesn't work from the .exe either, but it doesn't crash the program - installed_packages.sort() - for pkg in installed_packages: - pkg = pkg.split(' ') - lines.append(diagpad(pkg[0]) + pkg[1]) + lines = [ + "ALttP GwaaKiwi Randomizer Diagnostics", + "=====================================", + diagpad("UTC Time") + str(datetime.datetime.now(datetime.UTC))[:19], + diagpad("ALttP Door Randomizer Version") + DRVersion, + diagpad("ALttP Overworld Randomizer Version") + ORVersion, + diagpad("ALttP GwaaKiwi Randomizer Version") + GKVersion, + diagpad("Python Version") + platform.python_version(), + ] + lines.append(diagpad("OS Version") + "%s %s" % (platform.system(), platform.release())) + if hasattr(sys, "executable"): + lines.append(diagpad("Executable") + sys.executable) + lines.append(diagpad("Build Date") + platform.python_build()[1]) + lines.append(diagpad("Compiler") + platform.python_compiler()) + if hasattr(sys, "api_version"): + lines.append(diagpad("Python API") + str(sys.api_version)) + if hasattr(os, "sep"): + lines.append(diagpad("Filepath Separator") + os.sep) + if hasattr(os, "pathsep"): + lines.append(diagpad("Path Env Separator") + os.pathsep) + lines.append("") - return lines + lines.append("Packages") + lines.append("--------") + reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) + installed_packages = [r.decode() for r in reqs.split()] + for pkg in installed_packages: + pkg = pkg.split("==") + lines.append(diagpad(pkg[0]) + pkg[1]) + + installed_packages = [] + installed_packages = [str(d) for d in pkg_resources.working_set] + installed_packages.sort() + for pkg in installed_packages: + pkg = pkg.split(' ') + lines.append(diagpad(pkg[0]) + pkg[1]) + + return lines if __name__ == "__main__": raise AssertionError(f"Called main() on utility library {__file__}") diff --git a/source/enemizer/SpriteSheets.py b/source/enemizer/SpriteSheets.py index 97235910..47b79e92 100644 --- a/source/enemizer/SpriteSheets.py +++ b/source/enemizer/SpriteSheets.py @@ -471,10 +471,6 @@ vanilla_sheets = [ (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x00), (0x00, 0x00, 0x00, 0x08), (0x5D, 0x49, 0x00, 0x52), (0x55, 0x49, 0x42, 0x43), - (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), - (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x62, 0x63, 0x50), - (0x61, 0x62, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x56, 0x63, 0x50), (0x61, 0x56, 0x57, 0x50), - (0x61, 0x56, 0x33, 0x50), (0x61, 0x56, 0x57, 0x50), (0x61, 0x62, 0x63, 0x50), (0x61, 0x62, 0x63, 0x50) ] required_boss_sheets = {EnemySprite.ArmosKnight: 9, EnemySprite.Lanmolas: 11, EnemySprite.Moldorm: 12, diff --git a/source/gui/randomize/generation.py b/source/gui/randomize/generation.py index 37f28d68..65e546ca 100644 --- a/source/gui/randomize/generation.py +++ b/source/gui/randomize/generation.py @@ -20,8 +20,8 @@ from tkinter import ( import source.classes.diags as diagnostics import source.gui.widgets as widgets -from Main import __version__ from source.classes.Empty import Empty +from Versions import DRVersion def generation_page(parent,settings): @@ -165,9 +165,9 @@ def generation_page(parent,settings): "width": 120, "height": 50 } - } + } diag = Tk() - diag.title("Door Shuffle " + __version__) + diag.title("Door Shuffle " + DRVersion) diag.geometry(str(dims["window"]["width"]) + 'x' + str(dims["window"]["height"])) text = Text(diag, width=dims["textarea.characters"]["width"], height=dims["textarea.characters"]["height"]) text.pack() diff --git a/uv.lock b/uv.lock index dc2946cf..81ab815b 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,7 @@ resolution-markers = [ name = "aenum" version = "3.1.16" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/7a/61ed58e8be9e30c3fe518899cc78c284896d246d51381bab59b5db11e1f3/aenum-3.1.16.tar.gz", hash = "sha256:bfaf9589bdb418ee3a986d85750c7318d9d2839c1b1a1d6fe8fc53ec201cf140", size = 137693, upload-time = "2026-01-12T22:34:38.819Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/52/6ad8f63ec8da1bf40f96996d25d5b650fdd38f5975f8c813732c47388f18/aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf", size = 165627, upload-time = "2025-04-25T03:17:58.89Z" }, ] @@ -41,8 +42,8 @@ wheels = [ ] [[package]] -name = "alttpoverworldrandomizer" -version = "0.1.0" +name = "alttpr-python" +version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "aenum" },