47 Commits

Author SHA1 Message Date
36e57bcdf4 Merge branch 'beta' 2026-06-07 13:47:14 -05:00
b72a04511b Universal keys don't despawn in shops, and will never get placed in bee shop 2026-06-07 13:21:08 -05:00
5beeea2f83 Update Baserom 2026-06-07 01:36:58 -05:00
df928f0378 Text updates 2026-06-07 01:35:25 -05:00
1644535557 Update baserom 2026-06-06 12:36:17 -05:00
dcc4121c43 Separate map and hud loot icons 2026-06-03 20:52:25 -05:00
444ebda072 Up triforce count before triforces become lower priority 2026-06-01 18:00:38 -05:00
2055ed6d96 Minor gfx/patch updates 2026-05-29 19:21:17 -05:00
1a9cea452d Zora/Bottle Vendor ask about item before taking money when hints are on 2026-05-28 21:03:10 -05:00
8451a64984 Update baserom 2026-05-28 18:20:57 -05:00
f5dde931dc cave pots show on HUD 2026-05-28 17:20:17 -05:00
b82ec31f98 Update baserom 2026-05-28 06:30:13 -05:00
2ff19b3781 Update baserom 2026-05-27 23:16:45 -05:00
5278f9f2c6 Change more Dark Palace to Palace of Darkness to Appease Fouton 2026-05-25 10:51:55 -05:00
5f1c08920f Update baserom 2026-05-25 00:17:48 -05:00
57db996f27 Merge branch 'fouton_vanilla' into beta 2026-05-24 17:20:30 -05:00
5af7073ed1 Add bumper to enemy_deny 2026-05-24 17:16:51 -05:00
33cd2d076c Change intro text for mystery 2026-05-24 08:56:21 -05:00
cd5bc9a206 Update standard escape ammo refills to be more consistent 2026-05-19 22:46:00 -05:00
78819bbd9d Update standard escape ammo refills to be more consistent 2026-05-19 22:45:15 -05:00
b2d2aae61a Add RainDeathRefill spots to no_escape_assist 2026-05-19 20:33:23 -05:00
320e518519 Add no_escape_assist patch 2026-05-19 20:30:30 -05:00
c39a31b299 Add all_starting patch 2026-05-17 15:08:46 -05:00
0ef55c5f6a Merge branch 'beta' into fouton_vanilla 2026-05-16 22:21:40 -05:00
fb57e9fd34 Fix some enemy denials being overwritten 2026-05-16 22:18:51 -05:00
ab2e8f7818 Update baserom and enemy_deny.yaml 2026-05-16 21:29:42 -05:00
151765d8d2 Fix broken patches arg 2026-05-15 21:53:28 -05:00
1ae9da59bb Fix Sword and Shield item 2026-05-14 23:52:59 -05:00
84270544bf vanilla with universal keys 2026-05-14 22:28:41 -05:00
8ae1aefcf3 Add prize pack info to vanilla_placements 2026-05-14 22:09:49 -05:00
821c0ee46e Remove accidentally .bak file 2026-05-14 21:36:27 -05:00
a946c2b3b0 Allow applying patches to rom from CLI 2026-05-14 21:23:10 -05:00
ec89afaa5e Fix up junk items in vanilla_placements 2026-05-14 20:05:39 -05:00
d530b68e86 Fix crystal numbers 2026-05-14 10:31:03 -05:00
924bd7a616 Start of attempt for vanilla item placements 2026-05-14 00:14:54 -05:00
cdfdc15219 Baserom update and bugfix 2026-05-13 00:59:03 -05:00
d2a5bb56d8 One more fix for the road... 2026-05-11 23:26:07 -05:00
e86312f145 Fix broken 2026-05-11 23:24:03 -05:00
97aceb3f43 Update hash alphabet strings for clarity 2026-05-11 23:17:47 -05:00
b6f73ffda4 Update hashes in spoiler 2026-05-11 23:06:47 -05:00
c4687a3be6 Update baserom, add hashes to meta 2026-05-11 22:49:15 -05:00
4d63168e9a Bugfix update baserom 2026-05-04 17:54:09 -05:00
6474975b07 loothud option 2026-05-04 08:53:42 -05:00
4de84df7a8 Update baserom 2026-05-04 00:38:48 -05:00
b3afa57677 Show level of current supertile loot in hud 2026-05-04 00:14:28 -05:00
24ebf3037a Merge branch 'main' into beta_2 2026-05-03 17:38:13 -05:00
d64a58f636 extra_keys setting for crossed door rando 2026-05-03 17:37:40 -05:00
23 changed files with 296 additions and 141 deletions

View File

