mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
Ocarina of Time 7.0 (#1277)
## What is this fixing or adding? - Adds the majority of OoTR 7.0 features: - Pot shuffle, Freestanding item shuffle, Crate shuffle, Beehive shuffle - Key rings mode - Dungeon shortcuts to speed up dungeons - "Regional" shuffle for dungeon items - New options for shop pricing in shopsanity - Expanded Ganon's Boss Key shuffle options - Pre-planted beans - Improved Chest Appearance Matches Contents mode - Blue Fire Arrows - Bonk self-damage - Finer control over MQ dungeons and spawn position randomization - Several bugfixes as a result of the update: - Items recognized by the server and valid starting items are now in a 1-to-1 correspondence. In particular, starting with keys is now supported. - Entrance randomization success rate improved. Hopefully it is now at 100%. Co-authored-by: Zach Parks <zach@alliware.com>
This commit is contained in:
@@ -10,8 +10,10 @@ from urllib.error import URLError, HTTPError
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
from BaseClasses import Region
|
||||
from .Items import OOTItem
|
||||
from .HintList import getHint, getHintGroup, Hint, hintExclusions
|
||||
from .HintList import getHint, getHintGroup, Hint, hintExclusions, \
|
||||
misc_item_hint_table, misc_location_hint_table
|
||||
from .Messages import COLOR_MAP, update_message_by_id
|
||||
from .TextBox import line_wrap
|
||||
from .Utils import data_path, read_json
|
||||
@@ -24,7 +26,10 @@ bingoBottlesForHints = (
|
||||
)
|
||||
|
||||
defaultHintDists = [
|
||||
'balanced.json', 'bingo.json', 'ddr.json', 'scrubs.json', 'strong.json', 'tournament.json', 'useless.json', 'very_strong.json'
|
||||
'async.json', 'balanced.json', 'bingo.json', 'chaos.json', 'coop2.json',
|
||||
'ddr.json', 'league.json', 'mw3.json', 'scrubs.json', 'strong.json',
|
||||
'tournament.json', 'useless.json', 'very_strong.json',
|
||||
'very_strong_magic.json', 'weekly.json'
|
||||
]
|
||||
|
||||
class RegionRestriction(Enum):
|
||||
@@ -294,6 +299,151 @@ class HintAreaNotFound(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class HintArea(Enum):
|
||||
# internal name prepositions display name short name color internal dungeon name
|
||||
# vague clear
|
||||
ROOT = 'in', 'in', "Link's pocket", 'Free', 'White', None
|
||||
HYRULE_FIELD = 'in', 'in', 'Hyrule Field', 'Hyrule Field', 'Light Blue', None
|
||||
LON_LON_RANCH = 'at', 'at', 'Lon Lon Ranch', 'Lon Lon Ranch', 'Light Blue', None
|
||||
MARKET = 'in', 'in', 'the Market', 'Market', 'Light Blue', None
|
||||
TEMPLE_OF_TIME = 'inside', 'inside', 'the Temple of Time', 'Temple of Time', 'Light Blue', None
|
||||
CASTLE_GROUNDS = 'on', 'on', 'the Castle Grounds', None, 'Light Blue', None # required for warp songs
|
||||
HYRULE_CASTLE = 'at', 'at', 'Hyrule Castle', 'Hyrule Castle', 'Light Blue', None
|
||||
OUTSIDE_GANONS_CASTLE = None, None, "outside Ganon's Castle", "Outside Ganon's Castle", 'Light Blue', None
|
||||
INSIDE_GANONS_CASTLE = 'inside', None, "inside Ganon's Castle", "Inside Ganon's Castle", 'Light Blue', 'Ganons Castle'
|
||||
GANONDORFS_CHAMBER = 'in', 'in', "Ganondorf's Chamber", "Ganondorf's Chamber", 'Light Blue', None
|
||||
KOKIRI_FOREST = 'in', 'in', 'Kokiri Forest', "Kokiri Forest", 'Green', None
|
||||
DEKU_TREE = 'inside', 'inside', 'the Deku Tree', "Deku Tree", 'Green', 'Deku Tree'
|
||||
LOST_WOODS = 'in', 'in', 'the Lost Woods', "Lost Woods", 'Green', None
|
||||
SACRED_FOREST_MEADOW = 'at', 'at', 'the Sacred Forest Meadow', "Sacred Forest Meadow", 'Green', None
|
||||
FOREST_TEMPLE = 'in', 'in', 'the Forest Temple', "Forest Temple", 'Green', 'Forest Temple'
|
||||
DEATH_MOUNTAIN_TRAIL = 'on', 'on', 'the Death Mountain Trail', "Death Mountain Trail", 'Red', None
|
||||
DODONGOS_CAVERN = 'within', 'in', "Dodongo's Cavern", "Dodongo's Cavern", 'Red', 'Dodongos Cavern'
|
||||
GORON_CITY = 'in', 'in', 'Goron City', "Goron City", 'Red', None
|
||||
DEATH_MOUNTAIN_CRATER = 'in', 'in', 'the Death Mountain Crater', "Death Mountain Crater", 'Red', None
|
||||
FIRE_TEMPLE = 'on', 'in', 'the Fire Temple', "Fire Temple", 'Red', 'Fire Temple'
|
||||
ZORA_RIVER = 'at', 'at', "Zora's River", "Zora's River", 'Blue', None
|
||||
ZORAS_DOMAIN = 'at', 'at', "Zora's Domain", "Zora's Domain", 'Blue', None
|
||||
ZORAS_FOUNTAIN = 'at', 'at', "Zora's Fountain", "Zora's Fountain", 'Blue', None
|
||||
JABU_JABUS_BELLY = 'in', 'inside', "Jabu Jabu's Belly", "Jabu Jabu's Belly", 'Blue', 'Jabu Jabus Belly'
|
||||
ICE_CAVERN = 'inside', 'in' , 'the Ice Cavern', "Ice Cavern", 'Blue', 'Ice Cavern'
|
||||
LAKE_HYLIA = 'at', 'at', 'Lake Hylia', "Lake Hylia", 'Blue', None
|
||||
WATER_TEMPLE = 'under', 'in', 'the Water Temple', "Water Temple", 'Blue', 'Water Temple'
|
||||
KAKARIKO_VILLAGE = 'in', 'in', 'Kakariko Village', "Kakariko Village", 'Pink', None
|
||||
BOTTOM_OF_THE_WELL = 'within', 'at', 'the Bottom of the Well', "Bottom of the Well", 'Pink', 'Bottom of the Well'
|
||||
GRAVEYARD = 'in', 'in', 'the Graveyard', "Graveyard", 'Pink', None
|
||||
SHADOW_TEMPLE = 'within', 'in', 'the Shadow Temple', "Shadow Temple", 'Pink', 'Shadow Temple'
|
||||
GERUDO_VALLEY = 'at', 'at', 'Gerudo Valley', "Gerudo Valley", 'Yellow', None
|
||||
GERUDO_FORTRESS = 'at', 'at', "Gerudo's Fortress", "Gerudo's Fortress", 'Yellow', None
|
||||
GERUDO_TRAINING_GROUND = 'within', 'on', 'the Gerudo Training Ground', "Gerudo Training Ground", 'Yellow', 'Gerudo Training Ground'
|
||||
HAUNTED_WASTELAND = 'in', 'in', 'the Haunted Wasteland', "Haunted Wasteland", 'Yellow', None
|
||||
DESERT_COLOSSUS = 'at', 'at', 'the Desert Colossus', "Desert Colossus", 'Yellow', None
|
||||
SPIRIT_TEMPLE = 'inside', 'in', 'the Spirit Temple', "Spirit Temple", 'Yellow', 'Spirit Temple'
|
||||
|
||||
# Performs a breadth first search to find the closest hint area from a given spot (region, location, or entrance).
|
||||
# May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area
|
||||
@staticmethod
|
||||
def at(spot, use_alt_hint=False):
|
||||
if isinstance(spot, Region):
|
||||
original_parent = spot
|
||||
else:
|
||||
original_parent = spot.parent_region
|
||||
already_checked = []
|
||||
spot_queue = [spot]
|
||||
|
||||
while spot_queue:
|
||||
current_spot = spot_queue.pop(0)
|
||||
already_checked.append(current_spot)
|
||||
|
||||
if isinstance(current_spot, Region):
|
||||
parent_region = current_spot
|
||||
else:
|
||||
parent_region = current_spot.parent_region
|
||||
|
||||
if parent_region.hint and (original_parent.name == 'Root' or parent_region.name != 'Root'):
|
||||
if use_alt_hint and parent_region.alt_hint:
|
||||
return parent_region.alt_hint
|
||||
return parent_region.hint
|
||||
|
||||
spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances)))
|
||||
|
||||
raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.player))
|
||||
|
||||
@classmethod
|
||||
def for_dungeon(cls, dungeon_name: str):
|
||||
if '(' in dungeon_name and ')' in dungeon_name:
|
||||
# A dungeon item name was passed in - get the name of the dungeon from it.
|
||||
dungeon_name = dungeon_name[dungeon_name.index('(') + 1:dungeon_name.index(')')]
|
||||
|
||||
if dungeon_name == "Thieves Hideout":
|
||||
# Special case for Thieves' Hideout - change this if it gets its own hint area.
|
||||
return HintArea.GERUDO_FORTRESS
|
||||
|
||||
for hint_area in cls:
|
||||
if hint_area.dungeon_name == dungeon_name:
|
||||
return hint_area
|
||||
return None
|
||||
|
||||
def preposition(self, clearer_hints):
|
||||
return self.value[1 if clearer_hints else 0]
|
||||
|
||||
def __str__(self):
|
||||
return self.value[2]
|
||||
|
||||
# used for dungeon reward locations in the pause menu
|
||||
@property
|
||||
def short_name(self):
|
||||
return self.value[3]
|
||||
|
||||
# Hint areas are further grouped into colored sections of the map by association with the medallions.
|
||||
# These colors are used to generate the text boxes for shuffled warp songs.
|
||||
@property
|
||||
def color(self):
|
||||
return self.value[4]
|
||||
|
||||
@property
|
||||
def dungeon_name(self):
|
||||
return self.value[5]
|
||||
|
||||
@property
|
||||
def is_dungeon(self):
|
||||
return self.dungeon_name is not None
|
||||
|
||||
def is_dungeon_item(self, item):
|
||||
for dungeon in item.world.dungeons:
|
||||
if dungeon.name == self.dungeon_name:
|
||||
return dungeon.is_dungeon_item(item)
|
||||
return False
|
||||
|
||||
# Formats the hint text for this area with proper grammar.
|
||||
# Dungeons are hinted differently depending on the clearer_hints setting.
|
||||
def text(self, clearer_hints, preposition=False, world=None):
|
||||
if self.is_dungeon:
|
||||
text = getHint(self.dungeon_name, clearer_hints).text
|
||||
else:
|
||||
text = str(self)
|
||||
prefix, suffix = text.replace('#', '').split(' ', 1)
|
||||
if world is None:
|
||||
if prefix == "Link's":
|
||||
text = f"@'s {suffix}"
|
||||
else:
|
||||
replace_prefixes = ('a', 'an', 'the')
|
||||
move_prefixes = ('outside', 'inside')
|
||||
if prefix in replace_prefixes:
|
||||
text = f"world {world}'s {suffix}"
|
||||
elif prefix in move_prefixes:
|
||||
text = f"{prefix} world {world}'s {suffix}"
|
||||
elif prefix == "Link's":
|
||||
text = f"player {world}'s {suffix}"
|
||||
else:
|
||||
text = f"world {world}'s {text}"
|
||||
if '#' not in text:
|
||||
text = f'#{text}#'
|
||||
if preposition and self.preposition(clearer_hints) is not None:
|
||||
text = f'{self.preposition(clearer_hints)} {text}'
|
||||
return text
|
||||
|
||||
|
||||
# Peforms a breadth first search to find the closest hint area from a given spot (location or entrance)
|
||||
# May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area
|
||||
# Returns the name of the location if the spot is not in OoT
|
||||
@@ -643,7 +793,7 @@ def buildWorldGossipHints(world, checkedLocations=None):
|
||||
|
||||
# If Ganondorf hints Light Arrows and is reachable without them, add to checkedLocations to prevent extra hinting
|
||||
# Can only be forced with vanilla bridge or trials
|
||||
if world.bridge != 'vanilla' and world.trials == 0 and world.misc_hints:
|
||||
if world.bridge != 'vanilla' and world.trials == 0 and 'ganondorf' in world.misc_hints:
|
||||
try:
|
||||
light_arrow_location = world.multiworld.find_item("Light Arrows", world.player)
|
||||
checkedLocations[light_arrow_location.player].add(light_arrow_location.name)
|
||||
@@ -885,12 +1035,17 @@ def buildAltarHints(world, messages, include_rewards=True, include_wincons=True)
|
||||
|
||||
# pulls text string from hintlist for reward after sending the location to hintlist.
|
||||
def buildBossString(reward, color, world):
|
||||
for location in world.multiworld.get_filled_locations(world.player):
|
||||
if location.item.name == reward:
|
||||
item_icon = chr(location.item.special['item_id'])
|
||||
location_text = getHint(location.name, world.clearer_hints).text
|
||||
return str(GossipText("\x08\x13%s%s" % (item_icon, location_text), [color], prefix='')) + '\x04'
|
||||
return ''
|
||||
item_icon = chr(world.create_item(reward).special['item_id'])
|
||||
if world.multiworld.state.has(reward, world.player):
|
||||
if world.clearer_hints:
|
||||
text = GossipText(f"\x08\x13{item_icon}One #@ already has#...", [color], prefix='')
|
||||
else:
|
||||
text = GossipText(f"\x08\x13{item_icon}One in #@'s pocket#...", [color], prefix='')
|
||||
else:
|
||||
location = world.hinted_dungeon_reward_locations[reward]
|
||||
location_text = HintArea.at(location).text(world.clearer_hints, preposition=True)
|
||||
text = GossipText(f"\x08\x13{item_icon}One {location_text}...", [color], prefix='')
|
||||
return str(text) + '\x04'
|
||||
|
||||
|
||||
def buildBridgeReqsString(world):
|
||||
@@ -907,6 +1062,8 @@ def buildBridgeReqsString(world):
|
||||
item_req_string = str(world.bridge_rewards) + ' ' + item_req_string
|
||||
elif world.bridge == 'tokens':
|
||||
item_req_string = str(world.bridge_tokens) + ' ' + item_req_string
|
||||
elif world.bridge == 'hearts':
|
||||
item_req_string = str(world.bridge_hearts) + ' ' + item_req_string
|
||||
if '#' not in item_req_string:
|
||||
item_req_string = '#%s#' % item_req_string
|
||||
string += "The awakened ones will await for the Hero to collect %s." % item_req_string
|
||||
@@ -928,9 +1085,26 @@ def buildGanonBossKeyString(world):
|
||||
item_req_string = str(world.lacs_rewards) + ' ' + item_req_string
|
||||
elif world.lacs_condition == 'tokens':
|
||||
item_req_string = str(world.lacs_tokens) + ' ' + item_req_string
|
||||
elif world.lacs_condition == 'hearts':
|
||||
item_req_string = str(world.lacs_hearts) + ' ' + item_req_string
|
||||
if '#' not in item_req_string:
|
||||
item_req_string = '#%s#' % item_req_string
|
||||
bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
|
||||
elif world.shuffle_ganon_bosskey in ['stones', 'medallions', 'dungeons', 'tokens', 'hearts']:
|
||||
item_req_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
||||
if world.shuffle_ganon_bosskey == 'medallions':
|
||||
item_req_string = str(world.ganon_bosskey_medallions) + ' ' + item_req_string
|
||||
elif world.shuffle_ganon_bosskey == 'stones':
|
||||
item_req_string = str(world.ganon_bosskey_stones) + ' ' + item_req_string
|
||||
elif world.shuffle_ganon_bosskey == 'dungeons':
|
||||
item_req_string = str(world.ganon_bosskey_rewards) + ' ' + item_req_string
|
||||
elif world.shuffle_ganon_bosskey == 'tokens':
|
||||
item_req_string = str(world.ganon_bosskey_tokens) + ' ' + item_req_string
|
||||
elif world.shuffle_ganon_bosskey == 'hearts':
|
||||
item_req_string = str(world.ganon_bosskey_hearts) + ' ' + item_req_string
|
||||
if '#' not in item_req_string:
|
||||
item_req_string = '#%s#' % item_req_string
|
||||
bk_location_string = "automatically granted once %s are retrieved" % item_req_string
|
||||
else:
|
||||
bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
|
||||
string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
|
||||
@@ -950,30 +1124,52 @@ def buildGanonText(world, messages):
|
||||
text = get_raw_text(ganonLines.pop().text)
|
||||
update_message_by_id(messages, 0x70CB, text)
|
||||
|
||||
# light arrow hint or validation chest item
|
||||
if world.starting_items['Light Arrows'] > 0:
|
||||
text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
|
||||
text += "\x05\x42your pocket\x05\x40"
|
||||
else:
|
||||
try:
|
||||
find_light_arrows = world.multiworld.find_item('Light Arrows', world.player)
|
||||
text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
|
||||
location = find_light_arrows
|
||||
location_hint = get_hint_area(location)
|
||||
if world.player != location.player:
|
||||
text += "\x05\x42%s's\x05\x40 %s" % (world.multiworld.get_player_name(location.player), get_raw_text(location_hint))
|
||||
else:
|
||||
location_hint = location_hint.replace('Ganon\'s Castle', 'my castle')
|
||||
text += get_raw_text(location_hint)
|
||||
except StopIteration:
|
||||
text = get_raw_text(getHint('Validation Line', world.clearer_hints).text)
|
||||
for location in world.multiworld.get_filled_locations(world.player):
|
||||
if location.name == 'Ganons Tower Boss Key Chest':
|
||||
text += get_raw_text(getHint(getItemGenericName(location.item), world.clearer_hints).text)
|
||||
break
|
||||
text += '!'
|
||||
|
||||
update_message_by_id(messages, 0x70CC, text)
|
||||
# Modified from original. Uses optimized AP methods, no support for custom items.
|
||||
def buildMiscItemHints(world, messages):
|
||||
for hint_type, data in misc_item_hint_table.items():
|
||||
if hint_type in world.misc_hints:
|
||||
item_locations = world.multiworld.find_item_locations(data['default_item'], world.player)
|
||||
if data['local_only']:
|
||||
item_locations = [loc for loc in item_locations if loc.player == world.player]
|
||||
|
||||
if world.multiworld.state.has(data['default_item'], world.player) > 0:
|
||||
text = data['default_item_text'].format(area='#your pocket#')
|
||||
elif item_locations:
|
||||
location = item_locations[0]
|
||||
player_text = ''
|
||||
if location.player != world.player:
|
||||
player_text = world.multiworld.get_player_name(location.player) + "'s "
|
||||
if location.game == 'Ocarina of Time':
|
||||
area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None)
|
||||
else:
|
||||
area = location.name
|
||||
text = data['default_item_text'].format(area=(player_text + area))
|
||||
elif 'default_item_fallback' in data:
|
||||
text = data['default_item_fallback']
|
||||
else:
|
||||
text = getHint('Validation Line', world.clearer_hints).text
|
||||
location = world.get_location('Ganons Tower Boss Key Chest')
|
||||
text += f"#{getHint(getItemGenericName(location.item), world.clearer_hints).text}#"
|
||||
for find, replace in data.get('replace', {}).items():
|
||||
text = text.replace(find, replace)
|
||||
|
||||
update_message_by_id(messages, data['id'], str(GossipText(text, ['Green'], prefix='')))
|
||||
|
||||
|
||||
# Modified from original to use optimized AP methods
|
||||
def buildMiscLocationHints(world, messages):
|
||||
for hint_type, data in misc_location_hint_table.items():
|
||||
text = data['location_fallback']
|
||||
if hint_type in world.misc_hints:
|
||||
location = world.get_location(data['item_location'])
|
||||
item = location.item
|
||||
item_text = getHint(getItemGenericName(item), world.clearer_hints).text
|
||||
if item.player != world.player:
|
||||
item_text += f' for {world.multiworld.get_player_name(item.player)}'
|
||||
text = data['location_text'].format(item=item_text)
|
||||
|
||||
update_message_by_id(messages, data['id'], str(GossipText(text, ['Green'], prefix='')), 0x23)
|
||||
|
||||
|
||||
def get_raw_text(string):
|
||||
|
||||
Reference in New Issue
Block a user