Add random Ganon vulnerability item.

This commit is contained in:
2025-09-03 19:58:38 -05:00
parent 74a676ac17
commit c97260a337
9 changed files with 199 additions and 35 deletions

View File

@@ -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])

3
CLI.py
View File

@@ -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",

27
Main.py
View File

@@ -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()

98
Rom.py
View File

@@ -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)

View File

@@ -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

25
Text.py
View File

@@ -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 = [

Binary file not shown.

View File

@@ -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"

View File

@@ -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).",
"<item>: 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." ,