mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
40 Commits
use_sphinx
...
player-tra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5fceba2d | ||
|
|
813ee5ee3b | ||
|
|
be1158ad78 | ||
|
|
6d5ddf3cad | ||
|
|
809bda02d1 | ||
|
|
2d5ec6ce22 | ||
|
|
a95d0ce9ef | ||
|
|
267d9234e5 | ||
|
|
4686881566 | ||
|
|
101dab0ea4 | ||
|
|
c2d69cb05e | ||
|
|
58f66e0f42 | ||
|
|
0215e1fa28 | ||
|
|
1c0a93acad | ||
|
|
4fcde135e5 | ||
|
|
332dde154f | ||
|
|
8d51205e8f | ||
|
|
ff05e9d7d5 | ||
|
|
516a52c041 | ||
|
|
9daa64741b | ||
|
|
af11fa5150 | ||
|
|
156e9e0e43 | ||
|
|
ef46979bd8 | ||
|
|
b2aa251c47 | ||
|
|
e204a0fce6 | ||
|
|
bb386d3bd7 | ||
|
|
88a225764a | ||
|
|
e9e5511583 | ||
|
|
c546dcd5ff | ||
|
|
053fb14495 | ||
|
|
ed77d14618 | ||
|
|
3fb287e82b | ||
|
|
32431cfe04 | ||
|
|
ca8f4c38ec | ||
|
|
eb52454ccc | ||
|
|
14e5f54f59 | ||
|
|
2052cc55af | ||
|
|
63a8436240 | ||
|
|
e60719a20a | ||
|
|
8742aadc72 |
@@ -955,6 +955,13 @@ class Region:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance:
|
||||
for entrance in self.entrances:
|
||||
if is_main_entrance(entrance):
|
||||
return entrance
|
||||
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
|
||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -1422,7 +1429,6 @@ class Spoiler():
|
||||
"f" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Custom Potion Shop: %s\n' %
|
||||
bool_to_text("w" in self.world.shop_shuffle[player]))
|
||||
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
|
||||
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
|
||||
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
|
||||
outfile.write('Prize shuffle %s\n' %
|
||||
|
||||
@@ -5,6 +5,7 @@ import urllib.parse
|
||||
import sys
|
||||
import typing
|
||||
import time
|
||||
import functools
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
@@ -17,7 +18,8 @@ if __name__ == "__main__":
|
||||
Utils.init_logging("TextClient", exception_logger="Client")
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
|
||||
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
|
||||
from Utils import Version, stream_input
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -204,6 +206,10 @@ class CommonContext:
|
||||
# execution
|
||||
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
|
||||
|
||||
@functools.cached_property
|
||||
def raw_text_parser(self) -> RawJSONtoTextParser:
|
||||
return RawJSONtoTextParser(self)
|
||||
|
||||
@property
|
||||
def total_locations(self) -> typing.Optional[int]:
|
||||
"""Will return None until connected."""
|
||||
|
||||
53
FF1Client.py
53
FF1Client.py
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
@@ -6,7 +7,7 @@ from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
@@ -64,7 +65,7 @@ class FF1Context(CommonContext):
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
self.messages[time.time(), msg_id] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
@@ -73,32 +74,28 @@ class FF1Context(CommonContext):
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == 'PrintJSON':
|
||||
print_type = args['type']
|
||||
item = args['item']
|
||||
receiving_player_id = args['receiving']
|
||||
receiving_player_name = self.player_names[receiving_player_id]
|
||||
sending_player_id = item.player
|
||||
sending_player_name = self.player_names[item.player]
|
||||
if print_type == 'Hint':
|
||||
msg = f"Hint: Your {self.item_names[item.item]} is at" \
|
||||
f" {self.player_names[item.player]}'s {self.location_names[item.location]}"
|
||||
self._set_message(msg, item.item)
|
||||
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
|
||||
if sending_player_id == self.slot:
|
||||
if receiving_player_id == self.slot:
|
||||
msg = f"You found your own {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}"
|
||||
else:
|
||||
if receiving_player_id == sending_player_id:
|
||||
msg = f"{sending_player_name} found their {self.item_names[item.item]}"
|
||||
else:
|
||||
msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \
|
||||
f"{receiving_player_name}"
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
|
||||
150
Fill.py
150
Fill.py
@@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
placements: typing.List[Location] = []
|
||||
swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter()
|
||||
while locations and itempool:
|
||||
item_to_place = itempool.pop()
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
for i, location in enumerate(locations):
|
||||
if location.item_rule(item_to_place):
|
||||
# popping by index is faster than removing by content,
|
||||
spot_to_fill = locations.pop(i)
|
||||
# skipping a scan for the element
|
||||
break
|
||||
|
||||
else:
|
||||
# we filled all reachable spots.
|
||||
# try swapping this item with previously placed items
|
||||
|
||||
for (i, location) in enumerate(placements):
|
||||
placed_item = location.item
|
||||
# Unplaceable items can sometimes be swapped infinitely. Limit the
|
||||
# number of times we will swap an individual item to prevent this
|
||||
|
||||
if swapped_items[placed_item.player,
|
||||
placed_item.name] > 1:
|
||||
continue
|
||||
|
||||
location.item = None
|
||||
placed_item.location = None
|
||||
if location.item_rule(item_to_place):
|
||||
# Add this item to the existing placement, and
|
||||
# add the old item to the back of the queue
|
||||
spot_to_fill = placements.pop(i)
|
||||
|
||||
swapped_items[placed_item.player,
|
||||
placed_item.name] += 1
|
||||
|
||||
itempool.append(placed_item)
|
||||
|
||||
break
|
||||
|
||||
# Item can't be placed here, restore original item
|
||||
location.item = placed_item
|
||||
placed_item.location = location
|
||||
|
||||
if spot_to_fill is None:
|
||||
# Can't place this item, move on to the next
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
|
||||
if unplaced_items and locations:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
|
||||
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
|
||||
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
fill_locations = sorted(world.get_unfilled_locations())
|
||||
world.random.shuffle(fill_locations)
|
||||
|
||||
# get items to distribute
|
||||
itempool = sorted(world.itempool)
|
||||
world.random.shuffle(itempool)
|
||||
progitempool: typing.List[Item] = []
|
||||
nonexcludeditempool: typing.List[Item] = []
|
||||
localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)}
|
||||
nonlocalrestitempool: typing.List[Item] = []
|
||||
restitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
filleritempool: typing.List[Item] = []
|
||||
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
progitempool.append(item)
|
||||
elif item.useful: # this only gets nonprogression items which should not appear in excluded locations
|
||||
nonexcludeditempool.append(item)
|
||||
elif item.name in world.local_items[item.player].value:
|
||||
localrestitempool[item.player].append(item)
|
||||
elif item.name in world.non_local_items[item.player].value:
|
||||
nonlocalrestitempool.append(item)
|
||||
elif item.useful:
|
||||
usefulitempool.append(item)
|
||||
else:
|
||||
restitempool.append(item)
|
||||
filleritempool.append(item)
|
||||
|
||||
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
|
||||
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
|
||||
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
@@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
|
||||
if nonexcludeditempool:
|
||||
world.random.shuffle(defaultlocations)
|
||||
# needs logical fill to not conflict with local items
|
||||
fill_restrictive(
|
||||
world, world.state, defaultlocations, nonexcludeditempool)
|
||||
if nonexcludeditempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
|
||||
remaining_fill(world, excludedlocations, filleritempool)
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
defaultlocations = defaultlocations + excludedlocations
|
||||
world.random.shuffle(defaultlocations)
|
||||
restitempool = usefulitempool + filleritempool
|
||||
|
||||
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
|
||||
local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids}
|
||||
for location in defaultlocations:
|
||||
local_locations[location.player].append(location)
|
||||
for player_locations in local_locations.values():
|
||||
world.random.shuffle(player_locations)
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
|
||||
for player, items in localrestitempool.items(): # items already shuffled
|
||||
player_local_locations = local_locations[player]
|
||||
for item_to_place in items:
|
||||
if not player_local_locations:
|
||||
logging.warning(f"Ran out of local locations for player {player}, "
|
||||
f"cannot place {item_to_place}.")
|
||||
break
|
||||
spot_to_fill = player_local_locations.pop()
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
defaultlocations.remove(spot_to_fill)
|
||||
|
||||
for item_to_place in nonlocalrestitempool:
|
||||
for i, location in enumerate(defaultlocations):
|
||||
if location.player != item_to_place.player:
|
||||
world.push_item(defaultlocations.pop(i), item_to_place, False)
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. "
|
||||
f"Too many non-local items for too few remaining locations.")
|
||||
|
||||
world.random.shuffle(defaultlocations)
|
||||
|
||||
restitempool, defaultlocations = fast_fill(
|
||||
world, restitempool, defaultlocations)
|
||||
unplaced = progitempool + restitempool
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
|
||||
if unplaced or unfilled:
|
||||
@@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
logging.info(f'Per-Player counts: {print_data})')
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def flood_items(world: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
world.random.shuffle(world.itempool)
|
||||
|
||||
62
Generate.py
62
Generate.py
@@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
|
||||
from Main import main as ERmain
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
import Options
|
||||
from worlds.alttp import Bosses
|
||||
from worlds.alttp.Text import TextTable
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
import copy
|
||||
@@ -337,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]:
|
||||
return input_data
|
||||
|
||||
|
||||
available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
|
||||
{'Agahnim', 'Agahnim2', 'Ganon'}}
|
||||
available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
|
||||
Bosses.boss_location_table}
|
||||
|
||||
boss_shuffle_options = {None: 'none',
|
||||
'none': 'none',
|
||||
'basic': 'basic',
|
||||
'full': 'full',
|
||||
'chaos': 'chaos',
|
||||
'singularity': 'singularity'
|
||||
}
|
||||
|
||||
goals = {
|
||||
'ganon': 'ganon',
|
||||
'crystals': 'crystals',
|
||||
@@ -456,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str:
|
||||
if boss_shuffle in boss_shuffle_options:
|
||||
return boss_shuffle_options[boss_shuffle]
|
||||
elif PlandoSettings.bosses in plando_options:
|
||||
options = boss_shuffle.lower().split(";")
|
||||
remainder_shuffle = "none" # vanilla
|
||||
bosses = []
|
||||
for boss in options:
|
||||
if boss in boss_shuffle_options:
|
||||
remainder_shuffle = boss_shuffle_options[boss]
|
||||
elif "-" in boss:
|
||||
loc, boss_name = boss.split("-")
|
||||
if boss_name not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name {boss_name}")
|
||||
if loc not in available_boss_locations:
|
||||
raise ValueError(f"Unknown Boss Location {loc}")
|
||||
level = ''
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = f" {loc[-1]}"
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if not Bosses.can_place_boss(boss_name.title(), loc, level):
|
||||
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
|
||||
bosses.append(boss)
|
||||
elif boss not in available_boss_names:
|
||||
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
|
||||
else:
|
||||
bosses.append(boss)
|
||||
return ";".join(bosses + [remainder_shuffle])
|
||||
else:
|
||||
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings):
|
||||
if option_key in game_weights:
|
||||
try:
|
||||
if not option.supports_weighting:
|
||||
@@ -502,10 +453,9 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
||||
except Exception as e:
|
||||
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
|
||||
else:
|
||||
if hasattr(player_option, "verify"):
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game])
|
||||
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
|
||||
else:
|
||||
setattr(ret, option_key, option(option.default))
|
||||
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
|
||||
|
||||
|
||||
def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses):
|
||||
@@ -549,11 +499,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings
|
||||
|
||||
if ret.game in AutoWorldRegister.world_types:
|
||||
for option_key, option in world_type.option_definitions.items():
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
for option_key, option in Options.per_game_common_options.items():
|
||||
# skip setting this option if already set from common_options, defaulting to root option
|
||||
if not (option_key in Options.common_options and option_key not in game_weights):
|
||||
handle_option(ret, game_weights, option_key, option)
|
||||
handle_option(ret, game_weights, option_key, option, plando_options)
|
||||
if PlandoSettings.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
|
||||
@@ -636,8 +586,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
|
||||
|
||||
ret.item_functionality = get_choice_legacy('item_functionality', weights)
|
||||
|
||||
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
|
||||
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
|
||||
|
||||
ret.enemy_damage = {None: 'default',
|
||||
'default': 'default',
|
||||
|
||||
53
Main.py
53
Main.py
@@ -12,7 +12,7 @@ from typing import Dict, Tuple, Optional, Set
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
|
||||
from worlds.alttp.Regions import is_main_entrance
|
||||
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
|
||||
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
|
||||
from Utils import output_path, get_options, __version__, version_tuple
|
||||
@@ -249,24 +249,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
|
||||
return entrance
|
||||
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
|
||||
return get_entrance_to_region(entrance.parent_region)
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
|
||||
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in range(1, world.players + 1)}
|
||||
@@ -276,22 +261,23 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) is int:
|
||||
main_entrance = get_entrance_to_region(location.parent_region)
|
||||
if location.game != "A Link to the Past":
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif location.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
else:
|
||||
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
if location.parent_region.dungeon:
|
||||
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
|
||||
'Inverted Ganons Tower': 'Ganons Tower'} \
|
||||
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
|
||||
checks_in_area[location.player][dungeonname].append(location.address)
|
||||
elif location.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif location.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[location.player]["Light World"].append(location.address)
|
||||
elif main_entrance.parent_region.type == RegionType.DarkWorld:
|
||||
checks_in_area[location.player]["Dark World"].append(location.address)
|
||||
checks_in_area[location.player]["Total"] += 1
|
||||
|
||||
oldmancaves = []
|
||||
@@ -305,7 +291,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
main_entrance = get_entrance_to_region(region)
|
||||
main_entrance = region.get_connecting_entrance(is_main_entrance)
|
||||
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||
checks_in_area[player]["Light World"].append(location_id)
|
||||
else:
|
||||
@@ -340,7 +326,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
|
||||
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
|
||||
208
MultiServer.py
208
MultiServer.py
@@ -126,6 +126,7 @@ class Context:
|
||||
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
|
||||
all_item_and_group_names: typing.Dict[str, typing.Set[str]]
|
||||
forced_auto_forfeits: typing.Dict[str, bool]
|
||||
non_hintable_names: typing.Dict[str, typing.Set[str]]
|
||||
|
||||
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
|
||||
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
|
||||
@@ -196,7 +197,7 @@ class Context:
|
||||
self.item_name_groups = {}
|
||||
self.all_item_and_group_names = {}
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False)
|
||||
self.non_hintable_names = {}
|
||||
self.non_hintable_names = collections.defaultdict(frozenset)
|
||||
|
||||
self._load_game_data()
|
||||
self._init_game_data()
|
||||
@@ -221,11 +222,11 @@ class Context:
|
||||
self.all_item_and_group_names[game_name] = \
|
||||
set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name])
|
||||
|
||||
def item_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["item_name_to_id"]
|
||||
def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None
|
||||
|
||||
def location_names_for_game(self, game: str) -> typing.Dict[str, int]:
|
||||
return self.gamespackage[game]["location_name_to_id"]
|
||||
def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]:
|
||||
return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None
|
||||
|
||||
# General networking
|
||||
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
|
||||
@@ -743,6 +744,7 @@ async def countdown(ctx: Context, timer: int):
|
||||
broadcast_countdown(ctx, 0, f"[Server]: GO")
|
||||
ctx.countdown_timer = 0
|
||||
|
||||
|
||||
def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}):
|
||||
old_clients, new_clients = [], []
|
||||
|
||||
@@ -755,8 +757,10 @@ def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {})
|
||||
ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }])
|
||||
ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
|
||||
|
||||
|
||||
def broadcast_countdown(ctx: Context, timer: int, message: str):
|
||||
broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer })
|
||||
broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer})
|
||||
|
||||
|
||||
def get_players_string(ctx: Context):
|
||||
auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth}
|
||||
@@ -897,14 +901,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
ctx.save()
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
|
||||
hints = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
if slot in group:
|
||||
slots.add(group_id)
|
||||
|
||||
seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name]
|
||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||
for finding_player, check_data in ctx.locations.items():
|
||||
for location_id, (item_id, receiving_player, item_flags) in check_data.items():
|
||||
if receiving_player in slots and item_id == seeked_item_id:
|
||||
@@ -1332,13 +1336,33 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
|
||||
f"You have {points_available} points.")
|
||||
return True
|
||||
|
||||
elif input_text.isnumeric():
|
||||
game = self.ctx.games[self.client.slot]
|
||||
hint_id = int(input_text)
|
||||
hint_name = self.ctx.item_names[hint_id] \
|
||||
if not for_location and hint_id in self.ctx.item_names \
|
||||
else self.ctx.location_names[hint_id] \
|
||||
if for_location and hint_id in self.ctx.location_names \
|
||||
else None
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location:
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
else:
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
|
||||
else:
|
||||
game = self.ctx.games[self.client.slot]
|
||||
if game not in self.ctx.all_item_and_group_names:
|
||||
self.output("Can't look up item/location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
names = self.ctx.location_names_for_game(game) \
|
||||
if for_location else \
|
||||
self.ctx.all_item_and_group_names[game]
|
||||
hint_name, usable, response = get_intended_text(input_text,
|
||||
names)
|
||||
hint_name, usable, response = get_intended_text(input_text, names)
|
||||
|
||||
if usable:
|
||||
if hint_name in self.ctx.non_hintable_names[game]:
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
@@ -1352,63 +1376,65 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
if hints:
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
notify_hints(self.ctx, self.client.team, list(old_hints))
|
||||
if not new_hints:
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
if new_hints:
|
||||
found_hints = [hint for hint in new_hints if hint.found]
|
||||
not_found_hints = [hint for hint in new_hints if not hint.found]
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
|
||||
hints = found_hints
|
||||
while can_pay > 0:
|
||||
if not not_found_hints:
|
||||
break
|
||||
hint = not_found_hints.pop()
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
return False
|
||||
else:
|
||||
self.output(response)
|
||||
return False
|
||||
|
||||
if hints:
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
|
||||
old_hints = set(hints) - new_hints
|
||||
if old_hints:
|
||||
notify_hints(self.ctx, self.client.team, list(old_hints))
|
||||
if not new_hints:
|
||||
self.output("Hint was previously used, no points deducted.")
|
||||
if new_hints:
|
||||
found_hints = [hint for hint in new_hints if hint.found]
|
||||
not_found_hints = [hint for hint in new_hints if not hint.found]
|
||||
|
||||
if not not_found_hints: # everything's been found, no need to pay
|
||||
can_pay = 1000
|
||||
elif cost:
|
||||
can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
|
||||
else:
|
||||
can_pay = 1000
|
||||
|
||||
self.ctx.random.shuffle(not_found_hints)
|
||||
# By popular vote, make hints prefer non-local placements
|
||||
not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player))
|
||||
|
||||
hints = found_hints
|
||||
while can_pay > 0:
|
||||
if not not_found_hints:
|
||||
break
|
||||
hint = not_found_hints.pop()
|
||||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not_found_hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
||||
else:
|
||||
self.output("Nothing found. Item/Location may not exist.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_name: str = "") -> bool:
|
||||
"""Use !hint {item_name},
|
||||
@@ -1856,17 +1882,25 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
item_name = " ".join(item_name)
|
||||
game = self.ctx.games[slot]
|
||||
item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game])
|
||||
full_name = " ".join(item_name)
|
||||
|
||||
if full_name.isnumeric():
|
||||
item, usable, response = int(full_name), True, None
|
||||
elif game in self.ctx.all_item_and_group_names:
|
||||
item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game])
|
||||
else:
|
||||
self.output("Can't look up item for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
|
||||
if usable:
|
||||
if item_name in self.ctx.item_name_groups[game]:
|
||||
if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]:
|
||||
hints = []
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item_name]:
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||
else: # item name
|
||||
hints = collect_hints(self.ctx, team, slot, item_name)
|
||||
else: # item name or id
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
@@ -1887,11 +1921,22 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
|
||||
if usable:
|
||||
team, slot = self.ctx.player_name_lookup[seeked_player]
|
||||
location_name = " ".join(location_name)
|
||||
location_name, usable, response = get_intended_text(location_name,
|
||||
self.ctx.location_names_for_game(self.ctx.games[slot]))
|
||||
game = self.ctx.games[slot]
|
||||
full_name = " ".join(location_name)
|
||||
|
||||
if full_name.isnumeric():
|
||||
location, usable, response = int(full_name), True, None
|
||||
elif self.ctx.location_names_for_game(game) is not None:
|
||||
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
|
||||
else:
|
||||
self.output("Can't look up location for unknown game. Hint for ID instead.")
|
||||
return False
|
||||
|
||||
if usable:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location_name)
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
if hints:
|
||||
notify_hints(self.ctx, team, hints)
|
||||
else:
|
||||
@@ -2041,15 +2086,28 @@ async def main(args: argparse.Namespace):
|
||||
args.auto_shutdown, args.compatibility, args.log_network)
|
||||
data_filename = args.multidata
|
||||
|
||||
try:
|
||||
if not data_filename:
|
||||
if not data_filename:
|
||||
try:
|
||||
filetypes = (("Multiworld data", (".archipelago", ".zip")),)
|
||||
data_filename = Utils.open_filename("Select multiworld data", filetypes)
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)):
|
||||
if not isinstance(e, ImportError):
|
||||
logging.error(f"Failed to load tkinter ({e})")
|
||||
logging.info("Pass a multidata filename on command line to run headless.")
|
||||
exit(1)
|
||||
raise
|
||||
|
||||
if not data_filename:
|
||||
logging.info("No file selected. Exiting.")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception('Failed to read multiworld data (%s)' % e)
|
||||
logging.exception(f"Failed to read multiworld data ({e})")
|
||||
raise
|
||||
|
||||
ctx.init_save(not args.disable_save)
|
||||
|
||||
115
Options.py
115
Options.py
@@ -26,15 +26,31 @@ class AssembleOptions(abc.ABCMeta):
|
||||
|
||||
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
||||
options.update(new_options)
|
||||
|
||||
# apply aliases, without name_lookup
|
||||
aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
||||
name.startswith("alias_")}
|
||||
|
||||
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
|
||||
|
||||
# auto-alias Off and On being parsed as True and False
|
||||
if "off" in options:
|
||||
options["false"] = options["off"]
|
||||
if "on" in options:
|
||||
options["true"] = options["on"]
|
||||
|
||||
options.update(aliases)
|
||||
|
||||
if "verify" not in attrs:
|
||||
# not overridden by class -> look up bases
|
||||
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
|
||||
if len(verifiers) > 1: # verify multiple bases/mixins
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
for f in verifiers:
|
||||
f(self, *args, **kwargs)
|
||||
attrs["verify"] = verify
|
||||
else:
|
||||
assert verifiers, "class Option is supposed to implement def verify"
|
||||
|
||||
# auto-validate schema on __init__
|
||||
if "schema" in attrs.keys():
|
||||
|
||||
@@ -112,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
|
||||
def from_any(cls, data: typing.Any) -> Option[T]:
|
||||
raise NotImplementedError
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from Generate import PlandoSettings
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None:
|
||||
pass
|
||||
else:
|
||||
def verify(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class FreeText(Option):
|
||||
"""Text option that allows users to enter strings.
|
||||
Needs to be validated by the world or option definition."""
|
||||
|
||||
def __init__(self, value: str):
|
||||
assert isinstance(value, str), "value of FreeText must be a string"
|
||||
self.value = value
|
||||
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> FreeText:
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> FreeText:
|
||||
return cls.from_text(str(data))
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
return value
|
||||
|
||||
|
||||
class NumericOption(Option[int], numbers.Integral):
|
||||
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
|
||||
@@ -368,6 +419,53 @@ class Choice(NumericOption):
|
||||
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||
|
||||
|
||||
class TextChoice(Choice):
|
||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"{value} is not a valid option for {self.__class__.__name__}"
|
||||
self.value = value
|
||||
super(TextChoice, self).__init__()
|
||||
|
||||
@property
|
||||
def current_key(self) -> str:
|
||||
if isinstance(self.value, str):
|
||||
return self.value
|
||||
else:
|
||||
return self.name_lookup[self.value]
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> TextChoice:
|
||||
if text.lower() == "random": # chooses a random defined option but won't use any free text options
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name.lower() == text.lower():
|
||||
return cls(value)
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return cls.name_lookup[value]
|
||||
|
||||
def __eq__(self, other: typing.Any):
|
||||
if isinstance(other, self.__class__):
|
||||
return other.value == self.value
|
||||
elif isinstance(other, str):
|
||||
if other in self.options:
|
||||
return other == self.current_key
|
||||
return other == self.value
|
||||
elif isinstance(other, int):
|
||||
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
|
||||
return other == self.value
|
||||
elif isinstance(other, bool):
|
||||
return other == bool(self.value)
|
||||
else:
|
||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||
|
||||
|
||||
class Range(NumericOption):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
@@ -385,7 +483,7 @@ class Range(NumericOption):
|
||||
if text.startswith("random"):
|
||||
return cls.weighted_range(text)
|
||||
elif text == "default" and hasattr(cls, "default"):
|
||||
return cls(cls.default)
|
||||
return cls.from_any(cls.default)
|
||||
elif text == "high":
|
||||
return cls(cls.range_end)
|
||||
elif text == "low":
|
||||
@@ -396,7 +494,7 @@ class Range(NumericOption):
|
||||
and text in ("true", "false"):
|
||||
# these are the conditions where "true" and "false" make sense
|
||||
if text == "true":
|
||||
return cls(cls.default)
|
||||
return cls.from_any(cls.default)
|
||||
else: # "false"
|
||||
return cls(0)
|
||||
return cls(int(text))
|
||||
@@ -507,7 +605,7 @@ class VerifyKeys:
|
||||
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
|
||||
f"Allowed keys: {cls.valid_keys}.")
|
||||
|
||||
def verify(self, world):
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if self.convert_name_groups and self.verify_item_name:
|
||||
new_value = type(self.value)() # empty container of whatever value is
|
||||
for item_name in self.value:
|
||||
@@ -600,10 +698,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
if type(data) == list:
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
elif type(data) == set:
|
||||
if isinstance(data, (list, set, frozenset)):
|
||||
cls.verify_keys(data)
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
@@ -732,8 +827,8 @@ class ItemLinks(OptionList):
|
||||
pool |= {item_name}
|
||||
return pool
|
||||
|
||||
def verify(self, world):
|
||||
super(ItemLinks, self).verify(world)
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
super(ItemLinks, self).verify(world, player_name, plando_options)
|
||||
existing_links = set()
|
||||
for link in self.value:
|
||||
if link["name"] in existing_links:
|
||||
|
||||
@@ -15,9 +15,6 @@ import typing
|
||||
|
||||
from json import loads, dumps
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import init_logging, messagebox
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -19,10 +19,11 @@ from sc2.data import Race
|
||||
from sc2.main import run_game
|
||||
from sc2.player import Bot
|
||||
|
||||
import NetUtils
|
||||
from MultiServer import mark_raw
|
||||
from Utils import init_logging, is_windows
|
||||
from worlds.sc2wol import SC2WoLWorld
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table
|
||||
from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups
|
||||
from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET
|
||||
from worlds.sc2wol.MissionTables import lookup_id_to_mission
|
||||
from worlds.sc2wol.Regions import MissionInfo
|
||||
@@ -135,7 +136,7 @@ class SC2Context(CommonContext):
|
||||
last_loc_list = None
|
||||
difficulty_override = -1
|
||||
mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {}
|
||||
raw_text_parser: RawJSONtoTextParser
|
||||
last_bot: typing.Optional[ArchipelagoBot] = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SC2Context, self).__init__(*args, **kwargs)
|
||||
@@ -164,10 +165,13 @@ class SC2Context(CommonContext):
|
||||
check_mod_install()
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
# goes to this world
|
||||
if "receiving" in args and self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif "item" in args and self.slot_concerns_self(args["item"].player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
|
||||
@@ -291,34 +295,37 @@ class SC2Context(CommonContext):
|
||||
category_panel.add_widget(
|
||||
Label(text=category, size_hint_y=None, height=50, outline_width=1))
|
||||
|
||||
# Map is completed
|
||||
for mission in categories[category]:
|
||||
text = mission
|
||||
tooltip = ""
|
||||
text: str = mission
|
||||
tooltip: str = ""
|
||||
|
||||
# Map has uncollected locations
|
||||
if mission in unfinished_missions:
|
||||
text = f"[color=6495ED]{text}[/color]"
|
||||
|
||||
tooltip = f"Uncollected locations:\n"
|
||||
tooltip += "\n".join([self.ctx.location_names[loc] for loc in
|
||||
self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations])
|
||||
elif mission in available_missions:
|
||||
text = f"[color=FFFFFF]{text}[/color]"
|
||||
# Map requirements not met
|
||||
else:
|
||||
text = f"[color=a9a9a9]{text}[/color]"
|
||||
tooltip = f"Requires: "
|
||||
if len(self.ctx.mission_req_table[mission].required_world) > 0:
|
||||
if self.ctx.mission_req_table[mission].required_world:
|
||||
tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for
|
||||
req_mission in
|
||||
self.ctx.mission_req_table[mission].required_world)
|
||||
|
||||
if self.ctx.mission_req_table[mission].number > 0:
|
||||
if self.ctx.mission_req_table[mission].number:
|
||||
tooltip += " and "
|
||||
if self.ctx.mission_req_table[mission].number > 0:
|
||||
if self.ctx.mission_req_table[mission].number:
|
||||
tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed"
|
||||
remaining_location_names: typing.List[str] = [
|
||||
self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission)
|
||||
if loc in self.ctx.missing_locations]
|
||||
if remaining_location_names:
|
||||
if tooltip:
|
||||
tooltip += "\n"
|
||||
tooltip += f"Uncollected locations:\n"
|
||||
tooltip += "\n".join(remaining_location_names)
|
||||
|
||||
mission_button = MissionButton(text=text, size_hint_y=None, height=50)
|
||||
mission_button.tooltip_text = tooltip
|
||||
@@ -355,6 +362,8 @@ class SC2Context(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
await super(SC2Context, self).shutdown()
|
||||
if self.last_bot:
|
||||
self.last_bot.want_close = True
|
||||
if self.sc2_run_task:
|
||||
self.sc2_run_task.cancel()
|
||||
|
||||
@@ -431,47 +440,27 @@ wol_default_categories = [
|
||||
]
|
||||
|
||||
|
||||
def calculate_items(items):
|
||||
unit_unlocks = 0
|
||||
armory1_unlocks = 0
|
||||
armory2_unlocks = 0
|
||||
upgrade_unlocks = 0
|
||||
building_unlocks = 0
|
||||
merc_unlocks = 0
|
||||
lab_unlocks = 0
|
||||
protoss_unlock = 0
|
||||
minerals = 0
|
||||
vespene = 0
|
||||
supply = 0
|
||||
def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]:
|
||||
network_item: NetUtils.NetworkItem
|
||||
accumulators: typing.List[int] = [0 for _ in type_flaggroups]
|
||||
|
||||
for item in items:
|
||||
data = lookup_id_to_name[item.item]
|
||||
for network_item in items:
|
||||
name: str = lookup_id_to_name[network_item.item]
|
||||
item_data: ItemData = item_table[name]
|
||||
|
||||
if item_table[data].type == "Unit":
|
||||
unit_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Upgrade":
|
||||
upgrade_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 1":
|
||||
armory1_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Armory 2":
|
||||
armory2_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Building":
|
||||
building_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Mercenary":
|
||||
merc_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Laboratory":
|
||||
lab_unlocks += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Protoss":
|
||||
protoss_unlock += (1 << item_table[data].number)
|
||||
elif item_table[data].type == "Minerals":
|
||||
minerals += item_table[data].number
|
||||
elif item_table[data].type == "Vespene":
|
||||
vespene += item_table[data].number
|
||||
elif item_table[data].type == "Supply":
|
||||
supply += item_table[data].number
|
||||
# exists exactly once
|
||||
if item_data.quantity == 1:
|
||||
accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number
|
||||
|
||||
return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks,
|
||||
lab_unlocks, protoss_unlock, minerals, vespene, supply]
|
||||
# exists multiple times
|
||||
elif item_data.type == "Upgrade":
|
||||
accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number
|
||||
|
||||
# sum
|
||||
else:
|
||||
accumulators[type_flaggroups[item_data.type]] += item_data.number
|
||||
|
||||
return accumulators
|
||||
|
||||
|
||||
def calc_difficulty(difficulty):
|
||||
@@ -502,7 +491,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
setup_done: bool
|
||||
ctx: SC2Context
|
||||
mission_id: int
|
||||
|
||||
want_close: bool = False
|
||||
can_read_game = False
|
||||
|
||||
last_received_update: int = 0
|
||||
@@ -510,12 +499,17 @@ class ArchipelagoBot(sc2.bot_ai.BotAI):
|
||||
def __init__(self, ctx: SC2Context, mission_id):
|
||||
self.setup_done = False
|
||||
self.ctx = ctx
|
||||
self.ctx.last_bot = self
|
||||
self.mission_id = mission_id
|
||||
self.boni = [False for _ in range(max_bonus)]
|
||||
|
||||
super(ArchipelagoBot, self).__init__()
|
||||
|
||||
async def on_step(self, iteration: int):
|
||||
if self.want_close:
|
||||
self.want_close = False
|
||||
await self._client.leave()
|
||||
return
|
||||
game_state = 0
|
||||
if not self.setup_done:
|
||||
self.setup_done = True
|
||||
@@ -799,7 +793,12 @@ def check_game_install_path() -> bool:
|
||||
with open(einfo) as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
base = re.search(r" = (.*)Versions", content).group(1)
|
||||
try:
|
||||
base = re.search(r" = (.*)Versions", content).group(1)
|
||||
except AttributeError:
|
||||
sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then "
|
||||
f"try again.")
|
||||
return False
|
||||
if os.path.exists(base):
|
||||
executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions")
|
||||
|
||||
@@ -816,7 +815,8 @@ def check_game_install_path() -> bool:
|
||||
else:
|
||||
sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.")
|
||||
else:
|
||||
sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.")
|
||||
sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. "
|
||||
f"If that fails, please run /set_path with your SC2 install directory.")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -46,4 +46,4 @@ def get_datapackage_versions():
|
||||
return version_package
|
||||
|
||||
|
||||
from . import generate, user # trigger registration
|
||||
from . import generate, user, tracker # trigger registration
|
||||
|
||||
50
WebHostLib/api/tracker.py
Normal file
50
WebHostLib/api/tracker.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import collections
|
||||
|
||||
from flask import jsonify
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
from Utils import restricted_loads
|
||||
from uuid import UUID
|
||||
|
||||
from ..models import Room
|
||||
from . import api_endpoints
|
||||
from ..tracker import fill_tracker_data, get_static_room_data
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
from WebHostLib import cache
|
||||
|
||||
|
||||
@api_endpoints.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||
@cache.memoize(timeout=60)
|
||||
def update_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
|
||||
room: Optional[Room] = Room.get(tracker=tracker)
|
||||
locations = get_static_room_data(room)[0]
|
||||
items_counter: Dict[int, collections.Counter] = get_item_names_counter(locations)
|
||||
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
|
||||
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
|
||||
|
||||
# convert numbers to string
|
||||
for item in player_tracker.items_received:
|
||||
if items_counter[tracked_player][item] == 1:
|
||||
player_tracker.items_received[item] = '✔'
|
||||
else:
|
||||
player_tracker.items_received[item] = str(player_tracker.items_received[item])
|
||||
|
||||
return jsonify({
|
||||
"items_received": player_tracker.items_received,
|
||||
"checked_locations": list(sorted(player_tracker.checked_locations)),
|
||||
"icons": display_icons,
|
||||
"progressive_names": player_tracker.progressive_names
|
||||
})
|
||||
|
||||
|
||||
@cache.cached()
|
||||
def get_item_names_counter(locations: Dict[int, Dict[int, Tuple[int, int, int]]]):
|
||||
# create and fill dictionary of all progression items for players
|
||||
items_counters: Dict[int, collections.Counter] = {}
|
||||
for player in locations:
|
||||
for location in locations[player]:
|
||||
item, recipient, flags = locations[player][location]
|
||||
item_name = lookup_any_item_id_to_name[item]
|
||||
items_counters.setdefault(recipient, collections.Counter())[item_name] += 1
|
||||
|
||||
return items_counters
|
||||
@@ -1,15 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import websockets
|
||||
import asyncio
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import pickle
|
||||
import logging
|
||||
import datetime
|
||||
import websockets
|
||||
|
||||
import Utils
|
||||
from .models import db_session, Room, select, commit, Command, db
|
||||
@@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
|
||||
class WebHostContext(Context):
|
||||
room_id: int
|
||||
|
||||
def __init__(self, static_server_data: dict):
|
||||
# static server data is used during _load_game_data to load required data,
|
||||
# without needing to import worlds system, which takes quite a bit of memory
|
||||
@@ -62,6 +65,8 @@ class WebHostContext(Context):
|
||||
def _load_game_data(self):
|
||||
for key, value in self.static_server_data.items():
|
||||
setattr(self, key, value)
|
||||
self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
window.addEventListener('load', () => {
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item and location trackers
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
document.getElementById('location-table').innerHTML = fakeDOM.getElementById('location-table').innerHTML;
|
||||
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
});
|
||||
82
WebHostLib/static/assets/trackers/playerTracker.js
Normal file
82
WebHostLib/static/assets/trackers/playerTracker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker
|
||||
const update = () => {
|
||||
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
||||
|
||||
const request = new Request('/api/tracker/' + room);
|
||||
|
||||
fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// update locations blocks
|
||||
for (const location of data.checked_locations) {
|
||||
document.getElementById(location).classList.add('acquired');
|
||||
}
|
||||
// update totals checks done
|
||||
let total_checks_ele = document.getElementById('total-checks');
|
||||
const total_checks = document.getElementsByClassName('location').length;
|
||||
let checks_done = data.checked_locations.length;
|
||||
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
|
||||
// update item and icons blocks
|
||||
// update icons block
|
||||
if (data.icons.length > 0) {
|
||||
for (let item in data.icons) {
|
||||
if (data.progressive_names.length > 0) {
|
||||
for (let item_category in data.progressive_names) {
|
||||
let i = 0;
|
||||
for (let current_item in current_name) {
|
||||
if (current_item === item) {
|
||||
let doc_item = document.getElementById(item_category)
|
||||
doc_item.children[0].src = data.icons[item];
|
||||
if (item in data.items_received) {
|
||||
doc_item.children[0].classList.add('acquired');
|
||||
doc_item.children[1].innerText = item_category;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item in data.items_received) {
|
||||
let current_item = document.getElementById(item);
|
||||
current_item.children[0].classList.add('acquired');
|
||||
current_item.children[0].src = data.icons[item];
|
||||
current_item.children[1].innerText = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const item in data.items_received) {
|
||||
if (document.getElementById(item)) {
|
||||
let current_item = document.getElementById(item);
|
||||
current_item.innerText = item + data.items_received[item];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update()
|
||||
setInterval(update, 30000);
|
||||
|
||||
|
||||
// Collapsible regions section
|
||||
const regions = document.getElementsByClassName('regions-column');
|
||||
for (let i = 0; i < regions.length; i++) {
|
||||
let region_name = regions[i].id;
|
||||
|
||||
const tab_header = document.getElementById(region_name+'-header');
|
||||
const locations = document.getElementById(region_name+'-locations');
|
||||
// toggle locations display
|
||||
regions[i].addEventListener('click', function(event) {
|
||||
if (tab_header.innerHTML.includes("▼")) {
|
||||
locations.classList.remove('hidden');
|
||||
// change header text
|
||||
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
|
||||
} else {
|
||||
locations.classList.add('hidden');
|
||||
// change header text
|
||||
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
82
WebHostLib/static/assets/trackers/zeldaKeysTracker.js
Normal file
82
WebHostLib/static/assets/trackers/zeldaKeysTracker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
window.addEventListener('load', () => {
|
||||
// Reload tracker
|
||||
const update = () => {
|
||||
const room = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
|
||||
|
||||
const request = new Request('/api/tracker/' + room);
|
||||
|
||||
fetch(request)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// update locations blocks
|
||||
for (const location of data.checked_locations) {
|
||||
document.getElementById(location).classList.add('acquired');
|
||||
}
|
||||
// update totals checks done
|
||||
let total_checks_ele = document.getElementById('total-checks');
|
||||
const total_checks = document.getElementsByClassName('location').length;
|
||||
let checks_done = data.checked_locations.length;
|
||||
total_checks_ele.innerText = 'Total Checks Done: ' + checks_done + '/' + total_checks;
|
||||
// update item and icons blocks
|
||||
// update icons block
|
||||
if (data.icons.length > 0) {
|
||||
for (let item in data.icons) {
|
||||
if (data.progressive_names.length > 0) {
|
||||
for (let item_category in data.progressive_names) {
|
||||
let i = 0;
|
||||
for (let current_item in current_name) {
|
||||
if (current_item === item) {
|
||||
let doc_item = document.getElementById(item_category)
|
||||
doc_item.children[0].src = data.icons[item];
|
||||
if (item in data.items_received) {
|
||||
doc_item.children[0].classList.add('acquired');
|
||||
doc_item.children[1].innerText = item_category;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (item in data.items_received) {
|
||||
let current_item = document.getElementById(item);
|
||||
current_item.children[0].classList.add('acquired');
|
||||
current_item.children[0].src = data.icons[item];
|
||||
current_item.children[1].innerText = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const item in data.items_received) {
|
||||
if (document.getElementById(item)) {
|
||||
let current_item = document.getElementById(item);
|
||||
current_item.innerText = item + data.items_received[item];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update()
|
||||
setInterval(update, 30000);
|
||||
|
||||
|
||||
// Collapsible regions section
|
||||
const regions = document.getElementsByClassName('regions-column');
|
||||
for (let i = 0; i < regions.length; i++) {
|
||||
let region_name = regions[i].id;
|
||||
|
||||
const tab_header = document.getElementById(region_name+'-header');
|
||||
const locations = document.getElementById(region_name+'-locations');
|
||||
// toggle locations display
|
||||
regions[i].addEventListener('click', function(event) {
|
||||
if (tab_header.innerHTML.includes("▼")) {
|
||||
locations.classList.remove('hidden');
|
||||
// change header text
|
||||
tab_header.innerHTML = tab_header.innerHTML.replace('▼', '▲');
|
||||
} else {
|
||||
locations.classList.add('hidden');
|
||||
// change header text
|
||||
tab_header.innerHTML = tab_header.innerHTML.replace('▲', '▼');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
150
WebHostLib/static/styles/trackers/playerTracker.css
Normal file
150
WebHostLib/static/styles/trackers/playerTracker.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* CSS Overrides */
|
||||
.dirt-wrapper{
|
||||
background-color: #897249;
|
||||
}
|
||||
|
||||
.dirt-wrapper h1{}
|
||||
|
||||
.grass-wrapper{
|
||||
background-color: #3fb24a;
|
||||
}
|
||||
|
||||
.grass-wrapper h1{}
|
||||
|
||||
.grassFlowers-wrapper{
|
||||
background-color: #3fb24a;
|
||||
}
|
||||
|
||||
.grassFlowers-wrapper h1{}
|
||||
|
||||
.ice-wrapper{
|
||||
background-color: #afe0ef;
|
||||
}
|
||||
|
||||
.ice-wrapper h1{}
|
||||
|
||||
.jungle-wrapper{
|
||||
background-color: #2a7808;
|
||||
}
|
||||
|
||||
.jungle-wrapper h1{}
|
||||
|
||||
.ocean-wrapper{
|
||||
background-color: #3667b1;
|
||||
}
|
||||
|
||||
.ocean-wrapper h1{}
|
||||
|
||||
.partyTime-wrapper{
|
||||
background-color: #3a0f69;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.partyTime-wrapper h1{}
|
||||
|
||||
|
||||
/* Actual Styles */
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
color: #ffffff;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px black;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#player-keys-tracker{
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
#items-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#items-container div{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.image-container{
|
||||
display: absolute;
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.bottom-text{
|
||||
position: relative;
|
||||
align-items: bottom;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
max-width: 45px;
|
||||
max-height: 45px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(40%);
|
||||
}
|
||||
|
||||
.icon.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.total-checks{
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.locations-container{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 5px;
|
||||
margin-left: 50px;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.location.acquired{
|
||||
text-decoration: line-through;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.regions-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.regions-header{
|
||||
font-size: 18px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-link{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
line-height: 30px;
|
||||
background-color: lightgrey;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -51,6 +51,17 @@ table.dataTable{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.dataTable img.icon{
|
||||
height: 100%;
|
||||
max-width: 60px;
|
||||
max-height: 60px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(50%);
|
||||
}
|
||||
|
||||
table.dataTable img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
table.dataTable thead{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ bow_url }}" class="{{ 'acquired' if bow_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||
<td><img src="{{ glove_url }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ sword_url }}" class="{{ 'acquired' if sword_acquired }}" /></td>
|
||||
<td><img src="{{ shield_url }}" class="{{ 'acquired' if shield_acquired }}" /></td>
|
||||
<td><img src="{{ mail_url }}" class="acquired" /></td>
|
||||
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<th><img src="{{ icons["Big Key"] }}" /></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for area in sp_areas %}
|
||||
<tr>
|
||||
<td>{{ area }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<td class="counter">
|
||||
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<td>
|
||||
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -50,7 +50,7 @@
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=0, tracked_player=patch.player_id) }}">Tracker</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -13,6 +13,9 @@
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span class="info">This tracker will automatically update itself periodically.</span>
|
||||
<a href="/tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
|
||||
Go to Styled Tracker
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table non-unique-item-table">
|
||||
@@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/minecraftTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/minecraftTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/minecraftTracker.js') }}"></script>
|
||||
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
|
||||
</head>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/trackers/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackers/tracker.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
{%- for player, items in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
{%- if (team, loop.index) in video -%}
|
||||
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
||||
@@ -121,7 +121,7 @@
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("getPlayerTracker", tracker=room.tracker,
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
99
WebHostLib/templates/trackers/playerTracker.html
Normal file
99
WebHostLib/templates/trackers/playerTracker.html
Normal file
@@ -0,0 +1,99 @@
|
||||
{% block head %}
|
||||
<!--suppress XmlDuplicatedId -->
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/playerTracker.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
|
||||
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
|
||||
Go to Generic Tracker
|
||||
</a>
|
||||
|
||||
{% if icons %}
|
||||
|
||||
{% block icons_render %}
|
||||
|
||||
<h1>Items</h1>
|
||||
<div id="items-container">
|
||||
{%- for item in icons %}
|
||||
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
|
||||
<img
|
||||
src="{{ icons[item] }}"
|
||||
class="icon tooltip {{ 'acquired' if item in received_items }}"
|
||||
/>
|
||||
</div>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% block item_names_render %}
|
||||
<h1 class="items-header">Items</h1>
|
||||
<div class="items-container">
|
||||
{%- for item in received_items|sort -%}
|
||||
<div class="item" id="{{ item }}">
|
||||
{{ item }}
|
||||
{% if all_progression_items[item] > 1 %}
|
||||
{{ received_items[item] }}
|
||||
{% else %}
|
||||
✔
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# div for total checks done as percentage. Probably needs to be put somewhere else but I liked how it looked here #}
|
||||
<div class="total-checks" id="total-checks">
|
||||
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
|
||||
</div>
|
||||
|
||||
|
||||
{% if regions %}
|
||||
|
||||
{% block regions_render %}
|
||||
|
||||
<div class="regions-container">
|
||||
{% for region in regions %}
|
||||
<div class="regions-column" id="{{ region }}">
|
||||
<h1 class="regions-header" id="{{ region }}-header">{{ region }} ▼ {{ checks_done[region]|length }} / {{ regions[region]|length }}</h1>
|
||||
<div class="location-column hidden" id="{{ region }}-locations">
|
||||
{%- for location in regions[region] %}
|
||||
<div class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">{{ location }}</div>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% block locations_render %}
|
||||
|
||||
<h1>Locations</h1>
|
||||
<div class="locations-container" id="locations-container">
|
||||
{% for location in locations %}
|
||||
<div class="location {{ 'acquired' if name in checked_locations }}" id="{{ location }}">
|
||||
{{ location }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/supermetroidTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/supermetroidTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/supermetroidTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -2,8 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/timespinnerTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/timespinnerTracker.js') }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/timespinnerTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/timespinnerTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
77
WebHostLib/templates/trackers/zeldaKeysTracker.html
Normal file
77
WebHostLib/templates/trackers/zeldaKeysTracker.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% block head %}
|
||||
<!--suppress XmlDuplicatedId -->
|
||||
<title>{{ player_name }}'s Keys Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/trackers/playerTracker.css') }}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/tooltip.css') }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/trackers/zeldaKeysTracker.js') }}"/></script>
|
||||
{% endblock %}
|
||||
|
||||
{# this tracker is mostly similar to the generic player tracker but
|
||||
also adds a table with the key and checks counts for each region in the middle #}
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div id="tracker-wrapper" class="{{ theme }}-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}">
|
||||
<a href="/generic_tracker/{{ room.tracker|suuid }}/{{ team }}/{{ player }}" class="button-link">
|
||||
Go to Generic Tracker
|
||||
</a>
|
||||
<h1>Items</h1>
|
||||
<div id="items-container">
|
||||
{% for item in icons %}
|
||||
{% if item not in ['Small Key', 'Big Key'] %}
|
||||
<div class="image-container tooltip" id="{{ item }}" data-tooltip="{{ item }}">
|
||||
<img
|
||||
src="{{ icons[item] }}"
|
||||
class="icon tooltip {{ 'acquired' if item in received_items }}"
|
||||
/>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="total-checks" id="total-checks">
|
||||
Total Checks Done: {{ checked_locations|length }}/{{ locations|length }}
|
||||
</div>
|
||||
|
||||
<table id="regions-column">
|
||||
<tr class="keys-icons">
|
||||
<td><img src="{{icons['Small Key']}}" class="icon tooltip acquired" id="small-key-icon"/></td>
|
||||
<td><img src="{{icons['Big Key']}}" class="icon tooltip acquired" id="big-key-icon"/></td>
|
||||
<td class="right-align">Total</td>
|
||||
</tr>
|
||||
{% for region in regions %}
|
||||
<tr class="regions-column" id="{{ region }}">
|
||||
<td id="{{ region }}-header">{{ region }} ▼</td>
|
||||
{% if region in region_keys %}
|
||||
{%- if region_keys[region]|length > 1 %}
|
||||
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
|
||||
<td class="bigkeys">{{ received_items[region_keys[region][1]] if region_keys[region][1] in received_items else '-' }}</td>
|
||||
{%- else %}
|
||||
{% if 'Small Key' in region_keys[region][0] %}
|
||||
<td class="smallkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
|
||||
<td class="bigkeys">-</td>
|
||||
{% else %}
|
||||
<td class="smallkeys">-</td>
|
||||
<td class="bigkeys">{{ received_items[region_keys[region][0]] if region_keys[region][0] in received_items else '-' }}</td>
|
||||
{% endif %}
|
||||
{%- endif%}
|
||||
{% else %}
|
||||
<td class="smallkeys">-</td>
|
||||
<td class="bigkeys">-</td>
|
||||
{% endif %}
|
||||
<td class="counter">{{ checks_done[region]|length }} / {{ regions[region]|length }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hidden" id="{{ region }}-locations">
|
||||
{% for location in regions[region] %}
|
||||
<tr>
|
||||
<td class="location {{ 'acquired' if location in checked_locations }}" id="{{ location }}">
|
||||
{{ location }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
import collections
|
||||
import typing
|
||||
from typing import Counter, Optional, Dict, Any, Tuple
|
||||
from typing import Counter, Optional, Dict, Any, Tuple, Set, List, TYPE_CHECKING
|
||||
|
||||
from flask import render_template
|
||||
from werkzeug.exceptions import abort
|
||||
@@ -11,9 +11,53 @@ from worlds.alttp import Items
|
||||
from WebHostLib import app, cache, Room
|
||||
from Utils import restricted_loads
|
||||
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
from MultiServer import Context
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from MultiServer import get_item_name_from_id, Context
|
||||
from NetUtils import SlotType
|
||||
|
||||
|
||||
class PlayerTracker:
|
||||
"""This class will create a basic 'prettier' tracker for each world using their themes automatically. This
|
||||
can be overridden to customize how it will appear. Can provide icons and custom regions. The html used is also
|
||||
a jinja template that can be overridden if you want your tracker to look different in certain aspects. To render
|
||||
icons and regions add dictionaries to the relevant attributes of the tracker_info. To customize the layout of
|
||||
your icons you can create a new html in your world and extend playerTracker.html and overwrite the icons_render
|
||||
block then change the tracker_info template attribute to your template."""
|
||||
|
||||
template: str = 'playerTracker.html'
|
||||
icons: Dict[str, str] = {}
|
||||
progressive_items: List[str] = []
|
||||
progressive_names: Dict[str, List[str]] = {}
|
||||
regions: Dict[str, List[str]] = {}
|
||||
checks_done: Dict[str, Set[str]] = {}
|
||||
room: Any
|
||||
team: int
|
||||
player: int
|
||||
name: str
|
||||
all_locations: Set[str]
|
||||
checked_locations: Set[str]
|
||||
all_prog_items: Counter[str]
|
||||
items_received: Counter[str]
|
||||
received_prog_items: Counter[str]
|
||||
slot_data: Dict[any, any]
|
||||
theme: str
|
||||
|
||||
region_keys: Dict[str, str] = {}
|
||||
|
||||
def __init__(self, room: Any, team: int, player: int, name: str, all_locations: Set[str],
|
||||
checked_locations: set, all_progression_items: Counter[str], items_received: Counter[str],
|
||||
slot_data: Dict[any, any]):
|
||||
self.room = room
|
||||
self.team = team
|
||||
self.player = player
|
||||
self.name = name
|
||||
self.all_locations = all_locations
|
||||
self.checked_locations = checked_locations
|
||||
self.all_prog_items = all_progression_items
|
||||
self.items_received = items_received
|
||||
self.slot_data = slot_data
|
||||
|
||||
|
||||
alttp_icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
@@ -288,7 +332,7 @@ def get_static_room_data(room: Room):
|
||||
|
||||
@app.route('/tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||
@cache.memoize(timeout=60) # multisave is currently created at most every minute
|
||||
def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
|
||||
def get_player_tracker(tracker: UUID, tracked_team: int, tracked_player: int, want_generic: bool = False):
|
||||
# Team and player must be positive and greater than zero
|
||||
if tracked_team < 0 or tracked_player < 1:
|
||||
abort(404)
|
||||
@@ -297,13 +341,78 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
|
||||
if not room:
|
||||
abort(404)
|
||||
|
||||
# Collect seed information and pare it down to a single player
|
||||
player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, \
|
||||
slot_data, games, player_name, display_icons = fill_tracker_data(room, tracked_team, tracked_player)
|
||||
|
||||
game_name = games[tracked_player]
|
||||
# TODO move all games in game_specific_trackers to new system
|
||||
if game_name in game_specific_trackers and not want_generic:
|
||||
specific_tracker = game_specific_trackers.get(game_name, None)
|
||||
return specific_tracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player,
|
||||
player_name, seed_checks_in_area, lttp_checks_done, slot_data[tracked_player])
|
||||
elif game_name in AutoWorldRegister.world_types and not want_generic:
|
||||
return render_template(
|
||||
"trackers/" + player_tracker.template,
|
||||
all_progression_items=player_tracker.all_prog_items,
|
||||
player=tracked_player,
|
||||
team=tracked_team,
|
||||
room=player_tracker.room,
|
||||
player_name=player_tracker.name,
|
||||
checked_locations=sorted(player_tracker.checked_locations),
|
||||
locations=sorted(player_tracker.all_locations),
|
||||
theme=player_tracker.theme,
|
||||
icons=display_icons,
|
||||
regions=player_tracker.regions,
|
||||
checks_done=player_tracker.checks_done,
|
||||
region_keys=player_tracker.region_keys
|
||||
)
|
||||
else:
|
||||
return __renderGenericTracker(multisave, room, player_tracker.all_locations, inventory, tracked_team, tracked_player, player_name, seed_checks_in_area, lttp_checks_done)
|
||||
|
||||
|
||||
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||
@cache.memoize(timeout=60)
|
||||
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
return get_player_tracker(tracker, tracked_team, tracked_player, True)
|
||||
|
||||
|
||||
def get_tracker_icons_and_regions(player_tracker: PlayerTracker) -> Dict[str, str]:
|
||||
"""this function allows multiple icons to be used for the same item but it does require the world to submit both
|
||||
a progressive_items list and the icons dict together"""
|
||||
display_icons: Dict[str, str] = {}
|
||||
if player_tracker.progressive_names and player_tracker.icons:
|
||||
for item in player_tracker.progressive_items:
|
||||
if item in player_tracker.progressive_names:
|
||||
level = min(player_tracker.items_received[item], len(player_tracker.progressive_names[item]) - 1)
|
||||
display_name = player_tracker.progressive_names[item][level]
|
||||
if display_name in player_tracker.icons:
|
||||
display_icons[item] = player_tracker.icons[display_name]
|
||||
else:
|
||||
display_icons[item] = player_tracker.icons[item]
|
||||
else:
|
||||
display_icons[item] = player_tracker.icons[item]
|
||||
else:
|
||||
if player_tracker.progressive_items and player_tracker.icons:
|
||||
for item in player_tracker.progressive_items:
|
||||
display_icons[item] = player_tracker.icons[item]
|
||||
|
||||
if player_tracker.regions:
|
||||
for region in player_tracker.regions:
|
||||
for location in region:
|
||||
if location in player_tracker.checked_locations:
|
||||
player_tracker.checks_done.setdefault(region, set()).add(location)
|
||||
|
||||
return display_icons
|
||||
|
||||
|
||||
def fill_tracker_data(room: Room, tracked_team: int, tracked_player: int) -> Tuple:
|
||||
"""Collect seed information and pare it down to a single player"""
|
||||
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
|
||||
precollected_items, games, slot_data, groups = get_static_room_data(room)
|
||||
player_name = names[tracked_team][tracked_player - 1]
|
||||
location_to_area = player_location_to_area[tracked_player]
|
||||
inventory = collections.Counter()
|
||||
checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
lttp_checks_done = {loc_name: 0 for loc_name in default_locations}
|
||||
|
||||
# Add starting items to inventory
|
||||
starting_items = precollected_items[tracked_player]
|
||||
@@ -321,6 +430,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
|
||||
if tracked_player in group_members:
|
||||
slots_aimed_at_player.add(group_id)
|
||||
|
||||
checked_locations = set()
|
||||
# Add items to player inventory
|
||||
for (ms_team, ms_player), locations_checked in multisave.get("location_checks", {}).items():
|
||||
# Skip teams and players not matching the request
|
||||
@@ -332,383 +442,52 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
|
||||
item, recipient, flags = player_locations[location]
|
||||
if recipient in slots_aimed_at_player: # a check done for the tracked player
|
||||
attribute_item_solo(inventory, item)
|
||||
|
||||
if ms_player == tracked_player: # a check done by the tracked player
|
||||
checks_done[location_to_area[location]] += 1
|
||||
checks_done["Total"] += 1
|
||||
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
|
||||
if specific_tracker and not want_generic:
|
||||
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||
seed_checks_in_area, checks_done, slot_data[tracked_player])
|
||||
else:
|
||||
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
|
||||
seed_checks_in_area, checks_done)
|
||||
lttp_checks_done[location_to_area[location]] += 1
|
||||
lttp_checks_done["Total"] += 1
|
||||
checked_locations.add(lookup_any_location_id_to_name[location])
|
||||
|
||||
prog_items = collections.Counter
|
||||
all_location_names = set()
|
||||
|
||||
all_location_names = {lookup_any_location_id_to_name[id] for id in locations[tracked_player]}
|
||||
prog_items = collections.Counter()
|
||||
for player in locations:
|
||||
for location in locations[player]:
|
||||
item, recipient, flags = locations[player][location]
|
||||
if recipient == player:
|
||||
if flags & 1:
|
||||
item_name = lookup_any_item_id_to_name[item]
|
||||
prog_items[item_name] += 1
|
||||
|
||||
items_received = collections.Counter()
|
||||
for id in inventory:
|
||||
items_received[lookup_any_item_id_to_name[id]] = inventory[id]
|
||||
|
||||
player_tracker = PlayerTracker(
|
||||
room,
|
||||
tracked_team,
|
||||
tracked_player,
|
||||
player_name,
|
||||
all_location_names,
|
||||
checked_locations,
|
||||
prog_items,
|
||||
items_received,
|
||||
slot_data[tracked_player]
|
||||
)
|
||||
|
||||
# grab webworld and apply its theme to the tracker
|
||||
webworld = AutoWorldRegister.world_types[games[tracked_player]].web
|
||||
player_tracker.theme = webworld.theme
|
||||
# allow the world to add information to the tracker class
|
||||
webworld.modify_tracker(player_tracker)
|
||||
display_icons = get_tracker_icons_and_regions(player_tracker)
|
||||
|
||||
return player_tracker, multisave, inventory, seed_checks_in_area, lttp_checks_done, slot_data, games, player_name, display_icons
|
||||
|
||||
|
||||
@app.route('/generic_tracker/<suuid:tracker>/<int:tracked_team>/<int:tracked_player>')
|
||||
def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
||||
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
|
||||
|
||||
|
||||
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, player_name: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
||||
|
||||
# Note the presence of the triforce item
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
if game_state == 30:
|
||||
inventory[106] = 1 # Triforce
|
||||
|
||||
# Progressive items need special handling for icons and class
|
||||
progressive_items = {
|
||||
"Progressive Sword": 94,
|
||||
"Progressive Glove": 97,
|
||||
"Progressive Bow": 100,
|
||||
"Progressive Mail": 96,
|
||||
"Progressive Shield": 95,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Sword": [None, 'Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
"Progressive Glove": [None, 'Power Glove', 'Titan Mitts'],
|
||||
"Progressive Bow": [None, "Bow", "Silver Bow"],
|
||||
"Progressive Mail": ["Green Mail", "Blue Mail", "Red Mail"],
|
||||
"Progressive Shield": [None, "Blue Shield", "Red Shield", "Mirror Shield"]
|
||||
}
|
||||
|
||||
# Determine which icon to use
|
||||
display_data = {}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
acquired = True
|
||||
if not display_name:
|
||||
acquired = False
|
||||
display_name = progressive_names[item_name][level + 1]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower()
|
||||
display_data[base_name + "_acquired"] = acquired
|
||||
display_data[base_name + "_url"] = alttp_icons[display_name]
|
||||
|
||||
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
|
||||
sp_areas = ordered_areas[2:15]
|
||||
|
||||
player_big_key_locations = set()
|
||||
player_small_key_locations = set()
|
||||
for loc_data in locations.values():
|
||||
for values in loc_data.values():
|
||||
item_id, item_player, flags = values
|
||||
if item_player == player:
|
||||
if item_id in ids_big_key:
|
||||
player_big_key_locations.add(ids_big_key[item_id])
|
||||
elif item_id in ids_small_key:
|
||||
player_small_key_locations.add(ids_small_key[item_id])
|
||||
|
||||
return render_template("lttpTracker.html", inventory=inventory,
|
||||
player_name=player_name, room=room, icons=alttp_icons, checks_done=checks_done,
|
||||
checks_in_area=seed_checks_in_area[player],
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
|
||||
key_locations=player_small_key_locations,
|
||||
big_key_locations=player_big_key_locations,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
||||
|
||||
icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing Stand": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
|
||||
"Ender Pearl": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||
}
|
||||
|
||||
minecraft_location_ids = {
|
||||
"Story": [42073, 42023, 42027, 42039, 42002, 42009, 42010, 42070,
|
||||
42041, 42049, 42004, 42031, 42025, 42029, 42051, 42077],
|
||||
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
|
||||
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
|
||||
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
|
||||
"Adventure": [42047, 42050, 42096, 42097, 42098, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
|
||||
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42099, 42100],
|
||||
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028, 42036,
|
||||
42057, 42063, 42053, 42102, 42101, 42092, 42093, 42094, 42095],
|
||||
"Archipelago": [42080, 42081, 42082, 42083, 42084, 42085, 42086, 42087, 42088, 42089, 42090, 42091],
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Tools": 45013,
|
||||
"Progressive Weapons": 45012,
|
||||
"Progressive Armor": 45014,
|
||||
"Progressive Resource Crafting": 45001
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
display_data[base_name + "_url"] = icons[display_name]
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"3 Ender Pearls": 45029,
|
||||
"8 Netherite Scrap": 45015
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
if count >= 0:
|
||||
display_data[base_name + "_count"] = count
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
# Turn location IDs into advancement tab counts
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
|
||||
lookup_name = lambda id: lookup_any_location_id_to_name[id]
|
||||
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
|
||||
for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_done['Total'] = len(checked_locations)
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("minecraftTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
||||
|
||||
icons = {
|
||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
|
||||
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
|
||||
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
|
||||
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
|
||||
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
|
||||
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
|
||||
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
|
||||
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
|
||||
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
|
||||
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
|
||||
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
|
||||
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
|
||||
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
|
||||
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
|
||||
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
|
||||
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
|
||||
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
|
||||
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
|
||||
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
|
||||
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
|
||||
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
|
||||
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
|
||||
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
|
||||
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
|
||||
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
|
||||
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
|
||||
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
|
||||
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
|
||||
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
|
||||
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
|
||||
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
|
||||
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
|
||||
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
|
||||
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
|
||||
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
|
||||
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
|
||||
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
|
||||
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
|
||||
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
|
||||
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
|
||||
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
|
||||
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
|
||||
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
|
||||
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Hookshot": 66128,
|
||||
"Progressive Strength Upgrade": 66129,
|
||||
"Progressive Wallet": 66133,
|
||||
"Progressive Scale": 66134,
|
||||
"Magic Meter": 66138,
|
||||
"Ocarina": 66139,
|
||||
}
|
||||
|
||||
progressive_names = {
|
||||
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
|
||||
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
|
||||
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
|
||||
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
|
||||
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
|
||||
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
|
||||
}
|
||||
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
if item_name.startswith("Progressive"):
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
else:
|
||||
base_name = item_name.lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = icons[display_name]
|
||||
|
||||
if base_name == "hookshot":
|
||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||
if base_name == "wallet":
|
||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||
|
||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
|
||||
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
|
||||
display_data['bottle_url'] = icons['Rutos Letter'] if inventory[66021] > 0 else icons['Bottle']
|
||||
|
||||
# Determine bombchu display
|
||||
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"Gold Skulltula Token": 66091,
|
||||
"Triforce Piece": 66202,
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Gather dungeon locations
|
||||
area_id_ranges = {
|
||||
"Overworld": (67000, 67280),
|
||||
"Deku Tree": (67281, 67303),
|
||||
"Dodongo's Cavern": (67304, 67334),
|
||||
"Jabu Jabu's Belly": (67335, 67359),
|
||||
"Bottom of the Well": (67360, 67384),
|
||||
"Forest Temple": (67385, 67420),
|
||||
"Fire Temple": (67421, 67457),
|
||||
"Water Temple": (67458, 67484),
|
||||
"Shadow Temple": (67485, 67532),
|
||||
"Spirit Temple": (67533, 67582),
|
||||
"Ice Cavern": (67583, 67596),
|
||||
"Gerudo Training Ground": (67597, 67635),
|
||||
"Thieves' Hideout": (67259, 67263),
|
||||
"Ganon's Castle": (67636, 67673),
|
||||
}
|
||||
|
||||
def lookup_and_trim(id, area):
|
||||
full_name = lookup_any_location_id_to_name[id]
|
||||
if id == 67673:
|
||||
return full_name[13:] # Ganons Tower Boss Key Chest
|
||||
if area not in ["Overworld", "Thieves' Hideout"]:
|
||||
# trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
||||
return full_name[len(area):]
|
||||
return full_name
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((team, player), set()).intersection(set(locations[player]))
|
||||
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[player]}
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
|
||||
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[player]])
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
|
||||
# Remove Thieves' Hideout checks from Overworld, since it's in the middle of the range
|
||||
checks_in_area["Overworld"] -= checks_in_area["Thieves' Hideout"]
|
||||
checks_done["Overworld"] -= checks_done["Thieves' Hideout"]
|
||||
for loc in location_info["Thieves' Hideout"]:
|
||||
del location_info["Overworld"][loc]
|
||||
|
||||
checks_done['Total'] = sum(checks_done.values())
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
# Give skulltulas on non-tracked locations
|
||||
non_tracked_locations = multisave.get("location_checks", {}).get((team, player), set()).difference(set(locations[player]))
|
||||
for id in non_tracked_locations:
|
||||
if "GS" in lookup_and_trim(id, ''):
|
||||
display_data["token_count"] += 1
|
||||
|
||||
# Gather small and boss key info
|
||||
small_key_counts = {
|
||||
"Forest Temple": inventory[66175],
|
||||
"Fire Temple": inventory[66176],
|
||||
"Water Temple": inventory[66177],
|
||||
"Spirit Temple": inventory[66178],
|
||||
"Shadow Temple": inventory[66179],
|
||||
"Bottom of the Well": inventory[66180],
|
||||
"Gerudo Training Ground": inventory[66181],
|
||||
"Thieves' Hideout": inventory[66182],
|
||||
"Ganon's Castle": inventory[66183],
|
||||
}
|
||||
boss_key_counts = {
|
||||
"Forest Temple": '✔' if inventory[66149] else '✕',
|
||||
"Fire Temple": '✔' if inventory[66150] else '✕',
|
||||
"Water Temple": '✔' if inventory[66151] else '✕',
|
||||
"Spirit Temple": '✔' if inventory[66152] else '✕',
|
||||
"Shadow Temple": '✔' if inventory[66153] else '✕',
|
||||
"Ganon's Castle": '✔' if inventory[66154] else '✕',
|
||||
}
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
return render_template("ootTracker.html",
|
||||
inventory=inventory, player=player, team=team, room=room, player_name=playerName,
|
||||
icons=icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
|
||||
**display_data)
|
||||
|
||||
|
||||
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: set,
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
|
||||
|
||||
@@ -745,7 +524,7 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
||||
}
|
||||
|
||||
timespinner_location_ids = {
|
||||
"Present": [
|
||||
"Present": [
|
||||
1337000, 1337001, 1337002, 1337003, 1337004, 1337005, 1337006, 1337007, 1337008, 1337009,
|
||||
1337010, 1337011, 1337012, 1337013, 1337014, 1337015, 1337016, 1337017, 1337018, 1337019,
|
||||
1337020, 1337021, 1337022, 1337023, 1337024, 1337025, 1337026, 1337027, 1337028, 1337029,
|
||||
@@ -766,20 +545,20 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
||||
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
|
||||
1337171, 1337172, 1337173, 1337174, 1337175],
|
||||
"Ancient Pyramid": [
|
||||
1337236,
|
||||
1337236,
|
||||
1337246, 1337247, 1337248, 1337249]
|
||||
}
|
||||
|
||||
if(slot_data["DownloadableItems"]):
|
||||
timespinner_location_ids["Present"] += [
|
||||
1337156, 1337157, 1337159,
|
||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
|
||||
1337170]
|
||||
if(slot_data["Cantoran"]):
|
||||
timespinner_location_ids["Past"].append(1337176)
|
||||
if(slot_data["LoreChecks"]):
|
||||
timespinner_location_ids["Present"] += [
|
||||
1337177, 1337178, 1337179,
|
||||
1337177, 1337178, 1337179,
|
||||
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
|
||||
timespinner_location_ids["Past"] += [
|
||||
1337188, 1337189,
|
||||
@@ -808,13 +587,13 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
|
||||
acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}
|
||||
options = {k for k, v in slot_data.items() if v}
|
||||
|
||||
return render_template("timespinnerTracker.html",
|
||||
return render_template("trackers/" + "timespinnerTracker.html",
|
||||
inventory=inventory, icons=icons, acquired_items=acquired_items,
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
options=options, **display_data)
|
||||
|
||||
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: set,
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
|
||||
|
||||
@@ -889,6 +668,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
|
||||
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[0].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Victory condition
|
||||
@@ -906,7 +686,7 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
|
||||
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in supermetroid_location_ids.items()}
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
return render_template("supermetroidTracker.html",
|
||||
return render_template("trackers/" + "supermetroidTracker.html",
|
||||
inventory=inventory, icons=icons,
|
||||
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
|
||||
id in lookup_any_item_id_to_name},
|
||||
@@ -914,7 +694,8 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
|
||||
|
||||
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: set,
|
||||
inventory: Counter, team: int, player: int, playerName: str,
|
||||
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
|
||||
|
||||
@@ -929,11 +710,11 @@ def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dic
|
||||
for order_index, networkItem in enumerate(ordered_items, start=1):
|
||||
player_received_items[networkItem.item] = order_index
|
||||
|
||||
return render_template("genericTracker.html",
|
||||
return render_template("trackers/" + "genericTracker.html",
|
||||
inventory=inventory,
|
||||
player=player, team=team, room=room, player_name=playerName,
|
||||
checked_locations=checked_locations,
|
||||
not_checked_locations=set(locations[player]) - checked_locations,
|
||||
not_checked_locations=locations - checked_locations,
|
||||
received_items=player_received_items)
|
||||
|
||||
|
||||
@@ -975,9 +756,9 @@ def getTracker(tracker: UUID):
|
||||
continue
|
||||
|
||||
item, recipient, flags = player_locations[location]
|
||||
|
||||
if recipient in names:
|
||||
attribute_item(inventory, team, recipient, item)
|
||||
|
||||
checks_done[team][player][player_location_to_area[player][location]] += 1
|
||||
checks_done[team][player]["Total"] += 1
|
||||
|
||||
@@ -1021,7 +802,7 @@ def getTracker(tracker: UUID):
|
||||
for (team, player), data in multisave.get("video", []):
|
||||
video[(team, player)] = data
|
||||
|
||||
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||
return render_template("trackers/" + "multiworldTracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name,
|
||||
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
|
||||
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons,
|
||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||
@@ -1032,9 +813,6 @@ def getTracker(tracker: UUID):
|
||||
|
||||
|
||||
game_specific_trackers: typing.Dict[str, typing.Callable] = {
|
||||
"Minecraft": __renderMinecraftTracker,
|
||||
"Ocarina of Time": __renderOoTTracker,
|
||||
"Timespinner": __renderTimespinnerTracker,
|
||||
"A Link to the Past": __renderAlttpTracker,
|
||||
"Super Metroid": __renderSuperMetroidTracker
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,10 @@ No metadata is specified yet.
|
||||
## Extra Data
|
||||
|
||||
The zip can contain arbitrary files in addition what was specified above.
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
Imports from other files inside the apworld have to use relative imports.
|
||||
|
||||
Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py.
|
||||
|
||||
@@ -103,8 +103,9 @@ or boss drops for RPG-like games but could also be progress in a research tree.
|
||||
|
||||
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
|
||||
in a Region and has access rules.
|
||||
The name needs to be unique in each game, the ID needs to be unique across all
|
||||
games and is best in the same range as the item IDs.
|
||||
The name needs to be unique in each game and must not be numeric (has to
|
||||
contain least 1 letter or symbol). The ID needs to be unique across all games
|
||||
and is best in the same range as the item IDs.
|
||||
World-specific IDs are 1 to 2<sup>53</sup>-1, IDs ≤ 0 are global and reserved.
|
||||
|
||||
Special locations with ID `None` can hold events.
|
||||
@@ -121,6 +122,9 @@ their world. Progression items will be assigned to locations with higher
|
||||
priority and moved around to meet defined rules and accomplish progression
|
||||
balancing.
|
||||
|
||||
The name needs to be unique in each game, meaning a duplicate item has the
|
||||
same ID. Name must not be numeric (has to contain at least 1 letter or symbol).
|
||||
|
||||
Special items with ID `None` can mark events (read below).
|
||||
|
||||
Other classifications include
|
||||
@@ -188,15 +192,17 @@ the `/worlds` directory. The starting point for the package is `__init.py__`.
|
||||
Conventionally, your world class is placed in that file.
|
||||
|
||||
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
|
||||
which can be imported as `..AutoWorld.World` from your package.
|
||||
which can be imported as `worlds.AutoWorld.World` from your package.
|
||||
|
||||
AP will pick up your world automatically due to the `AutoWorld` implementation.
|
||||
|
||||
### Requirements
|
||||
|
||||
If your world needs specific python packages, they can be listed in
|
||||
`world/[world_name]/requirements.txt`.
|
||||
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
|
||||
`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically
|
||||
pick up and install them.
|
||||
|
||||
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format).
|
||||
|
||||
### Relative Imports
|
||||
|
||||
@@ -209,6 +215,10 @@ e.g. `from .Options import mygame_options` from your `__init__.py` will load
|
||||
When imported names pile up it may be easier to use `from . import Options`
|
||||
and access the variable as `Options.mygame_options`.
|
||||
|
||||
Imports from directories outside your world should use absolute imports.
|
||||
Correct use of relative / absolute imports is required for zipped worlds to
|
||||
function, see [apworld specification.md](apworld%20specification.md).
|
||||
|
||||
### Your Item Type
|
||||
|
||||
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
|
||||
@@ -274,14 +284,12 @@ Define a property `option_<name> = <number>` per selectable value and
|
||||
`default = <number>` to set the default selection. Aliases can be set by
|
||||
defining a property `alias_<name> = <same number>`.
|
||||
|
||||
One special case where aliases are required is when option name is `yes`, `no`,
|
||||
`on` or `off` because they parse to `True` or `False`:
|
||||
```python
|
||||
option_off = 0
|
||||
option_on = 1
|
||||
option_some = 2
|
||||
alias_false = 0
|
||||
alias_true = 1
|
||||
alias_disabled = 0
|
||||
alias_enabled = 1
|
||||
default = 0
|
||||
```
|
||||
|
||||
@@ -323,7 +331,7 @@ mygame_options: typing.Dict[str, type(Option)] = {
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from ..AutoWorld import World
|
||||
from worlds.AutoWorld import World
|
||||
from .Options import mygame_options # import the options dict
|
||||
|
||||
class MyGameWorld(World):
|
||||
@@ -352,7 +360,7 @@ more natural. These games typically have been edited to 'bake in' the items.
|
||||
from .Options import mygame_options # the options we defined earlier
|
||||
from .Items import mygame_items # data used below to add items to the World
|
||||
from .Locations import mygame_locations # same as above
|
||||
from ..AutoWorld import World
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification
|
||||
from Utils import get_options, output_path
|
||||
|
||||
@@ -553,7 +561,7 @@ def generate_basic(self) -> None:
|
||||
### Setting Rules
|
||||
|
||||
```python
|
||||
from ..generic.Rules import add_rule, set_rule, forbid_item
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from Items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
@@ -603,7 +611,7 @@ implement more complex logic in logic mixins, even if there is no need to add
|
||||
properties to the `BaseClasses.CollectionState` state object.
|
||||
|
||||
When importing a file that defines a class that inherits from
|
||||
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
|
||||
`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by
|
||||
the mixin's members. These members should be prefixed with underscore following
|
||||
the name of the implementing world. This is due to sharing a namespace with all
|
||||
other logic mixins.
|
||||
@@ -622,7 +630,7 @@ Please do this with caution and only when neccessary.
|
||||
```python
|
||||
# Logic.py
|
||||
|
||||
from ..AutoWorld import LogicMixin
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameLogic(LogicMixin):
|
||||
def _mygame_has_key(self, world: MultiWorld, player: int):
|
||||
@@ -633,7 +641,7 @@ class MyGameLogic(LogicMixin):
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
from ..generic.Rules import set_rule
|
||||
from worlds.generic.Rules import set_rule
|
||||
import .Logic # apply the mixin by importing its file
|
||||
|
||||
class MyGameWorld(World):
|
||||
|
||||
@@ -196,7 +196,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
@@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[0])
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
self.assertEqual(locations[1].item, prog_items[0])
|
||||
self.assertTrue(locations[1].event)
|
||||
self.assertEqual(locations[2].item, prog_items[1])
|
||||
self.assertTrue(locations[2].event)
|
||||
self.assertEqual(locations[3].item, basic_items[1])
|
||||
self.assertEqual(locations[3].item, basic_items[0])
|
||||
self.assertFalse(locations[3].event)
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
@@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
removed_item: list[Item] = []
|
||||
removed_location: list[Location] = []
|
||||
|
||||
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations):
|
||||
removed_item.append(restitempool.pop(0))
|
||||
def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
removed_item.append(filleritempool.pop(0))
|
||||
removed_location.append(fill_locations.pop(0))
|
||||
|
||||
multi_world.worlds[player1.id].fill_hook = fill_hook
|
||||
|
||||
20
test/general/TestNames.py
Normal file
20
test/general/TestNames.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestNames(unittest.TestCase):
|
||||
def testItemNamesFormat(self):
|
||||
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for item_name in world_type.item_name_to_id:
|
||||
self.assertFalse(item_name.isnumeric(),
|
||||
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
|
||||
|
||||
def testLocationNameFormat(self):
|
||||
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
|
||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=gamename):
|
||||
for location_name in world_type.location_name_to_id:
|
||||
self.assertFalse(location_name.isnumeric(),
|
||||
f"Location name \"{location_name}\" is invalid. It must not be numeric.")
|
||||
@@ -14,9 +14,20 @@ class TestFileGeneration(unittest.TestCase):
|
||||
|
||||
def testOptions(self):
|
||||
WebHost.create_options_files()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs")))
|
||||
target = os.path.join(self.correct_path, "static", "generated", "configs")
|
||||
self.assertTrue(os.path.exists(target))
|
||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs")))
|
||||
|
||||
# folder seems fine, so now we try to generate Options based on the default file
|
||||
from WebHostLib.check import roll_options
|
||||
file: os.DirEntry
|
||||
for file in os.scandir(target):
|
||||
if file.is_file() and file.name.endswith(".yaml"):
|
||||
with self.subTest(file=file.name):
|
||||
with open(file) as f:
|
||||
for value in roll_options({file.name: f.read()})[0].values():
|
||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||
|
||||
def testTutorial(self):
|
||||
WebHost.create_ordered_tutorials_file()
|
||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
||||
|
||||
@@ -98,10 +98,10 @@ def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
|
||||
class WebWorld:
|
||||
"""Webhost integration"""
|
||||
|
||||
|
||||
settings_page: Union[bool, str] = True
|
||||
"""display a settings page. Can be a link to a specific page or external tool."""
|
||||
|
||||
|
||||
game_info_languages: List[str] = ['en']
|
||||
"""docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'"""
|
||||
|
||||
@@ -115,6 +115,16 @@ class WebWorld:
|
||||
bug_report_page: Optional[str]
|
||||
"""display a link to a bug report page, most likely a link to a GitHub issue page."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from WebHostLib.tracker import PlayerTracker
|
||||
else:
|
||||
PlayerTracker = object
|
||||
|
||||
def modify_tracker(self, tracker: PlayerTracker):
|
||||
"""Can use this to modify tracker data and add icons and regions dictionaries to
|
||||
allow them to render on the game's tracker page."""
|
||||
pass
|
||||
|
||||
|
||||
class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
@@ -221,10 +231,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
@classmethod
|
||||
def fill_hook(cls,
|
||||
progitempool: List["Item"],
|
||||
nonexcludeditempool: List["Item"],
|
||||
localrestitempool: Dict[int, List["Item"]],
|
||||
nonlocalrestitempool: Dict[int, List["Item"]],
|
||||
restitempool: List["Item"],
|
||||
usefulitempool: List["Item"],
|
||||
filleritempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
"""Special method that gets called as part of distribute_items_restrictive (main fill).
|
||||
This gets called once per present world type."""
|
||||
@@ -242,6 +250,11 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Fill in the slot_data field in the Connected network package."""
|
||||
return {}
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
"""Fill in additional entrance information text into locations, which is displayed when hinted.
|
||||
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
|
||||
pass
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
|
||||
"""For deeper modification of server multidata."""
|
||||
pass
|
||||
|
||||
@@ -27,7 +27,8 @@ class WorldSource(typing.NamedTuple):
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
|
||||
for file in os.scandir(folder):
|
||||
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
||||
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
||||
if not file.name.startswith(("_", ".")):
|
||||
if file.is_dir():
|
||||
world_sources.append(WorldSource(file.name))
|
||||
elif file.is_file() and file.name.endswith(".apworld"):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Optional, Union, List, Tuple, Callable, Dict
|
||||
|
||||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
from .Options import Bosses
|
||||
|
||||
|
||||
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
@@ -12,7 +13,7 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
raise Exception('Unknown Boss: %s', boss)
|
||||
|
||||
|
||||
def ArmosKnightsDefeatRule(state, player: int):
|
||||
def ArmosKnightsDefeatRule(state, player: int) -> bool:
|
||||
# Magic amounts are probably a bit overkill
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
@@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int):
|
||||
state.has('Red Boomerang', player))
|
||||
|
||||
|
||||
def LanmolasDefeatRule(state, player: int):
|
||||
def LanmolasDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
state.has('Fire Rod', player) or
|
||||
@@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int):
|
||||
state.can_shoot_arrows(player))
|
||||
|
||||
|
||||
def MoldormDefeatRule(state, player: int):
|
||||
def MoldormDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player)
|
||||
|
||||
|
||||
def HelmasaurKingDefeatRule(state, player: int):
|
||||
def HelmasaurKingDefeatRule(state, player: int) -> bool:
|
||||
# TODO: technically possible with the hammer
|
||||
return state.has_sword(player) or state.can_shoot_arrows(player)
|
||||
|
||||
|
||||
def ArrghusDefeatRule(state, player: int):
|
||||
def ArrghusDefeatRule(state, player: int) -> bool:
|
||||
if not state.has('Hookshot', player):
|
||||
return False
|
||||
# TODO: ideally we would have a check for bow and silvers, which combined with the
|
||||
@@ -58,7 +59,7 @@ def ArrghusDefeatRule(state, player: int):
|
||||
(state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16))))
|
||||
|
||||
|
||||
def MothulaDefeatRule(state, player: int):
|
||||
def MothulaDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
state.has_melee_weapon(player) or
|
||||
(state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or
|
||||
@@ -70,11 +71,11 @@ def MothulaDefeatRule(state, player: int):
|
||||
)
|
||||
|
||||
|
||||
def BlindDefeatRule(state, player: int):
|
||||
def BlindDefeatRule(state, player: int) -> bool:
|
||||
return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player)
|
||||
|
||||
|
||||
def KholdstareDefeatRule(state, player: int):
|
||||
def KholdstareDefeatRule(state, player: int) -> bool:
|
||||
return (
|
||||
(
|
||||
state.has('Fire Rod', player) or
|
||||
@@ -96,11 +97,11 @@ def KholdstareDefeatRule(state, player: int):
|
||||
)
|
||||
|
||||
|
||||
def VitreousDefeatRule(state, player: int):
|
||||
def VitreousDefeatRule(state, player: int) -> bool:
|
||||
return state.can_shoot_arrows(player) or state.has_melee_weapon(player)
|
||||
|
||||
|
||||
def TrinexxDefeatRule(state, player: int):
|
||||
def TrinexxDefeatRule(state, player: int) -> bool:
|
||||
if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)):
|
||||
return False
|
||||
return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \
|
||||
@@ -108,11 +109,11 @@ def TrinexxDefeatRule(state, player: int):
|
||||
(state.has_sword(player) and state.can_extend_magic(player, 32))
|
||||
|
||||
|
||||
def AgahnimDefeatRule(state, player: int):
|
||||
def AgahnimDefeatRule(state, player: int) -> bool:
|
||||
return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player)
|
||||
|
||||
|
||||
def GanonDefeatRule(state, player: int):
|
||||
def GanonDefeatRule(state, player: int) -> bool:
|
||||
if state.world.swordless[player]:
|
||||
return state.has('Hammer', player) and \
|
||||
state.has_fire_source(player) and \
|
||||
@@ -132,7 +133,7 @@ def GanonDefeatRule(state, player: int):
|
||||
return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player)
|
||||
|
||||
|
||||
boss_table = {
|
||||
boss_table: Dict[str, Tuple[str, Optional[Callable]]] = {
|
||||
'Armos Knights': ('Armos', ArmosKnightsDefeatRule),
|
||||
'Lanmolas': ('Lanmola', LanmolasDefeatRule),
|
||||
'Moldorm': ('Moldorm', MoldormDefeatRule),
|
||||
@@ -147,7 +148,7 @@ boss_table = {
|
||||
'Agahnim2': ('Agahnim2', AgahnimDefeatRule)
|
||||
}
|
||||
|
||||
boss_location_table = [
|
||||
boss_location_table: List[Tuple[str, str]] = [
|
||||
('Ganons Tower', 'top'),
|
||||
('Tower of Hera', None),
|
||||
('Skull Woods', None),
|
||||
@@ -164,6 +165,34 @@ boss_location_table = [
|
||||
]
|
||||
|
||||
|
||||
def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
|
||||
# Most to least restrictive order
|
||||
boss_locations = boss_location_table.copy()
|
||||
world.random.shuffle(boss_locations)
|
||||
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
|
||||
already_placed_bosses: List[str] = []
|
||||
|
||||
for boss in bosses:
|
||||
if "-" in boss: # handle plando locations
|
||||
loc, boss = boss.split("-")
|
||||
boss = boss.title()
|
||||
level: str = None
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = loc[-1]
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
place_boss(world, player, boss, loc, level)
|
||||
already_placed_bosses.append(boss)
|
||||
boss_locations.remove((loc, level))
|
||||
else: # boss chosen with no specified locations
|
||||
boss = boss.title()
|
||||
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
|
||||
|
||||
return already_placed_bosses, boss_locations
|
||||
|
||||
|
||||
def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool:
|
||||
# blacklist approach
|
||||
if boss in {"Agahnim", "Agahnim2", "Ganon"}:
|
||||
@@ -187,62 +216,50 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) ->
|
||||
|
||||
return True
|
||||
|
||||
restrictive_boss_locations = {}
|
||||
|
||||
restrictive_boss_locations: Dict[Tuple[str, str], bool] = {}
|
||||
for location in boss_location_table:
|
||||
restrictive_boss_locations[location] = not all(can_place_boss(boss, *location)
|
||||
for boss in boss_table if not boss.startswith("Agahnim"))
|
||||
|
||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]):
|
||||
|
||||
def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
|
||||
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
|
||||
location = 'Inverted Ganons Tower'
|
||||
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
||||
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
|
||||
|
||||
def format_boss_location(location, level):
|
||||
|
||||
def format_boss_location(location: str, level: str) -> str:
|
||||
return location + (' (' + level + ')' if level else '')
|
||||
|
||||
def place_bosses(world, player: int):
|
||||
if world.boss_shuffle[player] == 'none':
|
||||
|
||||
def place_bosses(world, player: int) -> None:
|
||||
# will either be an int or a lower case string with ';' between options
|
||||
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value
|
||||
already_placed_bosses: List[str] = []
|
||||
remaining_locations: List[Tuple[str, str]] = []
|
||||
# handle plando
|
||||
if isinstance(boss_shuffle, str):
|
||||
# figure out our remaining mode, convert it to an int and remove it from plando_args
|
||||
options = boss_shuffle.split(";")
|
||||
boss_shuffle = Bosses.options[options.pop()]
|
||||
# place our plando bosses
|
||||
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player)
|
||||
if boss_shuffle == Bosses.option_none: # vanilla boss locations
|
||||
return
|
||||
|
||||
# Most to least restrictive order
|
||||
boss_locations = boss_location_table.copy()
|
||||
world.random.shuffle(boss_locations)
|
||||
boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location]))
|
||||
if not remaining_locations and not already_placed_bosses:
|
||||
remaining_locations = boss_location_table.copy()
|
||||
world.random.shuffle(remaining_locations)
|
||||
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
|
||||
|
||||
all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
|
||||
placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']]
|
||||
|
||||
shuffle_mode = world.boss_shuffle[player]
|
||||
already_placed_bosses = []
|
||||
if ";" in shuffle_mode:
|
||||
bosses = shuffle_mode.split(";")
|
||||
shuffle_mode = bosses.pop()
|
||||
for boss in bosses:
|
||||
if "-" in boss:
|
||||
loc, boss = boss.split("-")
|
||||
boss = boss.title()
|
||||
level = None
|
||||
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
|
||||
# split off level
|
||||
loc = loc.split(" ")
|
||||
level = loc[-1]
|
||||
loc = " ".join(loc[:-1])
|
||||
loc = loc.title().replace("Of", "of")
|
||||
if can_place_boss(boss, loc, level) and (loc, level) in boss_locations:
|
||||
place_boss(world, player, boss, loc, level)
|
||||
already_placed_bosses.append(boss)
|
||||
boss_locations.remove((loc, level))
|
||||
else:
|
||||
raise Exception(f"Cannot place {boss} at {format_boss_location(loc, level)} for player {player}.")
|
||||
else:
|
||||
boss = boss.title()
|
||||
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
|
||||
|
||||
if shuffle_mode == "none":
|
||||
return # vanilla bosses come pre-placed
|
||||
|
||||
if shuffle_mode in ["basic", "full"]:
|
||||
if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled
|
||||
if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full:
|
||||
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
|
||||
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
|
||||
else: # all bosses present, the three duplicates chosen at random
|
||||
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3)
|
||||
@@ -258,7 +275,7 @@ def place_bosses(world, player: int):
|
||||
logging.debug('Bosses chosen %s', bosses)
|
||||
|
||||
world.random.shuffle(bosses)
|
||||
for loc, level in boss_locations:
|
||||
for loc, level in remaining_locations:
|
||||
for _ in range(len(bosses)):
|
||||
boss = bosses.pop()
|
||||
if can_place_boss(boss, loc, level):
|
||||
@@ -272,8 +289,8 @@ def place_bosses(world, player: int):
|
||||
|
||||
place_boss(world, player, boss, loc, level)
|
||||
|
||||
elif shuffle_mode == "chaos": # all bosses chosen at random
|
||||
for loc, level in boss_locations:
|
||||
elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
|
||||
for loc, level in remaining_locations:
|
||||
try:
|
||||
boss = world.random.choice(
|
||||
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
|
||||
@@ -282,9 +299,9 @@ def place_bosses(world, player: int):
|
||||
else:
|
||||
place_boss(world, player, boss, loc, level)
|
||||
|
||||
elif shuffle_mode == "singularity":
|
||||
elif boss_shuffle == Bosses.option_singularity:
|
||||
primary_boss = world.random.choice(placeable_bosses)
|
||||
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations)
|
||||
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
|
||||
if remaining_boss_locations:
|
||||
# pick a boss to go into the remaining locations
|
||||
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
|
||||
@@ -293,12 +310,12 @@ def place_bosses(world, player: int):
|
||||
if remaining_boss_locations:
|
||||
raise Exception("Unfilled boss locations!")
|
||||
else:
|
||||
raise FillError(f"Could not find boss shuffle mode {shuffle_mode}")
|
||||
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")
|
||||
|
||||
|
||||
def place_where_possible(world, player: int, boss: str, boss_locations):
|
||||
remainder = []
|
||||
placed_bosses = []
|
||||
def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
|
||||
remainder: List[Tuple[str, str]] = []
|
||||
placed_bosses: List[str] = []
|
||||
for loc, level in boss_locations:
|
||||
# place that boss where it can go
|
||||
if can_place_boss(boss, loc, level):
|
||||
|
||||
@@ -480,7 +480,7 @@ def set_up_take_anys(world, player):
|
||||
old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots)
|
||||
world.shops.append(old_man_take_any.shop)
|
||||
|
||||
swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player]
|
||||
swords = [item for item in world.itempool if item.player == player and item.type == 'Sword']
|
||||
if swords:
|
||||
sword = world.random.choice(swords)
|
||||
world.itempool.remove(sword)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -39,8 +39,6 @@ class OpenPyramid(Choice):
|
||||
option_auto = 3
|
||||
default = option_goal
|
||||
|
||||
alias_true = option_open
|
||||
alias_false = option_closed
|
||||
alias_yes = option_open
|
||||
alias_no = option_closed
|
||||
|
||||
@@ -140,13 +138,143 @@ class WorldState(Choice):
|
||||
option_inverted = 2
|
||||
|
||||
|
||||
class Bosses(Choice):
|
||||
option_vanilla = 0
|
||||
option_simple = 1
|
||||
class Bosses(TextChoice):
|
||||
"""Shuffles bosses around to different locations.
|
||||
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
|
||||
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
|
||||
Chaos allows any boss to appear any number of times.
|
||||
Singularity places a single boss in as many places as possible, and a second boss in any remaining locations.
|
||||
Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en"""
|
||||
display_name = "Boss Shuffle"
|
||||
option_none = 0
|
||||
option_basic = 1
|
||||
option_full = 2
|
||||
option_chaos = 3
|
||||
option_singularity = 4
|
||||
|
||||
bosses: set = {
|
||||
"Armos Knights",
|
||||
"Lanmolas",
|
||||
"Moldorm",
|
||||
"Helmasaur King",
|
||||
"Arrghus",
|
||||
"Mothula",
|
||||
"Blind",
|
||||
"Kholdstare",
|
||||
"Vitreous",
|
||||
"Trinexx",
|
||||
}
|
||||
|
||||
locations: set = {
|
||||
"Ganons Tower Top",
|
||||
"Tower of Hera",
|
||||
"Skull Woods",
|
||||
"Ganons Tower Middle",
|
||||
"Eastern Palace",
|
||||
"Desert Palace",
|
||||
"Palace of Darkness",
|
||||
"Swamp Palace",
|
||||
"Thieves Town",
|
||||
"Ice Palace",
|
||||
"Misery Mire",
|
||||
"Turtle Rock",
|
||||
"Ganons Tower Bottom"
|
||||
}
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"{value} is not a valid option for {self.__class__.__name__}"
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
import random
|
||||
# set all of our text to lower case for name checking
|
||||
text = text.lower()
|
||||
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
|
||||
cls.locations = {boss_location.lower() for boss_location in cls.locations}
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.options.values())))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
options = text.split(";")
|
||||
|
||||
# since plando exists in the option verify the plando values given are valid
|
||||
cls.validate_plando_bosses(options)
|
||||
|
||||
# find out what type of boss shuffle we should use for placing bosses after plando
|
||||
# and add as a string to look nice in the spoiler
|
||||
if "random" in options:
|
||||
shuffle = random.choice(list(cls.options))
|
||||
options.remove("random")
|
||||
options = ";".join(options) + ";" + shuffle
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
for option in options:
|
||||
if option in cls.options:
|
||||
boss_class = cls(";".join(options))
|
||||
break
|
||||
else:
|
||||
if len(options) == 1:
|
||||
if cls.valid_boss_name(options[0]):
|
||||
options = options[0] + ";singularity"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = options[0] + ";none"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = ";".join(options) + ";none"
|
||||
boss_class = cls(options)
|
||||
return boss_class
|
||||
|
||||
@classmethod
|
||||
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
|
||||
from .Bosses import can_place_boss, format_boss_location
|
||||
for option in options:
|
||||
if option == "random" or option in cls.options:
|
||||
if option != options[-1]:
|
||||
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
|
||||
continue
|
||||
if "-" in option:
|
||||
location, boss = option.split("-")
|
||||
level = ''
|
||||
if not cls.valid_boss_name(boss):
|
||||
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
|
||||
if not cls.valid_location_name(location):
|
||||
raise ValueError(f"{location} is not a valid boss location name.")
|
||||
if location.split(" ")[-1] in ("top", "middle", "bottom"):
|
||||
location = location.split(" ")
|
||||
level = location[-1]
|
||||
location = " ".join(location[:-1])
|
||||
location = location.title().replace("Of", "of")
|
||||
if not can_place_boss(boss.title(), location, level):
|
||||
raise ValueError(f"{format_boss_location(location, level)} "
|
||||
f"is not a valid location for {boss.title()}.")
|
||||
else:
|
||||
if not cls.valid_boss_name(option):
|
||||
raise ValueError(f"{option} is not a valid boss name.")
|
||||
|
||||
@classmethod
|
||||
def valid_boss_name(cls, value: str) -> bool:
|
||||
return value.lower() in cls.bosses
|
||||
|
||||
@classmethod
|
||||
def valid_location_name(cls, value: str) -> bool:
|
||||
return value in cls.locations
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from Generate import PlandoSettings
|
||||
if not(PlandoSettings.bosses & plando_options):
|
||||
import logging
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
self.value = self.options[option]
|
||||
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
|
||||
f"boss shuffle will be used for player {player_name}.")
|
||||
|
||||
|
||||
class Enemies(Choice):
|
||||
option_vanilla = 0
|
||||
@@ -159,8 +287,6 @@ class Progressive(Choice):
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
|
||||
def want_progressives(self, random):
|
||||
@@ -168,8 +294,8 @@ class Progressive(Choice):
|
||||
|
||||
|
||||
class Swordless(Toggle):
|
||||
"""No swords. Curtains in Skull Woods and Agahnim\'s
|
||||
Tower are removed, Agahnim\'s Tower barrier can be
|
||||
"""No swords. Curtains in Skull Woods and Agahnim's
|
||||
Tower are removed, Agahnim's Tower barrier can be
|
||||
destroyed with hammer. Misery Mire and Turtle Rock
|
||||
can be opened without a sword. Hammer damages Ganon.
|
||||
Ether and Bombos Tablet can be activated with Hammer
|
||||
@@ -202,8 +328,6 @@ class Hints(Choice):
|
||||
option_on = 2
|
||||
option_full = 3
|
||||
default = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class Scams(Choice):
|
||||
@@ -213,7 +337,6 @@ class Scams(Choice):
|
||||
option_king_zora = 1
|
||||
option_bottle_merchant = 2
|
||||
option_all = 3
|
||||
alias_false = 0
|
||||
|
||||
@property
|
||||
def gives_king_zora_hint(self):
|
||||
@@ -293,7 +416,6 @@ class HeartBeep(Choice):
|
||||
option_half = 2
|
||||
option_quarter = 3
|
||||
option_off = 4
|
||||
alias_false = 4
|
||||
|
||||
|
||||
class HeartColor(Choice):
|
||||
@@ -375,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"hints": Hints,
|
||||
"scams": Scams,
|
||||
"restrict_dungeon_item_on_boss": RestrictBossItem,
|
||||
"boss_shuffle": Bosses,
|
||||
"pot_shuffle": PotShuffle,
|
||||
"enemy_shuffle": EnemyShuffle,
|
||||
"killable_thieves": KillableThieves,
|
||||
|
||||
@@ -4,6 +4,10 @@ import typing
|
||||
from BaseClasses import Region, Entrance, RegionType
|
||||
|
||||
|
||||
def is_main_entrance(entrance: Entrance) -> bool:
|
||||
return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic}
|
||||
|
||||
|
||||
def create_regions(world, player):
|
||||
|
||||
world.regions += [
|
||||
|
||||
@@ -12,7 +12,8 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||
from .Options import alttp_options, smallkey_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
@@ -24,6 +25,7 @@ lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
extras_list = sum(difficulties['normal'].extras[0:5], [])
|
||||
|
||||
|
||||
class ALTTPWeb(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
@@ -99,6 +101,232 @@ class ALTTPWeb(WebWorld):
|
||||
|
||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando]
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from WebHostLib.tracker import PlayerTracker
|
||||
else:
|
||||
PlayerTracker = object
|
||||
|
||||
def modify_tracker(self, tracker: PlayerTracker):
|
||||
tracker.template = 'zeldaKeysTracker.html'
|
||||
|
||||
tracker.icons = {
|
||||
"Blue Shield": r"https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": r"https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": r"https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": r"https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Small Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": r"https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": r"https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": r"https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": r"https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74"
|
||||
}
|
||||
|
||||
tracker.regions = {
|
||||
'Light World': [
|
||||
'Lost Woods Hideout', 'Lumberjack Tree', 'Mushroom', 'Master Sword Pedestal', 'Bottle Merchant', 'Flute Spot',
|
||||
'Blind\'s Hideout - Top', 'Blind\'s Hideout - Left', 'Blind\'s Hideout - Right', 'Blind\'s Hideout - Far Left', 'Blind\'s Hideout - Far Right',
|
||||
'Link\'s House', 'Link\'s Uncle', 'Secret Passage',
|
||||
'King Zora', 'Zora\'s Ledge', 'Waterfall Fairy - Left', 'Waterfall Fairy - Right',
|
||||
'King\'s Tomb', 'Graveyard Cave', 'Bonk Rock Cave',
|
||||
'Sunken Treasure', 'Floodgate Chest', 'Hobo', 'Ice Rod Cave', 'Lake Hylia Island',
|
||||
'Kakariko Tavern', 'Chicken House', 'Sick Kid',
|
||||
'Blacksmith', 'Purple Chest', 'Magic Bat',
|
||||
'Aginah\'s Cave', 'Cave 45', 'Checkerboard Cave',
|
||||
'Sahasrahla\'s Hut - Left', 'Sahasrahla\'s Hut - Middle', 'Sahasrahla\'s Hut - Right', 'Sahasrahla',
|
||||
'Kakariko Well - Top', 'Kakariko Well - Left', 'Kakariko Well - Middle', 'Kakariko Well - Right', 'Kakariko Well - Bottom',
|
||||
'Mini Moldorm Cave - Far Left', 'Mini Moldorm Cave - Left', 'Mini Moldorm Cave - Right', 'Mini Moldorm Cave - Far Right', 'Mini Moldorm Cave - Generous Guy',
|
||||
'Library', 'Maze Race', 'Potion Shop', 'Desert Ledge',
|
||||
'Old Man', 'Spectacle Rock',
|
||||
'Paradox Cave Lower - Far Left', 'Paradox Cave Lower - Left', 'Paradox Cave Lower - Right', 'Paradox Cave Lower - Far Right', 'Paradox Cave Lower - Middle',
|
||||
'Paradox Cave Upper - Left', 'Paradox Cave Upper - Right',
|
||||
'Spiral Cave', 'Ether Tablet'
|
||||
],
|
||||
'Dark World': [
|
||||
'Pyramid', 'Catfish', 'Pyramid Fairy - Left', 'Pyramid Fairy - Right',
|
||||
'Stumpy', 'Digging Game',
|
||||
'Bombos Tablet',
|
||||
'Hype Cave - Top', 'Hype Cave - Middle Right', 'Hype Cave - Middle Left', 'Hype Cave - Bottom', 'Hype Cave - Generous Guy',
|
||||
'Peg Cave', 'Brewery', 'C-Shaped House', 'Chest Game',
|
||||
'Bumper Cave Ledge',
|
||||
'Mire Shed - Left', 'Mire Shed - Right',
|
||||
'Superbunny Cave - Top', 'Superbunny Cave - Bottom',
|
||||
'Spike Cave', 'Floating Island', 'Mimic Cave',
|
||||
'Hookshot Cave - Top Right', 'Hookshot Cave - Top Left', 'Hookshot Cave - Bottom Right', 'Hookshot Cave - Bottom Left',
|
||||
|
||||
],
|
||||
'Desert Palace': [
|
||||
'Desert Palace - Big Chest', 'Desert Palace - Torch', 'Desert Palace - Map Chest',
|
||||
'Desert Palace - Compass Chest', 'Desert Palace - Big Key Chest',
|
||||
'Desert Palace - Boss'
|
||||
],
|
||||
'Eastern Palace': [
|
||||
'Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest', 'Eastern Palace - Cannonball Chest',
|
||||
'Eastern Palace - Big Key Chest', 'Eastern Palace - Map Chest', 'Eastern Palace - Boss'
|
||||
],
|
||||
'Hyrule Castle': [
|
||||
'Hyrule Castle - Map Chest', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Zelda\'s Chest',
|
||||
'Sewers - Dark Cross', 'Sewers - Secret Room - Left', 'Sewers - Secret Room - Middle', 'Sewers - Secret Room - Right',
|
||||
'Sanctuary'
|
||||
],
|
||||
'Agahnims Tower': [
|
||||
'Castle Tower - Room 03', 'Castle Tower - Dark Maze'
|
||||
],
|
||||
'Tower of Hera': [
|
||||
'Tower of Hera - Basement Cage', 'Tower of Hera - Map Chest', 'Tower of Hera - Big Key Chest',
|
||||
'Tower of Hera - Compass Chest', 'Tower of Hera - Big Chest', 'Tower of Hera - Boss'
|
||||
],
|
||||
'Swamp Palace': [
|
||||
'Swamp Palace - Entrance', 'Swamp Palace - Map Chest',
|
||||
'Swamp Palace - Big Chest', 'Swamp Palace - Compass Chest',
|
||||
'Swamp Palace - Big Key Chest', 'Swamp Palace - West Chest',
|
||||
'Swamp Palace - Flooded Room - Left', 'Swamp Palace - Flooded Room - Right',
|
||||
'Swamp Palace - Waterfall Room', 'Swamp Palace - Boss'
|
||||
],
|
||||
'Thieves Town': [
|
||||
'Thieves\' Town - Big Key Chest', 'Thieves\' Town - Map Chest', 'Thieves\' Town - Compass Chest', 'Thieves\' Town - Ambush Chest',
|
||||
'Thieves\' Town - Attic', 'Thieves\' Town - Big Chest', 'Thieves\' Town - Blind\'s Cell', 'Thieves\' Town - Boss'
|
||||
],
|
||||
'Skull Woods': [
|
||||
'Skull Woods - Map Chest', 'Skull Woods - Pinball Room',
|
||||
'Skull Woods - Compass Chest', 'Skull Woods - Pot Prison',
|
||||
'Skull Woods - Big Chest',
|
||||
'Skull Woods - Big Key Chest',
|
||||
'Skull Woods - Bridge Room', 'Skull Woods - Boss'
|
||||
],
|
||||
'Ice Palace': [
|
||||
'Ice Palace - Compass Chest', 'Ice Palace - Freezor Chest', 'Ice Palace - Big Chest', 'Ice Palace - Iced T Room',
|
||||
'Ice Palace - Spike Room', 'Ice Palace - Big Key Chest', 'Ice Palace - Map Chest', 'Ice Palace - Boss'
|
||||
],
|
||||
'Misery Mire': [
|
||||
'Misery Mire - Big Chest', 'Misery Mire - Map Chest', 'Misery Mire - Main Lobby', 'Misery Mire - Bridge Chest', 'Misery Mire - Spike Chest',
|
||||
'Misery Mire - Compass Chest', 'Misery Mire - Big Key Chest', 'Misery Mire - Boss'
|
||||
],
|
||||
'Turtle Rock': [
|
||||
'Turtle Rock - Compass Chest', 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right',
|
||||
'Turtle Rock - Chain Chomps', 'Turtle Rock - Big Key Chest', 'Turtle Rock - Big Chest',
|
||||
'Turtle Rock - Crystaroller Room',
|
||||
'Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right',
|
||||
'Turtle Rock - Boss'
|
||||
],
|
||||
'Palace of Darkness': [
|
||||
'Palace of Darkness - Shooter Room', 'Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement',
|
||||
'Palace of Darkness - Big Key Chest',
|
||||
'Palace of Darkness - The Arena - Ledge', 'Palace of Darkness - Map Chest',
|
||||
'Palace of Darkness - Compass Chest', 'Palace of Darkness - Dark Basement - Left', 'Palace of Darkness - Dark Basement - Right',
|
||||
'Palace of Darkness - Dark Maze - Top', 'Palace of Darkness - Dark Maze - Bottom', 'Palace of Darkness - Big Chest',
|
||||
'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Boss'
|
||||
],
|
||||
'Ganons Tower': [
|
||||
'Ganons Tower - Bob\'s Torch', 'Ganons Tower - Hope Room - Left', 'Ganons Tower - Hope Room - Right',
|
||||
'Ganons Tower - Tile Room',
|
||||
'Ganons Tower - Compass Room - Top Left', 'Ganons Tower - Compass Room - Top Right', 'Ganons Tower - Compass Room - Bottom Left', 'Ganons Tower - Compass Room - Bottom Right',
|
||||
'Ganons Tower - DMs Room - Top Left', 'Ganons Tower - DMs Room - Top Right', 'Ganons Tower - DMs Room - Bottom Left', 'Ganons Tower - DMs Room - Bottom Right',
|
||||
'Ganons Tower - Map Chest', 'Ganons Tower - Firesnake Room',
|
||||
'Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right',
|
||||
'Ganons Tower - Bob\'s Chest',
|
||||
'Ganons Tower - Big Chest', 'Ganons Tower - Big Key Room - Left', 'Ganons Tower - Big Key Room - Right', 'Ganons Tower - Big Key Chest',
|
||||
]
|
||||
}
|
||||
|
||||
tracker.progressive_items = [
|
||||
'Progressive Sword',
|
||||
'Progressive Shield',
|
||||
'Progressive Mail',
|
||||
'Progressive Bow',
|
||||
'Progressive Boomerang',
|
||||
'Hookshot',
|
||||
'Magic Powder',
|
||||
'Mushroom',
|
||||
'Bottle',
|
||||
'Lamp',
|
||||
'Progressive Glove',
|
||||
'Flippers',
|
||||
'Moon Pearl',
|
||||
'Bombos',
|
||||
'Ether',
|
||||
'Quake',
|
||||
'Fire Rod',
|
||||
'Ice Rod',
|
||||
'Hammer',
|
||||
'Book of Mudora',
|
||||
'Shovel',
|
||||
'Flute',
|
||||
'Bug Catching Net',
|
||||
'Cane of Somaria',
|
||||
'Cane of Byrna',
|
||||
'Cape',
|
||||
'Magic Mirror',
|
||||
'Small Key',
|
||||
'Big Key'
|
||||
]
|
||||
|
||||
tracker.progressive_names = {
|
||||
'Progressive Bow': ['Bow', 'Silver Arrows', 'Silver Bow', 'Progressive Bow (Alt)'],
|
||||
'Bottle': ['Bottle (Red Potion)', 'Bottle (Green Potion)', 'Bottle (Blue Potion)', 'Bottle (Fairy)', 'Bottle (Bee)', 'Bottle (Good Bee)'],
|
||||
'Progressive Sword': ['Fighter Sword', 'Master Sword', 'Tempered Sword', 'Golden Sword'],
|
||||
'Progressive Glove': ['Power Glove', 'Titans Mitts'],
|
||||
'Progressive Shield': ['Blue Shield', 'Red Shield', 'Mirror Shield'],
|
||||
'Progressive Boomerang': ['Red Boomerang', 'Blue Boomerang'],
|
||||
'Progressive Mail': ['Green Mail', 'Blue Mail', 'Red Mail'],
|
||||
'Small Key': [f'Small Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
|
||||
'Big Key': [f'Big Key ({region})' for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}],
|
||||
}
|
||||
|
||||
tracker.region_keys = {
|
||||
region: [f'Small Key ({region})', f'Big Key ({region})'] for region in tracker.regions.keys() if region not in {'Light World', 'Dark World'}
|
||||
}
|
||||
|
||||
|
||||
class ALTTPWorld(World):
|
||||
"""
|
||||
@@ -349,7 +577,7 @@ class ALTTPWorld(World):
|
||||
def use_enemizer(self):
|
||||
world = self.world
|
||||
player = self.player
|
||||
return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
|
||||
return (world.boss_shuffle[player] or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
@@ -410,6 +638,20 @@ class ALTTPWorld(World):
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
@classmethod
|
||||
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
|
||||
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = region.get_connecting_entrance(is_main_entrance)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
hint_data.update(er_hint_data)
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
# wait for self.rom_name to be available.
|
||||
@@ -424,8 +666,7 @@ class ALTTPWorld(World):
|
||||
return ALttPItem(name, self.player, **item_init_table[name])
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
trash_counts = {}
|
||||
standard_keyshuffle_players = set()
|
||||
for player in world.get_game_players("A Link to the Past"):
|
||||
@@ -472,26 +713,15 @@ class ALTTPWorld(World):
|
||||
for player, trash_count in trash_counts.items():
|
||||
gtower_locations = locations_mapping[player]
|
||||
world.random.shuffle(gtower_locations)
|
||||
localrest = localrestitempool[player]
|
||||
if localrest:
|
||||
gt_item_pool = restitempool + localrest
|
||||
world.random.shuffle(gt_item_pool)
|
||||
else:
|
||||
gt_item_pool = restitempool.copy()
|
||||
|
||||
while gtower_locations and gt_item_pool and trash_count > 0:
|
||||
while gtower_locations and filleritempool and trash_count > 0:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = gt_item_pool.pop()
|
||||
item_to_place = filleritempool.pop()
|
||||
if spot_to_fill.item_rule(item_to_place):
|
||||
if item_to_place in localrest:
|
||||
localrest.remove(item_to_place)
|
||||
else:
|
||||
restitempool.remove(item_to_place)
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.world.goal[self.player] == "icerodhunt":
|
||||
item = "Nothing"
|
||||
|
||||
@@ -26,10 +26,14 @@
|
||||
- Example: `Trinexx`
|
||||
- Takes a particular boss and places that boss in any remaining slots in which this boss can function.
|
||||
- In this example, it would fill Desert Palace, but not Tower of Hera.
|
||||
- If no other options are provided this will follow normal singularity rules with that boss.
|
||||
- Boss Shuffle:
|
||||
- Example: `simple`
|
||||
- Example: `basic`
|
||||
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
|
||||
a last instruction.
|
||||
- Supports `random` which will choose a random option from the normal choices.
|
||||
- If one is not supplied any remaining locations will be unshuffled unless a single specific boss is
|
||||
supplied in which case it will use singularity as noted above.
|
||||
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
|
||||
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ base_info = {
|
||||
"factorio_version": "1.1",
|
||||
"dependencies": [
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited"
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -137,8 +137,6 @@ class Progressive(Choice):
|
||||
option_off = 0
|
||||
option_grouped_random = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
|
||||
def want_progressives(self, random):
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "1.1",
|
||||
"dependencies": [
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited"
|
||||
]
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -183,6 +183,18 @@ end
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||
if mods["factory-levels"] then
|
||||
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
|
||||
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
|
||||
for i = 1, 25, 1 do
|
||||
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||
end
|
||||
for i = 1, 50, 1 do
|
||||
data.raw["assembling-machine"]["assembling-machine-2-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
end
|
||||
end
|
||||
|
||||
data.raw["ammo"]["artillery-shell"].stack_size = 10
|
||||
|
||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||
|
||||
@@ -66,7 +66,10 @@ class FF1World(World):
|
||||
def goal_rule_and_shards(state):
|
||||
return goal_rule(state) and state.has("Shard", self.player, 32)
|
||||
terminated_event.access_rule = goal_rule_and_shards
|
||||
|
||||
if "MARK" in items.keys():
|
||||
# Fail generation for Noverworld and provide link to old FFR website
|
||||
raise Exception("FFR Noverworld seeds must be generated on an older version of FFR. Please ensure you generated the settings using "
|
||||
"4-4-0.finalfantasyrandomizer.com")
|
||||
menu_region.locations.append(terminated_event)
|
||||
self.world.regions += [menu_region]
|
||||
|
||||
|
||||
@@ -409,7 +409,6 @@ class DeathLink(Choice):
|
||||
shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise.
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
alias_true = 1
|
||||
alias_on = 1
|
||||
@@ -435,10 +434,8 @@ class CostSanity(Choice):
|
||||
These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs
|
||||
"""
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
alias_no = 0
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
alias_yes = 1
|
||||
option_shopsonly = 2
|
||||
option_notshops = 3
|
||||
|
||||
@@ -49,6 +49,79 @@ class MinecraftWebWorld(WebWorld):
|
||||
|
||||
tutorials = [setup, setup_es, setup_sv]
|
||||
|
||||
def modify_tracker(self, tracker):
|
||||
tracker.icons = {
|
||||
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
|
||||
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
|
||||
"Iron Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d1/Iron_Pickaxe_JE3_BE2.png",
|
||||
"Diamond Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e7/Diamond_Pickaxe_JE3_BE3.png",
|
||||
"Wooden Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d5/Wooden_Sword_JE2_BE2.png",
|
||||
"Stone Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b1/Stone_Sword_JE2_BE2.png",
|
||||
"Iron Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/8/8e/Iron_Sword_JE2_BE2.png",
|
||||
"Diamond Sword": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/4/44/Diamond_Sword_JE3_BE3.png",
|
||||
"Leather Tunic": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b7/Leather_Tunic_JE4_BE2.png",
|
||||
"Iron Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Iron_Chestplate_JE2_BE2.png",
|
||||
"Diamond Chestplate": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/e/e0/Diamond_Chestplate_JE3_BE2.png",
|
||||
"Iron Ingot": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Iron_Ingot_JE3_BE2.png",
|
||||
"Block of Iron": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7e/Block_of_Iron_JE4_BE3.png",
|
||||
"Brewing": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b3/Brewing_Stand_%28empty%29_JE10.png",
|
||||
"Ender Pearls": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/f6/Ender_Pearl_JE3_BE2.png",
|
||||
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
|
||||
"Archery": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
|
||||
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
|
||||
"Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
|
||||
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
|
||||
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
|
||||
"Enchanting": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
|
||||
"Fishing Rod": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/7f/Fishing_Rod_JE2_BE2.png",
|
||||
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
|
||||
"Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
|
||||
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
|
||||
}
|
||||
|
||||
tracker.progressive_items = [
|
||||
"Progressive Tools", "Progressive Weapons", "Progressive Armor", "Progressive Resource Crafting",
|
||||
"Brewing", "Ender Pearls", "Bucket", "Archery", "Shield", "Bed", "Bottle", "Netherite Scrap",
|
||||
"Flint and Steel", "Enchanting", "Fishing Rod", "Campfire", "Spyglass"
|
||||
]
|
||||
|
||||
tracker.progressive_names = {
|
||||
"Progressive Tools": ["Wooden Pickaxe", "Stone Pickaxe", "Iron Pickaxe", "Diamond Pickaxe"],
|
||||
"Progressive Weapons": ["Wooden Sword", "Stone Sword", "Iron Sword", "Diamond Sword"],
|
||||
"Progressive Armor": ["Leather Tunic", "Iron Chestplate", "Diamond Chestplate"],
|
||||
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
|
||||
}
|
||||
|
||||
tracker.regions = {
|
||||
"Story": ["Minecraft", "Stone Age", "Getting an Upgrade", "Acquire Hardware", "Suit Up",
|
||||
"Not Today, Thank You", "Isn't It Iron Pick", "Diamonds!", "Cover Me With Diamonds", "Enchanter",
|
||||
"Hot Stuff", "Ice Bucket Challenge", "We Need to Go Deeper", "Zombie Doctor", "Eye Spy", "The End?"],
|
||||
|
||||
"Nether": ["Nether", "Return to Sender", "Uneasy Alliance", "Those Were the Days", "War Pigs",
|
||||
"Hidden in the Depths", "Country Lode, Take Me Home", "Cover Me in Debris", "Subspace Bubble",
|
||||
"A Terrible Fortress", "Spooky Scary SKeleton", "This Boat Has Legs", "Hot Tourist Destinations"],
|
||||
|
||||
"The End": ["The End", "Free the End", "The Next Generation", "Remote Getaway",
|
||||
"The City at the End of the Game", "Sky's the Limit", "Great View From Up Here",
|
||||
"The End... Again...", "You Need a Mint"],
|
||||
|
||||
"Adventure": ["Adventure", "Voluntary Exile", "Is It a Bird?", "Is It a Balloon?", "Is It a Plane?",
|
||||
"Hero of the Village", "Monster Hunter", "A Throwaway Joke", "Very Very Frightening",
|
||||
"Take Aim", "Sniper Duel", "Bullseye", "Monsters Hunted", "Postmortal", "What a Deal!",
|
||||
"Hired Help", "Sticky Situation", "Ol' Betsy", "Two Birds, One Arrow",
|
||||
"Who's the Pillager Now?", "Arbalistic", "Sweet Dreams", "Adventuring Time", "Surge Protector",
|
||||
"Light as a Rabbit"],
|
||||
|
||||
"Husbandry": ["Husbandry", "Bee Our Guest", "The Parrots and the Bats", "Two by Two", "Best Friends Forever",
|
||||
"A Complete Catalogue", "Fishy Business", "Tactical Fishing", "Total Beelocation",
|
||||
"A Seedy Place", "A Balanced Diet", "Serious Dedication", "Whatever Floats Your Goat!",
|
||||
"Glow and Behold!", "Wax On", "Wax Off", "The Cutest Predator",
|
||||
"The Healing Power of Friendship"],
|
||||
|
||||
"Archipelago": ["Getting Wood", "Time to Mine!", "Hot Topic", "Bake Bread", "The Lie", "On a Rail",
|
||||
"Time to Strike!", "Cow Tipper", "When Pigs Fly", "Overkill", "Librarian", "Overpowered"]
|
||||
}
|
||||
|
||||
|
||||
class MinecraftWorld(World):
|
||||
"""
|
||||
|
||||
@@ -101,7 +101,6 @@ class InteriorEntrances(Choice):
|
||||
option_off = 0
|
||||
option_simple = 1
|
||||
option_all = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
@@ -141,7 +140,6 @@ class MixEntrancePools(Choice):
|
||||
option_off = 0
|
||||
option_indoor = 1
|
||||
option_all = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class DecoupleEntrances(Toggle):
|
||||
@@ -308,7 +306,6 @@ class ShopShuffle(Choice):
|
||||
option_off = 0
|
||||
option_fixed_number = 1
|
||||
option_random_number = 2
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ShopSlots(Range):
|
||||
@@ -326,7 +323,6 @@ class TokenShuffle(Choice):
|
||||
option_dungeons = 1
|
||||
option_overworld = 2
|
||||
option_all = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class ScrubShuffle(Choice):
|
||||
@@ -336,7 +332,6 @@ class ScrubShuffle(Choice):
|
||||
option_low = 1
|
||||
option_regular = 2
|
||||
option_random_prices = 3
|
||||
alias_false = 0
|
||||
alias_affordable = 1
|
||||
alias_expensive = 2
|
||||
|
||||
@@ -569,7 +564,6 @@ class Hints(Choice):
|
||||
option_agony = 2
|
||||
option_always = 3
|
||||
default = 3
|
||||
alias_false = 0
|
||||
|
||||
|
||||
class MiscHints(DefaultOnToggle):
|
||||
@@ -673,8 +667,6 @@ class IceTraps(Choice):
|
||||
option_mayhem = 3
|
||||
option_onslaught = 4
|
||||
default = 1
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
alias_extra = 2
|
||||
|
||||
|
||||
@@ -742,7 +734,6 @@ class Music(Choice):
|
||||
option_normal = 0
|
||||
option_off = 1
|
||||
option_randomized = 2
|
||||
alias_false = 1
|
||||
|
||||
|
||||
class BackgroundMusic(Music):
|
||||
|
||||
@@ -9,7 +9,7 @@ from .Location import OOTLocation, LocationFactory, location_name_to_id
|
||||
from .Entrance import OOTEntrance
|
||||
from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError
|
||||
from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type
|
||||
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool
|
||||
from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool, normal_bottles
|
||||
from .Regions import OOTRegion, TimeOfDay
|
||||
from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
|
||||
from .RuleParser import Rule_AST_Transformer
|
||||
@@ -87,6 +87,133 @@ class OOTWeb(WebWorld):
|
||||
|
||||
tutorials = [setup, setup_es]
|
||||
|
||||
def modify_tracker(self, tracker):
|
||||
tracker.template = 'zeldaKeysTracker.html'
|
||||
tracker.icons = {
|
||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
|
||||
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
|
||||
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
|
||||
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
|
||||
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
|
||||
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
|
||||
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
|
||||
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
|
||||
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
|
||||
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
|
||||
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
|
||||
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
|
||||
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
|
||||
"Din's Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
|
||||
"Farore's Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
|
||||
"Nayru's Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
|
||||
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
|
||||
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
|
||||
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
|
||||
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
|
||||
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
|
||||
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
|
||||
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
|
||||
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
|
||||
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
|
||||
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
|
||||
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
|
||||
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
|
||||
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
|
||||
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
|
||||
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
|
||||
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
|
||||
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
|
||||
"Gold Skulltula Tokens": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
|
||||
"Triforce Pieces": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
|
||||
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
|
||||
"Zelda's Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Epona's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Saria's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Sun's Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
|
||||
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
|
||||
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
|
||||
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
|
||||
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
|
||||
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
|
||||
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
|
||||
"Big Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
|
||||
}
|
||||
|
||||
tracker.progressive_items = [
|
||||
"Ocarina", "Bombs", "Bow", "Fire Arrows", "Kokiri Sword", "Biggoron Sword", "Mirror Shield",
|
||||
"Slingshot", "Bombchus", "Progressive Hookshot", "Ice Arrows", "Progressive Strength", "Goron Tunic", "Zora Tunic",
|
||||
"Boomerang", "Lens of Truth", "Megaton Hammer", "Light Arrows", "Progressive Scale", "Iron Boots", "Hover Boots",
|
||||
"Bottles", "Din's Fire", "Farore's Wind", "Nayru's Love", "Progressive Wallet", "Magic Meter", "Gerudo Membership Card",
|
||||
"Zelda's Lullaby", "Epona's Song", "Saria's Song", "Sun's Song", "Song of Time", "Song of Storms", "Gold Skulltula Tokens",
|
||||
"Minuet of Forest", "Bolero of Fire", "Serenade of Water", "Requiem of Spirit", "Nocturne of Shadow", "Prelude of Light", "Triforce Pieces",
|
||||
"Small Key", "Big Key"
|
||||
]
|
||||
|
||||
tracker.progressive_names = {
|
||||
"Progressive Hookshot": ["Hookshot", "Longshot"],
|
||||
"Progressive Strength": ["Goron Bracelet", "Golden Gauntlets"],
|
||||
"Progressive Wallet": ["Adults Wallet", "Giants Wallet"],
|
||||
"Progressive Scale": ["Silver Scale", "Gold Scale"],
|
||||
"Magic Meter": ["Small Magic", "Large Magic"],
|
||||
"Ocarina": ["Fairy Ocarina", "Ocarina of Time"],
|
||||
"Bottles": normal_bottles + ["Rutos Letter"]
|
||||
}
|
||||
|
||||
location_id_to_name = {}
|
||||
for name, id in location_name_to_id.items():
|
||||
location_id_to_name[id] = name
|
||||
tracker.regions = {}
|
||||
for id in location_id_to_name.keys():
|
||||
if id in location_name_to_id.values() and location_id_to_name[id] in tracker.all_locations:
|
||||
if id < 67259:
|
||||
tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id])
|
||||
elif id < 67264:
|
||||
tracker.regions.setdefault("Thieves' Hideout", []).append(location_id_to_name[id])
|
||||
elif id < 67281:
|
||||
tracker.regions.setdefault("Overworld", []).append(location_id_to_name[id])
|
||||
elif id < 67304:
|
||||
tracker.regions.setdefault("Deku Tree", []).append(location_id_to_name[id])
|
||||
elif id < 67335:
|
||||
tracker.regions.setdefault("Dodongo's Cavern", []).append(location_id_to_name[id])
|
||||
elif id < 67360:
|
||||
tracker.regions.setdefault("Jabu Jabu's Belly", []).append(location_id_to_name[id])
|
||||
elif id < 67385:
|
||||
tracker.regions.setdefault("Bottom of the Well", []).append(location_id_to_name[id])
|
||||
elif id < 67421:
|
||||
tracker.regions.setdefault("Forest Temple", []).append(location_id_to_name[id])
|
||||
elif id < 67458:
|
||||
tracker.regions.setdefault("Fire Temple", []).append(location_id_to_name[id])
|
||||
elif id < 67485:
|
||||
tracker.regions.setdefault("Water Temple", []).append(location_id_to_name[id])
|
||||
elif id < 67533:
|
||||
tracker.regions.setdefault("Shadow Temple", []).append(location_id_to_name[id])
|
||||
elif id < 67583:
|
||||
tracker.regions.setdefault("Spirit Temple", []).append(location_id_to_name[id])
|
||||
elif id < 67597:
|
||||
tracker.regions.setdefault("Ice Cavern", []).append(location_id_to_name[id])
|
||||
elif id < 67636:
|
||||
tracker.regions.setdefault("Gerudo Training Ground", []).append(location_id_to_name[id])
|
||||
elif id < 67674:
|
||||
tracker.regions.setdefault("Ganon's Castle", []).append(location_id_to_name[id])
|
||||
|
||||
tracker.region_keys = {
|
||||
'Forest Temple': ['Small Key (Forest Temple)', 'Boss Key (Forest Temple)'],
|
||||
'Fire Temple': ['Small Key (Fire Temple)', 'Boss Key (Forest Temple)'],
|
||||
'Water Temple': ['Small Key (Water Temple)', 'Boss Key (Water Temple)'],
|
||||
'Spirit Temple': ['Small Key (Spirit Temple)', 'Boss Key (Spirit Temple)'],
|
||||
'Shadow Temple': ['Small Key (Shadow Temple)', 'Boss Key (Shadow Temple)'],
|
||||
'Bottom of the Well': ['Small Key (Bottom of the Well)', 'Boss Key (Bottom of the Well)'],
|
||||
'Gerudo Training Ground': ['Small Key (Gerudo Training Ground)', 'Boss Key (Gerudo Training Ground)'],
|
||||
'Thieves Hideout': ['Small Key (Thieves Hideout)', 'Boss Key (Thieves Hideout)'],
|
||||
'Ganons Castle': ['Small Key (Ganons Castle)', 'Boss Key (Ganons Castle)']
|
||||
}
|
||||
|
||||
|
||||
class OOTWorld(World):
|
||||
"""
|
||||
|
||||
@@ -282,8 +282,7 @@ class SA2BWorld(World):
|
||||
spoiler_handle.writelines(text)
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Sonic Adventure 2 Battle"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 0 if (item.name != 'Emblem') else 1)
|
||||
|
||||
@@ -163,3 +163,17 @@ filler_items: typing.Tuple[str, ...] = (
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
|
||||
data.code}
|
||||
# Map type to expected int
|
||||
type_flaggroups: typing.Dict[str, int] = {
|
||||
"Unit": 0,
|
||||
"Upgrade": 1,
|
||||
"Armory 1": 2,
|
||||
"Armory 2": 3,
|
||||
"Building": 4,
|
||||
"Mercenary": 5,
|
||||
"Laboratory": 6,
|
||||
"Protoss": 7,
|
||||
"Minerals": 8,
|
||||
"Vespene": 9,
|
||||
"Supply": 10,
|
||||
}
|
||||
|
||||
@@ -122,8 +122,6 @@ class AreaRandomization(Choice):
|
||||
option_off = 0
|
||||
option_light = 1
|
||||
option_on = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 0
|
||||
|
||||
class AreaLayout(Toggle):
|
||||
|
||||
@@ -660,8 +660,7 @@ class SMWorld(World):
|
||||
loc.address = loc.item.code = None
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
|
||||
restitempool, fill_locations):
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Super Metroid"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
|
||||
|
||||
@@ -1,48 +1,57 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
|
||||
|
||||
|
||||
class EnableCoinStars(DefaultOnToggle):
|
||||
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
|
||||
display_name = "Enable 100 Coin Stars"
|
||||
|
||||
|
||||
class StrictCapRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect special caps may have to be acquired without the caps"""
|
||||
display_name = "Strict Cap Requirements"
|
||||
|
||||
|
||||
class StrictCannonRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled"""
|
||||
display_name = "Strict Cannon Requirements"
|
||||
|
||||
|
||||
class FirstBowserStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door to Bowser in the Dark World"""
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 8
|
||||
|
||||
|
||||
class BasementStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door in the Basement"""
|
||||
range_start = 0
|
||||
range_end = 70
|
||||
default = 30
|
||||
|
||||
|
||||
class SecondFloorStarDoorCost(Range):
|
||||
"""How many stars are required to access the third floor"""
|
||||
range_start = 0
|
||||
range_end = 90
|
||||
default = 50
|
||||
|
||||
|
||||
class MIPS1Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the first time"""
|
||||
range_start = 0
|
||||
range_end = 40
|
||||
default = 15
|
||||
|
||||
|
||||
class MIPS2Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
|
||||
|
||||
class StarsToFinish(Range):
|
||||
"""How many stars are required at the infinite stairs"""
|
||||
display_name = "Endless Stairs Stars"
|
||||
@@ -50,35 +59,40 @@ class StarsToFinish(Range):
|
||||
range_end = 100
|
||||
default = 70
|
||||
|
||||
|
||||
class AmountOfStars(Range):
|
||||
"""How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
|
||||
range_start = 35
|
||||
range_end = 120
|
||||
default = 120
|
||||
|
||||
|
||||
class AreaRandomizer(Choice):
|
||||
"""Randomize Entrances"""
|
||||
display_name = "Entrance Randomizer"
|
||||
alias_false = 0
|
||||
option_Off = 0
|
||||
option_Courses_Only = 1
|
||||
option_Courses_and_Secrets = 2
|
||||
|
||||
|
||||
class BuddyChecks(Toggle):
|
||||
"""Bob-omb Buddies are checks, Cannon Unlocks are items"""
|
||||
display_name = "Bob-omb Buddy Checks"
|
||||
|
||||
|
||||
class ExclamationBoxes(Choice):
|
||||
"""Include 1Up Exclamation Boxes during randomization"""
|
||||
display_name = "Randomize 1Up !-Blocks"
|
||||
option_Off = 0
|
||||
option_1Ups_Only = 1
|
||||
|
||||
|
||||
class ProgressiveKeys(DefaultOnToggle):
|
||||
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
|
||||
display_name = "Progressive Keys"
|
||||
|
||||
sm64_options: typing.Dict[str,type(Option)] = {
|
||||
|
||||
sm64_options: typing.Dict[str, type(Option)] = {
|
||||
"AreaRandomizer": AreaRandomizer,
|
||||
"ProgressiveKeys": ProgressiveKeys,
|
||||
"EnableCoinStars": EnableCoinStars,
|
||||
@@ -93,5 +107,5 @@ sm64_options: typing.Dict[str,type(Option)] = {
|
||||
"StarsToFinish": StarsToFinish,
|
||||
"death_link": DeathLink,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
"ExclamationBoxes": ExclamationBoxes
|
||||
}
|
||||
"ExclamationBoxes": ExclamationBoxes,
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ def set_rules(world, player: int, area_connections):
|
||||
# which would make it impossible to reach downtown area without the cannon.
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
|
||||
if world.StrictCapRequirements[player]:
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))
|
||||
|
||||
@@ -9,6 +9,7 @@ from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internal
|
||||
from BaseClasses import Item, Tutorial, ItemClassification
|
||||
from ..AutoWorld import World, WebWorld
|
||||
|
||||
|
||||
class SM64Web(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
|
||||
@@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice):
|
||||
option_Half = 2
|
||||
option_Normal = 3
|
||||
option_Double = 4
|
||||
alias_false = 0
|
||||
default = 3
|
||||
|
||||
class HeartColor(Choice):
|
||||
|
||||
@@ -21,8 +21,6 @@ class OffOnFullChoice(Choice):
|
||||
option_on = 1
|
||||
option_full = 2
|
||||
alias_chaos = 2
|
||||
alias_false = 0
|
||||
alias_true = 1
|
||||
|
||||
|
||||
class Difficulty(EvermizerFlags, Choice):
|
||||
|
||||
@@ -139,7 +139,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Power Transmitter Fragment',
|
||||
'tech_type': 'PowerTransmitterFragment'},
|
||||
35032: {'classification': ItemClassification.progression,
|
||||
'count': 4,
|
||||
'count': 5,
|
||||
'name': 'Prawn Suit Fragment',
|
||||
'tech_type': 'ExosuitFragment'},
|
||||
35033: {'classification': ItemClassification.useful,
|
||||
@@ -163,7 +163,7 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Scanner Room Fragment',
|
||||
'tech_type': 'BaseMapRoomFragment'},
|
||||
35038: {'classification': ItemClassification.progression,
|
||||
'count': 5,
|
||||
'count': 4,
|
||||
'name': 'Seamoth Fragment',
|
||||
'tech_type': 'SeamothFragment'},
|
||||
35039: {'classification': ItemClassification.progression,
|
||||
@@ -203,9 +203,9 @@ item_table: Dict[int, ItemDict] = {
|
||||
'name': 'Picture Frame',
|
||||
'tech_type': 'PictureFrameFragment'},
|
||||
35048: {'classification': ItemClassification.filler,
|
||||
'count': 2,
|
||||
'name': 'Bench Fragment',
|
||||
'tech_type': 'BenchFragment'},
|
||||
'count': 1,
|
||||
'name': 'Bench',
|
||||
'tech_type': 'Bench'},
|
||||
35049: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Basic Plant Pot',
|
||||
@@ -333,7 +333,12 @@ item_table: Dict[int, ItemDict] = {
|
||||
35080: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'Water Filtration Machine',
|
||||
'tech_type': 'BaseFiltrationMachine'}}
|
||||
'tech_type': 'BaseFiltrationMachine'},
|
||||
35081: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Ultra High Capacity Tank',
|
||||
'tech_type': 'HighCapacityTank'},
|
||||
}
|
||||
|
||||
advancement_item_names: Set[str] = set()
|
||||
non_advancement_item_names: Set[str] = set()
|
||||
|
||||
@@ -41,7 +41,7 @@ class SubnauticaWorld(World):
|
||||
location_name_to_id = all_locations
|
||||
option_definitions = Options.options
|
||||
|
||||
data_version = 6
|
||||
data_version = 7
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
prefill_items: List[Item]
|
||||
|
||||
@@ -3,66 +3,82 @@ from BaseClasses import MultiWorld
|
||||
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict
|
||||
from schema import Schema, And, Optional
|
||||
|
||||
|
||||
class StartWithJewelryBox(Toggle):
|
||||
"Start with Jewelry Box unlocked"
|
||||
display_name = "Start with Jewelry Box"
|
||||
|
||||
|
||||
#class ProgressiveVerticalMovement(Toggle):
|
||||
# "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash"
|
||||
# display_name = "Progressive vertical movement"
|
||||
|
||||
|
||||
#class ProgressiveKeycards(Toggle):
|
||||
# "Always find Security Keycard's in the following order D -> C -> B -> A"
|
||||
# display_name = "Progressive keycards"
|
||||
|
||||
|
||||
class DownloadableItems(DefaultOnToggle):
|
||||
"With the tablet you will be able to download items at terminals"
|
||||
display_name = "Downloadable items"
|
||||
|
||||
|
||||
class EyeSpy(Toggle):
|
||||
"Requires Oculus Ring in inventory to be able to break hidden walls."
|
||||
display_name = "Eye Spy"
|
||||
|
||||
|
||||
class StartWithMeyef(Toggle):
|
||||
"Start with Meyef, ideal for when you want to play multiplayer."
|
||||
display_name = "Start with Meyef"
|
||||
|
||||
|
||||
class QuickSeed(Toggle):
|
||||
"Start with Talaria Attachment, Nyoom!"
|
||||
display_name = "Quick seed"
|
||||
|
||||
|
||||
class SpecificKeycards(Toggle):
|
||||
"Keycards can only open corresponding doors"
|
||||
display_name = "Specific Keycards"
|
||||
|
||||
|
||||
class Inverted(Toggle):
|
||||
"Start in the past"
|
||||
display_name = "Inverted"
|
||||
|
||||
|
||||
#class StinkyMaw(Toggle):
|
||||
# "Require gasmask for Maw"
|
||||
# display_name = "Stinky Maw"
|
||||
|
||||
|
||||
class GyreArchives(Toggle):
|
||||
"Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
|
||||
display_name = "Gyre Archives"
|
||||
|
||||
|
||||
class Cantoran(Toggle):
|
||||
"Cantoran's fight and check are available upon revisiting his room"
|
||||
display_name = "Cantoran"
|
||||
|
||||
|
||||
class LoreChecks(Toggle):
|
||||
"Memories and journal entries contain items."
|
||||
display_name = "Lore Checks"
|
||||
|
||||
|
||||
class BossRando(Toggle):
|
||||
"Shuffles the positions of all bosses."
|
||||
display_name = "Boss Randomization"
|
||||
|
||||
|
||||
class BossScaling(DefaultOnToggle):
|
||||
"When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)"
|
||||
display_name = "Scale Random Boss Stats"
|
||||
|
||||
|
||||
class DamageRando(Choice):
|
||||
"Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
|
||||
display_name = "Damage Rando"
|
||||
@@ -73,9 +89,9 @@ class DamageRando(Choice):
|
||||
option_mostlybuffs = 4
|
||||
option_allbuffs = 5
|
||||
option_manual = 6
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class DamageRandoOverrides(OptionDict):
|
||||
"Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds"
|
||||
schema = Schema({
|
||||
@@ -180,6 +196,7 @@ class DamageRandoOverrides(OptionDict):
|
||||
"Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
|
||||
}
|
||||
|
||||
|
||||
class HpCap(Range):
|
||||
"Sets the number that Lunais's HP maxes out at."
|
||||
display_name = "HP Cap"
|
||||
@@ -187,10 +204,12 @@ class HpCap(Range):
|
||||
range_end = 999
|
||||
default = 999
|
||||
|
||||
|
||||
class BossHealing(DefaultOnToggle):
|
||||
"Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
|
||||
display_name = "Heal After Bosses"
|
||||
|
||||
|
||||
class ShopFill(Choice):
|
||||
"""Sets the items for sale in Merchant Crow's shops.
|
||||
Default: No sunglasses or trendy jacket, but sand vials for sale.
|
||||
@@ -203,10 +222,12 @@ class ShopFill(Choice):
|
||||
option_vanilla = 2
|
||||
option_empty = 3
|
||||
|
||||
|
||||
class ShopWarpShards(DefaultOnToggle):
|
||||
"Shops always sell warp shards (when keys possessed), ignoring inventory setting."
|
||||
display_name = "Always Sell Warp Shards"
|
||||
|
||||
|
||||
class ShopMultiplier(Range):
|
||||
"Multiplier for the cost of items in the shop. Set to 0 for free shops."
|
||||
display_name = "Shop Price Multiplier"
|
||||
@@ -214,6 +235,7 @@ class ShopMultiplier(Range):
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class LootPool(Choice):
|
||||
"""Sets the items that drop from enemies (does not apply to boss reward checks)
|
||||
Vanilla: Drops are the same as the base game
|
||||
@@ -224,6 +246,7 @@ class LootPool(Choice):
|
||||
option_randomized = 1
|
||||
option_empty = 2
|
||||
|
||||
|
||||
class DropRateCategory(Choice):
|
||||
"""Sets the drop rate when 'Loot Pool' is set to 'Random'
|
||||
Tiered: Based on item rarity/value
|
||||
@@ -237,6 +260,7 @@ class DropRateCategory(Choice):
|
||||
option_randomized = 2
|
||||
option_fixed = 3
|
||||
|
||||
|
||||
class FixedDropRate(Range):
|
||||
"Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
|
||||
display_name = "Fixed Drop Rate"
|
||||
@@ -244,6 +268,7 @@ class FixedDropRate(Range):
|
||||
range_end = 100
|
||||
default = 5
|
||||
|
||||
|
||||
class LootTierDistro(Choice):
|
||||
"""Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
|
||||
Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
|
||||
@@ -255,14 +280,17 @@ class LootTierDistro(Choice):
|
||||
option_full_random = 1
|
||||
option_inverted_weight = 2
|
||||
|
||||
|
||||
class ShowBestiary(Toggle):
|
||||
"All entries in the bestiary are visible, without needing to kill one of a given enemy first"
|
||||
display_name = "Show Bestiary Entries"
|
||||
|
||||
|
||||
class ShowDrops(Toggle):
|
||||
"All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
|
||||
display_name = "Show Bestiary Item Drops"
|
||||
|
||||
|
||||
# Some options that are available in the timespinner randomizer arent currently implemented
|
||||
timespinner_options: Dict[str, Option] = {
|
||||
"StartWithJewelryBox": StartWithJewelryBox,
|
||||
@@ -296,9 +324,11 @@ timespinner_options: Dict[str, Option] = {
|
||||
"DeathLink": DeathLink,
|
||||
}
|
||||
|
||||
|
||||
def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
|
||||
return get_option_value(world, player, name) > 0
|
||||
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]:
|
||||
option = getattr(world, name, None)
|
||||
if option == None:
|
||||
|
||||
@@ -15,9 +15,9 @@ class DisableNonRandomizedPuzzles(DefaultOnToggle):
|
||||
|
||||
|
||||
class EarlySecretArea(Toggle):
|
||||
"""Opens the Mountainside shortcut to the Mountain Secret Area from the start.
|
||||
"""Opens the Mountainside shortcut to the Caves from the start.
|
||||
(Otherwise known as "UTM", "Caves" or the "Challenge Area")"""
|
||||
display_name = "Early Secret Area"
|
||||
display_name = "Early Caves"
|
||||
|
||||
|
||||
class ShuffleSymbols(DefaultOnToggle):
|
||||
@@ -58,15 +58,9 @@ class ShuffleVaultBoxes(Toggle):
|
||||
display_name = "Shuffle Vault Boxes"
|
||||
|
||||
|
||||
class ShuffleUncommonLocations(Toggle):
|
||||
"""Adds some optional puzzles that are somewhat difficult or out of the way.
|
||||
Examples: Mountaintop River Shape, Tutorial Patio Floor, Theater Videos"""
|
||||
display_name = "Shuffle Uncommon Locations"
|
||||
|
||||
|
||||
class ShufflePostgame(Toggle):
|
||||
"""Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with
|
||||
forfeit on victory."""
|
||||
"""Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal.
|
||||
Use this if you don't play with forfeit on victory."""
|
||||
display_name = "Shuffle Postgame"
|
||||
|
||||
|
||||
@@ -90,7 +84,7 @@ class MountainLasers(Range):
|
||||
|
||||
|
||||
class ChallengeLasers(Range):
|
||||
"""Sets the amount of beams required to enter the secret area through the Mountain Bottom Layer Discard."""
|
||||
"""Sets the amount of beams required to enter the Caves through the Mountain Bottom Floor Discard."""
|
||||
display_name = "Required Lasers for Challenge"
|
||||
range_start = 1
|
||||
range_end = 11
|
||||
@@ -122,7 +116,6 @@ the_witness_options: Dict[str, type] = {
|
||||
"disable_non_randomized_puzzles": DisableNonRandomizedPuzzles,
|
||||
"shuffle_discarded_panels": ShuffleDiscardedPanels,
|
||||
"shuffle_vault_boxes": ShuffleVaultBoxes,
|
||||
"shuffle_uncommon": ShuffleUncommonLocations,
|
||||
"shuffle_postgame": ShufflePostgame,
|
||||
"victory_condition": VictoryCondition,
|
||||
"mountain_lasers": MountainLasers,
|
||||
|
||||
@@ -28,38 +28,38 @@ Traps:
|
||||
610 - Power Surge
|
||||
|
||||
Doors:
|
||||
1100 - Glass Factory Entry Door (Panel) - 0x01A54
|
||||
1105 - Door to Symmetry Island Lower (Panel) - 0x000B0
|
||||
1107 - Door to Symmetry Island Upper (Panel) - 0x1C349
|
||||
1110 - Door to Desert Flood Light Room (Panel) - 0x0C339
|
||||
1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
|
||||
1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59
|
||||
1100 - Glass Factory Entry (Panel) - 0x01A54
|
||||
1105 - Symmetry Island Lower (Panel) - 0x000B0
|
||||
1107 - Symmetry Island Upper (Panel) - 0x1C349
|
||||
1110 - Desert Light Room Entry (Panel) - 0x0C339
|
||||
1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B
|
||||
1119 - Quarry Mill Entry (Panel) - 0x01E5A,0x01E59
|
||||
1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676
|
||||
1122 - Quarry Mill Elevator Controls (Panel) - 0x03679,0x03675
|
||||
1122 - Quarry Mill Lift Controls (Panel) - 0x03679,0x03675
|
||||
1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852
|
||||
1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858
|
||||
1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC
|
||||
1150 - Monastery Entry Door Left (Panel) - 0x00B10
|
||||
1151 - Monastery Entry Door Right (Panel) - 0x00C92
|
||||
1162 - Town Door to RGB House (Panel) - 0x28998
|
||||
1163 - Town Door to Church (Panel) - 0x28A0D
|
||||
1150 - Monastery Entry Left (Panel) - 0x00B10
|
||||
1151 - Monastery Entry Right (Panel) - 0x00C92
|
||||
1162 - Town Tinted Glass Door (Panel) - 0x28998
|
||||
1163 - Town Church Entry (Panel) - 0x28A0D
|
||||
1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79
|
||||
1169 - Windmill Door (Panel) - 0x17F5F
|
||||
1169 - Windmill Entry (Panel) - 0x17F5F
|
||||
1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886
|
||||
1202 - Treehouse Third Door (Panel) - 0x0A182
|
||||
1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC
|
||||
1208 - Treehouse Shortcut Drop-Down Bridge (Panel) - 0x17CBC
|
||||
1208 - Treehouse Drawbridge (Panel) - 0x17CBC
|
||||
1175 - Jungle Popup Wall (Panel) - 0x17CAB
|
||||
1180 - Bunker Entry Door (Panel) - 0x17C2E
|
||||
1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099
|
||||
1180 - Bunker Entry (Panel) - 0x17C2E
|
||||
1183 - Bunker Tinted Glass Door (Panel) - 0x0A099
|
||||
1186 - Bunker Elevator Control (Panel) - 0x0A079
|
||||
1190 - Swamp Entry Door (Panel) - 0x0056E
|
||||
1190 - Swamp Entry (Panel) - 0x0056E
|
||||
1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488
|
||||
1195 - Swamp Rotating Bridge (Panel) - 0x181F5
|
||||
1197 - Swamp Maze Control (Panel) - 0x17C0A
|
||||
1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054
|
||||
|
||||
1400 - Caves Mountain Shortcut - 0x2D73F
|
||||
1400 - Caves Mountain Shortcut (Door) - 0x2D73F
|
||||
|
||||
1500 - Symmetry Laser - 0x00509
|
||||
1501 - Desert Laser - 0x012FB,0x01317
|
||||
@@ -73,101 +73,101 @@ Doors:
|
||||
1509 - Swamp Laser - 0x00BF6
|
||||
1510 - Treehouse Laser - 0x028A4
|
||||
|
||||
1600 - Outside Tutorial Optional Door - 0x03BA2
|
||||
1603 - Outside Tutorial Outpost Entry Door - 0x0A170
|
||||
1606 - Outside Tutorial Outpost Exit Door - 0x04CA3
|
||||
1609 - Glass Factory Entry Door - 0x01A29
|
||||
1612 - Glass Factory Back Wall - 0x0D7ED
|
||||
1615 - Symmetry Island Lower Door - 0x17F3E
|
||||
1618 - Symmetry Island Upper Door - 0x18269
|
||||
1619 - Orchard Middle Gate - 0x03307
|
||||
1620 - Orchard Final Gate - 0x03313
|
||||
1621 - Desert Door to Flood Light Room - 0x09FEE
|
||||
1624 - Desert Door to Pond Room - 0x0C2C3
|
||||
1627 - Desert Door to Water Levels Room - 0x0A24B
|
||||
1630 - Desert Door to Elevator Room - 0x0C316
|
||||
1633 - Quarry Main Entry 1 - 0x09D6F
|
||||
1636 - Quarry Main Entry 2 - 0x17C07
|
||||
1639 - Quarry Door to Mill - 0x02010
|
||||
1642 - Quarry Mill Side Door - 0x275FF
|
||||
1645 - Quarry Mill Rooftop Shortcut - 0x17CE8
|
||||
1648 - Quarry Mill Stairs - 0x0368A
|
||||
1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163
|
||||
1653 - Quarry Boathouse First Barrier - 0x17C50
|
||||
1654 - Quarry Boathouse Shortcut - 0x3865F
|
||||
1656 - Shadows Timed Door - 0x19B24
|
||||
1657 - Shadows Laser Room Right Door - 0x194B2
|
||||
1660 - Shadows Laser Room Left Door - 0x19665
|
||||
1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF
|
||||
1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE
|
||||
1669 - Keep Hedge Maze 1 Exit Door - 0x01954
|
||||
1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC
|
||||
1675 - Keep Hedge Maze 2 Shortcut - 0x018CE
|
||||
1678 - Keep Hedge Maze 2 Exit Door - 0x019D8
|
||||
1681 - Keep Hedge Maze 3 Shortcut - 0x019B5
|
||||
1684 - Keep Hedge Maze 3 Exit Door - 0x019E6
|
||||
1687 - Keep Hedge Maze 4 Shortcut - 0x0199A
|
||||
1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E
|
||||
1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA
|
||||
1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5
|
||||
1699 - Keep Pressure Plates 4 Exit Door - 0x01D40
|
||||
1702 - Keep Shortcut to Shadows - 0x09E3D
|
||||
1705 - Keep Tower Shortcut - 0x04F8F
|
||||
1708 - Monastery Shortcut - 0x0364E
|
||||
1711 - Monastery Inner Door - 0x0C128
|
||||
1714 - Monastery Outer Door - 0x0C153
|
||||
1717 - Monastery Door to Garden - 0x03750
|
||||
1718 - Town Cargo Box Door - 0x0A0C9
|
||||
1720 - Town Wooden Roof Staircase - 0x034F5
|
||||
1723 - Town Tinted Door to RGB House - 0x28A61
|
||||
1726 - Town Door to Church - 0x03BB0
|
||||
1729 - Town Maze Staircase - 0x28AA2
|
||||
1732 - Town Windmill Door - 0x1845B
|
||||
1735 - Town RGB House Staircase - 0x2897B
|
||||
1738 - Town Tower Blue Panels Door - 0x27798
|
||||
1741 - Town Tower Lattice Door - 0x27799
|
||||
1744 - Town Tower Environmental Set Door - 0x2779A
|
||||
1747 - Town Tower Wooden Roof Set Door - 0x2779C
|
||||
1750 - Theater Entry Door - 0x17F88
|
||||
1753 - Theater Exit Door Left - 0x0A16D
|
||||
1756 - Theater Exit Door Right - 0x3CCDF
|
||||
1759 - Jungle Bamboo Shortcut to River - 0x3873B
|
||||
1760 - Jungle Popup Wall - 0x1475B
|
||||
1762 - River Shortcut to Monastery Garden - 0x0CF2A
|
||||
1765 - Bunker Bunker Entry Door - 0x0C2A4
|
||||
1768 - Bunker Tinted Glass Door - 0x17C79
|
||||
1771 - Bunker Door to Ultraviolet Room - 0x0C2A3
|
||||
1774 - Bunker Door to Elevator - 0x0A08D
|
||||
1777 - Swamp Entry Door - 0x00C1C
|
||||
1780 - Swamp Door to Broken Shapers - 0x184B7
|
||||
1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2
|
||||
1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170
|
||||
1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3
|
||||
1609 - Glass Factory Entry (Door) - 0x01A29
|
||||
1612 - Glass Factory Back Wall (Door) - 0x0D7ED
|
||||
1615 - Symmetry Island Lower (Door) - 0x17F3E
|
||||
1618 - Symmetry Island Upper (Door) - 0x18269
|
||||
1619 - Orchard First Gate (Door) - 0x03307
|
||||
1620 - Orchard Second Gate (Door) - 0x03313
|
||||
1621 - Desert Light Room Entry (Door) - 0x09FEE
|
||||
1624 - Desert Pond Room Entry (Door) - 0x0C2C3
|
||||
1627 - Desert Flood Room Entry (Door) - 0x0A24B
|
||||
1630 - Desert Elevator Room Entry (Door) - 0x0C316
|
||||
1633 - Quarry Entry 1 (Door) - 0x09D6F
|
||||
1636 - Quarry Entry 2 (Door) - 0x17C07
|
||||
1639 - Quarry Mill Entry (Door) - 0x02010
|
||||
1642 - Quarry Mill Side Exit (Door) - 0x275FF
|
||||
1645 - Quarry Mill Roof Exit (Door) - 0x17CE8
|
||||
1648 - Quarry Mill Stairs (Door) - 0x0368A
|
||||
1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163
|
||||
1653 - Quarry Boathouse First Barrier (Door) - 0x17C50
|
||||
1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F
|
||||
1656 - Shadows Timed Door (Door) - 0x19B24
|
||||
1657 - Shadows Laser Entry Right (Door) - 0x194B2
|
||||
1660 - Shadows Laser Entry Left (Door) - 0x19665
|
||||
1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF
|
||||
1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE
|
||||
1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954
|
||||
1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC
|
||||
1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE
|
||||
1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8
|
||||
1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5
|
||||
1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6
|
||||
1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A
|
||||
1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E
|
||||
1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA
|
||||
1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5
|
||||
1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40
|
||||
1702 - Keep Shadows Shortcut (Door) - 0x09E3D
|
||||
1705 - Keep Tower Shortcut (Door) - 0x04F8F
|
||||
1708 - Monastery Shortcut (Door) - 0x0364E
|
||||
1711 - Monastery Entry Inner (Door) - 0x0C128
|
||||
1714 - Monastery Entry Outer (Door) - 0x0C153
|
||||
1717 - Monastery Garden Entry (Door) - 0x03750
|
||||
1718 - Town Cargo Box Entry (Door) - 0x0A0C9
|
||||
1720 - Town Wooden Roof Stairs (Door) - 0x034F5
|
||||
1723 - Town Tinted Glass Door (Door) - 0x28A61
|
||||
1726 - Town Church Entry (Door) - 0x03BB0
|
||||
1729 - Town Maze Stairs (Door) - 0x28AA2
|
||||
1732 - Town Windmill Entry (Door) - 0x1845B
|
||||
1735 - Town RGB House Stairs (Door) - 0x2897B
|
||||
1738 - Town Tower First Door (Door) - 0x27798
|
||||
1741 - Town Tower Third Door (Door) - 0x27799
|
||||
1744 - Town Tower Fourth Door (Door) - 0x2779A
|
||||
1747 - Town Tower Second Door (Door) - 0x2779C
|
||||
1750 - Theater Entry (Door) - 0x17F88
|
||||
1753 - Theater Exit Left (Door) - 0x0A16D
|
||||
1756 - Theater Exit Right (Door) - 0x3CCDF
|
||||
1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B
|
||||
1760 - Jungle Popup Wall (Door) - 0x1475B
|
||||
1762 - River Monastery Shortcut (Door) - 0x0CF2A
|
||||
1765 - Bunker Entry (Door) - 0x0C2A4
|
||||
1768 - Bunker Tinted Glass Door (Door) - 0x17C79
|
||||
1771 - Bunker UV Room Entry (Door) - 0x0C2A3
|
||||
1774 - Bunker Elevator Room Entry (Door) - 0x0A08D
|
||||
1777 - Swamp Entry (Door) - 0x00C1C
|
||||
1780 - Swamp Between Bridges First Door - 0x184B7
|
||||
1783 - Swamp Platform Shortcut Door - 0x38AE6
|
||||
1786 - Swamp Cyan Water Pump - 0x04B7F
|
||||
1789 - Swamp Door to Rotated Shapers - 0x18507
|
||||
1792 - Swamp Red Water Pump - 0x183F2
|
||||
1795 - Swamp Red Underwater Exit - 0x305D5
|
||||
1798 - Swamp Blue Water Pump - 0x18482
|
||||
1801 - Swamp Purple Water Pump - 0x0A1D6
|
||||
1804 - Swamp Near Laser Shortcut - 0x2D880
|
||||
1807 - Treehouse First Door - 0x0C309
|
||||
1810 - Treehouse Second Door - 0x0C310
|
||||
1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181
|
||||
1816 - Treehouse Drawbridge - 0x0C32D
|
||||
1819 - Treehouse Timed Door to Laser House - 0x0C323
|
||||
1822 - Inside Mountain First Layer Exit Door - 0x09E54
|
||||
1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB
|
||||
1828 - Inside Mountain Second Layer Exit Door - 0x09EDD
|
||||
1831 - Inside Mountain Second Layer Staircase Far - 0x09E07
|
||||
1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89
|
||||
1840 - Inside Mountain Door to Final Room - 0x0C141
|
||||
1843 - Inside Mountain Bottom Layer Rock - 0x17F33
|
||||
1846 - Inside Mountain Door to Secret Area - 0x2D77D
|
||||
1849 - Caves Pillar Door - 0x019A5
|
||||
1855 - Caves Swamp Shortcut - 0x2D859
|
||||
1858 - Challenge Entry Door - 0x0A19A
|
||||
1861 - Challenge Door to Theater Walkway - 0x0348A
|
||||
1864 - Theater Walkway Door to Windmill Interior - 0x27739
|
||||
1867 - Theater Walkway Door to Desert Elevator Room - 0x27263
|
||||
1870 - Theater Walkway Door to Town - 0x09E87
|
||||
1786 - Swamp Cyan Water Pump (Door) - 0x04B7F
|
||||
1789 - Swamp Between Bridges Second Door - 0x18507
|
||||
1792 - Swamp Red Water Pump (Door) - 0x183F2
|
||||
1795 - Swamp Red Underwater Exit (Door) - 0x305D5
|
||||
1798 - Swamp Blue Water Pump (Door) - 0x18482
|
||||
1801 - Swamp Purple Water Pump (Door) - 0x0A1D6
|
||||
1804 - Swamp Laser Shortcut (Door) - 0x2D880
|
||||
1807 - Treehouse First Door (Door) - 0x0C309
|
||||
1810 - Treehouse Second Door (Door) - 0x0C310
|
||||
1813 - Treehouse Third Door (Door) - 0x0A181
|
||||
1816 - Treehouse Drawbridge (Door) - 0x0C32D
|
||||
1819 - Treehouse Laser House Entry (Door) - 0x0C323
|
||||
1822 - Mountain Floor 1 Exit (Door) - 0x09E54
|
||||
1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB
|
||||
1828 - Mountain Floor 2 Exit (Door) - 0x09EDD
|
||||
1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07
|
||||
1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89
|
||||
1840 - Mountain Bottom Floor Final Room Entry (Door) - 0x0C141
|
||||
1843 - Mountain Bottom Floor Rock (Door) - 0x17F33
|
||||
1846 - Caves Entry (Door) - 0x2D77D
|
||||
1849 - Caves Pillar Door (Door) - 0x019A5
|
||||
1855 - Caves Swamp Shortcut (Door) - 0x2D859
|
||||
1858 - Challenge Entry (Door) - 0x0A19A
|
||||
1861 - Challenge Tunnels Entry (Door) - 0x0348A
|
||||
1864 - Tunnels Theater Shortcut (Door) - 0x27739
|
||||
1867 - Tunnels Desert Shortcut (Door) - 0x27263
|
||||
1870 - Tunnels Town Shortcut (Door) - 0x09E87
|
||||
|
||||
1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3
|
||||
1906 - Symmetry Island Doors - 0x17F3E,0x18269
|
||||
@@ -181,18 +181,18 @@ Doors:
|
||||
1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E
|
||||
1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40
|
||||
1936 - Keep Shortcuts - 0x09E3D,0x04F8F
|
||||
1939 - Monastery Entry Door - 0x0C128,0x0C153
|
||||
1939 - Monastery Entry - 0x0C128,0x0C153
|
||||
1942 - Monastery Shortcuts - 0x0364E,0x03750
|
||||
1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B
|
||||
1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C
|
||||
1951 - Theater Exit Door - 0x0A16D,0x3CCDF
|
||||
1951 - Theater Exit - 0x0A16D,0x3CCDF
|
||||
1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A
|
||||
1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D
|
||||
1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507
|
||||
1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6
|
||||
1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181
|
||||
1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
|
||||
1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D
|
||||
1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07
|
||||
1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D
|
||||
1981 - Caves Doors to Challenge - 0x019A5,0x0A19A
|
||||
1984 - Caves Exits to Main Island - 0x2D859,0x2D73F
|
||||
1987 - Theater Walkway Doors - 0x27739,0x27263,0x09E87
|
||||
1987 - Tunnels Doors - 0x27739,0x27263,0x09E87
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ class WitnessWorld(World):
|
||||
"""
|
||||
game = "The Witness"
|
||||
topology_present = False
|
||||
data_version = 5
|
||||
data_version = 7
|
||||
|
||||
static_logic = StaticWitnessLogic()
|
||||
static_locat = StaticWitnessLocations()
|
||||
|
||||
@@ -30,17 +30,17 @@ class StaticWitnessLocations:
|
||||
|
||||
"Outside Tutorial Vault Box",
|
||||
"Outside Tutorial Discard",
|
||||
"Outside Tutorial Dots Introduction 5",
|
||||
"Outside Tutorial Squares Introduction 9",
|
||||
"Outside Tutorial Shed Row 5",
|
||||
"Outside Tutorial Tree Row 9",
|
||||
|
||||
"Glass Factory Discard",
|
||||
"Glass Factory Vertical Symmetry 5",
|
||||
"Glass Factory Rotational Symmetry 3",
|
||||
"Glass Factory Back Wall 5",
|
||||
"Glass Factory Front 3",
|
||||
"Glass Factory Melting 3",
|
||||
|
||||
"Symmetry Island Black Dots 5",
|
||||
"Symmetry Island Colored Dots 6",
|
||||
"Symmetry Island Fading Lines 7",
|
||||
"Symmetry Island Right 5",
|
||||
"Symmetry Island Back 6",
|
||||
"Symmetry Island Left 7",
|
||||
"Symmetry Island Scenery Outlines 5",
|
||||
"Symmetry Island Laser Panel",
|
||||
|
||||
@@ -48,26 +48,28 @@ class StaticWitnessLocations:
|
||||
|
||||
"Desert Vault Box",
|
||||
"Desert Discard",
|
||||
"Desert Sun Reflection 8",
|
||||
"Desert Artificial Light Reflection 3",
|
||||
"Desert Pond Reflection 5",
|
||||
"Desert Flood Reflection 6",
|
||||
"Desert Surface 8",
|
||||
"Desert Light Room 3",
|
||||
"Desert Pond Room 5",
|
||||
"Desert Flood Room 6",
|
||||
"Desert Final Bent 3",
|
||||
"Desert Final Hexagonal",
|
||||
"Desert Laser Panel",
|
||||
|
||||
"Quarry Mill Eraser and Dots 6",
|
||||
"Quarry Mill Eraser and Squares 8",
|
||||
"Quarry Mill Small Squares & Dots & Eraser",
|
||||
"Quarry Boathouse Intro Shapers",
|
||||
"Quarry Boathouse Intro Stars",
|
||||
"Quarry Boathouse Eraser and Shapers 5",
|
||||
"Quarry Boathouse Stars & Eraser & Shapers 2",
|
||||
"Quarry Boathouse Stars & Eraser & Shapers 5",
|
||||
"Quarry Mill Lower Row 6",
|
||||
"Quarry Mill Upper Row 8",
|
||||
"Quarry Mill Control Room Right",
|
||||
"Quarry Boathouse Intro Right",
|
||||
"Quarry Boathouse Intro Left",
|
||||
"Quarry Boathouse Front Row 5",
|
||||
"Quarry Boathouse Back First Row 9",
|
||||
"Quarry Boathouse Back Second Row 3",
|
||||
"Quarry Discard",
|
||||
"Quarry Laser Panel",
|
||||
|
||||
"Shadows Lower Avoid 8",
|
||||
"Shadows Environmental Avoid 8",
|
||||
"Shadows Follow 5",
|
||||
"Shadows Intro 8",
|
||||
"Shadows Far 8",
|
||||
"Shadows Near 5",
|
||||
"Shadows Laser Panel",
|
||||
|
||||
"Keep Hedge Maze 4",
|
||||
@@ -79,44 +81,44 @@ class StaticWitnessLocations:
|
||||
"Shipwreck Vault Box",
|
||||
"Shipwreck Discard",
|
||||
|
||||
"Monastery Rhombic Avoid 3",
|
||||
"Monastery Branch Follow 2",
|
||||
"Monastery Outside 3",
|
||||
"Monastery Inside 4",
|
||||
"Monastery Laser Panel",
|
||||
|
||||
"Town Cargo Box Discard",
|
||||
"Town Hexagonal Reflection",
|
||||
"Town Tall Hexagonal",
|
||||
"Town Church Lattice",
|
||||
"Town Rooftop Discard",
|
||||
"Town Symmetry Squares 5 + Dots",
|
||||
"Town Full Dot Grid Shapers 5",
|
||||
"Town Shapers & Dots & Eraser",
|
||||
"Town Red Rooftop 5",
|
||||
"Town Wooden Roof Lower Row 5",
|
||||
"Town Wooden Rooftop",
|
||||
"Town Laser Panel",
|
||||
|
||||
"Theater Discard",
|
||||
|
||||
"Jungle Discard",
|
||||
"Jungle Waves 3",
|
||||
"Jungle Waves 7",
|
||||
"Jungle First Row 3",
|
||||
"Jungle Second Row 4",
|
||||
"Jungle Popup Wall 6",
|
||||
"Jungle Laser Panel",
|
||||
|
||||
"River Vault Box",
|
||||
|
||||
"Bunker Drawn Squares 5",
|
||||
"Bunker Drawn Squares 9",
|
||||
"Bunker Drawn Squares through Tinted Glass 3",
|
||||
"Bunker Drop-Down Door Squares 2",
|
||||
"Bunker Intro Left 5",
|
||||
"Bunker Intro Back 4",
|
||||
"Bunker Glass Room 3",
|
||||
"Bunker UV Room 2",
|
||||
"Bunker Laser Panel",
|
||||
|
||||
"Swamp Seperatable Shapers 6",
|
||||
"Swamp Combinable Shapers 8",
|
||||
"Swamp Broken Shapers 4",
|
||||
"Swamp Cyan Underwater Negative Shapers 5",
|
||||
"Swamp Platform Shapers 4",
|
||||
"Swamp Rotated Shapers 4",
|
||||
"Swamp Red Underwater Negative Shapers 4",
|
||||
"Swamp More Rotated Shapers 4",
|
||||
"Swamp Blue Underwater Negative Shapers 5",
|
||||
"Swamp Intro Front 6",
|
||||
"Swamp Intro Back 8",
|
||||
"Swamp Between Bridges Near Row 4",
|
||||
"Swamp Cyan Underwater 5",
|
||||
"Swamp Platform Row 4",
|
||||
"Swamp Between Bridges Far Row 4",
|
||||
"Swamp Red Underwater 4",
|
||||
"Swamp Beyond Rotating Bridge 4",
|
||||
"Swamp Blue Underwater 5",
|
||||
"Swamp Laser Panel",
|
||||
|
||||
"Treehouse Yellow Bridge 9",
|
||||
@@ -125,73 +127,77 @@ class StaticWitnessLocations:
|
||||
"Treehouse Green Bridge 7",
|
||||
"Treehouse Green Bridge Discard",
|
||||
"Treehouse Left Orange Bridge 15",
|
||||
"Treehouse Burnt House Discard",
|
||||
"Treehouse Laser Discard",
|
||||
"Treehouse Right Orange Bridge 12",
|
||||
"Treehouse Laser Panel",
|
||||
|
||||
"Mountaintop Discard",
|
||||
"Mountaintop Vault Box",
|
||||
}
|
||||
"Mountainside Discard",
|
||||
"Mountainside Vault Box",
|
||||
|
||||
UNCOMMON_LOCATIONS = {
|
||||
"Mountaintop River Shape",
|
||||
"Tutorial Patio Floor",
|
||||
"Quarry Mill Big Squares & Dots & Eraser",
|
||||
"Quarry Mill Control Room Left",
|
||||
"Theater Tutorial Video",
|
||||
"Theater Desert Video",
|
||||
"Theater Jungle Video",
|
||||
"Theater Shipwreck Video",
|
||||
"Theater Mountain Video",
|
||||
"Town RGB Squares",
|
||||
"Town RGB Stars",
|
||||
"Swamp Underwater Back Optional",
|
||||
"Town RGB Room Left",
|
||||
"Town RGB Room Right",
|
||||
"Swamp Purple Underwater",
|
||||
}
|
||||
|
||||
CAVES_LOCATIONS = {
|
||||
"Inside Mountain Caves Dot Grid Triangles 4",
|
||||
"Inside Mountain Caves Symmetry Triangles",
|
||||
"Inside Mountain Caves Stars & Squares and Triangles 2",
|
||||
"Inside Mountain Caves Shapers and Triangles 2",
|
||||
"Inside Mountain Caves Symmetry Shapers",
|
||||
"Inside Mountain Caves Broken and Negative Shapers",
|
||||
"Inside Mountain Caves Broken Shapers",
|
||||
"Caves Blue Tunnel Right First 4",
|
||||
"Caves Blue Tunnel Left First 1",
|
||||
"Caves Blue Tunnel Left Second 5",
|
||||
"Caves Blue Tunnel Right Second 5",
|
||||
"Caves Blue Tunnel Right Third 1",
|
||||
"Caves Blue Tunnel Left Fourth 1",
|
||||
"Caves Blue Tunnel Left Third 1",
|
||||
|
||||
"Inside Mountain Caves Rainbow Squares",
|
||||
"Inside Mountain Caves Squares & Stars and Colored Eraser",
|
||||
"Inside Mountain Caves Rotated Broken Shapers",
|
||||
"Inside Mountain Caves Stars and Squares",
|
||||
"Inside Mountain Caves Lone Pillar",
|
||||
"Inside Mountain Caves Wooden Beam Shapers",
|
||||
"Inside Mountain Caves Wooden Beam Squares and Shapers",
|
||||
"Inside Mountain Caves Wooden Beam Stars and Squares",
|
||||
"Inside Mountain Caves Wooden Beam Shapers and Stars",
|
||||
"Inside Mountain Caves Upstairs Invisible Dots 8",
|
||||
"Inside Mountain Caves Upstairs Invisible Dot Symmetry 3",
|
||||
"Inside Mountain Caves Upstairs Dot Grid Negative Shapers",
|
||||
"Inside Mountain Caves Upstairs Dot Grid Rotated Shapers",
|
||||
"Caves First Floor Middle",
|
||||
"Caves First Floor Right",
|
||||
"Caves First Floor Left",
|
||||
"Caves First Floor Grounded",
|
||||
"Caves Lone Pillar",
|
||||
"Caves First Wooden Beam",
|
||||
"Caves Second Wooden Beam",
|
||||
"Caves Third Wooden Beam",
|
||||
"Caves Fourth Wooden Beam",
|
||||
"Caves Right Upstairs Left Row 8",
|
||||
"Caves Right Upstairs Right Row 3",
|
||||
"Caves Left Upstairs Single",
|
||||
"Caves Left Upstairs Left Row 5",
|
||||
|
||||
"Theater Walkway Vault Box",
|
||||
"Inside Mountain Bottom Layer Discard",
|
||||
"Tunnels Vault Box",
|
||||
"Mountain Bottom Floor Discard",
|
||||
"Theater Challenge Video",
|
||||
}
|
||||
|
||||
MOUNTAIN_UNREACHABLE_FROM_BEHIND = {
|
||||
"Mountaintop Trap Door Triple Exit",
|
||||
|
||||
"Inside Mountain Obscured Vision 5",
|
||||
"Inside Mountain Moving Background 7",
|
||||
"Inside Mountain Physically Obstructed 3",
|
||||
"Inside Mountain Angled Inside Trash 2",
|
||||
"Inside Mountain Color Cycle 5",
|
||||
"Inside Mountain Same Solution 6",
|
||||
"Mountain Floor 1 Right Row 5",
|
||||
"Mountain Floor 1 Left Row 7",
|
||||
"Mountain Floor 1 Back Row 3",
|
||||
"Mountain Floor 1 Trash Pillar 2",
|
||||
"Mountain Floor 2 Near Row 5",
|
||||
"Mountain Floor 2 Far Row 6",
|
||||
}
|
||||
|
||||
MOUNTAIN_REACHABLE_FROM_BEHIND = {
|
||||
"Inside Mountain Elevator Discard",
|
||||
"Inside Mountain Giant Puzzle",
|
||||
"Mountain Floor 2 Elevator Discard",
|
||||
"Mountain Bottom Floor Giant Puzzle",
|
||||
|
||||
"Inside Mountain Final Room Left Pillar 4",
|
||||
"Inside Mountain Final Room Right Pillar 4",
|
||||
"Mountain Final Room Left Pillar 4",
|
||||
"Mountain Final Room Right Pillar 4",
|
||||
}
|
||||
|
||||
MOUNTAIN_EXTRAS = {
|
||||
"Challenge Vault Box",
|
||||
"Theater Challenge Video",
|
||||
"Mountain Bottom Floor Discard"
|
||||
}
|
||||
|
||||
ALL_LOCATIONS_TO_ID = dict()
|
||||
@@ -241,37 +247,44 @@ class WitnessPlayerLocations:
|
||||
StaticWitnessLocations.GENERAL_LOCATIONS
|
||||
)
|
||||
|
||||
doors = get_option_value(world, player, "shuffle_doors")
|
||||
doors = get_option_value(world, player, "shuffle_doors") >= 2
|
||||
earlyutm = is_option_enabled(world, player, "early_secret_area")
|
||||
victory = get_option_value(world, player, "victory_condition")
|
||||
lasers = get_option_value(world, player, "challenge_lasers")
|
||||
mount_lasers = get_option_value(world, player, "mountain_lasers")
|
||||
chal_lasers = get_option_value(world, player, "challenge_lasers")
|
||||
laser_shuffle = get_option_value(world, player, "shuffle_lasers")
|
||||
|
||||
postgame = set()
|
||||
postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS
|
||||
postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
|
||||
postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
|
||||
postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS
|
||||
|
||||
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame
|
||||
|
||||
if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)):
|
||||
mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers)
|
||||
|
||||
if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently)
|
||||
postgame -= StaticWitnessLocations.CAVES_LOCATIONS
|
||||
|
||||
if doors >= 2:
|
||||
if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)):
|
||||
postgame -= {"Challenge Vault Box", "Theater Challenge Video"}
|
||||
|
||||
if doors or mountain_enterable_from_top:
|
||||
postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND
|
||||
|
||||
if victory != 2:
|
||||
if mountain_enterable_from_top:
|
||||
postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND
|
||||
|
||||
if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors):
|
||||
postgame -= {"Mountain Bottom Floor Discard"}
|
||||
|
||||
if is_option_enabled(world, player, "shuffle_discarded_panels"):
|
||||
self.PANEL_TYPES_TO_SHUFFLE.add("Discard")
|
||||
|
||||
if is_option_enabled(world, player, "shuffle_vault_boxes"):
|
||||
self.PANEL_TYPES_TO_SHUFFLE.add("Vault")
|
||||
|
||||
if is_option_enabled(world, player, "shuffle_uncommon"):
|
||||
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS
|
||||
|
||||
self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS
|
||||
|
||||
if not is_option_enabled(world, player, "shuffle_postgame"):
|
||||
|
||||
@@ -60,7 +60,10 @@ class WitnessPlayerLogic:
|
||||
for dependentItem in door_items:
|
||||
all_options.add(items_option.union(dependentItem))
|
||||
|
||||
return frozenset(all_options)
|
||||
if panel_hex != "0x28A0D":
|
||||
return frozenset(all_options)
|
||||
else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved
|
||||
these_items = all_options
|
||||
|
||||
these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"]
|
||||
|
||||
@@ -321,7 +324,7 @@ class WitnessPlayerLogic:
|
||||
self.VICTORY_LOCATION = "0x0356B"
|
||||
self.EVENT_ITEM_NAMES = {
|
||||
"0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates",
|
||||
"0x09D9B": "Monastery Overhead Doors Open",
|
||||
"0x09D9B": "Monastery Shutters Open",
|
||||
"0x193A6": "Monastery Laser Panel Activates",
|
||||
"0x00037": "Monastery Branch Panels Activate",
|
||||
"0x0A079": "Access to Bunker Laser",
|
||||
@@ -332,24 +335,24 @@ class WitnessPlayerLogic:
|
||||
"0x01D3F": "Keep Laser Panel (Pressure Plates) Activates",
|
||||
"0x09F7F": "Mountain Access",
|
||||
"0x0367C": "Quarry Laser Mill Requirement Met",
|
||||
"0x009A1": "Swamp Rotated Shapers 1 Activates",
|
||||
"0x009A1": "Swamp Between Bridges Far 1 Activates",
|
||||
"0x00006": "Swamp Cyan Water Drains",
|
||||
"0x00990": "Swamp Broken Shapers 1 Activates",
|
||||
"0x0A8DC": "Lower Avoid 6 Activates",
|
||||
"0x0000A": "Swamp More Rotated Shapers 1 Access",
|
||||
"0x09E86": "Inside Mountain Second Layer Blue Bridge Access",
|
||||
"0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access",
|
||||
"0x00990": "Swamp Between Bridges Near Row 1 Activates",
|
||||
"0x0A8DC": "Intro 6 Activates",
|
||||
"0x0000A": "Swamp Beyond Rotating Bridge 1 Access",
|
||||
"0x09E86": "Mountain Floor 2 Blue Bridge Access",
|
||||
"0x09ED8": "Mountain Floor 2 Yellow Bridge Access",
|
||||
"0x0A3D0": "Quarry Laser Boathouse Requirement Met",
|
||||
"0x00596": "Swamp Red Water Drains",
|
||||
"0x00E3A": "Swamp Purple Water Drains",
|
||||
"0x0343A": "Door to Symmetry Island Powers On",
|
||||
"0xFFF00": "Inside Mountain Bottom Layer Discard Turns On",
|
||||
"0xFFF00": "Mountain Bottom Floor Discard Turns On",
|
||||
"0x17CA6": "All Boat Panels Turn On",
|
||||
"0x17CDF": "All Boat Panels Turn On",
|
||||
"0x09DB8": "All Boat Panels Turn On",
|
||||
"0x17C95": "All Boat Panels Turn On",
|
||||
"0x03BB0": "Town Church Lattice Vision From Outside",
|
||||
"0x28AC1": "Town Shapers & Dots & Eraser Turns On",
|
||||
"0x28AC1": "Town Wooden Rooftop Turns On",
|
||||
"0x28A69": "Town Tower 1st Door Opens",
|
||||
"0x28ACC": "Town Tower 2nd Door Opens",
|
||||
"0x28AD9": "Town Tower 3rd Door Opens",
|
||||
@@ -357,9 +360,9 @@ class WitnessPlayerLogic:
|
||||
"0x03675": "Quarry Mill Ramp Activation From Above",
|
||||
"0x03679": "Quarry Mill Lift Lowering While Standing On It",
|
||||
"0x2FAF6": "Tutorial Gate Secret Solution Knowledge",
|
||||
"0x079DF": "Town Hexagonal Reflection Turns On",
|
||||
"0x079DF": "Town Tall Hexagonal Turns On",
|
||||
"0x17DA2": "Right Orange Bridge Fully Extended",
|
||||
"0x19B24": "Shadows Lower Avoid Patterns Visible",
|
||||
"0x19B24": "Shadows Intro Patterns Visible",
|
||||
"0x2700B": "Open Door to Treehouse Laser House",
|
||||
"0x00055": "Orchard Apple Trees 4 Turns On",
|
||||
"0x17DDB": "Left Orange Bridge Fully Extended",
|
||||
@@ -369,6 +372,8 @@ class WitnessPlayerLogic:
|
||||
"0x03481": "Tutorial Video Pattern Knowledge",
|
||||
"0x03702": "Jungle Video Pattern Knowledge",
|
||||
"0x0356B": "Challenge Video Pattern Knowledge",
|
||||
"0x0A15F": "Desert Laser Panel Shutters Open (1)",
|
||||
"0x012D7": "Desert Laser Panel Shutters Open (2)",
|
||||
}
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
|
||||
@@ -73,7 +73,7 @@ class WitnessRegions:
|
||||
all_locations = all_locations | set(locations_for_this_region)
|
||||
|
||||
world.regions += [
|
||||
create_region(world, player, region_name, self.locat,locations_for_this_region)
|
||||
create_region(world, player, region_name, self.locat, locations_for_this_region)
|
||||
]
|
||||
|
||||
for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items():
|
||||
|
||||
@@ -25,84 +25,84 @@ Disabled Locations:
|
||||
0x00055 (Orchard Apple Tree 3)
|
||||
0x032F7 (Orchard Apple Tree 4)
|
||||
0x032FF (Orchard Apple Tree 5)
|
||||
0x198B5 (Shadows Lower Avoid 1)
|
||||
0x198BD (Shadows Lower Avoid 2)
|
||||
0x198BF (Shadows Lower Avoid 3)
|
||||
0x19771 (Shadows Lower Avoid 4)
|
||||
0x0A8DC (Shadows Lower Avoid 5)
|
||||
0x0AC74 (Shadows Lower Avoid 6)
|
||||
0x0AC7A (Shadows Lower Avoid 7)
|
||||
0x0A8E0 (Shadows Lower Avoid 8)
|
||||
0x386FA (Shadows Environmental Avoid 1)
|
||||
0x1C33F (Shadows Environmental Avoid 2)
|
||||
0x196E2 (Shadows Environmental Avoid 3)
|
||||
0x1972A (Shadows Environmental Avoid 4)
|
||||
0x19809 (Shadows Environmental Avoid 5)
|
||||
0x19806 (Shadows Environmental Avoid 6)
|
||||
0x196F8 (Shadows Environmental Avoid 7)
|
||||
0x1972F (Shadows Environmental Avoid 8)
|
||||
0x19797 (Shadows Follow 1)
|
||||
0x1979A (Shadows Follow 2)
|
||||
0x197E0 (Shadows Follow 3)
|
||||
0x197E8 (Shadows Follow 4)
|
||||
0x197E5 (Shadows Follow 5)
|
||||
0x198B5 (Shadows Intro 1)
|
||||
0x198BD (Shadows Intro 2)
|
||||
0x198BF (Shadows Intro 3)
|
||||
0x19771 (Shadows Intro 4)
|
||||
0x0A8DC (Shadows Intro 5)
|
||||
0x0AC74 (Shadows Intro 6)
|
||||
0x0AC7A (Shadows Intro 7)
|
||||
0x0A8E0 (Shadows Intro 8)
|
||||
0x386FA (Shadows Far 1)
|
||||
0x1C33F (Shadows Far 2)
|
||||
0x196E2 (Shadows Far 3)
|
||||
0x1972A (Shadows Far 4)
|
||||
0x19809 (Shadows Far 5)
|
||||
0x19806 (Shadows Far 6)
|
||||
0x196F8 (Shadows Far 7)
|
||||
0x1972F (Shadows Far 8)
|
||||
0x19797 (Shadows Near 1)
|
||||
0x1979A (Shadows Near 2)
|
||||
0x197E0 (Shadows Near 3)
|
||||
0x197E8 (Shadows Near 4)
|
||||
0x197E5 (Shadows Near 5)
|
||||
0x19650 (Shadows Laser)
|
||||
0x00139 (Keep Hedge Maze 1)
|
||||
0x019DC (Keep Hedge Maze 2)
|
||||
0x019E7 (Keep Hedge Maze 3)
|
||||
0x01A0F (Keep Hedge Maze 4)
|
||||
0x0360E (Laser Hedges)
|
||||
0x00B10 (Monastery Door Open Left)
|
||||
0x00C92 (Monastery Door Open Right)
|
||||
0x00290 (Monastery Rhombic Avoid 1)
|
||||
0x00038 (Monastery Rhombic Avoid 2)
|
||||
0x00037 (Monastery Rhombic Avoid 3)
|
||||
0x193A7 (Monastery Branch Avoid 1)
|
||||
0x193AA (Monastery Branch Avoid 2)
|
||||
0x193AB (Monastery Branch Follow 1)
|
||||
0x193A6 (Monastery Branch Follow 2)
|
||||
0x00B10 (Monastery Entry Left)
|
||||
0x00C92 (Monastery Entry Right)
|
||||
0x00290 (Monastery Outside 1)
|
||||
0x00038 (Monastery Outside 2)
|
||||
0x00037 (Monastery Outside 3)
|
||||
0x193A7 (Monastery Inside 1)
|
||||
0x193AA (Monastery Inside 2)
|
||||
0x193AB (Monastery Inside 3)
|
||||
0x193A6 (Monastery Inside 4)
|
||||
0x17CA4 (Monastery Laser)
|
||||
0x18590 (Tree Outlines) - True - Symmetry & Environment
|
||||
0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment
|
||||
0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment
|
||||
0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection
|
||||
0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection
|
||||
0x18590 (Transparent) - True - Symmetry & Environment
|
||||
0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment
|
||||
0x28938 (Apple Tree) - 0x28AE3 - Environment
|
||||
0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection
|
||||
0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection
|
||||
0x03553 (Theater Tutorial Video)
|
||||
0x03552 (Theater Desert Video)
|
||||
0x0354E (Theater Jungle Video)
|
||||
0x03549 (Theater Challenge Video)
|
||||
0x0354F (Theater Shipwreck Video)
|
||||
0x03545 (Theater Mountain Video)
|
||||
0x002C4 (Waves 1)
|
||||
0x00767 (Waves 2)
|
||||
0x002C6 (Waves 3)
|
||||
0x0070E (Waves 4)
|
||||
0x0070F (Waves 5)
|
||||
0x0087D (Waves 6)
|
||||
0x002C7 (Waves 7)
|
||||
0x15ADD (River Rhombic Avoid Vault)
|
||||
0x002C4 (First Row 1)
|
||||
0x00767 (First Row 2)
|
||||
0x002C6 (First Row 3)
|
||||
0x0070E (Second Row 1)
|
||||
0x0070F (Second Row 2)
|
||||
0x0087D (Second Row 3)
|
||||
0x002C7 (Second Row 4)
|
||||
0x15ADD (River Outside Vault)
|
||||
0x03702 (River Vault Box)
|
||||
0x17CAA (Rhombic Avoid to Monastery Garden)
|
||||
0x17CAA (Monastery Shortcut Panel)
|
||||
0x17C2E (Door to Bunker)
|
||||
0x09F7D (Bunker Drawn Squares 1)
|
||||
0x09FDC (Bunker Drawn Squares 2)
|
||||
0x09FF7 (Bunker Drawn Squares 3)
|
||||
0x09F82 (Bunker Drawn Squares 4)
|
||||
0x09FF8 (Bunker Drawn Squares 5)
|
||||
0x09D9F (Bunker Drawn Squares 6)
|
||||
0x09DA1 (Bunker Drawn Squares 7)
|
||||
0x09DA2 (Bunker Drawn Squares 8)
|
||||
0x09DAF (Bunker Drawn Squares 9)
|
||||
0x0A010 (Bunker Drawn Squares through Tinted Glass 1)
|
||||
0x0A01B (Bunker Drawn Squares through Tinted Glass 2)
|
||||
0x0A01F (Bunker Drawn Squares through Tinted Glass 3)
|
||||
0x0A099 (Door to Bunker Proper)
|
||||
0x09F7D (Bunker Intro Left 1)
|
||||
0x09FDC (Bunker Intro Left 2)
|
||||
0x09FF7 (Bunker Intro Left 3)
|
||||
0x09F82 (Bunker Intro Left 4)
|
||||
0x09FF8 (Bunker Intro Left 5)
|
||||
0x09D9F (Bunker Intro Back 1)
|
||||
0x09DA1 (Bunker Intro Back 2)
|
||||
0x09DA2 (Bunker Intro Back 3)
|
||||
0x09DAF (Bunker Intro Back 4)
|
||||
0x0A010 (Bunker Glass Room 1)
|
||||
0x0A01B (Bunker Glass Room 2)
|
||||
0x0A01F (Bunker Glass Room 3)
|
||||
0x0A099 (Tinted Glass Door)
|
||||
0x34BC5 (Bunker Drop-Down Door Open)
|
||||
0x34BC6 (Bunker Drop-Down Door Close)
|
||||
0x17E63 (Bunker Drop-Down Door Squares 1)
|
||||
0x17E67 (Bunker Drop-Down Door Squares 2)
|
||||
0x17E63 (Bunker UV Room 1)
|
||||
0x17E67 (Bunker UV Room 2)
|
||||
0x09DE0 (Bunker Laser)
|
||||
0x0A079 (Bunker Elevator Control)
|
||||
0x0042D (Mountaintop River Shape)
|
||||
|
||||
0x17CAA (River Door to Garden Panel)
|
||||
0x17CAA (River Garden Entry Panel)
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
Items:
|
||||
Glass Factory Entry Door (Panel)
|
||||
Door to Symmetry Island Lower (Panel)
|
||||
Door to Symmetry Island Upper (Panel)
|
||||
Door to Desert Flood Light Room (Panel)
|
||||
Desert Flood Room Flood Controls (Panel)
|
||||
Quarry Door to Mill (Panel)
|
||||
Glass Factory Entry (Panel)
|
||||
Symmetry Island Lower (Panel)
|
||||
Symmetry Island Upper (Panel)
|
||||
Desert Light Room Entry (Panel)
|
||||
Desert Flood Controls (Panel)
|
||||
Quarry Mill Entry (Panel)
|
||||
Quarry Mill Ramp Controls (Panel)
|
||||
Quarry Mill Elevator Controls (Panel)
|
||||
Quarry Mill Lift Controls (Panel)
|
||||
Quarry Boathouse Ramp Height Control (Panel)
|
||||
Quarry Boathouse Ramp Horizontal Control (Panel)
|
||||
Shadows Door Timer (Panel)
|
||||
Monastery Entry Door Left (Panel)
|
||||
Monastery Entry Door Right (Panel)
|
||||
Town Door to RGB House (Panel)
|
||||
Town Door to Church (Panel)
|
||||
Monastery Entry Left (Panel)
|
||||
Monastery Entry Right (Panel)
|
||||
Town Tinted Glass Door (Panel)
|
||||
Town Church Entry (Panel)
|
||||
Town Maze Panel (Drop-Down Staircase) (Panel)
|
||||
Windmill Door (Panel)
|
||||
Windmill Entry (Panel)
|
||||
Treehouse First & Second Doors (Panel)
|
||||
Treehouse Third Door (Panel)
|
||||
Treehouse Laser House Door Timer (Panel)
|
||||
Treehouse Shortcut Drop-Down Bridge (Panel)
|
||||
Treehouse Drawbridge (Panel)
|
||||
Jungle Popup Wall (Panel)
|
||||
Bunker Entry Door (Panel)
|
||||
Inside Bunker Door to Bunker Proper (Panel)
|
||||
Bunker Entry (Panel)
|
||||
Bunker Tinted Glass Door (Panel)
|
||||
Bunker Elevator Control (Panel)
|
||||
Swamp Entry Door (Panel)
|
||||
Swamp Entry (Panel)
|
||||
Swamp Sliding Bridge (Panel)
|
||||
Swamp Rotating Bridge (Panel)
|
||||
Swamp Maze Control (Panel)
|
||||
Boat
|
||||
Boat
|
||||
@@ -1,128 +1,128 @@
|
||||
Items:
|
||||
Outside Tutorial Optional Door
|
||||
Outside Tutorial Outpost Entry Door
|
||||
Outside Tutorial Outpost Exit Door
|
||||
Glass Factory Entry Door
|
||||
Glass Factory Back Wall
|
||||
Symmetry Island Lower Door
|
||||
Symmetry Island Upper Door
|
||||
Orchard Middle Gate
|
||||
Orchard Final Gate
|
||||
Desert Door to Flood Light Room
|
||||
Desert Door to Pond Room
|
||||
Desert Door to Water Levels Room
|
||||
Desert Door to Elevator Room
|
||||
Quarry Main Entry 1
|
||||
Quarry Main Entry 2
|
||||
Quarry Door to Mill
|
||||
Quarry Mill Side Door
|
||||
Quarry Mill Rooftop Shortcut
|
||||
Quarry Mill Stairs
|
||||
Quarry Boathouse Boat Staircase
|
||||
Quarry Boathouse First Barrier
|
||||
Quarry Boathouse Shortcut
|
||||
Shadows Timed Door
|
||||
Shadows Laser Room Right Door
|
||||
Shadows Laser Room Left Door
|
||||
Shadows Barrier to Quarry
|
||||
Shadows Barrier to Ledge
|
||||
Keep Hedge Maze 1 Exit Door
|
||||
Keep Pressure Plates 1 Exit Door
|
||||
Keep Hedge Maze 2 Shortcut
|
||||
Keep Hedge Maze 2 Exit Door
|
||||
Keep Hedge Maze 3 Shortcut
|
||||
Keep Hedge Maze 3 Exit Door
|
||||
Keep Hedge Maze 4 Shortcut
|
||||
Keep Hedge Maze 4 Exit Door
|
||||
Keep Pressure Plates 2 Exit Door
|
||||
Keep Pressure Plates 3 Exit Door
|
||||
Keep Pressure Plates 4 Exit Door
|
||||
Keep Shortcut to Shadows
|
||||
Keep Tower Shortcut
|
||||
Monastery Shortcut
|
||||
Monastery Inner Door
|
||||
Monastery Outer Door
|
||||
Monastery Door to Garden
|
||||
Town Cargo Box Door
|
||||
Town Wooden Roof Staircase
|
||||
Town Tinted Door to RGB House
|
||||
Town Door to Church
|
||||
Town Maze Staircase
|
||||
Town Windmill Door
|
||||
Town RGB House Staircase
|
||||
Town Tower Blue Panels Door
|
||||
Town Tower Lattice Door
|
||||
Town Tower Environmental Set Door
|
||||
Town Tower Wooden Roof Set Door
|
||||
Theater Entry Door
|
||||
Theater Exit Door Left
|
||||
Theater Exit Door Right
|
||||
Jungle Bamboo Shortcut to River
|
||||
Jungle Popup Wall
|
||||
River Shortcut to Monastery Garden
|
||||
Bunker Bunker Entry Door
|
||||
Bunker Tinted Glass Door
|
||||
Bunker Door to Ultraviolet Room
|
||||
Bunker Door to Elevator
|
||||
Swamp Entry Door
|
||||
Swamp Door to Broken Shapers
|
||||
Outside Tutorial Outpost Path (Door)
|
||||
Outside Tutorial Outpost Entry (Door)
|
||||
Outside Tutorial Outpost Exit (Door)
|
||||
Glass Factory Entry (Door)
|
||||
Glass Factory Back Wall (Door)
|
||||
Symmetry Island Lower (Door)
|
||||
Symmetry Island Upper (Door)
|
||||
Orchard First Gate (Door)
|
||||
Orchard Second Gate (Door)
|
||||
Desert Light Room Entry (Door)
|
||||
Desert Pond Room Entry (Door)
|
||||
Desert Flood Room Entry (Door)
|
||||
Desert Elevator Room Entry (Door)
|
||||
Quarry Entry 1 (Door)
|
||||
Quarry Entry 2 (Door)
|
||||
Quarry Mill Entry (Door)
|
||||
Quarry Mill Side Exit (Door)
|
||||
Quarry Mill Roof Exit (Door)
|
||||
Quarry Mill Stairs (Door)
|
||||
Quarry Boathouse Dock (Door)
|
||||
Quarry Boathouse First Barrier (Door)
|
||||
Quarry Boathouse Second Barrier (Door)
|
||||
Shadows Timed Door (Door)
|
||||
Shadows Laser Entry Right (Door)
|
||||
Shadows Laser Entry Left (Door)
|
||||
Shadows Quarry Barrier (Door)
|
||||
Shadows Ledge Barrier (Door)
|
||||
Keep Hedge Maze 1 Exit (Door)
|
||||
Keep Pressure Plates 1 Exit (Door)
|
||||
Keep Hedge Maze 2 Shortcut (Door)
|
||||
Keep Hedge Maze 2 Exit (Door)
|
||||
Keep Hedge Maze 3 Shortcut (Door)
|
||||
Keep Hedge Maze 3 Exit (Door)
|
||||
Keep Hedge Maze 4 Shortcut (Door)
|
||||
Keep Hedge Maze 4 Exit (Door)
|
||||
Keep Pressure Plates 2 Exit (Door)
|
||||
Keep Pressure Plates 3 Exit (Door)
|
||||
Keep Pressure Plates 4 Exit (Door)
|
||||
Keep Shadows Shortcut (Door)
|
||||
Keep Tower Shortcut (Door)
|
||||
Monastery Shortcut (Door)
|
||||
Monastery Entry Inner (Door)
|
||||
Monastery Entry Outer (Door)
|
||||
Monastery Garden Entry (Door)
|
||||
Town Cargo Box Entry (Door)
|
||||
Town Wooden Roof Stairs (Door)
|
||||
Town Tinted Glass Door (Door)
|
||||
Town Church Entry (Door)
|
||||
Town Maze Stairs (Door)
|
||||
Town Windmill Entry (Door)
|
||||
Town RGB House Stairs (Door)
|
||||
Town Tower First Door (Door)
|
||||
Town Tower Third Door (Door)
|
||||
Town Tower Fourth Door (Door)
|
||||
Town Tower Second Door (Door)
|
||||
Theater Entry (Door)
|
||||
Theater Exit Left (Door)
|
||||
Theater Exit Right (Door)
|
||||
Jungle Bamboo Laser Shortcut (Door)
|
||||
Jungle Popup Wall (Door)
|
||||
River Monastery Shortcut (Door)
|
||||
Bunker Entry (Door)
|
||||
Bunker Tinted Glass Door (Door)
|
||||
Bunker UV Room Entry (Door)
|
||||
Bunker Elevator Room Entry (Door)
|
||||
Swamp Entry (Door)
|
||||
Swamp Between Bridges First Door
|
||||
Swamp Platform Shortcut Door
|
||||
Swamp Cyan Water Pump
|
||||
Swamp Door to Rotated Shapers
|
||||
Swamp Red Water Pump
|
||||
Swamp Red Underwater Exit
|
||||
Swamp Blue Water Pump
|
||||
Swamp Purple Water Pump
|
||||
Swamp Near Laser Shortcut
|
||||
Treehouse First Door
|
||||
Treehouse Second Door
|
||||
Treehouse Beyond Yellow Bridge Door
|
||||
Treehouse Drawbridge
|
||||
Treehouse Timed Door to Laser House
|
||||
Inside Mountain First Layer Exit Door
|
||||
Inside Mountain Second Layer Staircase Near
|
||||
Inside Mountain Second Layer Exit Door
|
||||
Inside Mountain Second Layer Staircase Far
|
||||
Inside Mountain Giant Puzzle Exit Door
|
||||
Inside Mountain Door to Final Room
|
||||
Inside Mountain Bottom Layer Rock
|
||||
Inside Mountain Door to Secret Area
|
||||
Caves Pillar Door
|
||||
Caves Mountain Shortcut
|
||||
Caves Swamp Shortcut
|
||||
Challenge Entry Door
|
||||
Challenge Door to Theater Walkway
|
||||
Theater Walkway Door to Windmill Interior
|
||||
Theater Walkway Door to Desert Elevator Room
|
||||
Theater Walkway Door to Town
|
||||
Swamp Cyan Water Pump (Door)
|
||||
Swamp Between Bridges Second Door
|
||||
Swamp Red Water Pump (Door)
|
||||
Swamp Red Underwater Exit (Door)
|
||||
Swamp Blue Water Pump (Door)
|
||||
Swamp Purple Water Pump (Door)
|
||||
Swamp Laser Shortcut (Door)
|
||||
Treehouse First Door (Door)
|
||||
Treehouse Second Door (Door)
|
||||
Treehouse Third Door (Door)
|
||||
Treehouse Drawbridge (Door)
|
||||
Treehouse Laser House Entry (Door)
|
||||
Mountain Floor 1 Exit (Door)
|
||||
Mountain Floor 2 Staircase Near (Door)
|
||||
Mountain Floor 2 Exit (Door)
|
||||
Mountain Floor 2 Staircase Far (Door)
|
||||
Mountain Bottom Floor Giant Puzzle Exit (Door)
|
||||
Mountain Bottom Floor Final Room Entry (Door)
|
||||
Mountain Bottom Floor Rock (Door)
|
||||
Caves Entry (Door)
|
||||
Caves Pillar Door (Door)
|
||||
Caves Mountain Shortcut (Door)
|
||||
Caves Swamp Shortcut (Door)
|
||||
Challenge Entry (Door)
|
||||
Challenge Tunnels Entry (Door)
|
||||
Tunnels Theater Shortcut (Door)
|
||||
Tunnels Desert Shortcut (Door)
|
||||
Tunnels Town Shortcut (Door)
|
||||
|
||||
Added Locations:
|
||||
Outside Tutorial Door to Outpost Panel
|
||||
Outside Tutorial Exit Door from Outpost Panel
|
||||
Glass Factory Entry Door Panel
|
||||
Glass Factory Vertical Symmetry 5
|
||||
Symmetry Island Door to Symmetry Island Lower Panel
|
||||
Symmetry Island Door to Symmetry Island Upper Panel
|
||||
Outside Tutorial Outpost Entry Panel
|
||||
Outside Tutorial Outpost Exit Panel
|
||||
Glass Factory Entry Panel
|
||||
Glass Factory Back Wall 5
|
||||
Symmetry Island Lower Panel
|
||||
Symmetry Island Upper Panel
|
||||
Orchard Apple Tree 3
|
||||
Orchard Apple Tree 5
|
||||
Desert Door to Desert Flood Light Room Panel
|
||||
Desert Artificial Light Reflection 3
|
||||
Desert Door to Water Levels Room Panel
|
||||
Desert Flood Reflection 6
|
||||
Quarry Door to Quarry 1 Panel
|
||||
Quarry Door to Quarry 2 Panel
|
||||
Quarry Door to Mill Right
|
||||
Quarry Door to Mill Left
|
||||
Quarry Mill Ground Floor Shortcut Door Panel
|
||||
Quarry Mill Door to Outside Quarry Stairs Panel
|
||||
Desert Light Room Entry Panel
|
||||
Desert Light Room 3
|
||||
Desert Flood Room Entry Panel
|
||||
Desert Flood Room 6
|
||||
Quarry Entry 1 Panel
|
||||
Quarry Entry 2 Panel
|
||||
Quarry Mill Entry Right Panel
|
||||
Quarry Mill Entry Left Panel
|
||||
Quarry Mill Side Exit Panel
|
||||
Quarry Mill Roof Exit Panel
|
||||
Quarry Mill Stair Control
|
||||
Quarry Boathouse Shortcut Door Panel
|
||||
Quarry Boathouse Second Barrier Panel
|
||||
Shadows Door Timer Inside
|
||||
Shadows Door Timer Outside
|
||||
Shadows Environmental Avoid 8
|
||||
Shadows Follow 5
|
||||
Shadows Lower Avoid 3
|
||||
Shadows Lower Avoid 5
|
||||
Shadows Far 8
|
||||
Shadows Near 5
|
||||
Shadows Intro 3
|
||||
Shadows Intro 5
|
||||
Keep Hedge Maze 1
|
||||
Keep Pressure Plates 1
|
||||
Keep Hedge Maze 2
|
||||
@@ -131,71 +131,70 @@ Keep Hedge Maze 4
|
||||
Keep Pressure Plates 2
|
||||
Keep Pressure Plates 3
|
||||
Keep Pressure Plates 4
|
||||
Keep Shortcut to Shadows Panel
|
||||
Keep Tower Shortcut to Keep Panel
|
||||
Monastery Shortcut Door Panel
|
||||
Monastery Door Open Left
|
||||
Monastery Door Open Right
|
||||
Monastery Rhombic Avoid 3
|
||||
Town Cargo Box Panel
|
||||
Town Full Dot Grid Shapers 5
|
||||
Town Tinted Door Panel
|
||||
Town Door to Church Stars Panel
|
||||
Keep Shadows Shortcut Panel
|
||||
Keep Tower Shortcut Panel
|
||||
Monastery Shortcut Panel
|
||||
Monastery Entry Left
|
||||
Monastery Entry Right
|
||||
Monastery Outside 3
|
||||
Town Cargo Box Entry Panel
|
||||
Town Wooden Roof Lower Row 5
|
||||
Town Tinted Glass Door Panel
|
||||
Town Church Entry Panel
|
||||
Town Maze Stair Control
|
||||
Town Windmill Door Panel
|
||||
Town Sound Room Left
|
||||
Town Windmill Entry Panel
|
||||
Town Sound Room Right
|
||||
Town Symmetry Squares 5 + Dots
|
||||
Town Red Rooftop 5
|
||||
Town Church Lattice
|
||||
Town Hexagonal Reflection
|
||||
Town Shapers & Dots & Eraser
|
||||
Windmill Door to Front of Theater Panel
|
||||
Theater Door to Cargo Box Left Panel
|
||||
Theater Door to Cargo Box Right Panel
|
||||
Jungle Shortcut to River Panel
|
||||
Town Tall Hexagonal
|
||||
Town Wooden Rooftop
|
||||
Windmill Theater Entry Panel
|
||||
Theater Exit Left Panel
|
||||
Theater Exit Right Panel
|
||||
Jungle Laser Shortcut Panel
|
||||
Jungle Popup Wall Control
|
||||
River Rhombic Avoid to Monastery Garden
|
||||
Bunker Bunker Entry Panel
|
||||
Bunker Door to Bunker Proper Panel
|
||||
Bunker Drawn Squares through Tinted Glass 3
|
||||
Bunker Drop-Down Door Squares 2
|
||||
River Monastery Shortcut Panel
|
||||
Bunker Entry Panel
|
||||
Bunker Tinted Glass Door Panel
|
||||
Bunker Glass Room 3
|
||||
Bunker UV Room 2
|
||||
Swamp Entry Panel
|
||||
Swamp Platform Shapers 4
|
||||
Swamp Platform Row 4
|
||||
Swamp Platform Shortcut Right Panel
|
||||
Swamp Blue Underwater Negative Shapers 5
|
||||
Swamp Broken Shapers 4
|
||||
Swamp Cyan Underwater Negative Shapers 5
|
||||
Swamp Red Underwater Negative Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp Near Laser Shortcut Right Panel
|
||||
Swamp Blue Underwater 5
|
||||
Swamp Between Bridges Near Row 4
|
||||
Swamp Cyan Underwater 5
|
||||
Swamp Red Underwater 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Laser Shortcut Right Panel
|
||||
Treehouse First Door Panel
|
||||
Treehouse Second Door Panel
|
||||
Treehouse Beyond Yellow Bridge Door Panel
|
||||
Treehouse Third Door Panel
|
||||
Treehouse Bridge Control
|
||||
Treehouse Left Orange Bridge 15
|
||||
Treehouse Right Orange Bridge 12
|
||||
Treehouse Laser House Door Timer Outside Control
|
||||
Treehouse Laser House Door Timer Inside Control
|
||||
Inside Mountain Moving Background 7
|
||||
Inside Mountain Obscured Vision 5
|
||||
Inside Mountain Physically Obstructed 3
|
||||
Inside Mountain Angled Inside Trash 2
|
||||
Inside Mountain Color Cycle 5
|
||||
Inside Mountain Light Bridge Controller 2
|
||||
Inside Mountain Light Bridge Controller 3
|
||||
Inside Mountain Same Solution 6
|
||||
Inside Mountain Giant Puzzle
|
||||
Inside Mountain Door to Final Room Left
|
||||
Inside Mountain Door to Final Room Right
|
||||
Inside Mountain Bottom Layer Discard
|
||||
Inside Mountain Rock Control
|
||||
Inside Mountain Secret Area Entry Panel
|
||||
Inside Mountain Caves Lone Pillar
|
||||
Inside Mountain Caves Shortcut to Mountain Panel
|
||||
Inside Mountain Caves Shortcut to Swamp Panel
|
||||
Inside Mountain Caves Challenge Entry Panel
|
||||
Challenge Door to Theater Walkway Panel
|
||||
Theater Walkway Theater Shortcut Panel
|
||||
Theater Walkway Desert Shortcut Panel
|
||||
Theater Walkway Town Shortcut Panel
|
||||
Treehouse Laser House Door Timer Inside
|
||||
Mountain Floor 1 Left Row 7
|
||||
Mountain Floor 1 Right Row 5
|
||||
Mountain Floor 1 Back Row 3
|
||||
Mountain Floor 1 Trash Pillar 2
|
||||
Mountain Floor 2 Near Row 5
|
||||
Mountain Floor 2 Light Bridge Controller Near
|
||||
Mountain Floor 2 Light Bridge Controller Far
|
||||
Mountain Floor 2 Far Row 6
|
||||
Mountain Bottom Floor Giant Puzzle
|
||||
Mountain Bottom Floor Final Room Entry Left
|
||||
Mountain Bottom Floor Final Room Entry Right
|
||||
Mountain Bottom Floor Discard
|
||||
Mountain Bottom Floor Rock Control
|
||||
Mountain Bottom Floor Caves Entry Panel
|
||||
Caves Lone Pillar
|
||||
Caves Mountain Shortcut Panel
|
||||
Caves Swamp Shortcut Panel
|
||||
Caves Challenge Entry Panel
|
||||
Challenge Tunnels Entry Panel
|
||||
Tunnels Theater Shortcut Panel
|
||||
Tunnels Desert Shortcut Panel
|
||||
Tunnels Town Shortcut Panel
|
||||
@@ -1,104 +1,104 @@
|
||||
Items:
|
||||
Outside Tutorial Optional Door
|
||||
Outside Tutorial Outpost Entry Door
|
||||
Outside Tutorial Outpost Exit Door
|
||||
Glass Factory Entry Door
|
||||
Glass Factory Back Wall
|
||||
Symmetry Island Lower Door
|
||||
Symmetry Island Upper Door
|
||||
Orchard Middle Gate
|
||||
Orchard Final Gate
|
||||
Desert Door to Flood Light Room
|
||||
Desert Door to Pond Room
|
||||
Desert Door to Water Levels Room
|
||||
Desert Door to Elevator Room
|
||||
Quarry Main Entry 1
|
||||
Quarry Main Entry 2
|
||||
Quarry Door to Mill
|
||||
Quarry Mill Side Door
|
||||
Quarry Mill Rooftop Shortcut
|
||||
Quarry Mill Stairs
|
||||
Quarry Boathouse Boat Staircase
|
||||
Quarry Boathouse First Barrier
|
||||
Quarry Boathouse Shortcut
|
||||
Shadows Timed Door
|
||||
Shadows Laser Room Right Door
|
||||
Shadows Laser Room Left Door
|
||||
Shadows Barrier to Quarry
|
||||
Shadows Barrier to Ledge
|
||||
Keep Hedge Maze 1 Exit Door
|
||||
Keep Pressure Plates 1 Exit Door
|
||||
Keep Hedge Maze 2 Shortcut
|
||||
Keep Hedge Maze 2 Exit Door
|
||||
Keep Hedge Maze 3 Shortcut
|
||||
Keep Hedge Maze 3 Exit Door
|
||||
Keep Hedge Maze 4 Shortcut
|
||||
Keep Hedge Maze 4 Exit Door
|
||||
Keep Pressure Plates 2 Exit Door
|
||||
Keep Pressure Plates 3 Exit Door
|
||||
Keep Pressure Plates 4 Exit Door
|
||||
Keep Shortcut to Shadows
|
||||
Keep Tower Shortcut
|
||||
Monastery Shortcut
|
||||
Monastery Inner Door
|
||||
Monastery Outer Door
|
||||
Monastery Door to Garden
|
||||
Town Cargo Box Door
|
||||
Town Wooden Roof Staircase
|
||||
Town Tinted Door to RGB House
|
||||
Town Door to Church
|
||||
Town Maze Staircase
|
||||
Town Windmill Door
|
||||
Town RGB House Staircase
|
||||
Town Tower Blue Panels Door
|
||||
Town Tower Lattice Door
|
||||
Town Tower Environmental Set Door
|
||||
Town Tower Wooden Roof Set Door
|
||||
Theater Entry Door
|
||||
Theater Exit Door Left
|
||||
Theater Exit Door Right
|
||||
Jungle Bamboo Shortcut to River
|
||||
Jungle Popup Wall
|
||||
River Shortcut to Monastery Garden
|
||||
Bunker Bunker Entry Door
|
||||
Bunker Tinted Glass Door
|
||||
Bunker Door to Ultraviolet Room
|
||||
Bunker Door to Elevator
|
||||
Swamp Entry Door
|
||||
Swamp Door to Broken Shapers
|
||||
Outside Tutorial Outpost Path (Door)
|
||||
Outside Tutorial Outpost Entry (Door)
|
||||
Outside Tutorial Outpost Exit (Door)
|
||||
Glass Factory Entry (Door)
|
||||
Glass Factory Back Wall (Door)
|
||||
Symmetry Island Lower (Door)
|
||||
Symmetry Island Upper (Door)
|
||||
Orchard First Gate (Door)
|
||||
Orchard Second Gate (Door)
|
||||
Desert Light Room Entry (Door)
|
||||
Desert Pond Room Entry (Door)
|
||||
Desert Flood Room Entry (Door)
|
||||
Desert Elevator Room Entry (Door)
|
||||
Quarry Entry 1 (Door)
|
||||
Quarry Entry 2 (Door)
|
||||
Quarry Mill Entry (Door)
|
||||
Quarry Mill Side Exit (Door)
|
||||
Quarry Mill Roof Exit (Door)
|
||||
Quarry Mill Stairs (Door)
|
||||
Quarry Boathouse Dock (Door)
|
||||
Quarry Boathouse First Barrier (Door)
|
||||
Quarry Boathouse Second Barrier (Door)
|
||||
Shadows Timed Door (Door)
|
||||
Shadows Laser Entry Right (Door)
|
||||
Shadows Laser Entry Left (Door)
|
||||
Shadows Quarry Barrier (Door)
|
||||
Shadows Ledge Barrier (Door)
|
||||
Keep Hedge Maze 1 Exit (Door)
|
||||
Keep Pressure Plates 1 Exit (Door)
|
||||
Keep Hedge Maze 2 Shortcut (Door)
|
||||
Keep Hedge Maze 2 Exit (Door)
|
||||
Keep Hedge Maze 3 Shortcut (Door)
|
||||
Keep Hedge Maze 3 Exit (Door)
|
||||
Keep Hedge Maze 4 Shortcut (Door)
|
||||
Keep Hedge Maze 4 Exit (Door)
|
||||
Keep Pressure Plates 2 Exit (Door)
|
||||
Keep Pressure Plates 3 Exit (Door)
|
||||
Keep Pressure Plates 4 Exit (Door)
|
||||
Keep Shadows Shortcut (Door)
|
||||
Keep Tower Shortcut (Door)
|
||||
Monastery Shortcut (Door)
|
||||
Monastery Entry Inner (Door)
|
||||
Monastery Entry Outer (Door)
|
||||
Monastery Garden Entry (Door)
|
||||
Town Cargo Box Entry (Door)
|
||||
Town Wooden Roof Stairs (Door)
|
||||
Town Tinted Glass Door (Door)
|
||||
Town Church Entry (Door)
|
||||
Town Maze Stairs (Door)
|
||||
Town Windmill Entry (Door)
|
||||
Town RGB House Stairs (Door)
|
||||
Town Tower First Door (Door)
|
||||
Town Tower Third Door (Door)
|
||||
Town Tower Fourth Door (Door)
|
||||
Town Tower Second Door (Door)
|
||||
Theater Entry (Door)
|
||||
Theater Exit Left (Door)
|
||||
Theater Exit Right (Door)
|
||||
Jungle Bamboo Laser Shortcut (Door)
|
||||
Jungle Popup Wall (Door)
|
||||
River Monastery Shortcut (Door)
|
||||
Bunker Entry (Door)
|
||||
Bunker Tinted Glass Door (Door)
|
||||
Bunker UV Room Entry (Door)
|
||||
Bunker Elevator Room Entry (Door)
|
||||
Swamp Entry (Door)
|
||||
Swamp Between Bridges First Door
|
||||
Swamp Platform Shortcut Door
|
||||
Swamp Cyan Water Pump
|
||||
Swamp Door to Rotated Shapers
|
||||
Swamp Red Water Pump
|
||||
Swamp Red Underwater Exit
|
||||
Swamp Blue Water Pump
|
||||
Swamp Purple Water Pump
|
||||
Swamp Near Laser Shortcut
|
||||
Treehouse First Door
|
||||
Treehouse Second Door
|
||||
Treehouse Beyond Yellow Bridge Door
|
||||
Treehouse Drawbridge
|
||||
Treehouse Timed Door to Laser House
|
||||
Inside Mountain First Layer Exit Door
|
||||
Inside Mountain Second Layer Staircase Near
|
||||
Inside Mountain Second Layer Exit Door
|
||||
Inside Mountain Second Layer Staircase Far
|
||||
Inside Mountain Giant Puzzle Exit Door
|
||||
Inside Mountain Door to Final Room
|
||||
Inside Mountain Bottom Layer Rock
|
||||
Inside Mountain Door to Secret Area
|
||||
Caves Pillar Door
|
||||
Caves Mountain Shortcut
|
||||
Caves Swamp Shortcut
|
||||
Challenge Entry Door
|
||||
Challenge Door to Theater Walkway
|
||||
Theater Walkway Door to Windmill Interior
|
||||
Theater Walkway Door to Desert Elevator Room
|
||||
Theater Walkway Door to Town
|
||||
Swamp Cyan Water Pump (Door)
|
||||
Swamp Between Bridges Second Door
|
||||
Swamp Red Water Pump (Door)
|
||||
Swamp Red Underwater Exit (Door)
|
||||
Swamp Blue Water Pump (Door)
|
||||
Swamp Purple Water Pump (Door)
|
||||
Swamp Laser Shortcut (Door)
|
||||
Treehouse First Door (Door)
|
||||
Treehouse Second Door (Door)
|
||||
Treehouse Third Door (Door)
|
||||
Treehouse Drawbridge (Door)
|
||||
Treehouse Laser House Entry (Door)
|
||||
Mountain Floor 1 Exit (Door)
|
||||
Mountain Floor 2 Staircase Near (Door)
|
||||
Mountain Floor 2 Exit (Door)
|
||||
Mountain Floor 2 Staircase Far (Door)
|
||||
Mountain Bottom Floor Giant Puzzle Exit (Door)
|
||||
Mountain Bottom Floor Final Room Entry (Door)
|
||||
Mountain Bottom Floor Rock (Door)
|
||||
Caves Entry (Door)
|
||||
Caves Pillar Door (Door)
|
||||
Caves Mountain Shortcut (Door)
|
||||
Caves Swamp Shortcut (Door)
|
||||
Challenge Entry (Door)
|
||||
Challenge Tunnels Entry (Door)
|
||||
Tunnels Theater Shortcut (Door)
|
||||
Tunnels Desert Shortcut (Door)
|
||||
Tunnels Town Shortcut (Door)
|
||||
|
||||
Desert Flood Room Flood Controls (Panel)
|
||||
Desert Flood Controls (Panel)
|
||||
Quarry Mill Ramp Controls (Panel)
|
||||
Quarry Mill Elevator Controls (Panel)
|
||||
Quarry Mill Lift Controls (Panel)
|
||||
Quarry Boathouse Ramp Height Control (Panel)
|
||||
Quarry Boathouse Ramp Horizontal Control (Panel)
|
||||
Bunker Elevator Control (Panel)
|
||||
@@ -108,32 +108,32 @@ Swamp Maze Control (Panel)
|
||||
Boat
|
||||
|
||||
Added Locations:
|
||||
Outside Tutorial Door to Outpost Panel
|
||||
Outside Tutorial Exit Door from Outpost Panel
|
||||
Glass Factory Entry Door Panel
|
||||
Glass Factory Vertical Symmetry 5
|
||||
Symmetry Island Door to Symmetry Island Lower Panel
|
||||
Symmetry Island Door to Symmetry Island Upper Panel
|
||||
Outside Tutorial Outpost Entry Panel
|
||||
Outside Tutorial Outpost Exit Panel
|
||||
Glass Factory Entry Panel
|
||||
Glass Factory Back Wall 5
|
||||
Symmetry Island Lower Panel
|
||||
Symmetry Island Upper Panel
|
||||
Orchard Apple Tree 3
|
||||
Orchard Apple Tree 5
|
||||
Desert Door to Desert Flood Light Room Panel
|
||||
Desert Artificial Light Reflection 3
|
||||
Desert Door to Water Levels Room Panel
|
||||
Desert Flood Reflection 6
|
||||
Quarry Door to Quarry 1 Panel
|
||||
Quarry Door to Quarry 2 Panel
|
||||
Quarry Door to Mill Right
|
||||
Quarry Door to Mill Left
|
||||
Quarry Mill Ground Floor Shortcut Door Panel
|
||||
Quarry Mill Door to Outside Quarry Stairs Panel
|
||||
Desert Light Room Entry Panel
|
||||
Desert Light Room 3
|
||||
Desert Flood Room Entry Panel
|
||||
Desert Flood Room 6
|
||||
Quarry Entry 1 Panel
|
||||
Quarry Entry 2 Panel
|
||||
Quarry Mill Entry Right Panel
|
||||
Quarry Mill Entry Left Panel
|
||||
Quarry Mill Side Exit Panel
|
||||
Quarry Mill Roof Exit Panel
|
||||
Quarry Mill Stair Control
|
||||
Quarry Boathouse Shortcut Door Panel
|
||||
Quarry Boathouse Second Barrier Panel
|
||||
Shadows Door Timer Inside
|
||||
Shadows Door Timer Outside
|
||||
Shadows Environmental Avoid 8
|
||||
Shadows Follow 5
|
||||
Shadows Lower Avoid 3
|
||||
Shadows Lower Avoid 5
|
||||
Shadows Far 8
|
||||
Shadows Near 5
|
||||
Shadows Intro 3
|
||||
Shadows Intro 5
|
||||
Keep Hedge Maze 1
|
||||
Keep Pressure Plates 1
|
||||
Keep Hedge Maze 2
|
||||
@@ -142,71 +142,70 @@ Keep Hedge Maze 4
|
||||
Keep Pressure Plates 2
|
||||
Keep Pressure Plates 3
|
||||
Keep Pressure Plates 4
|
||||
Keep Shortcut to Shadows Panel
|
||||
Keep Tower Shortcut to Keep Panel
|
||||
Monastery Shortcut Door Panel
|
||||
Monastery Door Open Left
|
||||
Monastery Door Open Right
|
||||
Monastery Rhombic Avoid 3
|
||||
Town Cargo Box Panel
|
||||
Town Full Dot Grid Shapers 5
|
||||
Town Tinted Door Panel
|
||||
Town Door to Church Stars Panel
|
||||
Keep Shadows Shortcut Panel
|
||||
Keep Tower Shortcut Panel
|
||||
Monastery Shortcut Panel
|
||||
Monastery Entry Left
|
||||
Monastery Entry Right
|
||||
Monastery Outside 3
|
||||
Town Cargo Box Entry Panel
|
||||
Town Wooden Roof Lower Row 5
|
||||
Town Tinted Glass Door Panel
|
||||
Town Church Entry Panel
|
||||
Town Maze Stair Control
|
||||
Town Windmill Door Panel
|
||||
Town Sound Room Left
|
||||
Town Windmill Entry Panel
|
||||
Town Sound Room Right
|
||||
Town Symmetry Squares 5 + Dots
|
||||
Town Red Rooftop 5
|
||||
Town Church Lattice
|
||||
Town Hexagonal Reflection
|
||||
Town Shapers & Dots & Eraser
|
||||
Windmill Door to Front of Theater Panel
|
||||
Theater Door to Cargo Box Left Panel
|
||||
Theater Door to Cargo Box Right Panel
|
||||
Jungle Shortcut to River Panel
|
||||
Town Tall Hexagonal
|
||||
Town Wooden Rooftop
|
||||
Windmill Theater Entry Panel
|
||||
Theater Exit Left Panel
|
||||
Theater Exit Right Panel
|
||||
Jungle Laser Shortcut Panel
|
||||
Jungle Popup Wall Control
|
||||
River Rhombic Avoid to Monastery Garden
|
||||
Bunker Bunker Entry Panel
|
||||
Bunker Door to Bunker Proper Panel
|
||||
Bunker Drawn Squares through Tinted Glass 3
|
||||
Bunker Drop-Down Door Squares 2
|
||||
River Monastery Shortcut Panel
|
||||
Bunker Entry Panel
|
||||
Bunker Tinted Glass Door Panel
|
||||
Bunker Glass Room 3
|
||||
Bunker UV Room 2
|
||||
Swamp Entry Panel
|
||||
Swamp Platform Shapers 4
|
||||
Swamp Platform Row 4
|
||||
Swamp Platform Shortcut Right Panel
|
||||
Swamp Blue Underwater Negative Shapers 5
|
||||
Swamp Broken Shapers 4
|
||||
Swamp Cyan Underwater Negative Shapers 5
|
||||
Swamp Red Underwater Negative Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp Near Laser Shortcut Right Panel
|
||||
Swamp Blue Underwater 5
|
||||
Swamp Between Bridges Near Row 4
|
||||
Swamp Cyan Underwater 5
|
||||
Swamp Red Underwater 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Laser Shortcut Right Panel
|
||||
Treehouse First Door Panel
|
||||
Treehouse Second Door Panel
|
||||
Treehouse Beyond Yellow Bridge Door Panel
|
||||
Treehouse Third Door Panel
|
||||
Treehouse Bridge Control
|
||||
Treehouse Left Orange Bridge 15
|
||||
Treehouse Right Orange Bridge 12
|
||||
Treehouse Laser House Door Timer Outside Control
|
||||
Treehouse Laser House Door Timer Inside Control
|
||||
Inside Mountain Moving Background 7
|
||||
Inside Mountain Obscured Vision 5
|
||||
Inside Mountain Physically Obstructed 3
|
||||
Inside Mountain Angled Inside Trash 2
|
||||
Inside Mountain Color Cycle 5
|
||||
Inside Mountain Light Bridge Controller 2
|
||||
Inside Mountain Light Bridge Controller 3
|
||||
Inside Mountain Same Solution 6
|
||||
Inside Mountain Giant Puzzle
|
||||
Inside Mountain Door to Final Room Left
|
||||
Inside Mountain Door to Final Room Right
|
||||
Inside Mountain Bottom Layer Discard
|
||||
Inside Mountain Rock Control
|
||||
Inside Mountain Secret Area Entry Panel
|
||||
Inside Mountain Caves Lone Pillar
|
||||
Inside Mountain Caves Shortcut to Mountain Panel
|
||||
Inside Mountain Caves Shortcut to Swamp Panel
|
||||
Inside Mountain Caves Challenge Entry Panel
|
||||
Challenge Door to Theater Walkway Panel
|
||||
Theater Walkway Theater Shortcut Panel
|
||||
Theater Walkway Desert Shortcut Panel
|
||||
Theater Walkway Town Shortcut Panel
|
||||
Treehouse Laser House Door Timer Inside
|
||||
Mountain Floor 1 Left Row 7
|
||||
Mountain Floor 1 Right Row 5
|
||||
Mountain Floor 1 Back Row 3
|
||||
Mountain Floor 1 Trash Pillar 2
|
||||
Mountain Floor 2 Near Row 5
|
||||
Mountain Floor 2 Light Bridge Controller Near
|
||||
Mountain Floor 2 Light Bridge Controller Far
|
||||
Mountain Floor 2 Far Row 6
|
||||
Mountain Bottom Floor Giant Puzzle
|
||||
Mountain Bottom Floor Final Room Entry Left
|
||||
Mountain Bottom Floor Final Room Entry Right
|
||||
Mountain Bottom Floor Discard
|
||||
Mountain Bottom Floor Rock Control
|
||||
Mountain Bottom Floor Caves Entry Panel
|
||||
Caves Lone Pillar
|
||||
Caves Mountain Shortcut Panel
|
||||
Caves Swamp Shortcut Panel
|
||||
Caves Challenge Entry Panel
|
||||
Challenge Tunnels Entry Panel
|
||||
Tunnels Theater Shortcut Panel
|
||||
Tunnels Desert Shortcut Panel
|
||||
Tunnels Town Shortcut Panel
|
||||
@@ -1,73 +1,73 @@
|
||||
Items:
|
||||
Glass Factory Back Wall
|
||||
Quarry Boathouse Boat Staircase
|
||||
Glass Factory Back Wall (Door)
|
||||
Quarry Boathouse Dock (Door)
|
||||
Outside Tutorial Outpost Doors
|
||||
Glass Factory Entry Door
|
||||
Glass Factory Entry (Door)
|
||||
Symmetry Island Doors
|
||||
Orchard Gates
|
||||
Desert Doors
|
||||
Quarry Main Entry
|
||||
Quarry Door to Mill
|
||||
Quarry Mill Entry (Door)
|
||||
Quarry Mill Shortcuts
|
||||
Quarry Boathouse Barriers
|
||||
Shadows Timed Door
|
||||
Shadows Timed Door (Door)
|
||||
Shadows Laser Room Door
|
||||
Shadows Barriers
|
||||
Keep Hedge Maze Doors
|
||||
Keep Pressure Plates Doors
|
||||
Keep Shortcuts
|
||||
Monastery Entry Door
|
||||
Monastery Entry
|
||||
Monastery Shortcuts
|
||||
Town Doors
|
||||
Town Tower Doors
|
||||
Theater Entry Door
|
||||
Theater Exit Door
|
||||
Theater Entry (Door)
|
||||
Theater Exit
|
||||
Jungle & River Shortcuts
|
||||
Jungle Popup Wall
|
||||
Jungle Popup Wall (Door)
|
||||
Bunker Doors
|
||||
Swamp Doors
|
||||
Swamp Near Laser Shortcut
|
||||
Swamp Laser Shortcut (Door)
|
||||
Swamp Water Pumps
|
||||
Treehouse Entry Doors
|
||||
Treehouse Drawbridge
|
||||
Treehouse Timed Door to Laser House
|
||||
Inside Mountain First Layer Exit Door
|
||||
Inside Mountain Second Layer Stairs & Doors
|
||||
Inside Mountain Giant Puzzle Exit Door
|
||||
Inside Mountain Door to Final Room
|
||||
Inside Mountain Bottom Layer Doors to Caves
|
||||
Treehouse Drawbridge (Door)
|
||||
Treehouse Laser House Entry (Door)
|
||||
Mountain Floor 1 Exit (Door)
|
||||
Mountain Floor 2 Stairs & Doors
|
||||
Mountain Bottom Floor Giant Puzzle Exit (Door)
|
||||
Mountain Bottom Floor Final Room Entry (Door)
|
||||
Mountain Bottom Floor Doors to Caves
|
||||
Caves Doors to Challenge
|
||||
Caves Exits to Main Island
|
||||
Challenge Door to Theater Walkway
|
||||
Theater Walkway Doors
|
||||
Challenge Tunnels Entry (Door)
|
||||
Tunnels Doors
|
||||
|
||||
Added Locations:
|
||||
Outside Tutorial Door to Outpost Panel
|
||||
Outside Tutorial Exit Door from Outpost Panel
|
||||
Glass Factory Entry Door Panel
|
||||
Glass Factory Vertical Symmetry 5
|
||||
Symmetry Island Door to Symmetry Island Lower Panel
|
||||
Symmetry Island Door to Symmetry Island Upper Panel
|
||||
Outside Tutorial Outpost Entry Panel
|
||||
Outside Tutorial Outpost Exit Panel
|
||||
Glass Factory Entry Panel
|
||||
Glass Factory Back Wall 5
|
||||
Symmetry Island Lower Panel
|
||||
Symmetry Island Upper Panel
|
||||
Orchard Apple Tree 3
|
||||
Orchard Apple Tree 5
|
||||
Desert Door to Desert Flood Light Room Panel
|
||||
Desert Artificial Light Reflection 3
|
||||
Desert Door to Water Levels Room Panel
|
||||
Desert Flood Reflection 6
|
||||
Quarry Door to Quarry 1 Panel
|
||||
Quarry Door to Quarry 2 Panel
|
||||
Quarry Door to Mill Right
|
||||
Quarry Door to Mill Left
|
||||
Quarry Mill Ground Floor Shortcut Door Panel
|
||||
Quarry Mill Door to Outside Quarry Stairs Panel
|
||||
Desert Light Room Entry Panel
|
||||
Desert Light Room 3
|
||||
Desert Flood Room Entry Panel
|
||||
Desert Flood Room 6
|
||||
Quarry Entry 1 Panel
|
||||
Quarry Entry 2 Panel
|
||||
Quarry Mill Entry Right Panel
|
||||
Quarry Mill Entry Left Panel
|
||||
Quarry Mill Side Exit Panel
|
||||
Quarry Mill Roof Exit Panel
|
||||
Quarry Mill Stair Control
|
||||
Quarry Boathouse Shortcut Door Panel
|
||||
Quarry Boathouse Second Barrier Panel
|
||||
Shadows Door Timer Inside
|
||||
Shadows Door Timer Outside
|
||||
Shadows Environmental Avoid 8
|
||||
Shadows Follow 5
|
||||
Shadows Lower Avoid 3
|
||||
Shadows Lower Avoid 5
|
||||
Shadows Far 8
|
||||
Shadows Near 5
|
||||
Shadows Intro 3
|
||||
Shadows Intro 5
|
||||
Keep Hedge Maze 1
|
||||
Keep Pressure Plates 1
|
||||
Keep Hedge Maze 2
|
||||
@@ -76,71 +76,70 @@ Keep Hedge Maze 4
|
||||
Keep Pressure Plates 2
|
||||
Keep Pressure Plates 3
|
||||
Keep Pressure Plates 4
|
||||
Keep Shortcut to Shadows Panel
|
||||
Keep Tower Shortcut to Keep Panel
|
||||
Monastery Shortcut Door Panel
|
||||
Monastery Door Open Left
|
||||
Monastery Door Open Right
|
||||
Monastery Rhombic Avoid 3
|
||||
Town Cargo Box Panel
|
||||
Town Full Dot Grid Shapers 5
|
||||
Town Tinted Door Panel
|
||||
Town Door to Church Stars Panel
|
||||
Keep Shadows Shortcut Panel
|
||||
Keep Tower Shortcut Panel
|
||||
Monastery Shortcut Panel
|
||||
Monastery Entry Left
|
||||
Monastery Entry Right
|
||||
Monastery Outside 3
|
||||
Town Cargo Box Entry Panel
|
||||
Town Wooden Roof Lower Row 5
|
||||
Town Tinted Glass Door Panel
|
||||
Town Church Entry Panel
|
||||
Town Maze Stair Control
|
||||
Town Windmill Door Panel
|
||||
Town Sound Room Left
|
||||
Town Windmill Entry Panel
|
||||
Town Sound Room Right
|
||||
Town Symmetry Squares 5 + Dots
|
||||
Town Red Rooftop 5
|
||||
Town Church Lattice
|
||||
Town Hexagonal Reflection
|
||||
Town Shapers & Dots & Eraser
|
||||
Windmill Door to Front of Theater Panel
|
||||
Theater Door to Cargo Box Left Panel
|
||||
Theater Door to Cargo Box Right Panel
|
||||
Jungle Shortcut to River Panel
|
||||
Town Tall Hexagonal
|
||||
Town Wooden Rooftop
|
||||
Windmill Theater Entry Panel
|
||||
Theater Exit Left Panel
|
||||
Theater Exit Right Panel
|
||||
Jungle Laser Shortcut Panel
|
||||
Jungle Popup Wall Control
|
||||
River Rhombic Avoid to Monastery Garden
|
||||
Bunker Bunker Entry Panel
|
||||
Bunker Door to Bunker Proper Panel
|
||||
Bunker Drawn Squares through Tinted Glass 3
|
||||
Bunker Drop-Down Door Squares 2
|
||||
River Monastery Shortcut Panel
|
||||
Bunker Entry Panel
|
||||
Bunker Tinted Glass Door Panel
|
||||
Bunker Glass Room 3
|
||||
Bunker UV Room 2
|
||||
Swamp Entry Panel
|
||||
Swamp Platform Shapers 4
|
||||
Swamp Platform Row 4
|
||||
Swamp Platform Shortcut Right Panel
|
||||
Swamp Blue Underwater Negative Shapers 5
|
||||
Swamp Broken Shapers 4
|
||||
Swamp Cyan Underwater Negative Shapers 5
|
||||
Swamp Red Underwater Negative Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp More Rotated Shapers 4
|
||||
Swamp Near Laser Shortcut Right Panel
|
||||
Swamp Blue Underwater 5
|
||||
Swamp Between Bridges Near Row 4
|
||||
Swamp Cyan Underwater 5
|
||||
Swamp Red Underwater 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Beyond Rotating Bridge 4
|
||||
Swamp Laser Shortcut Right Panel
|
||||
Treehouse First Door Panel
|
||||
Treehouse Second Door Panel
|
||||
Treehouse Beyond Yellow Bridge Door Panel
|
||||
Treehouse Third Door Panel
|
||||
Treehouse Bridge Control
|
||||
Treehouse Left Orange Bridge 15
|
||||
Treehouse Right Orange Bridge 12
|
||||
Treehouse Laser House Door Timer Outside Control
|
||||
Treehouse Laser House Door Timer Inside Control
|
||||
Inside Mountain Moving Background 7
|
||||
Inside Mountain Obscured Vision 5
|
||||
Inside Mountain Physically Obstructed 3
|
||||
Inside Mountain Angled Inside Trash 2
|
||||
Inside Mountain Color Cycle 5
|
||||
Inside Mountain Light Bridge Controller 2
|
||||
Inside Mountain Light Bridge Controller 3
|
||||
Inside Mountain Same Solution 6
|
||||
Inside Mountain Giant Puzzle
|
||||
Inside Mountain Door to Final Room Left
|
||||
Inside Mountain Door to Final Room Right
|
||||
Inside Mountain Bottom Layer Discard
|
||||
Inside Mountain Rock Control
|
||||
Inside Mountain Secret Area Entry Panel
|
||||
Inside Mountain Caves Lone Pillar
|
||||
Inside Mountain Caves Shortcut to Mountain Panel
|
||||
Inside Mountain Caves Shortcut to Swamp Panel
|
||||
Inside Mountain Caves Challenge Entry Panel
|
||||
Challenge Door to Theater Walkway Panel
|
||||
Theater Walkway Theater Shortcut Panel
|
||||
Theater Walkway Desert Shortcut Panel
|
||||
Theater Walkway Town Shortcut Panel
|
||||
Treehouse Laser House Door Timer Inside
|
||||
Mountain Floor 1 Left Row 7
|
||||
Mountain Floor 1 Right Row 5
|
||||
Mountain Floor 1 Back Row 3
|
||||
Mountain Floor 1 Trash Pillar 2
|
||||
Mountain Floor 2 Near Row 5
|
||||
Mountain Floor 2 Light Bridge Controller Near
|
||||
Mountain Floor 2 Light Bridge Controller Far
|
||||
Mountain Floor 2 Far Row 6
|
||||
Mountain Bottom Floor Giant Puzzle
|
||||
Mountain Bottom Floor Final Room Entry Left
|
||||
Mountain Bottom Floor Final Room Entry Right
|
||||
Mountain Bottom Floor Discard
|
||||
Mountain Bottom Floor Rock Control
|
||||
Mountain Bottom Floor Caves Entry Panel
|
||||
Caves Lone Pillar
|
||||
Caves Mountain Shortcut Panel
|
||||
Caves Swamp Shortcut Panel
|
||||
Caves Challenge Entry Panel
|
||||
Challenge Tunnels Entry Panel
|
||||
Tunnels Theater Shortcut Panel
|
||||
Tunnels Desert Shortcut Panel
|
||||
Tunnels Town Shortcut Panel
|
||||
@@ -5,5 +5,5 @@ Starting Inventory:
|
||||
Caves Exits to Main Island
|
||||
|
||||
Remove Items:
|
||||
Caves Mountain Shortcut
|
||||
Caves Swamp Shortcut
|
||||
Caves Mountain Shortcut (Door)
|
||||
Caves Swamp Shortcut (Door)
|
||||
Reference in New Issue
Block a user