@@ -164,6 +164,7 @@ class World(object):
set_player_attr('bigkeyshuffle', 'none')
set_player_attr('prizeshuffle', 'none')
set_player_attr('showloot', 'never')
set_player_attr('loothud', 'never')
set_player_attr('showmap', 'map')
set_player_attr('restrict_boss_items', 'none')
set_player_attr('bombbag', False)
@@ -202,6 +203,7 @@ class World(object):
set_player_attr('damage_challenge', 'normal')
set_player_attr('shuffle_damage_table', 'vanilla')
set_player_attr('crystal_book', False)
set_player_attr('extra_keys', 0)
set_player_attr('collection_rate', False)
set_player_attr('colorizepots', True)
set_player_attr('pot_pool', {})
@@ -1517,7 +1519,10 @@ class CollectionState(object):
if not item:
return
changed = False
if item.name.startswith('Progressive '):
if item.name == "Sword and Shield":
self.prog_items["Fighter Sword", item.player] += 1
self.prog_items["Blue Shield", item.player] += 1
elif item.name.startswith('Progressive '):
if 'Sword' in item.name:
if self.has('Golden Sword', item.player):
pass
@@ -1978,6 +1983,7 @@ class Dungeon(object):
self.prize = None
self.big_key = big_key
self.small_keys = small_keys
self.extra_small_keys = 0
self.dungeon_items = dungeon_items
self.bosses = dict()
self.player = player
@@ -2892,6 +2898,10 @@ class Item(object):
def compass(self):
return self.type == 'Compass'
@property
def event(self):
return self.type == 'Event'
@property
def dungeon(self):
if not self.smallkey and not self.bigkey and not self.map and not self.compass:
@@ -3154,6 +3164,7 @@ class Spoiler(object):
'bigkeyshuffle': self.world.bigkeyshuffle,
'prizeshuffle': self.world.prizeshuffle,
'showloot': self.world.showloot,
'loothud': self.world.loothud,
'showmap': self.world.showmap,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
@@ -3174,6 +3185,7 @@ class Spoiler(object):
'damage_challenge': self.world.damage_challenge,
'shuffle_damage_table': self.world.shuffle_damage_table,
'crystal_book': self.world.crystal_book,
'extra_keys': self.world.extra_keys,
'triforcegoal': self.world.treasure_hunt_count,
'triforcepool': self.world.treasure_hunt_total,
'race': self.world.settings.world_rep['meta']['race'],
@@ -3181,6 +3193,8 @@ class Spoiler(object):
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)},
'seed': self.world.seed
}
if self.hashes:
self.metadata['hash'] = {p: self.hashes[p, 0] for p in range(1, self.world.players + 1)} # TODO: make this work for multiple teams
for p in range(1, self.world.players + 1):
from ItemList import set_default_triforce
@@ -3427,6 +3441,7 @@ class Spoiler(object):
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 Value of Checks on HUD:'.ljust(line_width) + '%s\n' % self.metadata['loothud'][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')
@@ -3453,6 +3468,7 @@ class Spoiler(object):
outfile.write('Damage Challenge:'.ljust(line_width) + '%s\n' % self.metadata['damage_challenge'][player])
outfile.write('Damage Table Randomization:'.ljust(line_width) + '%s\n' % self.metadata['shuffle_damage_table'][player])
outfile.write('Crystal Book:'.ljust(line_width) + '%s\n' % yn(self.metadata['crystal_book'][player]))
outfile.write('Extra Keys:'.ljust(line_width) + '%d%%\n' % self.metadata['extra_keys'][player])
outfile.write('Hints:'.ljust(line_width) + '%s\n' % yn(self.metadata['hints'][player]))
outfile.write('Race:'.ljust(line_width) + '%s\n' % yn(self.world.settings.world_rep['meta']['race']))

63
CLI.py
View File

@@ -89,6 +89,7 @@ def parse_cli(argv, no_defaults=False):
parser.add_argument('--count', default=defval(int(settings["count"]) if settings["count"] != "" and settings["count"] is not None else 1), help="\n".join(fish.translate("cli", "help", "count")), type=int)
parser.add_argument('--tries', default=defval(int(settings["tries"]) if settings["tries"] != "" and settings["tries"] is not None else 1), help="\n".join(fish.translate("cli", "help", "tries")), type=int)
parser.add_argument('--customitemarray', default={}, help=argparse.SUPPRESS)
parser.add_argument('--skip_money_balance', action="store_true", help=argparse.SUPPRESS)
# included for backwards compatibility
parser.add_argument('--multi', default=defval(settings["multi"]), type=lambda value: min(max(int(value), 1), 255))
@@ -106,7 +107,14 @@ 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
if ret.mapshuffle == "none":
ret.mapshuffle = "wild"
if ret.compassshuffle == "none":
ret.compassshuffle = "wild"
if ret.keyshuffle == "none":
ret.keyshuffle = "wild"
if ret.bigkeyshuffle == "none":
ret.bigkeyshuffle = "wild"
if ret.keydropshuffle:
ret.dropshuffle = 'keys' if ret.dropshuffle == 'none' else ret.dropshuffle
@@ -153,31 +161,32 @@ def parse_cli(argv, no_defaults=False):
'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', 'skullwoods', 'linked_drops',
'pseudoboots', 'mirrorscroll', 'dark_rooms',
'damage_challenge', 'shuffle_damage_table',
'crystal_book', 'retro', 'accessibility', 'hints',
'beemizer', 'experimental', 'dungeon_counters',
'shufflebosses', 'shuffleenemies', 'enemy_health',
'enemy_damage', 'shufflepots', 'ow_palettes',
'uw_palettes', 'sprite', 'triforce_gfx', 'disablemusic',
'quickswap', 'fastmenu', 'heartcolor', 'heartbeep',
'remote_items', 'shopsanity', 'dropshuffle', 'pottery',
'keydropshuffle', 'mixed_travel',
'standardize_palettes', 'code', 'reduce_flashing',
'shuffle_sfx', 'shuffle_sfxinstruments',
'shuffle_songinstruments', 'msu_resume',
'collection_rate', 'colorizepots', 'decoupledoors',
'door_type_mode', 'bonk_drops', 'trap_door_mode',
'key_logic_algorithm', 'door_self_loops',
'any_enemy_logic', 'aga_randomness', 'money_balance']:
'bigkeyshuffle', 'prizeshuffle', 'showloot', 'loothud',
'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',
'skullwoods', 'linked_drops', 'pseudoboots',
'mirrorscroll', 'dark_rooms', 'damage_challenge',
'shuffle_damage_table', 'crystal_book', 'extra_keys',
'retro', 'accessibility', 'hints', 'beemizer',
'experimental', 'dungeon_counters', 'shufflebosses',
'shuffleenemies', 'enemy_health', 'enemy_damage',
'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite',
'triforce_gfx', 'disablemusic', 'quickswap', 'fastmenu',
'heartcolor', 'heartbeep', 'remote_items', 'shopsanity',
'dropshuffle', 'pottery', 'keydropshuffle',
'mixed_travel', 'standardize_palettes', 'code',
'reduce_flashing', 'shuffle_sfx',
'shuffle_sfxinstruments', 'shuffle_songinstruments',
'msu_resume', 'collection_rate', 'colorizepots',
'decoupledoors', 'door_type_mode', 'bonk_drops',
'trap_door_mode', 'key_logic_algorithm',
'door_self_loops', 'any_enemy_logic', 'aga_randomness',
'money_balance', 'patches']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1:
setattr(ret, name, {1: value})
@@ -254,6 +263,7 @@ def parse_settings():
"damage_challenge": "normal",
"shuffle_damage_table": "vanilla",
"crystal_book": False,
"extra_keys": 0,
"shuffleenemies": "none",
"shufflebosses": "none",
@@ -273,6 +283,7 @@ def parse_settings():
"bigkeyshuffle": "none",
"prizeshuffle": "none",
"showloot": "never",
"loothud": "never",
"showmap": "map",
"keysanity": False,
"door_shuffle": "vanilla",

