diff --git a/BaseClasses.py b/BaseClasses.py index 4e4b8827..3a0a1204 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -74,6 +74,8 @@ class World(object): self.dark_rooms = {} self.damage_challenge = {} self.shuffle_damage_table = {} + self.ganon_item = {} + self.ganon_item_orig = {} self.custom = custom self.customitemarray = customitemarray self.can_take_damage = {} @@ -161,8 +163,10 @@ 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('ganon_item', 'silver') set_player_attr('crystals_ganon_orig', {}) set_player_attr('crystals_gt_orig', {}) + set_player_attr('ganon_item_orig', 'silver') set_player_attr('open_pyramid', 'auto') set_player_attr('take_any', 'none') set_player_attr('treasure_hunt_icon', 'Triforce Piece') @@ -1346,6 +1350,42 @@ class CollectionState(object): def has_blunt_weapon(self, player): return self.has_sword(player) or self.has('Hammer', player) + def can_hit_stunned_ganon(self, player): + ganon_item = self.world.ganon_item[player] + if ganon_item == "silver": + return self.has("Silver Arroys", player) and self.can_shoot_arrows(player) + elif ganon_item == "boomerang": + return self.has("Blue Boomerang", player) or self.has("Red Boomerang", player) + elif ganon_item == "hookshot": + return self.has("Hookshot", player) + elif ganon_item == "bomb": + return self.can_use_bombs(player) + elif ganon_item == "powder": + return self.has("Magic Powder", player) + elif ganon_item == "fire_rod": + return self.has("Fire Rod", player) + elif ganon_item == "ice_rod": + return self.has("Ice Rod", player) + elif ganon_item == "bombos": + return self.has("Bombos", player) and self.can_use_medallions(player) + elif ganon_item == "ether": + return self.has("Ether", player) and self.can_use_medallions(player) + elif ganon_item == "quake": + return self.has("Quake", player) and self.can_use_medallions(player) + elif ganon_item == "hammer": + return self.has("Hammer", player) + elif ganon_item == "bee": + return (self.has_bottle(player) and (self.has("Bug Catching Net", player) or self.can_buy_unlimited("Bee", player))) + elif ganon_item == "somaria": + return self.has("Cane of Somaria", player) + elif ganon_item == "byrna": + return self.has("Cane of Byrna", player) + else: + return False + + def can_use_medallions(self, player): + return self.has_sword(player) + def has_Mirror(self, player): return self.has('Magic Mirror', player) @@ -1364,7 +1404,7 @@ class CollectionState(object): return self.has('Ocarina (Activated)', player) def can_melt_things(self, player): - return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player)) + return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.can_use_medallions(player)) def can_avoid_lasers(self, player): return (self.has('Mirror Shield', player) or self.has('Cape', player) @@ -3051,6 +3091,7 @@ class Spoiler(object): 'beemizer': self.world.beemizer, 'gt_crystals': self.world.crystals_needed_for_gt, 'ganon_crystals': self.world.crystals_needed_for_ganon, + 'ganon_item': self.world.ganon_item, 'open_pyramid': self.world.open_pyramid, 'accessibility': self.world.accessibility, 'restricted_boss_items': self.world.restrict_boss_items, @@ -3263,6 +3304,7 @@ class Spoiler(object): outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player]) outfile.write('Crystals Required for GT:'.ljust(line_width) + '%s\n' % str(self.world.crystals_gt_orig[player])) outfile.write('Crystals Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.crystals_ganon_orig[player])) + outfile.write('Item Required for Ganon:'.ljust(line_width) + '%s\n' % str(self.world.ganon_item_orig[player])) outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player]) outfile.write('\n') outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player]) diff --git a/CLI.py b/CLI.py index 8f51760c..4bedd4fd 100644 --- a/CLI.py +++ b/CLI.py @@ -133,7 +133,7 @@ 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', 'openpyramid', + 'shuffle', 'door_shuffle', 'intensity', 'crystals_ganon', 'crystals_gt', 'ganon_item', 'openpyramid', 'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'prizeshuffle', 'startinventory', 'usestartinventory', 'bombbag', 'shuffleganon', 'overworld_map', 'restrict_boss_items', 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', @@ -177,6 +177,7 @@ def parse_settings(): "goal": "ganon", "crystals_gt": "7", "crystals_ganon": "7", + "ganon_item": "silver", "swords": "random", "flute_mode": "normal", "bow_mode": "progressive", diff --git a/Main.py b/Main.py index 67c4c01e..4f476a6a 100644 --- a/Main.py +++ b/Main.py @@ -58,6 +58,26 @@ def check_python_version(): logging.warning(BabelFish().translate("cli","cli","old.python.version"), sys.version) +def random_ganon_item(sword_mode): + options = [ + "silver", + "boomerang", + "hookshot", + "powder", + "fire_rod", + "ice_rod", + "hammer", + "bee", + "somaria", + "byrna", + "bombos", + "ether", + "quake", + ] + max_choice = len(options) - 3 if sword_mode == "swordless" else len(options) + return options[random.randint(0, max_choice - 1)] + + def main(args, seed=None, fish=None): check_python_version() @@ -99,6 +119,8 @@ def main(args, seed=None, fish=None): world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)} world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)} + world.ganon_item = {player: random_ganon_item(args.swords[player]) if args.ganon_item[player] == 'random' else args.ganon_item[player] for player in range(1, world.players + 1)} + world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)} world.treasure_hunt_count = {} @@ -479,6 +501,7 @@ def init_world(args, fish): 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.owTerrain = args.ow_terrain.copy() world.owKeepSimilar = args.ow_keepsimilar.copy() world.owWhirlpoolShuffle = args.ow_whirlpool.copy() @@ -604,8 +627,10 @@ def copy_world(world): ret.bow_mode = world.bow_mode.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.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() + ret.ganon_item_orig = world.ganon_item_orig.copy() ret.owTerrain = world.owTerrain.copy() ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() @@ -829,8 +854,10 @@ def copy_world_premature(world, player): ret.bow_mode = world.bow_mode.copy() ret.crystals_needed_for_ganon = world.crystals_needed_for_ganon.copy() ret.crystals_needed_for_gt = world.crystals_needed_for_gt.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() + ret.ganon_item_orig = world.ganon_item_orig.copy() ret.owTerrain = world.owTerrain.copy() ret.owKeepSimilar = world.owKeepSimilar.copy() ret.owWhirlpoolShuffle = world.owWhirlpoolShuffle.copy() diff --git a/Rom.py b/Rom.py index b77523c8..f553529d 100644 --- a/Rom.py +++ b/Rom.py @@ -22,7 +22,7 @@ from Dungeons import dungeon_music_addresses, dungeon_table from Regions import location_table, shop_to_location_table, retro_shops from RoomData import DoorKind from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable -from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, TavernMan_texts, Sahasrahla2_texts +from Text import Uncle_texts, Ganon1_texts, Ganon_Phase_3_No_Silvers_texts, Ganon_Phase_3_No_Weakness_texts, TavernMan_texts, Sahasrahla2_texts from Text import Triforce_texts, Blind_texts, BombShop2_texts, junk_texts from Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, DeathMountain_texts from Text import LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts @@ -44,7 +44,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = 'b2566e746c7694b21cb19566763c42e3' +RANDOMIZERBASEHASH = 'f0d684ceb9639cb9bb3b13b52a7cea66' class JsonRom(object): @@ -1292,6 +1292,25 @@ def patch_rom(world, rom, player, team, is_mystery=False): rom.write_byte(0x1801A6, world.crystals_needed_for_ganon[player]) rom.write_byte(0x1801A2, 0x00) # ped requirement is vanilla, set to 0x1 for special requirements + ganon_item_byte = { + "silver": 0x00, + "boomerang": 0x02, + "hookshot": 0x03, + "bomb": 0x04, + "powder": 0x05, + "fire_rod": 0x06, + "ice_rod": 0x07, + "bombos": 0x08, + "ether": 0x09, + "quake": 0x0A, + "hammer": 0x0C, + "bee": 0x10, + "somaria": 0x11, + "byrna": 0x12, + "none": 0xFF, + } + rom.write_byte(0x18002E, ganon_item_byte[world.ganon_item[player]]) + # block HC upstairs doors in rain state in standard mode prevent_rain = world.mode[player] == 'standard' and world.shuffle[player] != 'vanilla' and world.logic[player] != 'nologic' rom.write_byte(0x18008A, 0x01 if prevent_rain else 0x00) @@ -2420,29 +2439,64 @@ def write_strings(rom, world, player, team): no_silver_text = Ganon_Phase_3_No_Silvers_texts[random.randint(0, len(Ganon_Phase_3_No_Silvers_texts) - 1)] - silverarrows = world.find_items('Silver Arrows', player) - random.shuffle(silverarrows) - if silverarrows: - hint_phrase = hint_text(silverarrows[0]).replace("Ganon's", "my") - silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' - else: - silverarrow_hint = no_silver_text - tt['ganon_phase_3_no_silvers'] = silverarrow_hint - tt['ganon_phase_3_no_silvers_alt'] = silverarrow_hint - - prog_bow_locs = world.find_items('Progressive Bow', player) - distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None) - progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or world.swords[player] == 'swordless' - if distinguished_prog_bow_loc: - prog_bow_locs.remove(distinguished_prog_bow_loc) - hint_phrase = hint_text(distinguished_prog_bow_loc).replace("Ganon's", "my") - silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text + ganon_item = world.ganon_item[player] + if ganon_item == "silver": + silverarrows = world.find_items('Silver Arrows', player) + random.shuffle(silverarrows) + if silverarrows: + hint_phrase = hint_text(silverarrows[0]).replace("Ganon's", "my") + silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' + else: + silverarrow_hint = no_silver_text tt['ganon_phase_3_no_silvers'] = silverarrow_hint - if any(prog_bow_locs): - hint_phrase = hint_text(random.choice(prog_bow_locs)).replace("Ganon's", "my") - silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text tt['ganon_phase_3_no_silvers_alt'] = silverarrow_hint + prog_bow_locs = world.find_items('Progressive Bow', player) + distinguished_prog_bow_loc = next((location for location in prog_bow_locs if location.item.code == 0x65), None) + progressive_silvers = world.difficulty_requirements[player].progressive_bow_limit >= 2 or world.swords[player] == 'swordless' + if distinguished_prog_bow_loc: + prog_bow_locs.remove(distinguished_prog_bow_loc) + hint_phrase = hint_text(distinguished_prog_bow_loc).replace("Ganon's", "my") + silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text + tt['ganon_phase_3_no_silvers'] = silverarrow_hint + if any(prog_bow_locs): + hint_phrase = hint_text(random.choice(prog_bow_locs)).replace("Ganon's", "my") + silverarrow_hint = f'Did you find the silver arrows {hint_phrase}?' if progressive_silvers else no_silver_text + tt['ganon_phase_3_no_silvers_alt'] = silverarrow_hint + elif ganon_item == "bomb": + tt['ganon_phase_3_no_bow'] = "You can't best\nme without\nexplosives!" + tt['ganon_phase_3_silvers'] = "Explosives!\nMy one true\nweakness!" + elif ganon_item == "bee": + tt['ganon_phase_3_no_bow'] = "You can't best\nme without\na bee!" + tt['ganon_phase_3_silvers'] = "Oh no! A bee!\nMy one true\nweakness!" + elif ganon_item == "none": + no_weakness_text = Ganon_Phase_3_No_Weakness_texts[random.randint(0, len(Ganon_Phase_3_No_Weakness_texts) - 1)] + tt['ganon_phase_3_no_bow'] = no_weakness_text + tt['ganon_phase_3_silvers'] = no_weakness_text + else: + name_table = { + 'boomerang': ['a boomerang', 'A boomerang', 'Red Boomerang'], + 'hookshot': ['a hookshot', 'A hookshot', 'Hookshot'], + 'powder': ['the powder', 'Powder', 'Magic Powder'], + 'fire_rod': ['the fire rod', 'The fire rod', 'Fire Rod'], + 'ice_rod': ['the ice rod', 'The ice rod', 'Ice Rod'], + 'bombos': ['bombos', 'Bombos', 'Bombos'], + 'ether': ['ether', 'Ether', 'Ether'], + 'quake': ['quake', 'Quake', 'Quake'], + 'hammer': ['a hammer', 'A hammer', 'Hammer'], + 'somaria': ['somaria', 'Somaria', 'Cane of Somaria'], + 'byrna': ['byrna', 'Byrna', 'Cane of Byrna'], + } + locations = world.find_items(name_table[ganon_item][2], player) + random.shuffle(locations) + location_hint = (' %s?' % hint_text(locations[0]).replace("Ganon's", "my")) if locations else "?\nI think not!" + have_text = name_table[ganon_item][1] + none_text = name_table[ganon_item][0] + if len(have_text) <= 6: + have_text = "Oh no! %s" % have_text + tt['ganon_phase_3_no_bow'] = "Did you find %s%s" % (none_text, location_hint) + tt['ganon_phase_3_silvers'] = "%s!\nMy one true\nweakness!" % have_text + crystal5 = world.find_items('Crystal 5', player) crystal6 = world.find_items('Crystal 6', player) greenpendant = world.find_items('Green Pendant', player) diff --git a/Rules.py b/Rules.py index a79aec03..c548646f 100644 --- a/Rules.py +++ b/Rules.py @@ -862,8 +862,15 @@ def global_rules(world, player): add_mc_rule('Agahnim 1') add_mc_rule('Agahnim 2') - set_rule(world.get_location('Ganon', player), lambda state: state.has_beam_sword(player) and state.has_fire_source(player) - and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (state.has('Silver Arrows', player) and state.can_shoot_arrows(player)) or state.has('Lamp', player) or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times + set_rule( + world.get_location('Ganon', player), + lambda state: state.has_beam_sword(player) + and state.has_fire_source(player) + and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) + or state.can_hit_stunned_ganon(player) + or state.has('Lamp', player) + or state.can_extend_magic(player, 12))) # need to light torch a sufficient amount of times + set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has_beam_sword(player)) # need to damage ganon to get tiles to drop diff --git a/Text.py b/Text.py index 1a66b22f..eb7af9b6 100644 --- a/Text.py +++ b/Text.py @@ -234,30 +234,37 @@ Ganon_Phase_3_No_Silvers_texts = [ "Did you find\nthe arrows on\nThe Moon?", "Did you find\nthe arrows\nIn dev null?", "I have sold\nthe arrows for\na green big 20", - "Did you find\nThe arrows in\nCount Dracula?", + "Did you find\nthe arrows in\nCount Dracula?", "Error 404\nSilver arrows\nnot found.", - "No arrows for\nYou today,\nSorry", + "No arrows for\nyou today.\nSorry.", "No arrows?\nCheck your\njunk mail." - "Careful, all\nthat spinning\nmakes me dizzy", "Did you find\nthe arrows in\nJabu's belly?", "Silver is not\nan appropriate\narrow material", "Did you find\nthe arrows in\nNarnia?", - "Are you ready\nTo spin\nTo win?", "DID YOU FIND\nTHE ARROWS IN\nKEFKA'S TOWER", - "Did you find\nthe arrows in\nRecycle Bin?", + "Did you find\nthe arrows in\nyour Recycle Bin?", "Silver Arrows?\n\nLUL", "Imagine\nfinding the\narrows", "Did you find\nsilvers in\nscenic Ohio?", - "Did you find\nThe arrows in\n*mumblemumble*", - "\nSpin To Win!\n", - "did you find\nthe arrows in\nthe hourglass?", + "Did you find\nthe arrows in\n*mumblemumble*", + "Did you find\nthe arrows in\nthe hourglass?", "Silver Arrows\nare so v30", "OH, NO, THEY\nACTUALLY SAID\nSILVER MARROW", "SURELY THE\nLEFTMOST TILES\nWILL STAY UP", "Did you find\nthe arrows in\nWorld 4-2?", + "SILLY HERO,\nSILVER IS FOR\nWEREWOLVES!", + "Did you find\nthe silvers in\nGanti's ears", +] + +Ganon_Phase_3_No_Weakness_texts = [ + "Error 404\nWeakness\nnot found.", + "No weakness\nfor you today.\nSorry", + "Careful, all\nthat spinning\nmakes me dizzy", + "Are you ready\nTo spin\nTo win?", + "\nSpin To Win!\n", + "SURELY THE\nLEFTMOST TILES\nWILL STAY UP", "You Spin Me\nRight Round\nLike A Record", "SILLY HERO,\nSILVER IS FOR\nWEREWOLVES!", - "Did you find\nthe silvers in\nganti's ears", ] TavernMan_texts = [ diff --git a/data/base2current.bps b/data/base2current.bps index 2099709d..5b012671 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 94308c69..26a2c49d 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -311,6 +311,25 @@ "random" ] }, + "ganon_item": { + "choices": [ + "silver", + "boomerang", + "hookshot", + "powder", + "fire_rod", + "ice_rod", + "bombos", + "ether", + "quake", + "hammer", + "bee", + "somaria", + "byrna", + "random", + "none" + ] + }, "beemizer": { "choices": [ "4", "3", "2", "1", "0" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index f554ff39..e5093801 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -331,6 +331,13 @@ "Random: Picks a random value between 0 and 7 (inclusive).", "0-7: Number of crystals needed" ], + "ganon_item": [ + "What item Ganon is vulnerable to while stunned in his final phase.", + "Silver: Silver arrows (as in the vanilla game).", + "Random: Picks a random damaging item (but not a medallion if swordless).", + ": The specified item will damage stunned ganon.", + "None: Stunned Ganon cannot be damaged. You must use the normal silverless methods." + ], "openpyramid": [ "Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it. (default: %(default)s)" ], "rom": [ "Path to an ALttP JP (1.0) rom to use as a base." ,