2506 lines
107 KiB
Python
2506 lines
107 KiB
Python
import base64
|
|
import copy
|
|
import json
|
|
import logging
|
|
from collections import OrderedDict, Counter, deque, defaultdict
|
|
from enum import Enum, IntEnum, unique
|
|
|
|
try:
|
|
from fast_enum import FastEnum
|
|
except ImportError:
|
|
from enum import IntFlag as FastEnum
|
|
|
|
|
|
from source.classes.BabelFish import BabelFish
|
|
from EntranceShuffle import door_addresses, indirect_connections
|
|
from Utils import int16_as_bytes
|
|
from Tables import normal_offset_table, spiral_offset_table, multiply_lookup, divisor_lookup
|
|
from RoomData import Room
|
|
|
|
class World(object):
|
|
|
|
def __init__(self, players, owShuffle, owSwap, shuffle, doorShuffle, logic, mode, swords, difficulty, difficulty_adjustments,
|
|
timer, progressive, goal, algorithm, accessibility, shuffle_ganon, retro, custom, customitemarray, hints):
|
|
self.players = players
|
|
self.teams = 1
|
|
self.owShuffle = owShuffle.copy()
|
|
self.owSwap = owSwap.copy()
|
|
self.owKeepSimilar = {}
|
|
self.shuffle = shuffle.copy()
|
|
self.doorShuffle = doorShuffle.copy()
|
|
self.intensity = {}
|
|
self.logic = logic.copy()
|
|
self.mode = mode.copy()
|
|
self.swords = swords.copy()
|
|
self.difficulty = difficulty.copy()
|
|
self.difficulty_adjustments = difficulty_adjustments.copy()
|
|
self.timer = timer
|
|
self.progressive = progressive
|
|
self.goal = goal.copy()
|
|
self.algorithm = algorithm
|
|
self.dungeons = []
|
|
self.regions = []
|
|
self.shops = {}
|
|
self.itempool = []
|
|
self.seed = None
|
|
self.precollected_items = []
|
|
self.state = CollectionState(self)
|
|
self._cached_entrances = None
|
|
self._cached_locations = None
|
|
self._entrance_cache = {}
|
|
self._location_cache = {}
|
|
self.required_locations = []
|
|
self.shuffle_bonk_prizes = False
|
|
self.light_world_light_cone = False
|
|
self.dark_world_light_cone = False
|
|
self.clock_mode = 'none'
|
|
self.rupoor_cost = 10
|
|
self.aga_randomness = True
|
|
self.lock_aga_door_in_escape = False
|
|
self.save_and_quit_from_boss = True
|
|
self.accessibility = accessibility.copy()
|
|
self.fix_skullwoods_exit = {}
|
|
self.fix_palaceofdarkness_exit = {}
|
|
self.fix_trock_exit = {}
|
|
self.shuffle_ganon = shuffle_ganon
|
|
self.fix_gtower_exit = self.shuffle_ganon
|
|
self.retro = retro.copy()
|
|
self.custom = custom
|
|
self.customitemarray = customitemarray
|
|
self.can_take_damage = True
|
|
self.hints = hints.copy()
|
|
self.dynamic_regions = []
|
|
self.dynamic_locations = []
|
|
self.spoiler = Spoiler(self)
|
|
self.lamps_needed_for_dark_rooms = 1
|
|
self.owswaps = {}
|
|
self.owedges = []
|
|
self._owedge_cache = {}
|
|
self.doors = []
|
|
self._door_cache = {}
|
|
self.paired_doors = {}
|
|
self.rooms = []
|
|
self._room_cache = {}
|
|
self.dungeon_layouts = {}
|
|
self.inaccessible_regions = {}
|
|
self.key_logic = {}
|
|
self.pool_adjustment = {}
|
|
self.key_layout = defaultdict(dict)
|
|
self.dungeon_portals = defaultdict(list)
|
|
self._portal_cache = {}
|
|
self.sanc_portal = {}
|
|
self.fish = BabelFish()
|
|
|
|
for player in range(1, players + 1):
|
|
# If World State is Retro, set to Open and set Retro flag
|
|
if self.mode[player] == "retro":
|
|
self.mode[player] = "open"
|
|
self.retro[player] = True
|
|
def set_player_attr(attr, val):
|
|
self.__dict__.setdefault(attr, {})[player] = val
|
|
set_player_attr('_region_cache', {})
|
|
set_player_attr('player_names', [])
|
|
set_player_attr('remote_items', False)
|
|
set_player_attr('required_medallions', ['Ether', 'Quake'])
|
|
set_player_attr('swamp_patch_required', False)
|
|
set_player_attr('powder_patch_required', False)
|
|
set_player_attr('ganon_at_pyramid', True)
|
|
set_player_attr('ganonstower_vanilla', True)
|
|
set_player_attr('sewer_light_cone', self.mode[player] == 'standard')
|
|
set_player_attr('fix_trock_doors', self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
|
|
set_player_attr('fix_skullwoods_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'] or self.doorShuffle[player] not in ['vanilla'])
|
|
set_player_attr('fix_palaceofdarkness_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
set_player_attr('fix_trock_exit', self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
|
|
set_player_attr('can_access_trock_eyebridge', None)
|
|
set_player_attr('can_access_trock_front', None)
|
|
set_player_attr('can_access_trock_big_chest', None)
|
|
set_player_attr('can_access_trock_middle', None)
|
|
set_player_attr('fix_fake_world', logic[player] not in ['owglitches', 'nologic'] or shuffle[player] in ['crossed', 'insanity', 'madness_legacy'])
|
|
set_player_attr('mapshuffle', False)
|
|
set_player_attr('compassshuffle', False)
|
|
set_player_attr('keyshuffle', False)
|
|
set_player_attr('bigkeyshuffle', False)
|
|
set_player_attr('difficulty_requirements', None)
|
|
set_player_attr('boss_shuffle', 'none')
|
|
set_player_attr('enemy_shuffle', 'none')
|
|
set_player_attr('enemy_health', 'default')
|
|
set_player_attr('enemy_damage', 'default')
|
|
set_player_attr('beemizer', 0)
|
|
set_player_attr('escape_assist', [])
|
|
set_player_attr('crystals_needed_for_ganon', 7)
|
|
set_player_attr('crystals_needed_for_gt', 7)
|
|
set_player_attr('crystals_ganon_orig', {})
|
|
set_player_attr('crystals_gt_orig', {})
|
|
set_player_attr('open_pyramid', False)
|
|
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
|
|
set_player_attr('treasure_hunt_count', 0)
|
|
set_player_attr('treasure_hunt_total', 0)
|
|
set_player_attr('potshuffle', False)
|
|
set_player_attr('pot_contents', None)
|
|
|
|
set_player_attr('shopsanity', False)
|
|
set_player_attr('keydropshuffle', False)
|
|
set_player_attr('mixed_travel', 'prevent')
|
|
set_player_attr('standardize_palettes', 'standardize')
|
|
set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False})
|
|
set_player_attr('owswaps', [[],[],[]])
|
|
|
|
def get_name_string_for_object(self, obj):
|
|
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
|
|
|
|
def get_player_names(self, player):
|
|
return ", ".join([name for i, name in enumerate(self.player_names[player]) if self.player_names[player].index(name) == i])
|
|
|
|
def initialize_regions(self, regions=None):
|
|
for region in regions if regions else self.regions:
|
|
region.world = self
|
|
self._region_cache[region.player][region.name] = region
|
|
for exit in region.exits:
|
|
self._entrance_cache[exit.name, exit.player] = exit
|
|
for r_location in region.locations:
|
|
self._location_cache[r_location.name, r_location.player] = r_location
|
|
|
|
def initialize_owedges(self, edges):
|
|
for edge in edges:
|
|
self._owedge_cache[(edge.name, edge.player)] = edge
|
|
self._entrance_cache[edge.name, edge.player].spot_type = 'OWEdge'
|
|
|
|
def initialize_doors(self, doors):
|
|
for door in doors:
|
|
self._door_cache[(door.name, door.player)] = door
|
|
|
|
def remove_owedge(self, edge, player):
|
|
if (edge.name, player) in self._owedge_cache.keys():
|
|
del self._owedge_cache[(edge.name, player)]
|
|
if edge in self.owedges:
|
|
self.owedges.remove(edge)
|
|
|
|
def remove_door(self, door, player):
|
|
if (door.name, player) in self._door_cache.keys():
|
|
del self._door_cache[(door.name, player)]
|
|
if door in self.doors:
|
|
self.doors.remove(door)
|
|
|
|
def get_regions(self, player=None):
|
|
return self.regions if player is None else self._region_cache[player].values()
|
|
|
|
def get_region(self, regionname, player):
|
|
if isinstance(regionname, Region):
|
|
return regionname
|
|
try:
|
|
return self._region_cache[player][regionname]
|
|
except KeyError:
|
|
for region in self.regions:
|
|
if region.name == regionname and region.player == player:
|
|
assert not region.world # this should only happen before initialization
|
|
return region
|
|
raise RuntimeError('No such region %s for player %d' % (regionname, player))
|
|
|
|
def get_owedge(self, edgename, player):
|
|
if isinstance(edgename, OWEdge):
|
|
return edgename
|
|
try:
|
|
return self._owedge_cache[(edgename, player)]
|
|
except KeyError:
|
|
for edge in self.owedges:
|
|
if edge.name == edgename and edge.player == player:
|
|
self._owedge_cache[(edgename, player)] = edge
|
|
return edge
|
|
raise RuntimeError('No such edge %s for player %d' % (edgename, player))
|
|
|
|
def get_entrance(self, entrance, player):
|
|
if isinstance(entrance, Entrance):
|
|
return entrance
|
|
try:
|
|
return self._entrance_cache[(entrance, player)]
|
|
except KeyError:
|
|
for region in self.regions:
|
|
for exit in region.exits:
|
|
if exit.name == entrance and exit.player == player:
|
|
self._entrance_cache[(entrance, player)] = exit
|
|
return exit
|
|
raise RuntimeError('No such entrance %s for player %d' % (entrance, player))
|
|
|
|
def remove_entrance(self, entrance, player):
|
|
if (entrance, player) in self._entrance_cache.keys():
|
|
del self._entrance_cache[(entrance, player)]
|
|
|
|
def get_location(self, location, player):
|
|
if isinstance(location, Location):
|
|
return location
|
|
try:
|
|
return self._location_cache[(location, player)]
|
|
except KeyError:
|
|
for region in self.regions:
|
|
for r_location in region.locations:
|
|
if r_location.name == location and r_location.player == player:
|
|
self._location_cache[(location, player)] = r_location
|
|
return r_location
|
|
raise RuntimeError('No such location %s for player %d' % (location, player))
|
|
|
|
def get_dungeon(self, dungeonname, player):
|
|
if isinstance(dungeonname, Dungeon):
|
|
return dungeonname
|
|
|
|
for dungeon in self.dungeons:
|
|
if dungeon.name == dungeonname and dungeon.player == player:
|
|
return dungeon
|
|
raise RuntimeError('No such dungeon %s for player %d' % (dungeonname, player))
|
|
|
|
def get_door(self, doorname, player):
|
|
if isinstance(doorname, Door):
|
|
return doorname
|
|
try:
|
|
return self._door_cache[(doorname, player)]
|
|
except KeyError:
|
|
for door in self.doors:
|
|
if door.name == doorname and door.player == player:
|
|
self._door_cache[(doorname, player)] = door
|
|
return door
|
|
raise RuntimeError('No such door %s for player %d' % (doorname, player))
|
|
|
|
def get_portal(self, portal_name, player):
|
|
if isinstance(portal_name, Portal):
|
|
return portal_name
|
|
try:
|
|
return self._portal_cache[(portal_name, player)]
|
|
except KeyError:
|
|
for portal in self.dungeon_portals[player]:
|
|
if portal.name == portal_name and portal.player == player:
|
|
self._portal_cache[(portal_name, player)] = portal
|
|
return portal
|
|
raise RuntimeError('No such portal %s for player %d' % (portal_name, player))
|
|
|
|
def check_for_owedge(self, edgename, player):
|
|
if isinstance(edgename, OWEdge):
|
|
return edgename
|
|
try:
|
|
return self._owedge_cache[(edgename, player)]
|
|
except KeyError:
|
|
for edge in self.owedges:
|
|
if edge.name == edgename and edge.player == player:
|
|
self._owedge_cache[(edgename, player)] = edge
|
|
return edge
|
|
return None
|
|
|
|
def check_for_door(self, doorname, player):
|
|
if isinstance(doorname, Door):
|
|
return doorname
|
|
try:
|
|
return self._door_cache[(doorname, player)]
|
|
except KeyError:
|
|
for door in self.doors:
|
|
if door.name == doorname and door.player == player:
|
|
self._door_cache[(doorname, player)] = door
|
|
return door
|
|
return None
|
|
|
|
def check_for_entrance(self, entrance, player):
|
|
if isinstance(entrance, Entrance):
|
|
return entrance
|
|
try:
|
|
return self._entrance_cache[(entrance, player)]
|
|
except KeyError:
|
|
for region in self.regions:
|
|
for ext in region.exits:
|
|
if ext.name == entrance and ext.player == player:
|
|
self._entrance_cache[(entrance, player)] = ext
|
|
return ext
|
|
return None
|
|
|
|
def get_room(self, room_idx, player):
|
|
if isinstance(room_idx, Room):
|
|
return room_idx
|
|
try:
|
|
return self._room_cache[(room_idx, player)]
|
|
except KeyError:
|
|
for room in self.rooms:
|
|
if room.index == room_idx and room.player == player:
|
|
self._room_cache[(room_idx, player)] = room
|
|
return room
|
|
raise RuntimeError('No such room %s for player %d' % (room_idx, player))
|
|
|
|
def get_all_state(self, keys=False):
|
|
ret = CollectionState(self)
|
|
|
|
def soft_collect(item):
|
|
if item.name.startswith('Progressive '):
|
|
if 'Sword' in item.name:
|
|
if ret.has('Golden Sword', item.player):
|
|
pass
|
|
elif ret.has('Tempered Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 4:
|
|
ret.prog_items['Golden Sword', item.player] += 1
|
|
elif ret.has('Master Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 3:
|
|
ret.prog_items['Tempered Sword', item.player] += 1
|
|
elif ret.has('Fighter Sword', item.player) and self.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
ret.prog_items['Master Sword', item.player] += 1
|
|
elif self.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
ret.prog_items['Fighter Sword', item.player] += 1
|
|
elif 'Glove' in item.name:
|
|
if ret.has('Titans Mitts', item.player):
|
|
pass
|
|
elif ret.has('Power Glove', item.player):
|
|
ret.prog_items['Titans Mitts', item.player] += 1
|
|
else:
|
|
ret.prog_items['Power Glove', item.player] += 1
|
|
elif 'Shield' in item.name:
|
|
if ret.has('Mirror Shield', item.player):
|
|
pass
|
|
elif ret.has('Red Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
ret.prog_items['Mirror Shield', item.player] += 1
|
|
elif ret.has('Blue Shield', item.player) and self.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
ret.prog_items['Red Shield', item.player] += 1
|
|
elif self.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
ret.prog_items['Blue Shield', item.player] += 1
|
|
elif 'Bow' in item.name:
|
|
if ret.has('Silver Arrows', item.player):
|
|
pass
|
|
elif ret.has('Bow', item.player) and self.difficulty_requirements[item.player].progressive_bow_limit >= 2:
|
|
ret.prog_items['Silver Arrows', item.player] += 1
|
|
elif self.difficulty_requirements[item.player].progressive_bow_limit >= 1:
|
|
ret.prog_items['Bow', item.player] += 1
|
|
elif item.name.startswith('Bottle'):
|
|
if ret.bottle_count(item.player) < self.difficulty_requirements[item.player].progressive_bottle_limit:
|
|
ret.prog_items[item.name, item.player] += 1
|
|
elif item.advancement or item.smallkey or item.bigkey:
|
|
ret.prog_items[item.name, item.player] += 1
|
|
|
|
for item in self.itempool:
|
|
soft_collect(item)
|
|
|
|
if keys:
|
|
for p in range(1, self.players + 1):
|
|
key_list = []
|
|
player_dungeons = [x for x in self.dungeons if x.player == p]
|
|
for dungeon in player_dungeons:
|
|
if dungeon.big_key is not None:
|
|
key_list += [dungeon.big_key.name]
|
|
if len(dungeon.small_keys) > 0:
|
|
key_list += [x.name for x in dungeon.small_keys]
|
|
from Items import ItemFactory
|
|
for item in ItemFactory(key_list, p):
|
|
soft_collect(item)
|
|
ret.sweep_for_events()
|
|
return ret
|
|
|
|
def get_items(self):
|
|
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
|
|
|
def find_items(self, item, player):
|
|
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player]
|
|
|
|
def find_items_not_key_only(self, item, player):
|
|
return [location for location in self.get_locations() if location.item is not None and location.item.name == item and location.item.player == player and location.forced_item is None]
|
|
|
|
def push_precollected(self, item):
|
|
item.world = self
|
|
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
|
|
item.advancement = True
|
|
self.precollected_items.append(item)
|
|
self.state.collect(item, True)
|
|
|
|
def push_item(self, location, item, collect=True):
|
|
if not isinstance(location, Location):
|
|
raise RuntimeError('Cannot assign item %s to location %s (player %d).' % (item, location, item.player))
|
|
|
|
if location.can_fill(self.state, item, False):
|
|
location.item = item
|
|
item.location = location
|
|
item.world = self
|
|
if collect:
|
|
self.state.collect(item, location.event, location)
|
|
|
|
logging.getLogger('').debug('Placed %s at %s', item, location)
|
|
else:
|
|
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
|
|
|
|
def get_entrances(self):
|
|
if self._cached_entrances is None:
|
|
self._cached_entrances = []
|
|
for region in self.regions:
|
|
self._cached_entrances.extend(region.entrances)
|
|
return self._cached_entrances
|
|
|
|
def clear_entrance_cache(self):
|
|
self._cached_entrances = None
|
|
|
|
def get_locations(self):
|
|
if self._cached_locations is None:
|
|
self._cached_locations = []
|
|
for region in self.regions:
|
|
self._cached_locations.extend(region.locations)
|
|
return self._cached_locations
|
|
|
|
def clear_location_cache(self):
|
|
self._cached_locations = None
|
|
|
|
def get_unfilled_locations(self, player=None):
|
|
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None]
|
|
|
|
def get_filled_locations(self, player=None):
|
|
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is not None]
|
|
|
|
def get_reachable_locations(self, state=None, player=None):
|
|
if state is None:
|
|
state = self.state
|
|
return [location for location in self.get_locations() if (player is None or location.player == player) and location.can_reach(state)]
|
|
|
|
def get_placeable_locations(self, state=None, player=None):
|
|
if state is None:
|
|
state = self.state
|
|
return [location for location in self.get_locations() if (player is None or location.player == player) and location.item is None and location.can_reach(state)]
|
|
|
|
def unlocks_new_location(self, item):
|
|
temp_state = self.state.copy()
|
|
temp_state.collect(item, True)
|
|
|
|
for location in self.get_unfilled_locations():
|
|
if temp_state.can_reach(location) and not self.state.can_reach(location):
|
|
return True
|
|
|
|
return False
|
|
|
|
def has_beaten_game(self, state, player=None):
|
|
if player:
|
|
return state.has('Triforce', player)
|
|
else:
|
|
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
|
|
|
def can_beat_game(self, starting_state=None):
|
|
if starting_state:
|
|
state = starting_state.copy()
|
|
else:
|
|
state = CollectionState(self)
|
|
|
|
if self.has_beaten_game(state):
|
|
return True
|
|
|
|
prog_locations = [location for location in self.get_locations() if location.item is not None and (location.item.advancement or location.event) and location not in state.locations_checked]
|
|
|
|
while prog_locations:
|
|
sphere = []
|
|
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
|
|
for location in prog_locations:
|
|
if location.can_reach(state) and state.not_flooding_a_key(state.world, location):
|
|
sphere.append(location)
|
|
|
|
if not sphere:
|
|
# ran out of places and did not finish yet, quit
|
|
return False
|
|
|
|
for location in sphere:
|
|
prog_locations.remove(location)
|
|
state.collect(location.item, True, location)
|
|
|
|
if self.has_beaten_game(state):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class CollectionState(object):
|
|
|
|
def __init__(self, parent):
|
|
self.prog_items = Counter()
|
|
self.world = parent
|
|
self.reachable_regions = {player: dict() for player in range(1, parent.players + 1)}
|
|
self.blocked_connections = {player: dict() for player in range(1, parent.players + 1)}
|
|
self.events = []
|
|
self.path = {}
|
|
self.locations_checked = set()
|
|
self.stale = {player: True for player in range(1, parent.players + 1)}
|
|
for item in parent.precollected_items:
|
|
self.collect(item, True)
|
|
|
|
def update_reachable_regions(self, player):
|
|
self.stale[player] = False
|
|
rrp = self.reachable_regions[player]
|
|
bc = self.blocked_connections[player]
|
|
|
|
# init on first call - this can't be done on construction since the regions don't exist yet
|
|
start = self.world.get_region('Menu', player)
|
|
if not start in rrp:
|
|
rrp[start] = CrystalBarrier.Orange
|
|
for exit in start.exits:
|
|
bc[exit] = CrystalBarrier.Orange
|
|
|
|
queue = deque(self.blocked_connections[player].items())
|
|
|
|
# run BFS on all connections, and keep track of those blocked by missing items
|
|
while True:
|
|
try:
|
|
connection, crystal_state = queue.popleft()
|
|
new_region = connection.connected_region
|
|
if new_region is None or new_region in rrp and (new_region.type != RegionType.Dungeon or (rrp[new_region] & crystal_state) == crystal_state):
|
|
bc.pop(connection, None)
|
|
elif connection.can_reach(self):
|
|
if new_region.type == RegionType.Dungeon:
|
|
new_crystal_state = crystal_state
|
|
for exit in new_region.exits:
|
|
door = exit.door
|
|
if door is not None and door.crystal == CrystalBarrier.Either and door.entrance.can_reach(self):
|
|
new_crystal_state = CrystalBarrier.Either
|
|
break
|
|
if new_region in rrp:
|
|
new_crystal_state |= rrp[new_region]
|
|
|
|
rrp[new_region] = new_crystal_state
|
|
|
|
for exit in new_region.exits:
|
|
door = exit.door
|
|
if door is not None and not door.blocked:
|
|
door_crystal_state = door.crystal if door.crystal else new_crystal_state
|
|
bc[exit] = door_crystal_state
|
|
queue.append((exit, door_crystal_state))
|
|
elif door is None:
|
|
queue.append((exit, new_crystal_state))
|
|
else:
|
|
new_crystal_state = CrystalBarrier.Orange
|
|
rrp[new_region] = new_crystal_state
|
|
bc.pop(connection, None)
|
|
for exit in new_region.exits:
|
|
bc[exit] = new_crystal_state
|
|
queue.append((exit, new_crystal_state))
|
|
|
|
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
|
|
|
# Retry connections if the new region can unblock them
|
|
if new_region.name in indirect_connections:
|
|
new_entrance = self.world.get_entrance(indirect_connections[new_region.name], player)
|
|
if new_entrance in bc and new_entrance not in queue and new_entrance.parent_region in rrp:
|
|
queue.append((new_entrance, rrp[new_entrance.parent_region]))
|
|
except IndexError:
|
|
break
|
|
|
|
|
|
def copy(self):
|
|
ret = CollectionState(self.world)
|
|
ret.prog_items = self.prog_items.copy()
|
|
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in range(1, self.world.players + 1)}
|
|
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
|
|
ret.events = copy.copy(self.events)
|
|
ret.path = copy.copy(self.path)
|
|
ret.locations_checked = copy.copy(self.locations_checked)
|
|
return ret
|
|
|
|
def can_reach(self, spot, resolution_hint=None, player=None):
|
|
try:
|
|
spot_type = spot.spot_type
|
|
except AttributeError:
|
|
# try to resolve a name
|
|
if resolution_hint == 'Location':
|
|
spot = self.world.get_location(spot, player)
|
|
elif resolution_hint in ['Entrance', 'OWEdge']:
|
|
spot = self.world.get_entrance(spot, player)
|
|
else:
|
|
# default to Region
|
|
spot = self.world.get_region(spot, player)
|
|
|
|
return spot.can_reach(self)
|
|
|
|
|
|
def sweep_for_events(self, key_only=False, locations=None):
|
|
# this may need improvement
|
|
if locations is None:
|
|
locations = self.world.get_filled_locations()
|
|
new_locations = True
|
|
checked_locations = 0
|
|
while new_locations:
|
|
reachable_events = [location for location in locations if location.event and
|
|
(not key_only or (not self.world.keyshuffle[location.item.player] and location.item.smallkey) or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
|
|
and location.can_reach(self)]
|
|
reachable_events = self._do_not_flood_the_keys(reachable_events)
|
|
for event in reachable_events:
|
|
if (event.name, event.player) not in self.events:
|
|
self.events.append((event.name, event.player))
|
|
self.collect(event.item, True, event)
|
|
new_locations = len(reachable_events) > checked_locations
|
|
checked_locations = len(reachable_events)
|
|
|
|
|
|
def can_reach_blue(self, region, player):
|
|
return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Blue, CrystalBarrier.Either]
|
|
|
|
def can_reach_orange(self, region, player):
|
|
return region in self.reachable_regions[player] and self.reachable_regions[player][region] in [CrystalBarrier.Orange, CrystalBarrier.Either]
|
|
|
|
def _do_not_flood_the_keys(self, reachable_events):
|
|
adjusted_checks = list(reachable_events)
|
|
for event in reachable_events:
|
|
if event.name in flooded_keys.keys():
|
|
flood_location = self.world.get_location(flooded_keys[event.name], event.player)
|
|
if (flood_location.item and flood_location not in self.locations_checked
|
|
and self.location_can_be_flooded(flood_location)):
|
|
adjusted_checks.remove(event)
|
|
if len(adjusted_checks) < len(reachable_events):
|
|
return adjusted_checks
|
|
return reachable_events
|
|
|
|
def not_flooding_a_key(self, world, location):
|
|
if location.name in flooded_keys.keys():
|
|
flood_location = world.get_location(flooded_keys[location.name], location.player)
|
|
item = flood_location.item
|
|
item_is_important = False if not item else item.advancement or item.bigkey or item.smallkey
|
|
return (flood_location in self.locations_checked or not item_is_important
|
|
or not self.location_can_be_flooded(flood_location))
|
|
return True
|
|
|
|
@staticmethod
|
|
def location_can_be_flooded(location):
|
|
return location.parent_region.name in ['Swamp Trench 1 Alcove', 'Swamp Trench 2 Alcove']
|
|
|
|
def has(self, item, player, count=1):
|
|
if count == 1:
|
|
return (item, player) in self.prog_items
|
|
return self.prog_items[item, player] >= count
|
|
|
|
def has_sm_key(self, item, player, count=1):
|
|
if self.world.retro[player]:
|
|
if self.world.mode[player] == 'standard' and self.world.doorShuffle[player] == 'vanilla' and item == 'Small Key (Escape)':
|
|
return True # Cannot access the shop until escape is finished. This is safe because the key is manually placed in make_custom_item_pool
|
|
return self.can_buy_unlimited('Small Key (Universal)', player)
|
|
if count == 1:
|
|
return (item, player) in self.prog_items
|
|
return self.prog_items[item, player] >= count
|
|
|
|
def can_buy_unlimited(self, item, player):
|
|
for shop in self.world.shops[player]:
|
|
if shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self):
|
|
return True
|
|
return False
|
|
|
|
def item_count(self, item, player):
|
|
return self.prog_items[item, player]
|
|
|
|
def has_crystals(self, count, player):
|
|
crystals = ['Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7']
|
|
return len([crystal for crystal in crystals if self.has(crystal, player)]) >= count
|
|
|
|
def can_lift_rocks(self, player):
|
|
return self.has('Power Glove', player) or self.has('Titans Mitts', player)
|
|
|
|
def has_bottle(self, player):
|
|
return self.bottle_count(player) > 0
|
|
|
|
def bottle_count(self, player):
|
|
return len([item for (item, itemplayer) in self.prog_items if item.startswith('Bottle') and itemplayer == player])
|
|
|
|
def has_hearts(self, player, count):
|
|
# Warning: This only considers items that are marked as advancement items
|
|
return self.heart_count(player) >= count
|
|
|
|
def heart_count(self, player):
|
|
# Warning: This only considers items that are marked as advancement items
|
|
diff = self.world.difficulty_requirements[player]
|
|
return (
|
|
min(self.item_count('Boss Heart Container', player), diff.boss_heart_container_limit)
|
|
+ self.item_count('Sanctuary Heart Container', player)
|
|
+ min(self.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4
|
|
+ 3 # starting hearts
|
|
)
|
|
|
|
def can_lift_heavy_rocks(self, player):
|
|
return self.has('Titans Mitts', player)
|
|
|
|
def can_extend_magic(self, player, smallmagic=16, fullrefill=False): #This reflects the total magic Link has, not the total extra he has.
|
|
basemagic = 8
|
|
if self.has('Magic Upgrade (1/4)', player):
|
|
basemagic = 32
|
|
elif self.has('Magic Upgrade (1/2)', player):
|
|
basemagic = 16
|
|
if self.can_buy_unlimited('Green Potion', player) or self.can_buy_unlimited('Blue Potion', player):
|
|
if self.world.difficulty_adjustments[player] == 'hard' and not fullrefill:
|
|
basemagic = basemagic + int(basemagic * 0.5 * self.bottle_count(player))
|
|
elif self.world.difficulty_adjustments[player] == 'expert' and not fullrefill:
|
|
basemagic = basemagic + int(basemagic * 0.25 * self.bottle_count(player))
|
|
else:
|
|
basemagic = basemagic + basemagic * self.bottle_count(player)
|
|
return basemagic >= smallmagic
|
|
|
|
def can_kill_most_things(self, player, enemies=5):
|
|
return (self.has_blunt_weapon(player)
|
|
or self.has('Cane of Somaria', player)
|
|
or (self.has('Cane of Byrna', player) and (enemies < 6 or self.can_extend_magic(player)))
|
|
or self.can_shoot_arrows(player)
|
|
or self.has('Fire Rod', player)
|
|
)
|
|
|
|
# In the future, this can be used to check if the player starts without bombs
|
|
def can_use_bombs(self, player):
|
|
StartingBombs = True
|
|
return StartingBombs or self.has('Bomb Upgrade (+10)', player)
|
|
|
|
def can_hit_crystal(self, player):
|
|
return (self.can_use_bombs(player)
|
|
or self.can_shoot_arrows(player)
|
|
or self.has_blunt_weapon(player)
|
|
or self.has('Blue Boomerang', player)
|
|
or self.has('Red Boomerang', player)
|
|
or self.has('Hookshot', player)
|
|
or self.has('Fire Rod', player)
|
|
or self.has('Ice Rod', player)
|
|
or self.has('Cane of Somaria', player)
|
|
or self.has('Cane of Byrna', player))
|
|
|
|
def can_hit_crystal_through_barrier(self, player):
|
|
return (self.can_use_bombs(player)
|
|
or self.can_shoot_arrows(player)
|
|
or self.has('Blue Boomerang', player)
|
|
or self.has('Red Boomerang', player)
|
|
or self.has('Fire Rod', player)
|
|
or self.has('Ice Rod', player)
|
|
or self.has('Cane of Somaria', player))
|
|
|
|
def can_shoot_arrows(self, player):
|
|
if self.world.retro[player]:
|
|
#todo: Non-progressive silvers grant wooden arrows, but progressive bows do not. Always require shop arrows to be safe
|
|
return self.has('Bow', player) and (self.can_buy_unlimited('Single Arrow', player) or self.has('Single Arrow', player))
|
|
return self.has('Bow', player)
|
|
|
|
def can_get_good_bee(self, player):
|
|
cave = self.world.get_region('Good Bee Cave', player)
|
|
return (
|
|
self.has_bottle(player) and
|
|
self.has('Bug Catching Net', player) and
|
|
(self.has_Boots(player) or (self.has_sword(player) and self.has('Quake', player))) and
|
|
cave.can_reach(self) and
|
|
self.is_not_bunny(cave, player)
|
|
)
|
|
|
|
def has_sword(self, player):
|
|
return self.has('Fighter Sword', player) or self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
|
|
|
|
def has_beam_sword(self, player):
|
|
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
|
|
|
|
def has_blunt_weapon(self, player):
|
|
return self.has_sword(player) or self.has('Hammer', player)
|
|
|
|
def has_Mirror(self, player):
|
|
return self.has('Magic Mirror', player)
|
|
|
|
def has_Boots(self, player):
|
|
return self.has('Pegasus Boots', player)
|
|
|
|
def has_Pearl(self, player):
|
|
return self.has('Moon Pearl', player)
|
|
|
|
def has_fire_source(self, player):
|
|
return self.has('Fire Rod', player) or self.has('Lamp', player)
|
|
|
|
def can_flute(self, player):
|
|
lw = self.world.get_region('Kakariko Area', player)
|
|
return self.has('Ocarina', player) and lw.can_reach(self) and self.is_not_bunny(lw, player)
|
|
|
|
def can_melt_things(self, player):
|
|
return self.has('Fire Rod', player) or (self.has('Bombos', player) and self.has_sword(player))
|
|
|
|
def can_avoid_lasers(self, player):
|
|
return self.has('Mirror Shield', player) or self.has('Cane of Byrna', player) or self.has('Cape', player)
|
|
|
|
def is_not_bunny(self, region, player):
|
|
if self.has_Pearl(player):
|
|
return True
|
|
|
|
return region.is_light_world if self.world.mode[player] != 'inverted' else region.is_dark_world
|
|
|
|
def can_reach_light_world(self, player):
|
|
if True in [i.is_light_world for i in self.reachable_regions[player]]:
|
|
return True
|
|
return False
|
|
|
|
def can_reach_dark_world(self, player):
|
|
if True in [i.is_dark_world for i in self.reachable_regions[player]]:
|
|
return True
|
|
return False
|
|
|
|
def has_misery_mire_medallion(self, player):
|
|
return self.has(self.world.required_medallions[player][0], player)
|
|
|
|
def has_turtle_rock_medallion(self, player):
|
|
return self.has(self.world.required_medallions[player][1], player)
|
|
|
|
def can_boots_clip_lw(self, player):
|
|
if self.world.mode[player] == 'inverted':
|
|
return self.has_Boots(player) and self.has_Pearl(player)
|
|
return self.has_Boots(player)
|
|
|
|
def can_boots_clip_dw(self, player):
|
|
if self.world.mode[player] != 'inverted':
|
|
return self.has_Boots(player) and self.has_Pearl(player)
|
|
return self.has_Boots(player)
|
|
|
|
def can_get_glitched_speed_lw(self, player):
|
|
rules = [self.has_Boots(player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
if self.world.mode[player] == 'inverted':
|
|
rules.append(self.has_Pearl(player))
|
|
return all(rules)
|
|
|
|
def can_get_glitched_speed_dw(self, player):
|
|
rules = [self.has_Boots(player), any([self.has('Hookshot', player), self.has_sword(player)])]
|
|
if self.world.mode[player] != 'inverted':
|
|
rules.append(self.has_Pearl(player))
|
|
return all(rules)
|
|
|
|
def can_superbunny_mirror_with_sword(self, player):
|
|
return self.has_Mirror(player) and self.has_sword(player)
|
|
|
|
def collect(self, item, event=False, location=None):
|
|
if location:
|
|
self.locations_checked.add(location)
|
|
changed = False
|
|
if item.name.startswith('Progressive '):
|
|
if 'Sword' in item.name:
|
|
if self.has('Golden Sword', item.player):
|
|
pass
|
|
elif self.has('Tempered Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 4:
|
|
self.prog_items['Golden Sword', item.player] += 1
|
|
changed = True
|
|
elif self.has('Master Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 3:
|
|
self.prog_items['Tempered Sword', item.player] += 1
|
|
changed = True
|
|
elif self.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
|
|
self.prog_items['Master Sword', item.player] += 1
|
|
changed = True
|
|
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
|
|
self.prog_items['Fighter Sword', item.player] += 1
|
|
changed = True
|
|
elif 'Glove' in item.name:
|
|
if self.has('Titans Mitts', item.player):
|
|
pass
|
|
elif self.has('Power Glove', item.player):
|
|
self.prog_items['Titans Mitts', item.player] += 1
|
|
changed = True
|
|
else:
|
|
self.prog_items['Power Glove', item.player] += 1
|
|
changed = True
|
|
elif 'Shield' in item.name:
|
|
if self.has('Mirror Shield', item.player):
|
|
pass
|
|
elif self.has('Shield Level', item.player, 2) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
|
|
self.prog_items['Mirror Shield', item.player] += 1
|
|
self.prog_items['Shield Level', item.player] += 1
|
|
changed = True
|
|
elif self.has('Shield Level', item.player, 1) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
|
|
self.prog_items['Red Shield', item.player] += 1
|
|
self.prog_items['Shield Level', item.player] += 1
|
|
changed = True
|
|
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
|
|
self.prog_items['Blue Shield', item.player] += 1
|
|
self.prog_items['Shield Level', item.player] += 1
|
|
changed = True
|
|
elif 'Bow' in item.name:
|
|
if self.has('Silver Arrows', item.player):
|
|
pass
|
|
elif self.has('Bow', item.player):
|
|
self.prog_items['Silver Arrows', item.player] += 1
|
|
changed = True
|
|
else:
|
|
self.prog_items['Bow', item.player] += 1
|
|
changed = True
|
|
elif 'Armor' in item.name:
|
|
if self.has('Red Mail', item.player):
|
|
pass
|
|
elif self.has('Blue Mail', item.player):
|
|
self.prog_items['Red Mail', item.player] += 1
|
|
changed = True
|
|
else:
|
|
self.prog_items['Blue Mail', item.player] += 1
|
|
changed = True
|
|
|
|
elif item.name.startswith('Bottle'):
|
|
if self.bottle_count(item.player) < self.world.difficulty_requirements[item.player].progressive_bottle_limit:
|
|
self.prog_items[item.name, item.player] += 1
|
|
changed = True
|
|
elif event or item.advancement:
|
|
self.prog_items[item.name, item.player] += 1
|
|
changed = True
|
|
|
|
self.stale[item.player] = True
|
|
|
|
if changed:
|
|
if not event:
|
|
self.sweep_for_events()
|
|
|
|
def remove(self, item):
|
|
if item.advancement:
|
|
to_remove = item.name
|
|
if to_remove.startswith('Progressive '):
|
|
if 'Sword' in to_remove:
|
|
if self.has('Golden Sword', item.player):
|
|
to_remove = 'Golden Sword'
|
|
elif self.has('Tempered Sword', item.player):
|
|
to_remove = 'Tempered Sword'
|
|
elif self.has('Master Sword', item.player):
|
|
to_remove = 'Master Sword'
|
|
elif self.has('Fighter Sword', item.player):
|
|
to_remove = 'Fighter Sword'
|
|
else:
|
|
to_remove = None
|
|
elif 'Glove' in item.name:
|
|
if self.has('Titans Mitts', item.player):
|
|
to_remove = 'Titans Mitts'
|
|
elif self.has('Power Glove', item.player):
|
|
to_remove = 'Power Glove'
|
|
else:
|
|
to_remove = None
|
|
elif 'Shield' in item.name:
|
|
if self.has('Mirror Shield', item.player):
|
|
to_remove = 'Mirror Shield'
|
|
elif self.has('Red Shield', item.player):
|
|
to_remove = 'Red Shield'
|
|
elif self.has('Blue Shield', item.player):
|
|
to_remove = 'Blue Shield'
|
|
else:
|
|
to_remove = 'None'
|
|
elif 'Bow' in item.name:
|
|
if self.has('Silver Arrows', item.player):
|
|
to_remove = 'Silver Arrows'
|
|
elif self.has('Bow', item.player):
|
|
to_remove = 'Bow'
|
|
else:
|
|
to_remove = None
|
|
|
|
if to_remove is not None:
|
|
|
|
self.prog_items[to_remove, item.player] -= 1
|
|
if self.prog_items[to_remove, item.player] < 1:
|
|
del (self.prog_items[to_remove, item.player])
|
|
# invalidate caches, nothing can be trusted anymore now
|
|
self.reachable_regions[item.player] = dict()
|
|
self.blocked_connections[item.player] = dict()
|
|
self.stale[item.player] = True
|
|
|
|
def __getattr__(self, item):
|
|
if item.startswith('can_reach_'):
|
|
return self.can_reach(item[10])
|
|
#elif item.startswith('has_'):
|
|
# return self.has(item[4])
|
|
if item == '__len__':
|
|
return
|
|
|
|
raise RuntimeError('Cannot parse %s.' % item)
|
|
|
|
@unique
|
|
class RegionType(Enum):
|
|
Menu = 0
|
|
LightWorld = 1
|
|
DarkWorld = 2
|
|
Cave = 3 # Also includes Houses
|
|
Dungeon = 4
|
|
|
|
@property
|
|
def is_indoors(self):
|
|
"""Shorthand for checking if Cave or Dungeon"""
|
|
return self in (RegionType.Cave, RegionType.Dungeon)
|
|
|
|
|
|
class Region(object):
|
|
|
|
def __init__(self, name, type, hint, player):
|
|
self.name = name
|
|
self.type = type
|
|
self.entrances = []
|
|
self.exits = []
|
|
self.locations = []
|
|
self.dungeon = None
|
|
self.shop = None
|
|
self.world = None
|
|
self.is_light_world = False # will be set aftermaking connections.
|
|
self.is_dark_world = False
|
|
self.spot_type = 'Region'
|
|
self.hint_text = hint
|
|
self.recursion_count = 0
|
|
self.player = player
|
|
self.crystal_switch = False
|
|
|
|
def can_reach(self, state):
|
|
if state.stale[self.player]:
|
|
state.update_reachable_regions(self.player)
|
|
return self in state.reachable_regions[self.player]
|
|
|
|
def can_reach_private(self, state):
|
|
for entrance in self.entrances:
|
|
if entrance.can_reach(state):
|
|
if not self in state.path:
|
|
state.path[self] = (self.name, state.path.get(entrance, None))
|
|
return True
|
|
return False
|
|
|
|
def can_fill(self, item):
|
|
inside_dungeon_item = ((item.smallkey and not self.world.keyshuffle[item.player])
|
|
or (item.bigkey and not self.world.bigkeyshuffle[item.player])
|
|
or (item.map and not self.world.mapshuffle[item.player])
|
|
or (item.compass and not self.world.compassshuffle[item.player]))
|
|
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Escape)'
|
|
if sewer_hack or inside_dungeon_item:
|
|
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
|
|
|
|
return True
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
|
|
|
|
|
class Entrance(object):
|
|
|
|
def __init__(self, player, name='', parent=None):
|
|
self.name = name
|
|
self.parent_region = parent
|
|
self.connected_region = None
|
|
self.target = None
|
|
self.addresses = None
|
|
self.spot_type = 'Entrance'
|
|
self.recursion_count = 0
|
|
self.vanilla = None
|
|
self.access_rule = lambda state: True
|
|
self.player = player
|
|
self.door = None
|
|
self.hide_path = False
|
|
|
|
def can_reach(self, state):
|
|
if self.parent_region.can_reach(state) and self.access_rule(state):
|
|
if not self.hide_path and not self in state.path:
|
|
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
|
return True
|
|
|
|
return False
|
|
|
|
def connect(self, region, addresses=None, target=None, vanilla=None):
|
|
self.connected_region = region
|
|
self.target = target
|
|
self.addresses = addresses
|
|
self.vanilla = vanilla
|
|
region.entrances.append(self)
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
world = self.parent_region.world if self.parent_region else None
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
|
|
|
|
|
class Dungeon(object):
|
|
|
|
def __init__(self, name, regions, big_key, small_keys, dungeon_items, player, dungeon_id):
|
|
self.name = name
|
|
self.regions = regions
|
|
self.big_key = big_key
|
|
self.small_keys = small_keys
|
|
self.dungeon_items = dungeon_items
|
|
self.bosses = dict()
|
|
self.player = player
|
|
self.world = None
|
|
self.dungeon_id = dungeon_id
|
|
|
|
self.entrance_regions = []
|
|
|
|
@property
|
|
def boss(self):
|
|
return self.bosses.get(None, None)
|
|
|
|
@boss.setter
|
|
def boss(self, value):
|
|
self.bosses[None] = value
|
|
|
|
@property
|
|
def keys(self):
|
|
return self.small_keys + ([self.big_key] if self.big_key else [])
|
|
|
|
@property
|
|
def all_items(self):
|
|
return self.dungeon_items + self.keys
|
|
|
|
def is_dungeon_item(self, item):
|
|
return item.player == self.player and item.name in [dungeon_item.name for dungeon_item in self.all_items]
|
|
|
|
def count_dungeon_item(self):
|
|
return len(self.dungeon_items) + 1 if self.big_key_required else 0 + self.key_number
|
|
|
|
def incomplete_paths(self):
|
|
ret = 0
|
|
for path in self.paths:
|
|
if not self.path_completion[path]:
|
|
ret += 1
|
|
return ret
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
|
|
|
|
|
@unique
|
|
class DoorType(Enum):
|
|
Normal = 1
|
|
SpiralStairs = 2
|
|
StraightStairs = 3
|
|
Ladder = 4
|
|
Open = 5
|
|
Hole = 6
|
|
Warp = 7
|
|
Interior = 8
|
|
Logical = 9
|
|
|
|
|
|
@unique
|
|
class Direction(Enum):
|
|
North = 0
|
|
West = 1
|
|
South = 2
|
|
East = 3
|
|
Up = 4
|
|
Down = 5
|
|
|
|
|
|
@unique
|
|
class Hook(Enum):
|
|
North = 0
|
|
West = 1
|
|
South = 2
|
|
East = 3
|
|
Stairs = 4
|
|
|
|
|
|
hook_dir_map = {
|
|
Direction.North: Hook.North,
|
|
Direction.South: Hook.South,
|
|
Direction.West: Hook.West,
|
|
Direction.East: Hook.East,
|
|
}
|
|
|
|
|
|
def hook_from_door(door):
|
|
if door.type == DoorType.SpiralStairs:
|
|
return Hook.Stairs
|
|
if door.type in [DoorType.Normal, DoorType.Open, DoorType.StraightStairs, DoorType.Ladder]:
|
|
return hook_dir_map[door.direction]
|
|
return None
|
|
|
|
|
|
class Polarity:
|
|
def __init__(self):
|
|
self.vector = [0, 0, 0]
|
|
|
|
def __len__(self):
|
|
return len(self.vector)
|
|
|
|
def __add__(self, other):
|
|
result = Polarity()
|
|
for i in range(len(self.vector)):
|
|
result.vector[i] = pol_add[pol_idx_2[i]](self.vector[i], other.vector[i])
|
|
return result
|
|
|
|
def __iadd__(self, other):
|
|
for i in range(len(self.vector)):
|
|
self.vector[i] = pol_add[pol_idx_2[i]](self.vector[i], other.vector[i])
|
|
return self
|
|
|
|
def __getitem__(self, item):
|
|
return self.vector[item]
|
|
|
|
def __eq__(self, other):
|
|
for i in range(len(self.vector)):
|
|
if self.vector[i] != other.vector[i]:
|
|
return False
|
|
return True
|
|
|
|
def __hash__(self):
|
|
h = 17
|
|
spot = self.vector[0]
|
|
h *= 31 + (spot if spot >= 0 else spot + 100)
|
|
spot = self.vector[1]
|
|
h *= 43 + (spot if spot >= 0 else spot + 100)
|
|
spot = self.vector[2]
|
|
h *= 73 + (spot if spot >= 0 else spot + 100)
|
|
return h
|
|
|
|
def is_neutral(self):
|
|
for i in range(len(self.vector)):
|
|
if self.vector[i] != 0:
|
|
return False
|
|
return True
|
|
|
|
def complement(self):
|
|
result = Polarity()
|
|
for i in range(len(self.vector)):
|
|
result.vector[i] = pol_comp[pol_idx_2[i]](self.vector[i])
|
|
return result
|
|
|
|
def charge(self):
|
|
result = 0
|
|
for i in range(len(self.vector)):
|
|
result += abs(self.vector[i])
|
|
return result
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return f'{self.vector}'
|
|
|
|
|
|
pol_idx = {
|
|
Direction.North: (0, 'Pos'),
|
|
Direction.South: (0, 'Neg'),
|
|
Direction.East: (1, 'Pos'),
|
|
Direction.West: (1, 'Neg'),
|
|
Direction.Up: (2, 'Mod'),
|
|
Direction.Down: (2, 'Mod')
|
|
}
|
|
pol_idx_2 = {
|
|
0: 'Add',
|
|
1: 'Add',
|
|
2: 'Mod'
|
|
}
|
|
pol_inc = {
|
|
'Pos': lambda x: x + 1,
|
|
'Neg': lambda x: x - 1,
|
|
'Mod': lambda x: (x + 1) % 2
|
|
}
|
|
pol_add = {
|
|
'Add': lambda x, y: x + y,
|
|
'Mod': lambda x, y: (x + y) % 2
|
|
}
|
|
pol_comp = {
|
|
'Add': lambda x: -x,
|
|
'Mod': lambda x: 0 if x == 0 else 1
|
|
}
|
|
|
|
|
|
@unique
|
|
class PolSlot(Enum):
|
|
NorthSouth = 0
|
|
EastWest = 1
|
|
Stairs = 2
|
|
|
|
|
|
class CrystalBarrier(FastEnum):
|
|
Null = 0 # no special requirement
|
|
Blue = 1 # blue must be down and explore state set to Blue
|
|
Orange = 2 # orange must be down and explore state set to Orange
|
|
Either = 3 # you choose to leave this room in Either state
|
|
|
|
|
|
class Door(object):
|
|
def __init__(self, player, name, type, entrance=None):
|
|
self.player = player
|
|
self.name = name
|
|
self.type = type
|
|
self.direction = None
|
|
|
|
# rom properties
|
|
self.roomIndex = -1
|
|
# 0,1,2 for normal
|
|
# 0-7 for ladder
|
|
# 0-4 for spiral offset thing
|
|
self.doorIndex = -1
|
|
self.layer = -1 # 0 for normal floor, 1 for the inset layer
|
|
self.pseudo_bg = 0 # 0 for normal floor, 1 for pseudo bg
|
|
self.toggle = False
|
|
self.trapFlag = 0x0
|
|
self.quadrant = 2
|
|
self.shiftX = 78
|
|
self.shiftY = 78
|
|
self.zeroHzCam = False
|
|
self.zeroVtCam = False
|
|
self.doorListPos = -1
|
|
self.edge_id = None
|
|
self.edge_width = None
|
|
|
|
#portal items
|
|
self.portalAble = False
|
|
self.roomLayout = 0x22 # free scroll- both directions
|
|
self.entranceFlag = False
|
|
self.deadEnd = False
|
|
self.passage = True
|
|
self.dungeonLink = None
|
|
self.bk_shuffle_req = False
|
|
self.standard_restricted = False # flag if portal is not allowed in HC in standard
|
|
self.lw_restricted = False # flag if portal is not allowed in DW
|
|
# self.incognitoPos = -1
|
|
# self.sectorLink = False
|
|
|
|
# logical properties
|
|
# self.connected = False # combine with Dest?
|
|
self.dest = None
|
|
self.blocked = False # Indicates if the door is normally blocked off as an exit. (Sanc door or always closed)
|
|
self.blocked_orig = False
|
|
self.stonewall = False # Indicate that the door cannot be enter until exited (Desert Torches, PoD Eye Statue)
|
|
self.smallKey = False # There's a small key door on this side
|
|
self.bigKey = False # There's a big key door on this side
|
|
self.ugly = False # Indicates that it can't be seen from the front (e.g. back of a big key door)
|
|
self.crystal = CrystalBarrier.Null # How your crystal state changes if you use this door
|
|
self.req_event = None # if a dungeon event is required for this door - swamp palace mostly
|
|
self.controller = None
|
|
self.dependents = []
|
|
self.dead = False
|
|
|
|
self.entrance = entrance
|
|
if entrance is not None and not entrance.door:
|
|
entrance.door = self
|
|
|
|
def getAddress(self):
|
|
if self.type in [DoorType.Normal, DoorType.StraightStairs]:
|
|
return 0x13A000 + normal_offset_table[self.roomIndex] * 24 + (self.doorIndex + self.direction.value * 3) * 2
|
|
elif self.type == DoorType.SpiralStairs:
|
|
return 0x13B000 + (spiral_offset_table[self.roomIndex] + self.doorIndex) * 4
|
|
elif self.type == DoorType.Ladder:
|
|
return 0x13C700 + self.doorIndex * 2
|
|
elif self.type == DoorType.Open:
|
|
base_address = {
|
|
Direction.North: 0x13C500,
|
|
Direction.South: 0x13C521,
|
|
Direction.West: 0x13C542,
|
|
Direction.East: 0x13C55D,
|
|
}
|
|
return base_address[self.direction] + self.edge_id * 3
|
|
|
|
def getTarget(self, src):
|
|
if self.type in [DoorType.Normal, DoorType.StraightStairs]:
|
|
bitmask = 4 * (self.layer ^ 1 if src.toggle else self.layer)
|
|
bitmask += 0x08 * int(self.trapFlag)
|
|
if src.type == DoorType.StraightStairs:
|
|
bitmask += 0x40
|
|
return [self.roomIndex, bitmask + self.doorIndex]
|
|
if self.type == DoorType.Ladder:
|
|
bitmask = 4 * (self.layer ^ 1 if src.toggle else self.layer)
|
|
bitmask += 0x08 * self.doorIndex
|
|
if src.type == DoorType.StraightStairs:
|
|
bitmask += 0x40
|
|
return [self.roomIndex, bitmask + 0x03]
|
|
if self.type == DoorType.SpiralStairs:
|
|
bitmask = int(self.layer) << 2
|
|
bitmask += 0x10 * int(self.zeroHzCam)
|
|
bitmask += 0x20 * int(self.zeroVtCam)
|
|
bitmask += 0x80 if self.direction == Direction.Up else 0
|
|
return [self.roomIndex, bitmask + self.quadrant, self.shiftX, self.shiftY]
|
|
if self.type == DoorType.Open:
|
|
bitmask = self.edge_id
|
|
bitmask += 0x10 * (self.layer ^ 1 if src.toggle else self.layer)
|
|
bitmask += 0x80
|
|
if src.type == DoorType.StraightStairs:
|
|
bitmask += 0x40
|
|
if src.type == DoorType.Open:
|
|
bitmask += 0x20 * self.quadrant
|
|
fraction = 0x10 * multiply_lookup[src.edge_width][self.edge_width]
|
|
fraction += divisor_lookup[src.edge_width][self.edge_width]
|
|
return [self.roomIndex, bitmask, fraction]
|
|
else:
|
|
bitmask += 0x20 * self.quad_indicator()
|
|
return [self.roomIndex, bitmask]
|
|
|
|
def quad_indicator(self):
|
|
if self.direction in [Direction.North, Direction.South]:
|
|
return self.quadrant & 0x1
|
|
elif self.direction in [Direction.East, Direction.West]:
|
|
return (self.quadrant & 0x2) >> 1
|
|
return 0
|
|
|
|
def dir(self, direction, room, doorIndex, layer):
|
|
self.direction = direction
|
|
self.roomIndex = room
|
|
self.doorIndex = doorIndex
|
|
self.layer = layer
|
|
return self
|
|
|
|
def ss(self, quadrant, shift_y, shift_x, zero_hz_cam=False, zero_vt_cam=False):
|
|
self.quadrant = quadrant
|
|
self.shiftY = shift_y
|
|
self.shiftX = shift_x
|
|
self.zeroHzCam = zero_hz_cam
|
|
self.zeroVtCam = zero_vt_cam
|
|
return self
|
|
|
|
def edge(self, edge_id, quadrant, width):
|
|
self.edge_id = edge_id
|
|
self.quadrant = quadrant
|
|
self.edge_width = width
|
|
return self
|
|
|
|
def kind(self, world):
|
|
if self.roomIndex != -1 and self.doorListPos != -1:
|
|
return world.get_room(self.roomIndex, self.player).kind(self)
|
|
return None
|
|
|
|
def small_key(self):
|
|
self.smallKey = True
|
|
return self
|
|
|
|
def big_key(self):
|
|
self.bigKey = True
|
|
return self
|
|
|
|
def toggler(self):
|
|
self.toggle = True
|
|
return self
|
|
|
|
def no_exit(self):
|
|
self.blocked = self.blocked_orig = True
|
|
return self
|
|
|
|
def no_entrance(self):
|
|
self.stonewall = True
|
|
return self
|
|
|
|
def trap(self, trapFlag):
|
|
self.trapFlag = trapFlag
|
|
return self
|
|
|
|
def pos(self, pos):
|
|
self.doorListPos = pos
|
|
return self
|
|
|
|
def event(self, event):
|
|
self.req_event = event
|
|
return self
|
|
|
|
def barrier(self, crystal):
|
|
self.crystal = crystal
|
|
return self
|
|
|
|
def c_switch(self):
|
|
self.crystal = CrystalBarrier.Either
|
|
return self
|
|
|
|
def kill(self):
|
|
self.dead = True
|
|
return self
|
|
|
|
def portal(self, quadrant, roomLayout, pseudo_bg=0):
|
|
self.quadrant = quadrant
|
|
self.roomLayout = roomLayout
|
|
self.pseudo_bg = pseudo_bg
|
|
self.portalAble = True
|
|
return self
|
|
|
|
def dead_end(self, allowPassage=False):
|
|
self.deadEnd = True
|
|
if allowPassage:
|
|
self.passage = True
|
|
else:
|
|
self.passage = False
|
|
|
|
def kind(self, world):
|
|
if self.roomIndex != -1 and self.doorListPos != -1:
|
|
return world.get_room(self.roomIndex, self.player).kind(self)
|
|
return None
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, self.__class__) and self.name == other.name
|
|
|
|
def __hash__(self):
|
|
return hash(self.name)
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return '%s' % self.name
|
|
|
|
|
|
@unique
|
|
class WorldType(IntEnum):
|
|
Light = 0
|
|
Dark = 1
|
|
Special = 2
|
|
|
|
|
|
@unique
|
|
class Terrain(Enum):
|
|
Land = 0
|
|
Water = 1
|
|
|
|
|
|
class OWEdge(object):
|
|
def __init__(self, player, name, owIndex, direction, terrain, edge_id, owSlotIndex=0xff):
|
|
self.player = player
|
|
self.name = name
|
|
self.type = DoorType.Open
|
|
self.direction = direction
|
|
self.terrain = terrain
|
|
self.deadEnd = False
|
|
|
|
# rom properties
|
|
self.owIndex = owIndex
|
|
if owSlotIndex == 0xff:
|
|
self.owSlotIndex = owIndex
|
|
else:
|
|
self.owSlotIndex = owSlotIndex
|
|
self.shiftX = 78
|
|
self.shiftY = 78
|
|
self.zeroHzCam = False
|
|
self.zeroVtCam = False
|
|
self.edge_id = edge_id
|
|
|
|
self.midpoint = 0x0
|
|
self.linkOpp = 0x0
|
|
self.scrollPos = 0x0
|
|
self.scrollOpp = 0x0
|
|
self.camPos = 0x0
|
|
self.camOpp = 0x0
|
|
self.vramLoc = 0x0
|
|
self.unknownX = 0x0
|
|
self.unknownY = 0x0
|
|
|
|
if self.owIndex < 0x40:
|
|
self.worldType = WorldType.Light
|
|
elif self.owIndex < 0x80:
|
|
self.worldType = WorldType.Dark
|
|
else:
|
|
self.worldType = WorldType.Special
|
|
|
|
# logical properties
|
|
# self.connected = False # combine with Dest?
|
|
self.dest = None
|
|
self.dependents = []
|
|
self.dead = False
|
|
|
|
def getAddress(self):
|
|
base_address = {
|
|
Direction.North: 0x152800,
|
|
Direction.South: 0x152800 + (0x42 * 16),
|
|
Direction.West: 0x152800 + (0x84 * 16),
|
|
Direction.East: 0x152800 + (0xcf * 16),
|
|
}
|
|
return base_address[self.direction] + (self.edge_id * 16)
|
|
|
|
def getTarget(self):
|
|
return self.dest.edge_id
|
|
|
|
def dead_end(self):
|
|
self.deadEnd = True
|
|
|
|
def coordInfo(self, vram_loc):
|
|
self.vramLoc = vram_loc
|
|
return self
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, self.__class__) and self.name == other.name
|
|
|
|
def __hash__(self):
|
|
return hash(self.name)
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return '%s' % self.name
|
|
|
|
|
|
class Sector(object):
|
|
|
|
def __init__(self):
|
|
self.regions = []
|
|
self.outstanding_doors = []
|
|
self.name = None
|
|
self.r_name_set = None
|
|
self.chest_locations = 0
|
|
self.key_only_locations = 0
|
|
self.c_switch = False
|
|
self.orange_barrier = False
|
|
self.blue_barrier = False
|
|
self.bk_required = False
|
|
self.bk_provided = False
|
|
self.conn_balance = None
|
|
self.branch_factor = None
|
|
self.dead_end_cnt = None
|
|
self.entrance_sector = None
|
|
self.destination_entrance = False
|
|
self.equations = None
|
|
|
|
def region_set(self):
|
|
if self.r_name_set is None:
|
|
self.r_name_set = dict.fromkeys(map(lambda r: r.name, self.regions))
|
|
return self.r_name_set.keys()
|
|
|
|
def polarity(self):
|
|
pol = Polarity()
|
|
for door in self.outstanding_doors:
|
|
idx, inc = pol_idx[door.direction]
|
|
pol.vector[idx] = pol_inc[inc](pol.vector[idx])
|
|
return pol
|
|
|
|
def magnitude(self):
|
|
magnitude = [0, 0, 0]
|
|
for door in self.outstanding_doors:
|
|
idx, inc = pol_idx[door.direction]
|
|
magnitude[idx] = magnitude[idx] + 1
|
|
return magnitude
|
|
|
|
def hook_magnitude(self):
|
|
magnitude = [0] * len(Hook)
|
|
for door in self.outstanding_doors:
|
|
idx = hook_from_door(door).value
|
|
magnitude[idx] = magnitude[idx] + 1
|
|
return magnitude
|
|
|
|
def outflow(self):
|
|
outflow = 0
|
|
for door in self.outstanding_doors:
|
|
if not door.blocked:
|
|
outflow = outflow + 1
|
|
return outflow
|
|
|
|
def adj_outflow(self):
|
|
outflow = 0
|
|
for door in self.outstanding_doors:
|
|
if not door.blocked and not door.dead:
|
|
outflow = outflow + 1
|
|
return outflow
|
|
|
|
def branching_factor(self):
|
|
if self.branch_factor is None:
|
|
self.branch_factor = len(self.outstanding_doors)
|
|
cnt_dead = len([x for x in self.outstanding_doors if x.dead])
|
|
if cnt_dead > 1:
|
|
self.branch_factor -= cnt_dead - 1
|
|
for region in self.regions:
|
|
for ent in region.entrances:
|
|
if (ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] and ent.parent_region.name != 'Menu' and ent.parent_region.name != 'Flute Sky') or ent.parent_region.name == 'Sewer Drop':
|
|
self.branch_factor += 1
|
|
break # you only ever get one allowance for an entrance region, multiple entrances don't help
|
|
return self.branch_factor
|
|
|
|
def branches(self):
|
|
return max(0, self.branching_factor() - 2)
|
|
|
|
def dead_ends(self):
|
|
if self.dead_end_cnt is None:
|
|
if self.branching_factor() <= 1:
|
|
self.dead_end_cnt = 1
|
|
else:
|
|
dead_cnt = len([x for x in self.outstanding_doors if x.dead])
|
|
self.dead_end_cnt = dead_cnt - 1 if dead_cnt > 2 else 0
|
|
return self.dead_end_cnt
|
|
|
|
def is_entrance_sector(self):
|
|
if self.entrance_sector is None:
|
|
self.entrance_sector = False
|
|
for region in self.regions:
|
|
for ent in region.entrances:
|
|
if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] or ent.parent_region.name == 'Sewer Drop':
|
|
self.entrance_sector = True
|
|
return self.entrance_sector
|
|
|
|
def get_start_regions(self):
|
|
if self.is_entrance_sector():
|
|
starts = []
|
|
for region in self.regions:
|
|
for ent in region.entrances:
|
|
if ent.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld] or ent.parent_region.name == 'Sewer Drop':
|
|
starts.append(region)
|
|
return starts
|
|
return None
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
if len(self.regions) > 0:
|
|
return f'{self.regions[0].name}'
|
|
return f'{next(iter(self.region_set()))}'
|
|
|
|
|
|
class Portal(object):
|
|
|
|
def __init__(self, player, name, door, entrance_offset, exit_offset, boss_exit_idx):
|
|
self.player = player
|
|
self.name = name
|
|
self.door = door
|
|
self.ent_offset = entrance_offset
|
|
self.exit_offset = exit_offset
|
|
self.boss_exit_idx = boss_exit_idx
|
|
self.default = True
|
|
self.destination = False
|
|
self.dependent = None
|
|
self.deadEnd = False
|
|
self.light_world = False
|
|
|
|
def change_boss_exit(self, exit_idx):
|
|
self.default = False
|
|
self.boss_exit_idx = exit_idx
|
|
|
|
def change_door(self, new_door):
|
|
if new_door != self.door:
|
|
self.default = False
|
|
self.door = new_door
|
|
|
|
def current_room(self):
|
|
return self.door.roomIndex
|
|
|
|
def relative_coords(self):
|
|
y_rel = (self.door.roomIndex & 0xf0) >> 3 #todo: fix the shift!!!!
|
|
x_rel = (self.door.roomIndex & 0x0f) * 2
|
|
quad = self.door.quadrant
|
|
if quad == 0:
|
|
return [y_rel, y_rel, y_rel, y_rel+1, x_rel, x_rel, x_rel, x_rel+1]
|
|
elif quad == 1:
|
|
return [y_rel, y_rel, y_rel, y_rel+1, x_rel+1, x_rel, x_rel+1, x_rel+1]
|
|
elif quad == 2:
|
|
return [y_rel+1, y_rel, y_rel+1, y_rel+1, x_rel, x_rel, x_rel, x_rel+1]
|
|
else:
|
|
return [y_rel+1, y_rel, y_rel+1, y_rel+1, x_rel+1, x_rel, x_rel+1, x_rel+1]
|
|
|
|
def scroll_x(self):
|
|
x_rel = (self.door.roomIndex & 0x0f) * 2
|
|
if self.door.doorIndex == 0:
|
|
return [0x00, x_rel]
|
|
elif self.door.doorIndex == 1:
|
|
return [0x80, x_rel]
|
|
else:
|
|
return [0x00, x_rel+1]
|
|
|
|
def scroll_y(self):
|
|
y_rel = ((self.door.roomIndex & 0xf0) >> 3) + 1
|
|
return [0x10, y_rel]
|
|
|
|
def link_y(self):
|
|
y_rel = ((self.door.roomIndex & 0xf0) >> 3) + 1
|
|
inset = False
|
|
if self.door.pseudo_bg == 1 or self.door.layer == 1:
|
|
inset = True
|
|
return [(0xd8 if not inset else 0xc0), y_rel]
|
|
|
|
def link_x(self):
|
|
x_rel = (self.door.roomIndex & 0x0f) * 2
|
|
if self.door.doorIndex == 0:
|
|
return [0x78, x_rel]
|
|
elif self.door.doorIndex == 1:
|
|
return [0xf8, x_rel]
|
|
else:
|
|
return [0x78, x_rel+1]
|
|
|
|
# def camera_y(self):
|
|
# return [0x87, 0x01]
|
|
|
|
def camera_x(self):
|
|
if self.door.doorIndex == 0:
|
|
return [0x7f, 0x00]
|
|
elif self.door.doorIndex == 1:
|
|
return [0xff, 0x00]
|
|
else:
|
|
return [0x7f, 0x01]
|
|
|
|
def bg_setting(self):
|
|
if self.door.layer == 0:
|
|
return 0x00 | self.door.pseudo_bg
|
|
else:
|
|
return 0x10 | self.door.pseudo_bg
|
|
|
|
def hv_scroll(self):
|
|
return self.door.roomLayout
|
|
|
|
def scroll_quad(self):
|
|
quad = self.door.quadrant
|
|
if quad == 0:
|
|
return 0x00
|
|
elif quad == 1:
|
|
return 0x10
|
|
elif quad == 2:
|
|
return 0x02
|
|
else:
|
|
return 0x12
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return f'{self.name}:{self.door.name}'
|
|
|
|
|
|
class DungeonInfo(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.total = 0
|
|
self.required_passage = {}
|
|
self.sole_entrance = None
|
|
# self.dead_ends = 0 total - 1 - req = dead_ends possible
|
|
|
|
|
|
class Boss(object):
|
|
def __init__(self, name, enemizer_name, defeat_rule, player):
|
|
self.name = name
|
|
self.enemizer_name = enemizer_name
|
|
self.defeat_rule = defeat_rule
|
|
self.player = player
|
|
|
|
def can_defeat(self, state):
|
|
return self.defeat_rule(state, self.player)
|
|
|
|
class Location(object):
|
|
def __init__(self, player, name='', address=None, crystal=False, hint_text=None, parent=None, forced_item=None, player_address=None):
|
|
self.name = name
|
|
self.parent_region = parent
|
|
if forced_item is not None:
|
|
from Items import ItemFactory
|
|
self.forced_item = ItemFactory([forced_item], player)[0]
|
|
self.item = self.forced_item
|
|
self.item.location = self
|
|
self.event = True
|
|
else:
|
|
self.forced_item = None
|
|
self.item = None
|
|
self.event = False
|
|
self.crystal = crystal
|
|
self.address = address
|
|
self.player_address = player_address
|
|
self.spot_type = 'Location'
|
|
self.hint_text = hint_text if hint_text is not None else 'Hyrule'
|
|
self.recursion_count = 0
|
|
self.staleness_count = 0
|
|
self.locked = False
|
|
self.always_allow = lambda item, state: False
|
|
self.access_rule = lambda state: True
|
|
self.item_rule = lambda item: True
|
|
self.player = player
|
|
self.skip = False
|
|
|
|
def can_fill(self, state, item, check_access=True):
|
|
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state)))
|
|
|
|
def can_reach(self, state):
|
|
if self.parent_region.can_reach(state) and self.access_rule(state):
|
|
return True
|
|
return False
|
|
|
|
def forced_big_key(self):
|
|
if self.forced_item and self.forced_item.bigkey and self.player == self.forced_item.player:
|
|
item_dungeon = self.forced_item.name.split('(')[1][:-1]
|
|
if item_dungeon == 'Escape':
|
|
item_dungeon = 'Hyrule Castle'
|
|
if self.parent_region.dungeon.name == item_dungeon:
|
|
return True
|
|
return False
|
|
|
|
def gen_name(self):
|
|
name = self.name
|
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
|
if self.parent_region.dungeon and world and world.doorShuffle[self.player] == 'crossed':
|
|
name += f' @ {self.parent_region.dungeon.name}'
|
|
if world and world.players > 1:
|
|
name += f' ({world.get_player_names(self.player)})'
|
|
return name
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
world = self.parent_region.world if self.parent_region and self.parent_region.world else None
|
|
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
|
|
|
|
|
class Item(object):
|
|
|
|
def __init__(self, name='', advancement=False, priority=False, type=None, code=None, price=999, pedestal_hint=None,
|
|
pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None,
|
|
hint_text=None, player=None):
|
|
self.name = name
|
|
self.advancement = advancement
|
|
self.priority = priority
|
|
self.type = type
|
|
self.pedestal_hint_text = pedestal_hint
|
|
self.pedestal_credit_text = pedestal_credit
|
|
self.sickkid_credit_text = sickkid_credit
|
|
self.zora_credit_text = zora_credit
|
|
self.magicshop_credit_text = witch_credit
|
|
self.fluteboy_credit_text = fluteboy_credit
|
|
self.hint_text = hint_text
|
|
self.code = code
|
|
self.price = price
|
|
self.location = None
|
|
self.world = None
|
|
self.player = player
|
|
|
|
@property
|
|
def crystal(self):
|
|
return self.type == 'Crystal'
|
|
|
|
@property
|
|
def smallkey(self):
|
|
return self.type == 'SmallKey'
|
|
|
|
@property
|
|
def bigkey(self):
|
|
return self.type == 'BigKey'
|
|
|
|
@property
|
|
def map(self):
|
|
return self.type == 'Map'
|
|
|
|
@property
|
|
def compass(self):
|
|
return self.type == 'Compass'
|
|
|
|
def __str__(self):
|
|
return str(self.__unicode__())
|
|
|
|
def __unicode__(self):
|
|
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
|
|
|
|
|
|
# have 6 address that need to be filled
|
|
class Crystal(Item):
|
|
pass
|
|
|
|
@unique
|
|
class ShopType(Enum):
|
|
Shop = 0
|
|
TakeAny = 1
|
|
UpgradeShop = 2
|
|
|
|
class Shop(object):
|
|
def __init__(self, region, room_id, type, shopkeeper_config, custom, locked, sram_address):
|
|
self.region = region
|
|
self.room_id = room_id
|
|
self.type = type
|
|
self.inventory = [None, None, None]
|
|
self.shopkeeper_config = shopkeeper_config
|
|
self.custom = custom
|
|
self.locked = locked
|
|
self.sram_address = sram_address
|
|
|
|
@property
|
|
def item_count(self):
|
|
return (3 if self.inventory[2] else
|
|
2 if self.inventory[1] else
|
|
1 if self.inventory[0] else
|
|
0)
|
|
|
|
def get_bytes(self):
|
|
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
|
|
entrances = self.region.entrances
|
|
config = self.item_count
|
|
if len(entrances) == 1 and entrances[0].name in door_addresses:
|
|
door_id = door_addresses[entrances[0].name][0]+1
|
|
else:
|
|
door_id = 0
|
|
config |= 0x40 # ignore door id
|
|
if self.type == ShopType.TakeAny:
|
|
config |= 0x80
|
|
if self.type == ShopType.UpgradeShop:
|
|
config |= 0x10 # Alt. VRAM
|
|
return [0x00]+int16_as_bytes(self.room_id)+[door_id, 0x00, config, self.shopkeeper_config, 0x00]
|
|
|
|
def has_unlimited(self, item):
|
|
for inv in self.inventory:
|
|
if inv is None:
|
|
continue
|
|
if inv['max'] != 0 and inv['replacement'] is not None and inv['replacement'] == item:
|
|
return True
|
|
elif inv['item'] is not None and inv['item'] == item:
|
|
return True
|
|
return False
|
|
|
|
def clear_inventory(self):
|
|
self.inventory = [None, None, None]
|
|
|
|
def add_inventory(self, slot: int, item, price, max=0, replacement=None, replacement_price=0,
|
|
create_location=False, player=0):
|
|
self.inventory[slot] = {
|
|
'item': item,
|
|
'price': price,
|
|
'max': max,
|
|
'replacement': replacement,
|
|
'replacement_price': replacement_price,
|
|
'create_location': create_location,
|
|
'player': player
|
|
}
|
|
|
|
|
|
class Spoiler(object):
|
|
|
|
def __init__(self, world):
|
|
self.world = world
|
|
self.hashes = {}
|
|
self.overworlds = {}
|
|
self.entrances = {}
|
|
self.doors = {}
|
|
self.doorTypes = {}
|
|
self.lobbies = {}
|
|
self.medallions = {}
|
|
self.playthrough = {}
|
|
self.unreachables = []
|
|
self.startinventory = []
|
|
self.locations = {}
|
|
self.paths = {}
|
|
self.metadata = {}
|
|
self.shops = []
|
|
self.bosses = OrderedDict()
|
|
|
|
def set_overworld(self, entrance, exit, direction, player):
|
|
if self.world.players == 1:
|
|
self.overworlds[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
|
|
else:
|
|
self.overworlds[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
|
|
|
def set_entrance(self, entrance, exit, direction, player):
|
|
if self.world.players == 1:
|
|
self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
|
|
else:
|
|
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
|
|
|
|
def set_door(self, entrance, exit, direction, player, d_name):
|
|
if self.world.players == 1:
|
|
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction), ('dname', d_name)])
|
|
else:
|
|
self.doors[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction), ('dname', d_name)])
|
|
|
|
def set_lobby(self, lobby_name, door_name, player):
|
|
if self.world.players == 1:
|
|
self.lobbies[(lobby_name, player)] = {'lobby_name': lobby_name, 'door_name': door_name}
|
|
else:
|
|
self.lobbies[(lobby_name, player)] = {'player': player, 'lobby_name': lobby_name, 'door_name': door_name}
|
|
|
|
def set_door_type(self, doorNames, type, player):
|
|
if self.world.players == 1:
|
|
self.doorTypes[(doorNames, player)] = OrderedDict([('doorNames', doorNames), ('type', type)])
|
|
else:
|
|
self.doorTypes[(doorNames, player)] = OrderedDict([('player', player), ('doorNames', doorNames), ('type', type)])
|
|
|
|
def parse_data(self):
|
|
self.medallions = OrderedDict()
|
|
if self.world.players == 1:
|
|
self.medallions['Misery Mire'] = self.world.required_medallions[1][0]
|
|
self.medallions['Turtle Rock'] = self.world.required_medallions[1][1]
|
|
else:
|
|
for player in range(1, self.world.players + 1):
|
|
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0]
|
|
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1]
|
|
|
|
self.startinventory = list(map(str, self.world.precollected_items))
|
|
|
|
self.locations = OrderedDict()
|
|
listed_locations = set()
|
|
|
|
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld]
|
|
self.locations['Light World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
|
|
listed_locations.update(lw_locations)
|
|
|
|
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld]
|
|
self.locations['Dark World'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
|
|
listed_locations.update(dw_locations)
|
|
|
|
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and not loc.skip]
|
|
self.locations['Caves'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
|
|
listed_locations.update(cave_locations)
|
|
|
|
for dungeon in self.world.dungeons:
|
|
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and not loc.forced_item]
|
|
self.locations[str(dungeon)] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
|
|
listed_locations.update(dungeon_locations)
|
|
|
|
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations]
|
|
if other_locations:
|
|
self.locations['Other Locations'] = OrderedDict([(location.gen_name(), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
|
|
listed_locations.update(other_locations)
|
|
|
|
self.shops = []
|
|
for player in range(1, self.world.players + 1):
|
|
for shop in self.world.shops[player]:
|
|
if not shop.custom:
|
|
continue
|
|
shopdata = {'location': str(shop.region),
|
|
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
|
}
|
|
for index, item in enumerate(shop.inventory):
|
|
if item is None:
|
|
continue
|
|
if self.world.players == 1:
|
|
shopdata[f'item_{index}'] = f"{item['item']} — {item['price']}" if item['price'] else item['item']
|
|
else:
|
|
shopdata[f'item_{index}'] = f"{item['item']} (Player {item['player']}) — {item['price']}"
|
|
self.shops.append(shopdata)
|
|
|
|
for player in range(1, self.world.players + 1):
|
|
self.bosses[str(player)] = OrderedDict()
|
|
self.bosses[str(player)]["Eastern Palace"] = self.world.get_dungeon("Eastern Palace", player).boss.name
|
|
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
|
|
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
|
|
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
|
|
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name
|
|
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
|
|
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
|
|
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
|
|
self.bosses[str(player)]["Ice Palace"] = self.world.get_dungeon("Ice Palace", player).boss.name
|
|
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
|
|
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
|
|
self.bosses[str(player)]["Ganons Tower Basement"] = [x for x in self.world.dungeons if x.player == player and 'bottom' in x.bosses.keys()][0].bosses['bottom'].name
|
|
self.bosses[str(player)]["Ganons Tower Middle"] = [x for x in self.world.dungeons if x.player == player and 'middle' in x.bosses.keys()][0].bosses['middle'].name
|
|
self.bosses[str(player)]["Ganons Tower Top"] = [x for x in self.world.dungeons if x.player == player and 'top' in x.bosses.keys()][0].bosses['top'].name
|
|
|
|
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
|
|
self.bosses[str(player)]["Ganon"] = "Ganon"
|
|
|
|
if self.world.players == 1:
|
|
self.bosses = self.bosses["1"]
|
|
|
|
for player in range(1, self.world.players + 1):
|
|
if self.world.intensity[player] >= 3 and self.world.doorShuffle[player] != 'vanilla':
|
|
for portal in self.world.dungeon_portals[player]:
|
|
self.set_lobby(portal.name, portal.door.name, player)
|
|
|
|
from Main import __version__ as ERVersion
|
|
from OverworldShuffle import __version__ as ORVersion
|
|
self.metadata = {'version': ERVersion,
|
|
'versions': {'Door':ERVersion, 'Overworld':ORVersion},
|
|
'logic': self.world.logic,
|
|
'mode': self.world.mode,
|
|
'retro': self.world.retro,
|
|
'weapons': self.world.swords,
|
|
'goal': self.world.goal,
|
|
'ow_shuffle': self.world.owShuffle,
|
|
'ow_swap': self.world.owSwap,
|
|
'ow_keepsimilar': self.world.owKeepSimilar,
|
|
'shuffle': self.world.shuffle,
|
|
'door_shuffle': self.world.doorShuffle,
|
|
'intensity': self.world.intensity,
|
|
'item_pool': self.world.difficulty,
|
|
'item_functionality': self.world.difficulty_adjustments,
|
|
'gt_crystals': self.world.crystals_needed_for_gt,
|
|
'ganon_crystals': self.world.crystals_needed_for_ganon,
|
|
'open_pyramid': self.world.open_pyramid,
|
|
'accessibility': self.world.accessibility,
|
|
'hints': self.world.hints,
|
|
'mapshuffle': self.world.mapshuffle,
|
|
'compassshuffle': self.world.compassshuffle,
|
|
'keyshuffle': self.world.keyshuffle,
|
|
'bigkeyshuffle': self.world.bigkeyshuffle,
|
|
'boss_shuffle': self.world.boss_shuffle,
|
|
'enemy_shuffle': self.world.enemy_shuffle,
|
|
'enemy_health': self.world.enemy_health,
|
|
'enemy_damage': self.world.enemy_damage,
|
|
'potshuffle': self.world.potshuffle,
|
|
'players': self.world.players,
|
|
'teams': self.world.teams,
|
|
'experimental': self.world.experimental,
|
|
'keydropshuffle': self.world.keydropshuffle,
|
|
'shopsanity': self.world.shopsanity,
|
|
'triforcegoal': self.world.treasure_hunt_count,
|
|
'triforcepool': self.world.treasure_hunt_total,
|
|
'code': {p: Settings.make_code(self.world, p) for p in range(1, self.world.players + 1)}
|
|
}
|
|
|
|
def to_json(self):
|
|
self.parse_data()
|
|
out = OrderedDict()
|
|
out['Overworld'] = list(self.overworlds.values())
|
|
out['Entrances'] = list(self.entrances.values())
|
|
out['Doors'] = list(self.doors.values())
|
|
out['Lobbies'] = list(self.lobbies.values())
|
|
out['DoorTypes'] = list(self.doorTypes.values())
|
|
out.update(self.locations)
|
|
out['Starting Inventory'] = self.startinventory
|
|
out['Special'] = self.medallions
|
|
if self.hashes:
|
|
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()}
|
|
if self.shops:
|
|
out['Shops'] = self.shops
|
|
out['playthrough'] = self.playthrough
|
|
out['paths'] = self.paths
|
|
out['Bosses'] = self.bosses
|
|
out['meta'] = self.metadata
|
|
|
|
return json.dumps(out)
|
|
|
|
def to_file(self, filename):
|
|
self.parse_data()
|
|
with open(filename, 'w') as outfile:
|
|
line_width = 35
|
|
outfile.write('ALttP Entrance Randomizer - Seed: %s\n\n' % (self.world.seed))
|
|
for k,v in self.metadata["versions"].items():
|
|
outfile.write((k + ' Version:').ljust(line_width) + '%s\n' % v)
|
|
outfile.write('Filling Algorithm:'.ljust(line_width) + '%s\n' % self.world.algorithm)
|
|
outfile.write('Players:'.ljust(line_width) + '%d\n' % self.world.players)
|
|
outfile.write('Teams:'.ljust(line_width) + '%d\n' % self.world.teams)
|
|
for player in range(1, self.world.players + 1):
|
|
if self.world.players > 1:
|
|
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player)))
|
|
if len(self.hashes) > 0:
|
|
for team in range(self.world.teams):
|
|
outfile.write('%s%s\n' % (f"Hash - {self.world.player_names[player][team]} (Team {team+1}): " if self.world.teams > 1 else 'Hash: ', self.hashes[player, team]))
|
|
outfile.write('Settings Code:'.ljust(line_width) + '%s\n' % self.metadata["code"][player])
|
|
outfile.write('Logic:'.ljust(line_width) + '%s\n' % self.metadata['logic'][player])
|
|
outfile.write('Mode:'.ljust(line_width) + '%s\n' % self.metadata['mode'][player])
|
|
outfile.write('Retro:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['retro'][player] else 'No'))
|
|
outfile.write('Swords:'.ljust(line_width) + '%s\n' % self.metadata['weapons'][player])
|
|
outfile.write('Goal:'.ljust(line_width) + '%s\n' % self.metadata['goal'][player])
|
|
if self.metadata['goal'][player] == 'triforcehunt':
|
|
outfile.write('Triforce Pieces Required:'.ljust(line_width) + '%s\n' % self.metadata['triforcegoal'][player])
|
|
outfile.write('Triforce Pieces Total:'.ljust(line_width) + '%s\n' % self.metadata['triforcepool'][player])
|
|
outfile.write('Difficulty:'.ljust(line_width) + '%s\n' % self.metadata['item_pool'][player])
|
|
outfile.write('Item Functionality:'.ljust(line_width) + '%s\n' % self.metadata['item_functionality'][player])
|
|
outfile.write('Overworld Layout Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['ow_shuffle'][player])
|
|
outfile.write('Overworld Tile Swap:'.ljust(line_width) + '%s\n' % self.metadata['ow_swap'][player])
|
|
if self.metadata['ow_shuffle'][player] != 'vanilla':
|
|
outfile.write('Keep Similar OW Edges Together:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['ow_keepsimilar'][player] else 'No'))
|
|
outfile.write('Entrance Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['shuffle'][player])
|
|
outfile.write('Door Shuffle:'.ljust(line_width) + '%s\n' % self.metadata['door_shuffle'][player])
|
|
outfile.write('Intensity:'.ljust(line_width) + '%s\n' % self.metadata['intensity'][player])
|
|
addition = ' (Random)' if self.world.crystals_gt_orig[player] == 'random' else ''
|
|
outfile.write('Crystals required for GT:'.ljust(line_width) + '%s\n' % (str(self.metadata['gt_crystals'][player]) + addition))
|
|
addition = ' (Random)' if self.world.crystals_ganon_orig[player] == 'random' else ''
|
|
outfile.write('Crystals required for Ganon:'.ljust(line_width) + '%s\n' % (str(self.metadata['ganon_crystals'][player]) + addition))
|
|
outfile.write('Pyramid hole pre-opened:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['open_pyramid'][player] else 'No'))
|
|
outfile.write('Accessibility:'.ljust(line_width) + '%s\n' % self.metadata['accessibility'][player])
|
|
outfile.write('Map shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['mapshuffle'][player] else 'No'))
|
|
outfile.write('Compass shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['compassshuffle'][player] else 'No'))
|
|
outfile.write('Small Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keyshuffle'][player] else 'No'))
|
|
outfile.write('Big Key shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
|
|
outfile.write('Boss shuffle:'.ljust(line_width) + '%s\n' % self.metadata['boss_shuffle'][player])
|
|
outfile.write('Enemy shuffle:'.ljust(line_width) + '%s\n' % self.metadata['enemy_shuffle'][player])
|
|
outfile.write('Enemy health:'.ljust(line_width) + '%s\n' % self.metadata['enemy_health'][player])
|
|
outfile.write('Enemy damage:'.ljust(line_width) + '%s\n' % self.metadata['enemy_damage'][player])
|
|
outfile.write('Pot shuffle:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['potshuffle'][player] else 'No'))
|
|
outfile.write('Hints:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
|
|
outfile.write('Experimental:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['experimental'][player] else 'No'))
|
|
outfile.write('Key Drops shuffled:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['keydropshuffle'][player] else 'No'))
|
|
outfile.write('Shopsanity:'.ljust(line_width) + '%s\n' % ('Yes' if self.metadata['shopsanity'][player] else 'No'))
|
|
if self.doors:
|
|
outfile.write('\n\nDoors:\n\n')
|
|
outfile.write('\n'.join(
|
|
['%s%s %s %s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '',
|
|
self.world.fish.translate("meta","doors",entry['entrance']),
|
|
'<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>',
|
|
self.world.fish.translate("meta","doors",entry['exit']),
|
|
'({0})'.format(entry['dname']) if self.world.doorShuffle[entry['player']] == 'crossed' else '') for
|
|
entry in self.doors.values()]))
|
|
if self.lobbies:
|
|
outfile.write('\n\nDungeon Lobbies:\n\n')
|
|
outfile.write('\n'.join(
|
|
[f"{'Player {0}: '.format(entry['player']) if self.world.players > 1 else ''}{entry['lobby_name']}: {entry['door_name']}"
|
|
for
|
|
entry in self.lobbies.values()]))
|
|
if self.doorTypes:
|
|
# doorNames: For some reason these come in combined, somehow need to split on the thing to translate
|
|
# doorTypes: Small Key, Bombable, Bonkable
|
|
outfile.write('\n\nDoor Types:\n\n')
|
|
outfile.write('\n'.join(['%s%s %s' % ('Player {0}: '.format(entry['player']) if self.world.players > 1 else '', self.world.fish.translate("meta","doors",entry['doorNames']), self.world.fish.translate("meta","doorTypes",entry['type'])) for entry in self.doorTypes.values()]))
|
|
if self.entrances:
|
|
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
|
|
outfile.write('\n\nEntrances:\n\n')
|
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","entrances",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","entrances",entry['exit'])) for entry in self.entrances.values()]))
|
|
if self.overworlds:
|
|
# overworlds: overworld transitions;
|
|
outfile.write('\n\nOverworld:\n\n')
|
|
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' if self.world.players > 1 else '', self.world.fish.translate("meta","overworlds",entry['entrance']), '<=>' if entry['direction'] == 'both' else '<=' if entry['direction'] == 'exit' else '=>', self.world.fish.translate("meta","overworlds",entry['exit'])) for entry in self.overworlds.values()]))
|
|
outfile.write('\n\nMedallions:\n')
|
|
for dungeon, medallion in self.medallions.items():
|
|
outfile.write(f'\n{dungeon}: {medallion} Medallion')
|
|
if self.startinventory:
|
|
outfile.write('\n\nStarting Inventory:\n\n')
|
|
outfile.write('\n'.join(self.startinventory))
|
|
|
|
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
|
|
# items: Item names
|
|
outfile.write('\n\nLocations:\n\n')
|
|
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "locations", location), self.world.fish.translate("meta", "items", item)) for grouping in self.locations.values() for (location, item) in grouping.items()]))
|
|
|
|
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
|
|
# items: Item names
|
|
outfile.write('\n\nShops:\n\n')
|
|
outfile.write('\n'.join("{} [{}]\n {}".format(self.world.fish.translate("meta","locations",shop['location']), shop['type'], "\n ".join(self.world.fish.translate("meta","items",item) for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
|
|
|
|
for player in range(1, self.world.players + 1):
|
|
if self.world.boss_shuffle[player] != 'none':
|
|
bossmap = self.bosses[player] if self.world.players > 1 else self.bosses
|
|
outfile.write(f'\n\nBosses ({self.world.get_player_names(player)}):\n\n')
|
|
outfile.write('\n'.join([f'{x}: {y}' for x, y in bossmap.items() if y not in ['Agahnim', 'Agahnim 2', 'Ganon']]))
|
|
|
|
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
|
|
# items: Item names
|
|
outfile.write('\n\nPlaythrough:\n\n')
|
|
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (self.world.fish.translate("meta","locations",location), self.world.fish.translate("meta","items",item)) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
|
if self.unreachables:
|
|
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
|
|
# items: Item names
|
|
outfile.write('\n\nUnreachable Items:\n\n')
|
|
outfile.write('\n'.join(['%s: %s' % (self.world.fish.translate("meta", "items", unreachable.item.name),
|
|
self.world.fish.translate("meta", "locations", unreachable.name))
|
|
for unreachable in self.unreachables]))
|
|
|
|
# rooms: Change up room names; only if it's got no locations in it
|
|
# entrances: To/From overworld; Checking w/ & w/out "Exit" and translating accordingly
|
|
# locations: Change up location names; in the instance of a location with multiple sections, it'll try to translate the room name
|
|
outfile.write('\n\nPaths:\n\n')
|
|
path_listings = []
|
|
for location, path in sorted(self.paths.items()):
|
|
path_lines = []
|
|
for region, exit in path:
|
|
if exit is not None:
|
|
path_lines.append("{} -> {}".format(self.world.fish.translate("meta","rooms",region), self.world.fish.translate("meta","entrances",exit)))
|
|
else:
|
|
path_lines.append(self.world.fish.translate("meta","rooms",region))
|
|
path_listings.append("{}\n {}".format(self.world.fish.translate("meta","locations",location), "\n => ".join(path_lines)))
|
|
|
|
outfile.write('\n'.join(path_listings))
|
|
|
|
|
|
flooded_keys = {
|
|
'Trench 1 Switch': 'Swamp Palace - Trench 1 Pot Key',
|
|
'Trench 2 Switch': 'Swamp Palace - Trench 2 Pot Key'
|
|
}
|
|
|
|
dungeon_names = [
|
|
'Hyrule Castle', 'Eastern Palace', 'Desert Palace', 'Tower of Hera', 'Agahnims Tower', 'Palace of Darkness',
|
|
'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', 'Misery Mire', 'Turtle Rock', 'Ganons Tower'
|
|
]
|
|
|
|
|
|
class PotItem(FastEnum):
|
|
Nothing = 0x0
|
|
OneRupee = 0x1
|
|
RockCrab = 0x2
|
|
Bee = 0x3
|
|
Random = 0x4
|
|
Bomb_0 = 0x5
|
|
Heart_0 = 0x6
|
|
FiveRupees = 0x7
|
|
Key = 0x8
|
|
FiveArrows = 0x9
|
|
Bomb = 0xA
|
|
Heart = 0xB
|
|
SmallMagic = 0xC
|
|
BigMagic = 0xD
|
|
Chicken = 0xE
|
|
GreenSoldier = 0xF
|
|
AliveRock = 0x10
|
|
BlueSoldier = 0x11
|
|
GroundBomb = 0x12
|
|
Heart_2 = 0x13
|
|
Fairy = 0x14
|
|
Heart_3 = 0x15
|
|
Hole = 0x80
|
|
Warp = 0x82
|
|
Staircase = 0x84
|
|
Bombable = 0x86
|
|
Switch = 0x88
|
|
|
|
|
|
class PotFlags(FastEnum):
|
|
Normal = 0x0
|
|
NoSwitch = 0x1 # A switch should never go here
|
|
SwitchLogicChange = 0x2 # A switch can go here, but requires a logic change
|
|
|
|
|
|
class Pot(object):
|
|
def __init__(self, x, y, item, room, flags = PotFlags.Normal):
|
|
self.x = x
|
|
self.y = y
|
|
self.item = item
|
|
self.room = room
|
|
self.flags = flags
|
|
|
|
|
|
# byte 0: DDOO EEEE (DR, OR, ER)
|
|
dr_mode = {"basic": 1, "crossed": 2, "vanilla": 0}
|
|
or_mode = {"parallel": 1, "full": 2, "vanilla": 0}
|
|
er_mode = {"vanilla": 0, "simple": 1, "restricted": 2, "full": 3, "crossed": 4, "insanity": 5, "restricted_legacy": 8,
|
|
"full_legacy": 9, "madness_legacy": 10, "insanity_legacy": 11, "dungeonsfull": 7, "dungeonssimple": 6}
|
|
|
|
# byte 1: LLLW WSSR (logic, mode, sword, retro)
|
|
logic_mode = {"noglitches": 0, "minorglitches": 1, "nologic": 2, "owglitches": 3, "majorglitches": 4}
|
|
world_mode = {"open": 0, "standard": 1, "inverted": 2}
|
|
sword_mode = {"random": 0, "assured": 1, "swordless": 2, "vanilla": 3}
|
|
|
|
# byte 2: GGGD DFFH (goal, diff, item_func, hints)
|
|
goal_mode = {"ganon": 0, "pedestal": 1, "dungeons": 2, "triforcehunt": 3, "crystals": 4}
|
|
diff_mode = {"normal": 0, "hard": 1, "expert": 2}
|
|
func_mode = {"normal": 0, "hard": 1, "expert": 2}
|
|
|
|
# byte 3: SKMM PIII (shop, keydrop, mixed, palettes, intensity)
|
|
mixed_travel_mode = {"prevent": 0, "allow": 1, "force": 2}
|
|
# intensity is 3 bits (reserves 4-7 levels)
|
|
|
|
# byte 4: CCCC CTTX (crystals gt, ctr2, experimental)
|
|
counter_mode = {"default": 0, "off": 1, "on": 2, "pickup": 3}
|
|
|
|
# byte 5: CCCC CPAA (crystals ganon, pyramid, access
|
|
access_mode = {"items": 0, "locations": 1, "none": 2}
|
|
|
|
# byte 6: BSMC BBEE (big, small, maps, compass, bosses, enemies)
|
|
boss_mode = {"none": 0, "simple": 1, "full": 2, "random": 3, "chaos": 3}
|
|
enemy_mode = {"none": 0, "shuffled": 1, "random": 2, "chaos": 2}
|
|
|
|
# byte 7: HHHD DP?? (enemy_health, enemy_dmg, potshuffle, ?)
|
|
e_health = {"default": 0, "easy": 1, "normal": 2, "hard": 3, "expert": 4}
|
|
e_dmg = {"default": 0, "shuffled": 1, "random": 2}
|
|
|
|
class Settings(object):
|
|
|
|
@staticmethod
|
|
def make_code(w, p):
|
|
code = bytes([
|
|
(dr_mode[w.doorShuffle[p]] << 6) | (or_mode[w.owShuffle[p]] << 4) | er_mode[w.shuffle[p]],
|
|
|
|
(logic_mode[w.logic[p]] << 5) | (world_mode[w.mode[p]] << 3)
|
|
| (sword_mode[w.swords[p]] << 1) | (1 if w.retro[p] else 0),
|
|
|
|
(goal_mode[w.goal[p]] << 5) | (diff_mode[w.difficulty[p]] << 3)
|
|
| (func_mode[w.difficulty_adjustments[p]] << 1) | (1 if w.hints[p] else 0),
|
|
|
|
(0x80 if w.shopsanity[p] else 0) | (0x40 if w.keydropshuffle[p] else 0)
|
|
| (mixed_travel_mode[w.mixed_travel[p]] << 4) | (0x8 if w.standardize_palettes[p] == "original" else 0)
|
|
| (0 if w.intensity[p] == "random" else w.intensity[p]),
|
|
|
|
((8 if w.crystals_gt_orig[p] == "random" else int(w.crystals_gt_orig[p])) << 3)
|
|
| (counter_mode[w.dungeon_counters[p]] << 1) | (1 if w.experimental[p] else 0),
|
|
|
|
((8 if w.crystals_ganon_orig[p] == "random" else int(w.crystals_ganon_orig[p])) << 3)
|
|
| (0x4 if w.open_pyramid[p] else 0) | access_mode[w.accessibility[p]],
|
|
|
|
(0x80 if w.bigkeyshuffle[p] else 0) | (0x40 if w.keyshuffle[p] else 0)
|
|
| (0x20 if w.mapshuffle[p] else 0) | (0x10 if w.compassshuffle[p] else 0)
|
|
| (boss_mode[w.boss_shuffle[p]] << 2) | (enemy_mode[w.enemy_shuffle[p]]),
|
|
|
|
(e_health[w.enemy_health[p]] << 5) | (e_dmg[w.enemy_damage[p]] << 3) | (0x4 if w.potshuffle[p] else 0)])
|
|
return base64.b64encode(code, "+-".encode()).decode()
|
|
|
|
@staticmethod
|
|
def adjust_args_from_code(code, player, args):
|
|
settings, p = base64.b64decode(code.encode(), "+-".encode()), player
|
|
|
|
def r(d):
|
|
return {y: x for x, y in d.items()}
|
|
|
|
args.shuffle[p] = r(er_mode)[settings[0] & 0x0F]
|
|
args.ow_shuffle[p] = r(or_mode)[(settings[0] & 0x30) >> 4]
|
|
args.door_shuffle[p] = r(dr_mode)[(settings[0] & 0xC0) >> 6]
|
|
args.logic[p] = r(logic_mode)[(settings[1] & 0xE0) >> 5]
|
|
args.mode[p] = r(world_mode)[(settings[1] & 0x18) >> 3]
|
|
args.swords[p] = r(sword_mode)[(settings[1] & 0x6) >> 1]
|
|
args.difficulty[p] = r(diff_mode)[(settings[2] & 0x18) >> 3]
|
|
args.item_functionality[p] = r(func_mode)[(settings[2] & 0x6) >> 1]
|
|
args.goal[p] = r(goal_mode)[(settings[2] & 0xE0) >> 5]
|
|
args.accessibility[p] = r(access_mode)[settings[5] & 0x3]
|
|
args.retro[p] = True if settings[1] & 0x01 else False
|
|
args.hints[p] = True if settings[2] & 0x01 else False
|
|
args.retro[p] = True if settings[1] & 0x01 else False
|
|
args.shopsanity[p] = True if settings[3] & 0x80 else False
|
|
args.keydropshuffle[p] = True if settings[3] & 0x40 else False
|
|
args.mixed_travel[p] = r(mixed_travel_mode)[(settings[3] & 0x30) >> 4]
|
|
args.standardize_palettes[p] = "original" if settings[3] & 0x8 else "standardize"
|
|
intensity = settings[3] & 0x7
|
|
args.intensity[p] = "random" if intensity == 0 else intensity
|
|
args.dungeon_counters[p] = r(counter_mode)[(settings[4] & 0x6) >> 1]
|
|
cgt = (settings[4] & 0xf8) >> 3
|
|
args.crystals_gt[p] = "random" if cgt == 8 else cgt
|
|
args.experimental[p] = True if settings[4] & 0x1 else False
|
|
cgan = (settings[5] & 0xf8) >> 3
|
|
args.crystals_ganon[p] = "random" if cgan == 8 else cgan
|
|
args.openpyramid[p] = True if settings[5] & 0x4 else False
|
|
args.bigkeyshuffle[p] = True if settings[6] & 0x80 else False
|
|
args.keyshuffle[p] = True if settings[6] & 0x40 else False
|
|
args.mapshuffle[p] = True if settings[6] & 0x20 else False
|
|
args.compassshuffle[p] = True if settings[6] & 0x10 else False
|
|
args.shufflebosses[p] = r(boss_mode)[(settings[6] & 0xc) >> 2]
|
|
args.shuffleenemies[p] = r(enemy_mode)[settings[6] & 0x3]
|
|
args.enemy_health[p] = r(e_health)[(settings[7] & 0xE0) >> 5]
|
|
args.enemy_damage[p] = r(e_dmg)[(settings[7] & 0x18) >> 3]
|
|
args.shufflepots[p] = True if settings[7] & 0x4 else False
|