View File

@@ -3,6 +3,7 @@ import time
from collections import defaultdict, deque
from enum import Flag, unique
from itertools import chain
from math import ceil
from typing import DefaultDict, Dict, List
import RaceRandom as random
@@ -1599,7 +1600,11 @@ def assign_cross_keys(dungeon_builders, world, player):
if actual_chest_keys == 0:
dungeon.small_keys = []
else:
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * actual_chest_keys
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100)
logger.debug(f'Adding {extra_keys} extra small keys to {name}')
dungeon.extra_small_keys = extra_keys
created_keys = actual_chest_keys + extra_keys
dungeon.small_keys = [ItemFactory(dungeon_keys[name], player)] * created_keys
logger.info(f'{world.fish.translate("cli", "cli", "keydoor.shuffle.time.crossed")}: {time.process_time()-start}')
@@ -2171,7 +2176,10 @@ def shuffle_small_key_doors(door_type_pools, used_doors, start_regions_map, all_
if actual_chest_keys == 0:
dungeon.small_keys = []
else:
dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(actual_chest_keys)]
extra_keys = ceil(actual_chest_keys * world.extra_keys[player] / 100)
dungeon.extra_small_keys = extra_keys
created_keys = actual_chest_keys + extra_keys
dungeon.small_keys = [ItemFactory(dungeon_keys[dungeon_name], player) for _ in range(created_keys)]
for name, small_list in small_map.items():
used_doors.update(flatten_pair_list(small_list))

View File

@@ -252,6 +252,7 @@ def verify_spot_to_fill(location, item_to_place, max_exp_state, single_player_pl
or (world.algorithm == 'vanilla_fill' and item_to_place.is_near_dungeon_item(world)))) \
or valid_dungeon_placement(item_to_place, location, world):
return location
if item_to_place.smallkey or item_to_place.bigkey or item_to_place.prize:
location.item = None
location.event = False
@@ -311,7 +312,9 @@ def valid_dungeon_placement(item, location, world):
dungeon = check_dungeon
if dungeon:
layout = world.dungeon_layouts[location.player][dungeon.name]
if not is_dungeon_item(item, world) or item.player != location.player:
if item.event:
return True
elif not is_dungeon_item(item, world) or item.player != location.player:
if item.prize and item.is_near_dungeon_item(world):
return item.dungeon_object == dungeon and layout.free_items > 0
return layout.free_items > 0

View File

@@ -372,6 +372,13 @@ def generate_itempool(world, player):
world.push_precollected(ItemFactory(item, player))
if world.mode[player] == 'standard' and not world.state.has_blunt_weapon(player):
if world.customizer:
placements = world.customizer.get_placements()
if placements:
custom_uncle = placements.get(player, {}).get("Link's Uncle")
if custom_uncle:
placed_items["Link's Uncle"] = custom_uncle
if "Link's Uncle" not in placed_items:
found_sword = False
found_bow = False
@@ -865,7 +872,7 @@ def set_up_shops(world, player):
rss.locked = True
cap_shop = world.get_region('Capacity Upgrade', player).shop
cap_shop.inventory[1] = None # remove arrow capacity upgrades in retro
for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.player == player], 5):
for shop in random.sample([s for s in world.shops[player] if not s.locked and s.region.name != "Red Shield Shop" and s.region.player == player], 5):
shop.custom = True
shop.locked = True
if retro_bow:
@@ -1669,12 +1676,15 @@ def fill_specific_items(world):
item_to_place, event_flag = get_item_and_event_flag(item, world, player,
dungeon_pool, prize_set, prize_pool)
if item_to_place:
world.push_item(loc, item_to_place, False)
loc.locked = True
track_outside_keys(item_to_place, loc, world)
track_dungeon_items(item_to_place, loc, world)
loc.event = (event_flag or item_to_place.advancement
or item_to_place.bigkey or item_to_place.smallkey)
if not loc.item:
world.push_item(loc, item_to_place, False)
loc.locked = True
track_outside_keys(item_to_place, loc, world)
track_dungeon_items(item_to_place, loc, world)
loc.event = (event_flag or item_to_place.advancement
or item_to_place.bigkey or item_to_place.smallkey)
elif loc.item != item_to_place:
logging.getLogger('').warning("Failed to place item %s at location %s because it already contained %s", item_to_place, loc.name, loc.item)
else:
raise Exception(f'Did not find "{item}" in item pool to place at "{location}"')
advanced_placements = world.customizer.get_advanced_placements()
@@ -1802,7 +1812,7 @@ def shuffle_event_items(world, player):
continue
break
else:
raise FillError(f'Unable to place followers: {", ".join(list(map(lambda d: d.hint_text, follower_locations)))}')
raise FillError(f'Unable to place followers: {", ".join(list(map(lambda f: f.name, pickup_items)))}')
def get_item_and_event_flag(item, world, player, dungeon_pool, prize_set, prize_pool):

View File

