773 lines
30 KiB
Python
773 lines
30 KiB
Python
from collections import namedtuple
|
|
from itertools import chain
|
|
from .Items import item_table
|
|
from .Location import DisableType
|
|
from .LocationList import location_groups
|
|
from decimal import Decimal, ROUND_HALF_UP
|
|
|
|
|
|
# Generates item pools and places fixed items based on settings.
|
|
|
|
plentiful_items = ([
|
|
'Biggoron Sword',
|
|
'Boomerang',
|
|
'Lens of Truth',
|
|
'Megaton Hammer',
|
|
'Iron Boots',
|
|
'Goron Tunic',
|
|
'Zora Tunic',
|
|
'Hover Boots',
|
|
'Mirror Shield',
|
|
'Fire Arrows',
|
|
'Light Arrows',
|
|
'Dins Fire',
|
|
'Progressive Hookshot',
|
|
'Progressive Strength Upgrade',
|
|
'Progressive Scale',
|
|
'Progressive Wallet',
|
|
'Magic Meter',
|
|
'Deku Stick Capacity',
|
|
'Deku Nut Capacity',
|
|
'Bow',
|
|
'Slingshot',
|
|
'Bomb Bag',
|
|
'Double Defense'] +
|
|
['Heart Container'] * 8
|
|
)
|
|
|
|
# Ludicrous replaces all health upgrades with heart containers
|
|
# as done in plentiful. The item list is used separately to
|
|
# dynamically replace all junk with even levels of each item.
|
|
ludicrous_health = ['Heart Container'] * 8
|
|
|
|
# List of items that can be multiplied in ludicrous mode.
|
|
# Used to filter the pre-plando pool for candidates instead
|
|
# of appending directly, making this list settings-independent.
|
|
# Excludes Gold Skulltula Tokens, Triforce Pieces, and health
|
|
# upgrades as they are directly tied to win conditions and
|
|
# already have a large count relative to available locations
|
|
# in the game.
|
|
#
|
|
# Base items will always be candidates to replace junk items,
|
|
# even if the player starts with all "normal" copies of an item.
|
|
ludicrous_items_base = [
|
|
'Light Arrows',
|
|
'Megaton Hammer',
|
|
'Progressive Hookshot',
|
|
'Progressive Strength Upgrade',
|
|
'Dins Fire',
|
|
'Hover Boots',
|
|
'Mirror Shield',
|
|
'Boomerang',
|
|
'Iron Boots',
|
|
'Fire Arrows',
|
|
'Progressive Scale',
|
|
'Progressive Wallet',
|
|
'Magic Meter',
|
|
'Bow',
|
|
'Slingshot',
|
|
'Bomb Bag',
|
|
'Bombchus',
|
|
'Lens of Truth',
|
|
'Goron Tunic',
|
|
'Zora Tunic',
|
|
'Biggoron Sword',
|
|
'Double Defense',
|
|
'Farores Wind',
|
|
'Nayrus Love',
|
|
'Stone of Agony',
|
|
'Ice Arrows',
|
|
'Deku Stick Capacity',
|
|
'Deku Nut Capacity'
|
|
]
|
|
|
|
ludicrous_items_extended = [
|
|
'Zeldas Lullaby',
|
|
'Eponas Song',
|
|
'Suns Song',
|
|
'Sarias Song',
|
|
'Song of Time',
|
|
'Song of Storms',
|
|
'Minuet of Forest',
|
|
'Prelude of Light',
|
|
'Bolero of Fire',
|
|
'Serenade of Water',
|
|
'Nocturne of Shadow',
|
|
'Requiem of Spirit',
|
|
'Ocarina',
|
|
'Kokiri Sword',
|
|
'Boss Key (Ganons Castle)',
|
|
'Boss Key (Forest Temple)',
|
|
'Boss Key (Fire Temple)',
|
|
'Boss Key (Water Temple)',
|
|
'Boss Key (Shadow Temple)',
|
|
'Boss Key (Spirit Temple)',
|
|
'Gerudo Membership Card',
|
|
'Small Key (Thieves Hideout)',
|
|
'Small Key (Shadow Temple)',
|
|
'Small Key (Ganons Castle)',
|
|
'Small Key (Forest Temple)',
|
|
'Small Key (Spirit Temple)',
|
|
'Small Key (Fire Temple)',
|
|
'Small Key (Water Temple)',
|
|
'Small Key (Bottom of the Well)',
|
|
'Small Key (Gerudo Training Ground)',
|
|
'Small Key Ring (Thieves Hideout)',
|
|
'Small Key Ring (Shadow Temple)',
|
|
'Small Key Ring (Ganons Castle)',
|
|
'Small Key Ring (Forest Temple)',
|
|
'Small Key Ring (Spirit Temple)',
|
|
'Small Key Ring (Fire Temple)',
|
|
'Small Key Ring (Water Temple)',
|
|
'Small Key Ring (Bottom of the Well)',
|
|
'Small Key Ring (Gerudo Training Ground)',
|
|
'Magic Bean Pack'
|
|
]
|
|
|
|
ludicrous_exclusions = [
|
|
'Triforce Piece',
|
|
'Gold Skulltula Token',
|
|
'Rutos Letter',
|
|
'Heart Container',
|
|
'Piece of Heart',
|
|
'Piece of Heart (Treasure Chest Game)'
|
|
]
|
|
|
|
item_difficulty_max = {
|
|
'ludicrous': {
|
|
'Piece of Heart': 3,
|
|
},
|
|
'plentiful': {
|
|
'Piece of Heart': 3,
|
|
},
|
|
'balanced': {},
|
|
'scarce': {
|
|
'Bombchus': 3,
|
|
'Bombchus (5)': 1,
|
|
'Bombchus (10)': 2,
|
|
'Bombchus (20)': 0,
|
|
'Magic Meter': 1,
|
|
'Double Defense': 0,
|
|
'Deku Stick Capacity': 1,
|
|
'Deku Nut Capacity': 1,
|
|
'Bow': 2,
|
|
'Slingshot': 2,
|
|
'Bomb Bag': 2,
|
|
'Heart Container': 0,
|
|
},
|
|
'minimal': {
|
|
'Bombchus': 1,
|
|
'Bombchus (5)': 1,
|
|
'Bombchus (10)': 0,
|
|
'Bombchus (20)': 0,
|
|
'Magic Meter': 1,
|
|
'Nayrus Love': 1,
|
|
'Double Defense': 0,
|
|
'Deku Stick Capacity': 0,
|
|
'Deku Nut Capacity': 0,
|
|
'Bow': 1,
|
|
'Slingshot': 1,
|
|
'Bomb Bag': 1,
|
|
'Heart Container': 0,
|
|
'Piece of Heart': 0,
|
|
},
|
|
}
|
|
|
|
shopsanity_rupees = (
|
|
['Rupees (20)'] * 5 +
|
|
['Rupees (50)'] * 3 +
|
|
['Rupees (200)'] * 2
|
|
)
|
|
|
|
min_shop_items = (
|
|
['Buy Deku Shield'] +
|
|
['Buy Hylian Shield'] +
|
|
['Buy Goron Tunic'] +
|
|
['Buy Zora Tunic'] +
|
|
['Buy Deku Nut (5)'] * 2 + ['Buy Deku Nut (10)'] +
|
|
['Buy Deku Stick (1)'] * 2 +
|
|
['Buy Deku Seeds (30)'] +
|
|
['Buy Arrows (10)'] * 2 + ['Buy Arrows (30)'] + ['Buy Arrows (50)'] +
|
|
['Buy Bombchu (5)'] + ['Buy Bombchu (10)'] * 2 + ['Buy Bombchu (20)'] +
|
|
['Buy Bombs (5) for 25 Rupees'] + ['Buy Bombs (5) for 35 Rupees'] + ['Buy Bombs (10)'] + ['Buy Bombs (20)'] +
|
|
['Buy Green Potion'] +
|
|
['Buy Red Potion for 30 Rupees'] +
|
|
['Buy Blue Fire'] +
|
|
["Buy Fairy's Spirit"] +
|
|
['Buy Bottle Bug'] +
|
|
['Buy Fish']
|
|
)
|
|
|
|
deku_scrubs_items = {
|
|
'Buy Deku Shield': 'Deku Shield',
|
|
'Buy Deku Nut (5)': 'Deku Nuts (5)',
|
|
'Buy Deku Stick (1)': 'Deku Stick (1)',
|
|
'Buy Bombs (5) for 35 Rupees': 'Bombs (5)',
|
|
'Buy Red Potion for 30 Rupees': 'Recovery Heart',
|
|
'Buy Green Potion': 'Rupees (5)',
|
|
'Buy Arrows (30)': [('Arrows (30)', 3), ('Deku Seeds (30)', 1)],
|
|
'Buy Deku Seeds (30)': [('Arrows (30)', 3), ('Deku Seeds (30)', 1)],
|
|
}
|
|
|
|
trade_items = (
|
|
"Pocket Egg",
|
|
"Pocket Cucco",
|
|
"Cojiro",
|
|
"Odd Mushroom",
|
|
#"Odd Potion",
|
|
"Poachers Saw",
|
|
"Broken Sword",
|
|
"Prescription",
|
|
"Eyeball Frog",
|
|
"Eyedrops",
|
|
"Claim Check",
|
|
)
|
|
|
|
def get_spec(tup, key, default):
|
|
special = tup[3]
|
|
if special is None:
|
|
return default
|
|
return special.get(key, default)
|
|
|
|
normal_bottles = [k for k, v in item_table.items() if get_spec(v, 'bottle', False) and k not in {'Deliver Letter', 'Sell Big Poe'}]
|
|
normal_bottles.append('Bottle with Big Poe')
|
|
song_list = [k for k, v in item_table.items() if v[0] == 'Song']
|
|
junk_pool_base = [(k, v[3]['junk']) for k, v in item_table.items() if get_spec(v, 'junk', -1) > 0]
|
|
remove_junk_items = [k for k, v in item_table.items() if get_spec(v, 'junk', -1) >= 0]
|
|
|
|
remove_junk_ludicrous_items = [
|
|
'Ice Arrows',
|
|
'Deku Nut Capacity',
|
|
'Deku Stick Capacity',
|
|
'Double Defense',
|
|
'Biggoron Sword'
|
|
]
|
|
|
|
# a useless placeholder item placed at some skipped and inaccessible locations
|
|
# (e.g. HC Malon Egg with Skip Child Zelda, or the carpenters with Open Gerudo Fortress)
|
|
IGNORE_LOCATION = 'Recovery Heart'
|
|
|
|
pending_junk_pool = []
|
|
junk_pool = []
|
|
|
|
exclude_from_major = [
|
|
'Deliver Letter',
|
|
'Sell Big Poe',
|
|
'Magic Bean',
|
|
'Buy Magic Bean',
|
|
'Zeldas Letter',
|
|
'Bombchus (5)',
|
|
'Bombchus (10)',
|
|
'Bombchus (20)',
|
|
'Odd Potion',
|
|
'Triforce Piece',
|
|
'Heart Container',
|
|
'Piece of Heart',
|
|
'Piece of Heart (Treasure Chest Game)',
|
|
]
|
|
|
|
item_groups = {
|
|
'Junk': remove_junk_items,
|
|
'JunkSong': ('Prelude of Light', 'Serenade of Water'),
|
|
'AdultTrade': trade_items,
|
|
'Bottle': normal_bottles,
|
|
'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'),
|
|
'Shield': ('Deku Shield', 'Hylian Shield'),
|
|
'Song': song_list,
|
|
'NonWarpSong': song_list[6:],
|
|
'WarpSong': song_list[0:6],
|
|
'HealthUpgrade': ('Heart Container', 'Piece of Heart', 'Piece of Heart (Treasure Chest Game)'),
|
|
'ProgressItem': sorted([name for name, item in item_table.items() if item[0] == 'Item' and item[1]]),
|
|
'MajorItem': sorted([name for name, item in item_table.items() if item[0] in ['Item', 'Song'] and item[1] and name not in exclude_from_major]),
|
|
'DungeonReward': [name for name in sorted([n for n, i in item_table.items() if i[0] == 'DungeonReward'],
|
|
key=lambda x: item_table[x][3]['item_id'])],
|
|
'Map': sorted([name for name, item in item_table.items() if item[0] == 'Map']),
|
|
'Compass': sorted([name for name, item in item_table.items() if item[0] == 'Compass']),
|
|
'BossKey': sorted([name for name, item in item_table.items() if item[0] == 'BossKey']),
|
|
'SmallKey': sorted([name for name, item in item_table.items() if item[0] == 'SmallKey']),
|
|
|
|
'ForestFireWater': ('Forest Medallion', 'Fire Medallion', 'Water Medallion'),
|
|
'FireWater': ('Fire Medallion', 'Water Medallion'),
|
|
}
|
|
|
|
random = None
|
|
|
|
|
|
def get_junk_pool(ootworld):
|
|
junk_pool[:] = list(junk_pool_base)
|
|
if ootworld.junk_ice_traps == 'on':
|
|
junk_pool.append(('Ice Trap', 10))
|
|
elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
|
|
junk_pool[:] = [('Ice Trap', 1)]
|
|
return junk_pool
|
|
|
|
|
|
def get_junk_item(count=1, pool=None, plando_pool=None):
|
|
global random
|
|
|
|
if count < 1:
|
|
raise ValueError("get_junk_item argument 'count' must be greater than 0.")
|
|
|
|
return_pool = []
|
|
if pending_junk_pool:
|
|
pending_count = min(len(pending_junk_pool), count)
|
|
return_pool = [pending_junk_pool.pop() for _ in range(pending_count)]
|
|
count -= pending_count
|
|
|
|
if pool and plando_pool:
|
|
jw_list = [(junk, weight) for (junk, weight) in junk_pool
|
|
if junk not in plando_pool or pool.count(junk) < plando_pool[junk].count]
|
|
try:
|
|
junk_items, junk_weights = zip(*jw_list)
|
|
except ValueError:
|
|
raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
|
|
else:
|
|
junk_items, junk_weights = zip(*junk_pool)
|
|
return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
|
|
|
|
return return_pool
|
|
|
|
|
|
def replace_max_item(items, item, max):
|
|
count = 0
|
|
for i,val in enumerate(items):
|
|
if val == item:
|
|
if count >= max:
|
|
items[i] = get_junk_item()[0]
|
|
count += 1
|
|
|
|
|
|
def generate_itempool(ootworld):
|
|
world = ootworld.multiworld
|
|
player = ootworld.player
|
|
global random
|
|
random = world.random
|
|
|
|
junk_pool = get_junk_pool(ootworld)
|
|
|
|
# set up item pool
|
|
(pool, placed_items) = get_pool_core(ootworld)
|
|
ootworld.itempool = [ootworld.create_item(item) for item in pool]
|
|
for (location_name, item) in placed_items.items():
|
|
location = world.get_location(location_name, player)
|
|
location.place_locked_item(ootworld.create_item(item, allow_arbitrary_name=True))
|
|
|
|
|
|
def get_pool_core(world):
|
|
global random
|
|
|
|
pool = []
|
|
placed_items = {}
|
|
remain_shop_items = []
|
|
ruto_bottles = 1
|
|
|
|
if world.zora_fountain == 'open':
|
|
ruto_bottles = 0
|
|
|
|
if world.shopsanity not in ['off', '0']:
|
|
pending_junk_pool.append('Progressive Wallet')
|
|
|
|
if world.item_pool_value == 'plentiful':
|
|
pending_junk_pool.extend(plentiful_items)
|
|
if world.zora_fountain != 'open':
|
|
ruto_bottles += 1
|
|
if world.shuffle_kokiri_sword:
|
|
pending_junk_pool.append('Kokiri Sword')
|
|
if world.shuffle_ocarinas:
|
|
pending_junk_pool.append('Ocarina')
|
|
if world.shuffle_beans and world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0):
|
|
pending_junk_pool.append('Magic Bean Pack')
|
|
if (world.gerudo_fortress != "open"
|
|
and world.shuffle_hideoutkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']):
|
|
if 'Thieves Hideout' in world.key_rings and world.gerudo_fortress != "fast":
|
|
pending_junk_pool.extend(['Small Key Ring (Thieves Hideout)'])
|
|
else:
|
|
pending_junk_pool.append('Small Key (Thieves Hideout)')
|
|
if world.shuffle_gerudo_card:
|
|
pending_junk_pool.append('Gerudo Membership Card')
|
|
if world.shuffle_smallkeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']:
|
|
for dungeon in ['Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple',
|
|
'Bottom of the Well', 'Gerudo Training Ground', 'Ganons Castle']:
|
|
if dungeon in world.key_rings:
|
|
pending_junk_pool.append(f"Small Key Ring ({dungeon})")
|
|
else:
|
|
pending_junk_pool.append(f"Small Key ({dungeon})")
|
|
if world.shuffle_bosskeys in ['any_dungeon', 'overworld', 'keysanity', 'regional']:
|
|
for dungeon in ['Forest Temple', 'Fire Temple', 'Water Temple', 'Shadow Temple', 'Spirit Temple']:
|
|
pending_junk_pool.append(f"Boss Key ({dungeon})")
|
|
if world.shuffle_ganon_bosskey in ['any_dungeon', 'overworld', 'keysanity', 'regional']:
|
|
pending_junk_pool.append('Boss Key (Ganons Castle)')
|
|
if world.shuffle_song_items == 'any':
|
|
pending_junk_pool.extend(song_list)
|
|
|
|
if world.item_pool_value == 'ludicrous':
|
|
pending_junk_pool.extend(ludicrous_health)
|
|
|
|
if world.triforce_hunt:
|
|
triforce_count = int((Decimal(100 + world.extra_triforce_percentage)/100 * world.triforce_goal).to_integral_value(rounding=ROUND_HALF_UP))
|
|
pending_junk_pool.extend(['Triforce Piece'] * triforce_count)
|
|
|
|
# Use the vanilla items in the world's locations when appropriate.
|
|
for location in world.get_locations():
|
|
if location.vanilla_item is None:
|
|
continue
|
|
|
|
item = location.vanilla_item
|
|
shuffle_item = None # None for don't handle, False for place item, True for add to pool.
|
|
|
|
# Always Placed Items
|
|
if (location.vanilla_item in ['Zeldas Letter', 'Triforce', 'Scarecrow Song',
|
|
'Deliver Letter', 'Time Travel', 'Bombchu Drop']
|
|
or location.type == 'Drop'):
|
|
shuffle_item = False
|
|
if location.vanilla_item != 'Zeldas Letter':
|
|
location.show_in_spoiler = False
|
|
|
|
# Gold Skulltula Tokens
|
|
elif location.vanilla_item == 'Gold Skulltula Token':
|
|
shuffle_item = (world.tokensanity == 'all'
|
|
or (world.tokensanity == 'dungeons' and location.dungeon)
|
|
or (world.tokensanity == 'overworld' and not location.dungeon))
|
|
location.show_in_spoiler = shuffle_item
|
|
|
|
# Shops
|
|
elif location.type == "Shop":
|
|
if world.shopsanity == 'off':
|
|
if world.bombchus_in_logic and location.name in ['KF Shop Item 8', 'Market Bazaar Item 4', 'Kak Bazaar Item 4']:
|
|
item = 'Buy Bombchu (5)'
|
|
shuffle_item = False
|
|
location.show_in_spoiler = False
|
|
else:
|
|
remain_shop_items.append(item)
|
|
|
|
# Business Scrubs
|
|
elif location.type in ["Scrub", "GrottoScrub"]:
|
|
if location.vanilla_item in ['Piece of Heart', 'Deku Stick Capacity', 'Deku Nut Capacity']:
|
|
shuffle_item = True
|
|
elif world.shuffle_scrubs == 'off':
|
|
shuffle_item = False
|
|
location.show_in_spoiler = False
|
|
else:
|
|
item = deku_scrubs_items[location.vanilla_item]
|
|
if isinstance(item, list):
|
|
item = random.choices([i[0] for i in item], weights=[i[1] for i in item], k=1)[0]
|
|
shuffle_item = True
|
|
|
|
# Kokiri Sword
|
|
elif location.vanilla_item == 'Kokiri Sword':
|
|
shuffle_item = world.shuffle_kokiri_sword
|
|
|
|
# Weird Egg
|
|
elif location.vanilla_item == 'Weird Egg':
|
|
if world.shuffle_child_trade == 'skip_child_zelda':
|
|
item = IGNORE_LOCATION
|
|
shuffle_item = False
|
|
location.show_in_spoiler = False
|
|
world.multiworld.push_precollected(world.create_item('Weird Egg'))
|
|
world.remove_from_start_inventory.append('Weird Egg')
|
|
else:
|
|
shuffle_item = world.shuffle_child_trade != 'vanilla'
|
|
|
|
# Ocarinas
|
|
elif location.vanilla_item == 'Ocarina':
|
|
shuffle_item = world.shuffle_ocarinas
|
|
|
|
# Giant's Knife
|
|
elif location.vanilla_item == 'Giants Knife':
|
|
shuffle_item = world.shuffle_medigoron_carpet_salesman
|
|
if not shuffle_item:
|
|
location.show_in_spoiler = False
|
|
|
|
# Bombchus
|
|
elif location.vanilla_item in ['Bombchus', 'Bombchus (5)', 'Bombchus (10)', 'Bombchus (20)']:
|
|
if world.bombchus_in_logic:
|
|
item = 'Bombchus'
|
|
shuffle_item = location.name != 'Wasteland Bombchu Salesman' or world.shuffle_medigoron_carpet_salesman
|
|
if not shuffle_item:
|
|
location.show_in_spoiler = False
|
|
|
|
# Cows
|
|
elif location.vanilla_item == 'Milk':
|
|
if world.shuffle_cows:
|
|
item = get_junk_item()[0]
|
|
shuffle_item = world.shuffle_cows
|
|
if not shuffle_item:
|
|
location.show_in_spoiler = False
|
|
|
|
# Gerudo Card
|
|
elif location.vanilla_item == 'Gerudo Membership Card':
|
|
shuffle_item = world.shuffle_gerudo_card and world.gerudo_fortress != 'open'
|
|
if world.shuffle_gerudo_card and world.gerudo_fortress == 'open':
|
|
pending_junk_pool.append(item)
|
|
item = IGNORE_LOCATION
|
|
location.show_in_spoiler = False
|
|
|
|
# Bottles
|
|
elif location.vanilla_item in ['Bottle', 'Bottle with Milk', 'Rutos Letter']:
|
|
if ruto_bottles:
|
|
item = 'Rutos Letter'
|
|
ruto_bottles -= 1
|
|
else:
|
|
item = random.choice(normal_bottles)
|
|
shuffle_item = True
|
|
|
|
# Magic Beans
|
|
elif location.vanilla_item == 'Buy Magic Bean':
|
|
if world.shuffle_beans:
|
|
item = 'Magic Bean Pack' if not world.multiworld.start_inventory[world.player].value.get('Magic Bean Pack', 0) else get_junk_item()[0]
|
|
shuffle_item = world.shuffle_beans
|
|
if not shuffle_item:
|
|
location.show_in_spoiler = False
|
|
|
|
# Frogs Purple Rupees
|
|
elif location.scene == 0x54 and location.vanilla_item == 'Rupees (50)':
|
|
shuffle_item = world.shuffle_frog_song_rupees
|
|
if not shuffle_item:
|
|
location.show_in_spoiler = False
|
|
|
|
# Adult Trade Item
|
|
elif location.vanilla_item == 'Pocket Egg':
|
|
potential_trade_items = world.adult_trade_start if world.adult_trade_start else trade_items
|
|
item = random.choice(sorted(potential_trade_items))
|
|
world.selected_adult_trade_item = item
|
|
shuffle_item = True
|
|
|
|
# Thieves' Hideout
|
|
elif location.vanilla_item == 'Small Key (Thieves Hideout)':
|
|
shuffle_item = world.shuffle_hideoutkeys != 'vanilla'
|
|
if (world.gerudo_fortress == 'open'
|
|
or world.gerudo_fortress == 'fast' and location.name != 'Hideout 1 Torch Jail Gerudo Key'):
|
|
item = IGNORE_LOCATION
|
|
shuffle_item = False
|
|
location.show_in_spoiler = False
|
|
if shuffle_item and world.gerudo_fortress == 'normal' and 'Thieves Hideout' in world.key_rings:
|
|
item = get_junk_item()[0] if location.name != 'Hideout 1 Torch Jail Gerudo Key' else 'Small Key Ring (Thieves Hideout)'
|
|
|
|
# Freestanding Rupees and Hearts
|
|
elif location.type in ['ActorOverride', 'Freestanding', 'RupeeTower']:
|
|
if world.shuffle_freestanding_items == 'all':
|
|
shuffle_item = True
|
|
elif world.shuffle_freestanding_items == 'dungeons' and location.dungeon is not None:
|
|
shuffle_item = True
|
|
elif world.shuffle_freestanding_items == 'overworld' and location.dungeon is None:
|
|
shuffle_item = True
|
|
else:
|
|
shuffle_item = False
|
|
location.disabled = DisableType.DISABLED
|
|
location.show_in_spoiler = False
|
|
|
|
# Pots
|
|
elif location.type in ['Pot', 'FlyingPot']:
|
|
if world.shuffle_pots == 'all':
|
|
shuffle_item = True
|
|
elif world.shuffle_pots == 'dungeons' and (location.dungeon is not None or location.parent_region.is_boss_room):
|
|
shuffle_item = True
|
|
elif world.shuffle_pots == 'overworld' and not (location.dungeon is not None or location.parent_region.is_boss_room):
|
|
shuffle_item = True
|
|
else:
|
|
shuffle_item = False
|
|
location.disabled = DisableType.DISABLED
|
|
location.show_in_spoiler = False
|
|
|
|
# Crates
|
|
elif location.type in ['Crate', 'SmallCrate']:
|
|
if world.shuffle_crates == 'all':
|
|
shuffle_item = True
|
|
elif world.shuffle_crates == 'dungeons' and location.dungeon is not None:
|
|
shuffle_item = True
|
|
elif world.shuffle_crates == 'overworld' and location.dungeon is None:
|
|
shuffle_item = True
|
|
else:
|
|
shuffle_item = False
|
|
location.disabled = DisableType.DISABLED
|
|
location.show_in_spoiler = False
|
|
|
|
# Beehives
|
|
elif location.type == 'Beehive':
|
|
if world.shuffle_beehives:
|
|
shuffle_item = True
|
|
else:
|
|
shuffle_item = False
|
|
location.disabled = DisableType.DISABLED
|
|
location.show_in_spoiler = False
|
|
|
|
# Dungeon Items
|
|
elif location.dungeon is not None:
|
|
dungeon = location.dungeon
|
|
shuffle_setting = None
|
|
dungeon_collection = None
|
|
|
|
# Boss Key
|
|
if location.vanilla_item == dungeon.item_name("Boss Key"):
|
|
shuffle_setting = world.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else world.shuffle_ganon_bosskey
|
|
dungeon_collection = dungeon.boss_key
|
|
if shuffle_setting == 'vanilla':
|
|
shuffle_item = False
|
|
# Map or Compass
|
|
elif location.vanilla_item in [dungeon.item_name("Map"), dungeon.item_name("Compass")]:
|
|
shuffle_setting = world.shuffle_mapcompass
|
|
dungeon_collection = dungeon.dungeon_items
|
|
if shuffle_setting == 'vanilla':
|
|
shuffle_item = False
|
|
# Small Key
|
|
elif location.vanilla_item == dungeon.item_name("Small Key"):
|
|
shuffle_setting = world.shuffle_smallkeys
|
|
dungeon_collection = dungeon.small_keys
|
|
if shuffle_setting == 'vanilla':
|
|
shuffle_item = False
|
|
elif dungeon.name in world.key_rings and not dungeon.small_keys:
|
|
item = dungeon.item_name("Small Key Ring")
|
|
elif dungeon.name in world.key_rings:
|
|
item = get_junk_item()[0]
|
|
shuffle_item = True
|
|
# Any other item in a dungeon.
|
|
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
|
|
shuffle_item = True
|
|
|
|
# Handle dungeon item.
|
|
if shuffle_setting is not None and not shuffle_item:
|
|
dungeon_collection.append(world.create_item(item))
|
|
if shuffle_setting in ['remove', 'startwith']:
|
|
world.multiworld.push_precollected(dungeon_collection[-1])
|
|
world.remove_from_start_inventory.append(dungeon_collection[-1].name)
|
|
item = get_junk_item()[0]
|
|
shuffle_item = True
|
|
elif shuffle_setting in ['any_dungeon', 'overworld', 'regional']:
|
|
dungeon_collection[-1].priority = True
|
|
|
|
# The rest of the overworld items.
|
|
elif location.type in ["Chest", "NPC", "Song", "Collectable", "Cutscene", "BossHeart"]:
|
|
shuffle_item = True
|
|
|
|
# Now, handle the item as necessary.
|
|
if shuffle_item:
|
|
pool.append(item)
|
|
elif shuffle_item is not None:
|
|
placed_items[location.name] = item
|
|
# End of Locations loop.
|
|
|
|
# add unrestricted dungeon items to main item pool
|
|
pool.extend([item.name for item in get_unrestricted_dungeon_items(world)])
|
|
|
|
if world.shopsanity != 'off':
|
|
pool.extend(min_shop_items)
|
|
for item in min_shop_items:
|
|
remain_shop_items.remove(item)
|
|
|
|
shop_slots_count = len(remain_shop_items)
|
|
shop_non_item_count = len(world.shop_prices)
|
|
shop_item_count = shop_slots_count - shop_non_item_count
|
|
|
|
pool.extend(random.sample(remain_shop_items, shop_item_count))
|
|
if shop_non_item_count:
|
|
pool.extend(get_junk_item(shop_non_item_count))
|
|
|
|
# Extra rupees for shopsanity.
|
|
if world.shopsanity not in ['off', '0']:
|
|
for rupee in shopsanity_rupees:
|
|
if 'Rupees (5)' in pool:
|
|
pool[pool.index('Rupees (5)')] = rupee
|
|
else:
|
|
pending_junk_pool.append(rupee)
|
|
|
|
if world.free_scarecrow:
|
|
world.multiworld.push_precollected(world.create_item('Scarecrow Song'))
|
|
world.remove_from_start_inventory.append('Scarecrow Song')
|
|
|
|
if world.no_epona_race:
|
|
world.multiworld.push_precollected(world.create_item('Epona', allow_arbitrary_name=True))
|
|
world.remove_from_start_inventory.append('Epona')
|
|
|
|
if world.shuffle_smallkeys == 'vanilla':
|
|
# Logic cannot handle vanilla key layout in some dungeons
|
|
# this is because vanilla expects the dungeon major item to be
|
|
# locked behind the keys, which is not always true in rando.
|
|
# We can resolve this by starting with some extra keys
|
|
if world.dungeon_mq['Spirit Temple']:
|
|
# Yes somehow you need 3 keys. This dungeon is bonkers
|
|
keys = [world.create_item('Small Key (Spirit Temple)') for _ in range(3)]
|
|
for k in keys:
|
|
world.multiworld.push_precollected(k)
|
|
world.remove_from_start_inventory.append(k.name)
|
|
if 'Shadow Temple' in world.dungeon_shortcuts:
|
|
# Reverse Shadow is broken with vanilla keys in both vanilla/MQ
|
|
keys = [world.create_item('Small Key (Shadow Temple)') for _ in range(2)]
|
|
for k in keys:
|
|
world.multiworld.push_precollected(k)
|
|
world.remove_from_start_inventory.append(k.name)
|
|
|
|
if (not world.keysanity or (world.empty_dungeons['Fire Temple'] and world.shuffle_smallkeys != 'remove'))\
|
|
and not world.dungeon_mq['Fire Temple']:
|
|
world.multiworld.push_precollected(world.create_item('Small Key (Fire Temple)'))
|
|
world.remove_from_start_inventory.append('Small Key (Fire Temple)')
|
|
|
|
if world.shuffle_ganon_bosskey == 'on_lacs':
|
|
placed_items['ToT Light Arrows Cutscene'] = 'Boss Key (Ganons Castle)'
|
|
|
|
if world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts', 'triforce']:
|
|
placed_items['Gift from Sages'] = 'Boss Key (Ganons Castle)'
|
|
pool.extend(get_junk_item())
|
|
else:
|
|
placed_items['Gift from Sages'] = IGNORE_LOCATION
|
|
world.get_location('Gift from Sages').show_in_spoiler = False
|
|
|
|
if world.junk_ice_traps == 'off':
|
|
replace_max_item(pool, 'Ice Trap', 0)
|
|
elif world.junk_ice_traps == 'onslaught':
|
|
for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
|
|
replace_max_item(pool, item, 0)
|
|
|
|
for item, maximum in item_difficulty_max[world.item_pool_value].items():
|
|
replace_max_item(pool, item, maximum)
|
|
|
|
# world.distribution.alter_pool(world, pool)
|
|
|
|
# Make sure our pending_junk_pool is empty. If not, remove some random junk here.
|
|
if pending_junk_pool:
|
|
# for item in set(pending_junk_pool):
|
|
# # Ensure pending_junk_pool contents don't exceed values given by distribution file
|
|
# if item in world.distribution.item_pool:
|
|
# while pending_junk_pool.count(item) > world.distribution.item_pool[item].count:
|
|
# pending_junk_pool.remove(item)
|
|
# # Remove pending junk already added to the pool by alter_pool from the pending_junk_pool
|
|
# if item in pool:
|
|
# count = min(pool.count(item), pending_junk_pool.count(item))
|
|
# for _ in range(count):
|
|
# pending_junk_pool.remove(item)
|
|
|
|
remove_junk_pool, _ = zip(*junk_pool_base)
|
|
# Omits Rupees (200) and Deku Nuts (10)
|
|
remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
|
|
|
|
junk_candidates = [item for item in pool if item in remove_junk_pool]
|
|
if len(pending_junk_pool) > len(junk_candidates):
|
|
excess = len(pending_junk_pool) - len(junk_candidates)
|
|
if world.triforce_hunt:
|
|
raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).")
|
|
while pending_junk_pool:
|
|
pending_item = pending_junk_pool.pop()
|
|
if not junk_candidates:
|
|
raise RuntimeError("Not enough junk exists in item pool for %s (+%d others) to be added." % (pending_item, len(pending_junk_pool) - 1))
|
|
junk_item = random.choice(junk_candidates)
|
|
junk_candidates.remove(junk_item)
|
|
pool.remove(junk_item)
|
|
pool.append(pending_item)
|
|
|
|
return pool, placed_items
|
|
|
|
|
|
def get_unrestricted_dungeon_items(ootworld):
|
|
"""Adds maps, compasses, small keys, boss keys, and Ganon boss key into item pool if they are not placed."""
|
|
unrestricted_dungeon_items = []
|
|
add_settings = {'dungeon', 'any_dungeon', 'overworld', 'keysanity', 'regional'}
|
|
for dungeon in ootworld.dungeons:
|
|
if ootworld.shuffle_mapcompass in add_settings:
|
|
unrestricted_dungeon_items.extend(dungeon.dungeon_items)
|
|
if ootworld.shuffle_smallkeys in add_settings:
|
|
unrestricted_dungeon_items.extend(dungeon.small_keys)
|
|
if dungeon.name != 'Ganons Castle' and ootworld.shuffle_bosskeys in add_settings:
|
|
unrestricted_dungeon_items.extend(dungeon.boss_key)
|
|
if dungeon.name == 'Ganons Castle' and ootworld.shuffle_ganon_bosskey in add_settings:
|
|
unrestricted_dungeon_items.extend(dungeon.boss_key)
|
|
return unrestricted_dungeon_items
|