@@ -55,7 +55,7 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Master Sword': (True, False, 'Sword', 0x50, 100, 'Master Sword!\nEvil\'s bane!', 'and the master sword', 'sword-wielding kid', 'glow sword for sale', 'fungus for blue slasher', 'sword boy fights again', 'the Master Sword'),
'Tempered Sword': (True, False, 'Sword', 0x02, 150, 'Tempered Sword!\nMore slashy!', 'the tempered sword', 'sword-wielding kid', 'flame sword for sale', 'fungus for red slasher', 'sword boy fights again', 'the Tempered Sword'),
'Fighter Sword': (True, False, 'Sword', 0x49, 50, 'Fighter Sword!\nStarter level slashy!', 'the tiny sword', 'sword-wielding kid', 'tiny sword for sale', 'fungus for tiny slasher', 'sword boy fights again', 'the Fighter Sword'),
'Sword and Shield': (True, False, 'Sword', 0x00, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'),
'Sword and Shield': (True, False, 'Sword', 0x00, 50, 'Sword and Shield!\nUncle sword ahoy!', 'the sword and shield', 'sword and shield-wielding kid', 'training set for sale', 'fungus for training set', 'sword and shield boy fights again', 'the small sword and shield'),
'Golden Sword': (True, False, 'Sword', 0x03, 200, 'Golden Sword!\nBest slashy!', 'and the butter sword', 'sword-wielding kid', 'butter for sale', 'cap churned to butter', 'sword boy fights again', 'the Golden Sword'),
'Progressive Sword': (True, False, 'Sword', 0x5E, 150, 'Sword!\nA better sword for your time!', 'the unknown sword', 'sword-wielding kid', 'sword for sale', 'fungus for some slasher', 'sword boy fights again', 'a Sword'),
'Progressive Glove': (True, False, None, 0x61, 150, 'Glove!\nLift more than you can now!', 'and the lift upgrade', 'body-building kid', 'some glove for sale', 'fungus for gloves', 'body-building boy lifts again', 'a Glove'),
@@ -134,9 +134,9 @@ item_table = {'Bow': (True, False, None, 0x0B, 200, 'Bow!\nJoin the archer class
'Big Key (Agahnims Tower)': (False, False, 'BigKey', 0x9B, 60, 'A big key for\nAgahnim\'s Tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Compass (Agahnims Tower)': (False, True, 'Compass', 0x8B, 10, 'A compass for\nAgahnim\'s Tower', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
'Map (Agahnims Tower)': (False, True, 'Map', 0x7B, 10, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nDark Palace', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nDark Palace', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nDark Palace', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
'Small Key (Palace of Darkness)': (False, False, 'SmallKey', 0xA6, 40, 'A small key for\nPalace of Darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': (False, False, 'BigKey', 0x99, 60, 'A big key for\nPalace of Darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': (False, True, 'Compass', 0x89, 10, 'A compass for\nPalace of Darkness', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
'Map (Palace of Darkness)': (False, True, 'Map', 0x79, 20, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'),
'Small Key (Thieves Town)': (False, False, 'SmallKey', 0xAB, 40, 'A small key for\nThieves Town', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'),
'Big Key (Thieves Town)': (False, False, 'BigKey', 0x94, 60, 'A big key for\nThieves Town', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'),

13
Main.py
View File

@@ -67,6 +67,7 @@ from Rom import (
JsonRom,
LocalRom,
apply_rom_settings,
apply_rom_patches,
get_hash_string,
patch_race_rom,
patch_rom,
@@ -335,7 +336,7 @@ def main(args, seed=None, fish=None):
for player in range(1, world.players+1):
if world.shopsanity[player]:
customize_shops(world, player)
if args.algorithm in ['balanced', 'equitable']:
if not args.skip_money_balance and args.algorithm in ['balanced', 'equitable']:
balance_money_progression(world)
ensure_good_items(world, True)
@@ -364,6 +365,8 @@ def main(args, seed=None, fish=None):
rom_names.append((player, team, list(rom.name)))
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
if args.patches[player]:
apply_rom_patches(rom, map(lambda arg: arg.strip(), args.patches[player].split(",")))
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player], args.triforce_gfx[player],
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player],
@@ -516,6 +519,7 @@ def init_world(args, fish):
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.prizeshuffle = args.prizeshuffle.copy()
world.showloot = args.showloot.copy()
world.loothud = args.loothud.copy()
world.showmap = args.showmap.copy()
world.bombbag = args.bombbag.copy()
world.flute_mode = args.flute_mode.copy()
@@ -564,13 +568,14 @@ def init_world(args, fish):
world.damage_challenge = args.damage_challenge.copy()
world.shuffle_damage_table = args.shuffle_damage_table.copy()
world.crystal_book = args.crystal_book.copy()
world.extra_keys = {player: int(args.extra_keys[player]) for player in range(1, world.players + 1)}
world.overworld_map = args.overworld_map.copy()
world.take_any = args.take_any.copy()
world.restrict_boss_items = args.restrict_boss_items.copy()
world.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy()
world.aga_randomness = args.aga_randomness.copy()
world.money_balance = args.money_balance.copy()
world.money_balance = {player: int(args.money_balance[player]) for player in range(1, world.players + 1)}
# custom settings - these haven't been promoted to full settings yet
in_progress_settings = ['force_enemy']
@@ -836,6 +841,7 @@ def copy_world(world):
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy()
ret.loothud = world.loothud.copy()
ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
@@ -868,6 +874,7 @@ def copy_world(world):
ret.damage_challenge = world.damage_challenge.copy()
ret.shuffle_damage_table = world.shuffle_damage_table.copy()
ret.crystal_book = world.crystal_book.copy()
ret.extra_keys = world.extra_keys.copy()
ret.overworld_map = world.overworld_map.copy()
ret.take_any = world.take_any.copy()
ret.boss_shuffle = world.boss_shuffle.copy()
@@ -1068,6 +1075,7 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.bigkeyshuffle = world.bigkeyshuffle.copy()
ret.prizeshuffle = world.prizeshuffle.copy()
ret.showloot = world.showloot.copy()
ret.loothud = world.loothud.copy()
ret.showmap = world.showmap.copy()
ret.bombbag = world.bombbag.copy()
ret.flute_mode = world.flute_mode.copy()
@@ -1100,6 +1108,7 @@ def copy_world_premature(world, player, create_flute_exits=True):
ret.damage_challenge = world.damage_challenge.copy()
ret.shuffle_damage_table = world.shuffle_damage_table.copy()
ret.crystal_book = world.crystal_book.copy()
ret.extra_keys = world.extra_keys.copy()
ret.overworld_map = world.overworld_map.copy()
ret.take_any = world.take_any.copy()
ret.boss_shuffle = world.boss_shuffle.copy()

View File

@@ -509,7 +509,7 @@ boots_clips = [
mirror_clips_local = [
('Desert East Mirror Clip', 'Mire Area', 'Desert Mouth'),
('EDDM Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'),
('EDDM Bridge Mirror Clip', 'East Dark Death Mountain (Bottom Left)', 'East Dark Death Mountain (Bottom)'),
('EDDM Mirror Clip', 'East Dark Death Mountain (Top)', 'Dark Death Mountain Ledge')
]
@@ -520,4 +520,4 @@ mirror_clips = [
mirror_offsets = [
(['DM Offset Mirror', 'DDM Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Courtyard Northeast', 'Pyramid Crack'], ['Pyramid Area', 'Hyrule Castle Courtyard']),
(['DM To HC Ledge Offset Mirror', 'DDM To Pyramid Offset Mirror'], ['West Death Mountain (Bottom)', 'West Dark Death Mountain (Bottom)'], ['Hyrule Castle Ledge', 'Pyramid Area'], ['Pyramid Area', 'Hyrule Castle Area'])
]
]

202
Rom.py
View File

@@ -85,7 +85,7 @@ from Utils import int16_as_bytes, int32_as_bytes, local_path, snes_to_pc
from Versions import DRVersion, GKVersion, ORVersion
JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '8945e9fdcefc02eb3ff3ad2a8892a180'
RANDOMIZERBASEHASH = 'abaa7ce289fd18e0ab898f2ca1263db1'
class JsonRom(object):
@@ -818,11 +818,13 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x138002, 2)
for name, layout in world.key_layout[player].items():
offset = compass_data[name][4]//2
dungeon = world.get_dungeon(name, player)
if world.keyshuffle[player] == 'universal':
rom.write_byte(0x187010+offset, layout.max_chests + layout.max_drops)
else:
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops) # not currently used
rom.write_byte(0x187010+offset, layout.max_chests)
rom.write_byte(0x13f020+offset, layout.max_chests + layout.max_drops + dungeon.extra_small_keys) # not currently used
rom.write_byte(0x187010+offset, layout.max_chests + dungeon.extra_small_keys)
rom.write_byte(0x187000+offset, dungeon.extra_small_keys)
builder = world.dungeon_layouts[player][name]
bk_status = 1 if builder.bk_required else 0
bk_status = 2 if builder.bk_provided else bk_status
@@ -946,7 +948,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(cr_pc+0x1e, 0xEE) # slash
rom.write_byte(cr_pc+0x1f, thousands_bot)
# modify stat config
stat_address = 0x23983E
stat_address = 0xA39895
stat_pc = snes_to_pc(stat_address)
rom.write_byte(stat_pc, 0xa9) # change to pos 21 (from b1)
rom.write_byte(stat_pc+2, 0xc0) # change to 12 bits (from a0)
@@ -992,7 +994,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
set_inverted_mode(world, player, rom, inverted_buffer)
uncle_location = world.get_location('Link\'s Uncle', player)
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword']:
if uncle_location.item is None or uncle_location.item.name not in ['Master Sword', 'Tempered Sword', 'Fighter Sword', 'Golden Sword', 'Progressive Sword', 'Sword and Shield']:
# disable sword sprite from uncle
rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
@@ -1005,6 +1007,10 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_bytes(0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
bridge_item = world.get_location("Hobo", player).item
if bridge_item is None or not bridge_item.name.startswith("Bottle"):
rom.write_bytes(0x1E9C0, [0xFB, 0xFF, 0x03, 0x00, 0xAB, 0x00, 0x00, 0x00])
# set light cones
if world.dark_rooms[player] == 'no_dark_rooms':
light_cone = 0x20
@@ -1495,24 +1501,40 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
loot_source = 0x09
if world.prizeshuffle[player] != 'none':
loot_source |= 0x10
if world.pottery[player] not in ['none', 'cave']:
if world.pottery[player] != 'none':
loot_source |= 0x02
if world.dropshuffle[player] != 'none':
loot_source |= 0x04
rom.write_byte(0x1CFF10, loot_source)
rom.write_byte(0x1CFF20, loot_source)
if world.loothud[player] == 'never':
rom.write_bytes(0x1CFF10, [0x00, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x00)
elif world.loothud[player] == 'presence':
rom.write_bytes(0x1CFF10, [0x01, 0x01, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'value':
rom.write_bytes(0x1CFF10, [0x03, 0x03, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'dungeon_value':
rom.write_bytes(0x1CFF10, [0x01, 0x03, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
elif world.loothud[player] == 'cave_value':
rom.write_bytes(0x1CFF10, [0x03, 0x01, 0x00, 0x00])
rom.write_byte(0x1CFF17, 0x01)
if world.showloot[player] == 'never':
rom.write_bytes(0x1CFF08, [0x00, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
rom.write_byte(0x1CFF0F, 0x00)
elif world.showloot[player] == 'presence':
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
rom.write_byte(0x1CFF0F, 0x01)
elif world.showloot[player] == 'compass':
rom.write_bytes(0x1CFF08, [0x01, 0x00, 0x02, 0x00])
rom.write_byte(0x1CFF11, 0x01)
rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x03, 0x00])
rom.write_byte(0x1CFF0F, 0x01)
elif world.showloot[player] == 'always':
rom.write_bytes(0x1CFF08, [0x02, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF11, 0x00)
rom.write_bytes(0x1CFF08, [0x03, 0x00, 0x00, 0x00])
rom.write_byte(0x1CFF0F, 0x01)
if world.showmap[player] == 'visited':
rom.write_bytes(0x1CFF00, [0x01, 0x00, 0x00, 0x05])
@@ -1526,7 +1548,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(loot_icons + 0x52, 0x0B) # bomb bag is major
triforce_piece_ids = [0x6B, 0x6C]
if world.treasure_hunt_count[player] > 20:
if world.treasure_hunt_count[player] > 100:
for triforce_piece_id in triforce_piece_ids:
rom.write_byte(loot_icons + triforce_piece_id, 0x04)
@@ -1764,53 +1786,40 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
rom.write_byte(0x180358, 0x01 if glitches_enabled else 0x00)
rom.write_byte(0x18008B, 0x01 if glitches_enabled else 0x00)
# remove shield from uncle
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
if uncle_location.item is None or uncle_location.item.name not in ['Blue Shield', 'Red Shield', 'Mirror Shield', 'Progressive Shield', 'Sword and Shield']:
# remove shield from uncle
rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E])
rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E])
rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
write_int16(rom, 0x180183, 0) # Escape fill rupee bow
# Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [0] * 9)
rom.write_byte(0x18004E, 0) # Escape Fill (nothing)
write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
bow_max, bomb_max, magic_max = 0, 0, 0
bow_small, bomb_small, magic_small = 10, 3, 0x20
if world.mode[player] == 'standard':
if world.doorShuffle[player] not in ['vanilla']:
# If door shuffle, give player small bit of all three ammos on respawn during escape
# Uncle / Zelda / Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [0x20, 3, 10] * 3)
# Always fully fill ammo of starting weapon on respawn during escape
if uncle_location.item is not None and uncle_location.item.name in ['Bow', 'Progressive Bow']:
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
write_int16(rom, 0x180183, 300) # Escape fill rupee bow
rom.write_bytes(0x180185, [0, 0, 70]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 0, 70]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 0, 70]) # Mantle respawn refills (magic, bombs, arrows)
bow_max = 70
rom.write_byte(0x180187, 70) # Uncle respawn refill arrows
rom.write_byte(0x18018A, 70) # Zelda respawn refill arrows
rom.write_byte(0x18018D, 70) # Mantle respawn refill arrowss
elif uncle_location.item is not None and uncle_location.item.name in ['Bomb Upgrade (+10)' if world.bombbag[player] else 'Bombs (10)']:
rom.write_byte(0x18004E, 2) # Escape Fill (bombs)
rom.write_bytes(0x180185, [0, 50, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0, 50, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0, 50, 0]) # Mantle respawn refills (magic, bombs, arrows)
bomb_max = 50
rom.write_byte(0x180186, 50) # Uncle respawn refill bombs
rom.write_byte(0x180189, 50) # Zelda respawn refill bombs
rom.write_byte(0x18018C, 50) # Mantle respawn refill bombs
elif uncle_location.item is not None and uncle_location.item.name in ['Cane of Somaria', 'Cane of Byrna', 'Fire Rod']:
rom.write_byte(0x18004E, 4) # Escape Fill (magic)
rom.write_bytes(0x180185, [0x80, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [0x80, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [0x80, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
magic_max = 0x80
if world.doorShuffle[player] not in ['vanilla', 'basic']:
# Uncle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)])
# Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x180188, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)])
# Mantle respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)])
elif world.doorShuffle[player] == 'basic': # just in case a bomb is needed to get to a chest
rom.write_bytes(0x180185, [max(magic_small, magic_max), max(bomb_small, bomb_max), max(bow_small, bow_max)])
rom.write_bytes(0x180188, [magic_small, max(bomb_small, bomb_max), bow_small]) # Zelda respawn refills (magic, bombs, arrows)
rom.write_bytes(0x18018B, [magic_small, max(bomb_small, bomb_max), bow_small]) # Mantle respawn refills (magic, bombs, arrows)
rom.write_byte(0x180185, 0x80) # Uncle respawn refill magic
rom.write_byte(0x180188, 0x80) # Zelda respawn refill magic
rom.write_byte(0x18018B, 0x80) # Mantle respawn refill magic
# patch swamp: Need to enable permanent drain of water as dam or swamp were moved
rom.write_byte(0x18003D, 0x01 if world.swamp_patch_required[player] else 0x00)
@@ -1930,7 +1939,7 @@ def patch_rom(world, rom, player, team, is_mystery=False, rom_header=None):
write_enemizer_tweaks(rom, world, player)
randomize_damage_table(rom, world, player)
write_strings(rom, world, player, team)
write_strings(rom, world, player, team, is_mystery)
# write initial sram
rom.write_initial_sram()
@@ -2073,6 +2082,49 @@ def hud_format_text(text):
output += b'\x7f\x00'
return output[:32]
def read_bytes(f, count):
values = f.read(count)
if len(values) < count:
raise EOFError
return values
def apply_rom_patches(rom, patches):
for patch in patches:
if not os.path.exists(f"patches/{patch}.ips"):
logging.getLogger('').warning("Patch %s not found -- skipping", patch)
continue
with open(f"patches/{patch}.ips", "rb") as f:
byte_changes = []
try:
header = read_bytes(f, 5)
if header != b"PATCH":
logging.getLogger('').warning("Patch %s invalid -- skipping", patch)
continue
while True:
address = read_bytes(f, 3)
if address == b"EOF":
break
address = int.from_bytes(address, byteorder="big")
length = int.from_bytes(read_bytes(f, 2), byteorder="big")
if length > 0:
byte_changes.append((address, list(f.read(length))))
else:
length = int.from_bytes(read_bytes(f, 2), byteorder="big")
value = read_bytes(f, 1)
byte_changes.append((address, length * list(value)))
for address, values in byte_changes:
rom.write_bytes(address, values)
logging.getLogger("").info("Patch %s applied successfully", patch)
except EOFError:
logging.getLogger('').warning("Patch %s invalid -- skipping", patch)
continue
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite, triforce_gfx,
ow_palettes, uw_palettes, reduce_flashing, shuffle_sfx,
@@ -2389,7 +2441,7 @@ def write_string_to_rom(rom, target, string):
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world, player, team):
def write_strings(rom, world, player, team, is_mystery=False):
tt = TextTable()
tt.removeUnwantedText()
if world.shuffle[player] != 'vanilla':
@@ -2440,6 +2492,15 @@ def write_strings(rom, world, player, team):
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
if world.hints[player]:
zoraitem = world.get_location('King Zora', player).item.hint_text
if len(zoraitem) <= 15:
tt['zora_meeting'] = f"Whaddaya want?\n{zoraitem.title()}\n Nothin'{{CHOICE}}"
else:
tt['zora_meeting'] = f"Do you want {zoraitem}?\n ≥ I'll pay\n No thanks{{CHOICE}}"
bottleitem = world.get_location('Bottle Merchant', player).item.hint_text
tt['bottle_vendor_choice'] = f"Do you want {bottleitem}?\n ≥ I'll take it\n No thanks!\n{{CHOICE}}"
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
hint_locations = HintLocations.copy()
random.shuffle(hint_locations)
@@ -2904,7 +2965,22 @@ def write_strings(rom, world, player, team):
sanc_text = "Dark Chapel"
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n{CHOICE3}"
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s " + lh_text + "\n " + sanc_text + "\n Mountain Cave\n{CHOICE2}"
if world.mode[player] == 'inverted':
if is_mystery:
tt['intro_main'] = CompressedTextMapper.convert(
"{INTRO}\n Episode III"
+ "{PAUSE3}\n A Link to the Past"
+ "{PAUSE3}\n Mystery Randomizer"
+ "{PAUSE3}\n\n\n"
+ "{PAUSE3}\nAfter mostly disregarding what happened in the first two games,"
+ "{PAUSE3}\nLink awakens to a massive mystery."
+ "{PAUSE3}\nHe doesn't know why he's here, or what he must do."
+ "{PAUSE3} {CHANGEPIC}\nGanon has moved around all the items in Hyrule."
+ "{PAUSE7}\nYou will have to find all the items necessary to achieve your goal."
+ "{PAUSE7}\nThis is your chance to be a hero."
+ "{PAUSE3} {CHANGEPIC}\nGood luck out there, and try not to die."
+ "{PAUSE9} {CHANGEPIC}", False)
elif world.mode[player] == 'inverted':
tt['intro_main'] = CompressedTextMapper.convert(
"{INTRO}\n Episode III"
+ "{PAUSE3}\n A Link to the Past"
@@ -2918,6 +2994,7 @@ def write_strings(rom, world, player, team):
+ "{PAUSE7}\nThis is your chance to be a hero."
+ "{PAUSE3} {CHANGEPIC}\nYou must get the 7 crystals to beat Ganon."
+ "{PAUSE9} {CHANGEPIC}", False)
rom.write_bytes(0xE0000, tt.getBytes())
credits = Credits()
@@ -3526,7 +3603,10 @@ Prizes = ['Green Pendant',
]
hash_alphabet = [
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder", "Rod", "Pendant", "Bombos", "Ether", "Quake",
"Lamp", "Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
"Bow", "Boomerang", "Hookshot", "Bomb", "Mushroom", "Powder",
"Ice Rod", "Green Pendant", "Bombos", "Ether", "Quake", "Lamp",
"Hammer", "Shovel", "Ocarina", "Bug Net", "Book", "Bottle",
"Green Potion", "Somaria", "Cape", "Mirror", "Boots", "Gloves",
"Flippers", "Pearl", "Shield", "Green Tunic", "Heart", "Map",
"Compass", "Key",
]

View File

@@ -505,7 +505,7 @@ def global_rules(world, player):
set_rule(world.get_entrance('Skull Woods Rock (West)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Skull Woods Rock (East)', player), lambda state: state.can_lift_rocks(player))
# this more like an ohko rule - dependent on bird being present too - so enemizer could turn this off?
set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: world.can_take_damage or state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player))
set_rule(world.get_entrance('Bumper Cave Ledge Drop', player), lambda state: state.has('Cape', player) or state.has('Cane of Byrna', player) or state.has_sword(player))
set_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), lambda state: state.can_lift_rocks(player))
set_rule(world.get_entrance('Skull Woods Pass Rock (North)', player), lambda state: state.can_lift_heavy_rocks(player))
@@ -1404,8 +1404,7 @@ def ow_bunny_rules(world, player):
add_bunny_rule(world.get_entrance('Skull Woods Forgotten Bush (East)', player), player)
add_bunny_rule(world.get_entrance('Skull Woods Second Section Hole', player), player)
add_bunny_rule(world.get_entrance('East Dark Death Mountain Bushes', player), player)
if not world.can_take_damage:
add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Ledge Drop', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Rock (Outer)', player), player)
add_bunny_rule(world.get_entrance('Bumper Cave Rock (Inner)', player), player)
add_bunny_rule(world.get_entrance('Skull Woods Pass Bush Row (West)', player), player)

35
Text.py
View File

@@ -376,31 +376,30 @@ Sahasrahla_names = [
Kakariko_texts = ["{}'s homecoming"]
Blacksmiths_texts = [
'frogs for bread',
'That\'s not a sword',
'The Rupeesmiths'
] * 1 + [
"the dwarven breadsmiths"
"Frogs for bread",
"That's not a sword",
"The rupeesmiths",
"The dwarven breadsmiths",
]
DeathMountain_texts = [
"the lost old man",
"gary the old man",
"Your ad here"
"The lost old man",
"Gary the old man",
"Your ad here",
]
LostWoods_texts = [
'thieves\' stump',
] * 2 + [
"the forest thief",
"dancing pickles",
"flying crows",
"Thieves' stump",
"Thieves' stump",
"The forest thief",
"Dancing pickles",
"Flying crows",
]
WishingWell_texts = [
"venus. queen of faeries",
"Venus, Queen of Faeries",
"Venus was her name",
"I'm your Venus",
"Yeah, baby, shes got it",
"Yeah, baby, she's got it",
"Venus, I'm your fire",
"Venus, At your desire",
"Venus, at your desire",
"Venus Love Chain",
"Venus Crescent Beam",
]
@@ -1766,7 +1765,7 @@ class TextTable(object):
text['sign_catfish'] = CompressedTextMapper.convert("Toss rocks\nToss items\nToss cookies")
text['sign_north_village_of_outcasts'] = CompressedTextMapper.convert("↑ Skull Woods\n\n↓ Steve's Town")
text['sign_south_of_bumper_cave'] = CompressedTextMapper.convert("\n→ Karkats cave")
text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n Dark Palace")
text['sign_east_of_pyramid'] = CompressedTextMapper.convert("\n→ Palace of Darkness")
text['sign_east_of_bomb_shop'] = CompressedTextMapper.convert("\n← Bomb Shoppe")
text['sign_east_of_mire'] = CompressedTextMapper.convert("\n← Misery Mire\n no way in.\n no way out.")
text['sign_village_of_outcasts'] = CompressedTextMapper.convert("Have a Trulie Awesome Day!")
@@ -1870,7 +1869,7 @@ class TextTable(object):
# 100
text['dark_sanctuary_no'] = CompressedTextMapper.convert("Then go away!")
text['dark_sanctuary_hint_1'] = CompressedTextMapper.convert("There is a thief in the desert, he can open creepy chests that follow you. But now that we have that out of the way, Do you like my hair? I've spent eons getting it this way.")
text['dark_sanctuary_yes'] = CompressedTextMapper.convert("With Crystals 5&6, you can find a great fairy in the pyramid.\n\nFlomp Flomp, Whizzle Whomp")
text['dark_sanctuary_yes'] = CompressedTextMapper.convert("With crystals 5 and 6, you can find a great fairy in the pyramid.\n\nFlomp Flomp, Whizzle Whomp")
text['dark_sanctuary_hint_2'] = CompressedTextMapper.convert(
"All I can say is that my life is pretty plain,\n"
+ "I like watchin' the puddles gather rain,\n"

Binary file not shown.

BIN
patches/2way_mirror.ips Normal file

Binary file not shown.

BIN
patches/all_starting.ips Normal file

Binary file not shown.

BIN
patches/colordorm.ips Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
patches/pod_key_swap.ips Normal file

Binary file not shown.

BIN
patches/quiet_zora.ips Normal file

Binary file not shown.

View File

@@ -486,6 +486,15 @@
"always"
]
},
"loothud": {
"choices": [
"never",
"presence",
"value",
"dungeon_value",
"cave_value"
]
},
"showmap": {
"choices": [
"visited",
@@ -665,6 +674,9 @@
"action": "store_true",
"type": "bool"
},
"extra_keys": {
"type": "int"
},
"calc_playthrough": {
"action": "store_false",
"type": "bool"
@@ -743,6 +755,10 @@
"type": "bool"
},
"money_balance": {},
"patches": {
"type": "str",
"help": "suppress"
},
"settingsonload": {
"choices": [
"default",

View File

@@ -425,6 +425,7 @@
"AlwaysInLogic: Dark rooms are always considered to be in logic, even if the player cannot see"
],
"crystal_book": [ " Book can be used indoors to flip the state of colored pegs (default: %(default)s)"],
"extra_keys": [ " Percentage of extra small keys to create for each dungeon when door shuffle is enabled (default: %(default)s)"],
"bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ],
"any_enemy_logic": [
"How to handle potential traversal between dungeon in Crossed door shuffle",

View File

@@ -84,7 +84,10 @@ UwGeneralDeny:
- [ 0x0039, 4, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 1"
- [0x0039, 5, ["RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "Bumper", "AntiFairyCircle"]] #"Skull Woods - Play Pen - Hardhat Beetle"
- [ 0x0039, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "FirebarCW", "FirebarCCW" ] ] #"Skull Woods - Play Pen - Spike Trap 2"
- [0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003a, 0, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003a, 1, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003a, 3, ["RollerHorizontalLeft", "RollerHorizontalRight"]]
- [ 0x003a, 4, ["RollerHorizontalLeft", "RollerHorizontalRight"]]
- [ 0x003b, 1, [ "Bumper", "AntiFairyCircle" ]]
- [ 0x003b, 4, ["RollerVerticalUp", "RollerVerticalDown"]]
- [ 0x003c, 0, ["BigSpike"]]
@@ -306,7 +309,7 @@ UwGeneralDeny:
- [0x00b0, 7, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay
- [0x00b0, 8, [ "StalfosKnight", "Blob", "Stal", "Wizzrobe"]] # blocked, but Geldmen are probably okay
- [ 0x00b1, 2, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 1"
- [ 0x00b1, 3, [ "RollerVerticalUp", "RollerVerticalDown" ] ] #"Misery Mire - Hourglass - Spike Trap 2"
- [ 0x00b1, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Bumper" ] ] #"Misery Mire - Hourglass - Spike Trap 2"
- [0x00b1, 4, ["Bumper", "BigSpike", "AntiFairyCircle", "Statue"]] # Wizzrobe near door
- [ 0x00b2, 1, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00b2, 3, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
@@ -339,11 +342,11 @@ UwGeneralDeny:
- [ 0x00c2, 5, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00c5, 6, [ "RollerVerticalUp", "RollerVerticalDown", "RollerHorizontalLeft", "RollerHorizontalRight", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Turtle Rock - Catwalk - Mini Helmasaur"
- [ 0x00c5, 7, [ "Statue" ] ] #"Turtle Rock - Catwalk - Laser Eye (Left) 4"
- [0x00c6, 2, ["Bumper", "AntiFairyCircle"]]
- [0x00c6, 3, ["Bumper", "AntiFairyCircle"]]
- [0x00c6, 4, ["Bumper", "AntiFairyCircle"]]
- [0x00c6, 5, ["Bumper", "AntiFairyCircle"]]
- [0x00c6, 6, ["Bumper", "AntiFairyCircle"]]
- [ 0x00c6, 2, ["Bumper", "AntiFairyCircle", "BigSpike" ]]
- [ 0x00c6, 3, ["Bumper", "AntiFairyCircle", "BigSpike" ]]
- [ 0x00c6, 4, ["Bumper", "AntiFairyCircle", "BigSpike" ]]
- [ 0x00c6, 5, ["Bumper", "AntiFairyCircle", "BigSpike" ]]
- [ 0x00c6, 6, ["Bumper", "AntiFairyCircle", "BigSpike" ]]
- [ 0x00cb, 0, [ "Wizzrobe", "Statue" ] ] # Wizzrobes can't spawn on pots
- [ 0x00cb, 3, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 1"
- [ 0x00cb, 5, [ "RollerVerticalUp", "RollerVerticalDown", "Beamos", "AntiFairyCircle", "BigSpike", "SpikeBlock", "Bumper" ] ] #"Thieves' Town - Grand Room NW - Zol 2"

View File

@@ -58,17 +58,17 @@ class DataTables:
self.bush_sprite_table = {}
# enemizer conditions
self.uw_enemy_denials = {}
self.ow_enemy_denials = {}
self.uw_enemy_drop_denials = {}
self.uw_enemy_denials = defaultdict(set)
self.ow_enemy_denials = defaultdict(set)
self.uw_enemy_drop_denials = defaultdict(set)
self.sheet_choices = []
denial_data = load_cached_yaml(['source', 'enemizer', 'enemy_deny.yaml'])
for denial in denial_data['UwGeneralDeny']:
self.uw_enemy_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]}
self.uw_enemy_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]}
for denial in denial_data['OwGeneralDeny']:
self.ow_enemy_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]}
self.ow_enemy_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]}
for denial in denial_data['UwEnemyDrop']:
self.uw_enemy_drop_denials[denial[0], denial[1]] = {sprite_translation[x] for x in denial[2]}
self.uw_enemy_drop_denials[denial[0], denial[1]] |= {sprite_translation[x] for x in denial[2]}
weights = load_cached_yaml(['source', 'enemizer', 'enemy_weight.yaml'])
self.uw_weights = {sprite_translation[k]: v for k, v in weights['UW'].items()}
self.ow_weights = {sprite_translation[k]: v for k, v in weights['OW'].items()}