mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 01:23:48 -07:00
Compare commits
29 Commits
appimage-0
...
factorio_d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63fb888191 | ||
|
|
38eef5ac00 | ||
|
|
3e627f80fd | ||
|
|
0d6aeea9fd | ||
|
|
6cd1e8a295 | ||
|
|
1cbe5ae669 | ||
|
|
c5b5ad495c | ||
|
|
813ee5ee3b | ||
|
|
be1158ad78 | ||
|
|
5b3f4460b8 | ||
|
|
de8eff39b3 | ||
|
|
6d5ddf3cad | ||
|
|
809bda02d1 | ||
|
|
2d5ec6ce22 | ||
|
|
a95d0ce9ef | ||
|
|
267d9234e5 | ||
|
|
4686881566 | ||
|
|
101dab0ea4 | ||
|
|
c2d69cb05e | ||
|
|
58f66e0f42 | ||
|
|
0215e1fa28 | ||
|
|
1c0a93acad | ||
|
|
4fcde135e5 | ||
|
|
332dde154f | ||
|
|
8d51205e8f | ||
|
|
ff05e9d7d5 | ||
|
|
516a52c041 | ||
|
|
9daa64741b | ||
|
|
af11fa5150 |
@@ -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' %
|
||||
|
||||
@@ -65,6 +65,7 @@ class FactorioContext(CommonContext):
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
self.energy_link_increment = 0
|
||||
self.last_deplete = 0
|
||||
self.custom_data_package = 0
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
@@ -170,7 +171,7 @@ async def game_watcher(ctx: FactorioContext):
|
||||
if ctx.locations_checked != research_data:
|
||||
bridge_logger.debug(
|
||||
f"New researches done: "
|
||||
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
|
||||
f"{[lookup_id_to_name.get(rid, f'Unknown Research (ID: {rid})') for rid in research_data - ctx.locations_checked]}")
|
||||
ctx.locations_checked = research_data
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
|
||||
death_link_tick = data.get("death_link_tick", 0)
|
||||
@@ -268,7 +269,11 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
||||
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
|
||||
item_id = transfer_item.item
|
||||
player_name = ctx.player_names[transfer_item.player]
|
||||
if item_id not in Factorio.item_id_to_name:
|
||||
if ctx.custom_data_package:
|
||||
item_name = Factorio.item_id_to_name.get(item_id, f"Unknown Item (ID: {item_id})")
|
||||
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.{(' (Item name might not match the seed.)' if Factorio.data_version else '')}")
|
||||
commands[ctx.send_index] = f'/ap-get-technology {item_id}\t{ctx.send_index}\t{player_name}'
|
||||
elif item_id not in Factorio.item_id_to_name:
|
||||
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
|
||||
else:
|
||||
item_name = Factorio.item_id_to_name[item_id]
|
||||
@@ -297,6 +302,7 @@ async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
|
||||
# 0.2.0 addition, not present earlier
|
||||
death_link = bool(info.get("death_link", False))
|
||||
ctx.energy_link_increment = info.get("energy_link", 0)
|
||||
ctx.custom_data_package = info.get("custom_data_package", 0)
|
||||
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
|
||||
if ctx.energy_link_increment and ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
203
MultiServer.py
203
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:
|
||||
@@ -900,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:
|
||||
@@ -1335,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.")
|
||||
@@ -1355,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},
|
||||
@@ -1859,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)
|
||||
@@ -1890,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:
|
||||
@@ -2044,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:
|
||||
|
||||
@@ -295,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
|
||||
@@ -790,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")
|
||||
|
||||
@@ -807,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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
8
setup.py
8
setup.py
@@ -17,7 +17,7 @@ from Launcher import components, icon_paths
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
import subprocess
|
||||
import pkg_resources
|
||||
requirement = 'cx-Freeze==6.10'
|
||||
requirement = 'cx-Freeze>=6.11'
|
||||
try:
|
||||
pkg_resources.require(requirement)
|
||||
import cx_Freeze
|
||||
@@ -70,7 +70,7 @@ def _threaded_hash(filepath):
|
||||
|
||||
|
||||
# cx_Freeze's build command runs other commands. Override to accept --yes and store that.
|
||||
class BuildCommand(cx_Freeze.dist.build):
|
||||
class BuildCommand(cx_Freeze.command.build.Build):
|
||||
user_options = [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
]
|
||||
@@ -87,8 +87,8 @@ class BuildCommand(cx_Freeze.dist.build):
|
||||
|
||||
|
||||
# Override cx_Freeze's build_exe command for pre and post build steps
|
||||
class BuildExeCommand(cx_Freeze.dist.build_exe):
|
||||
user_options = cx_Freeze.dist.build_exe.user_options + [
|
||||
class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [
|
||||
('yes', 'y', 'Answer "yes" to all questions.'),
|
||||
('extra-data=', None, 'Additional files to add.'),
|
||||
]
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -221,10 +221,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 +240,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",
|
||||
@@ -349,7 +351,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 +412,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 +440,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 +487,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)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import Patch
|
||||
from . import Options
|
||||
|
||||
from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \
|
||||
base_tech_table, tech_to_progressive_lookup, fluids
|
||||
base_tech_table, tech_to_progressive_lookup, fluids, mods, tech_table, factorio_base_id
|
||||
|
||||
template_env: Optional[jinja2.Environment] = None
|
||||
|
||||
@@ -34,7 +34,9 @@ base_info = {
|
||||
"factorio_version": "1.1",
|
||||
"dependencies": [
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited"
|
||||
"? science-not-invited",
|
||||
"? factory-levels",
|
||||
"! archipelago-extractor"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -116,6 +118,10 @@ def generate_mod(world, output_directory: str):
|
||||
return base - (base - low) * distance
|
||||
return random.uniform(low, high)
|
||||
|
||||
all_items = tech_table.copy()
|
||||
all_items["Attack Trap"] = factorio_base_id - 1
|
||||
all_items["Evolution Trap"] = factorio_base_id - 2
|
||||
|
||||
template_data = {
|
||||
"locations": locations, "player_names": multiworld.player_name, "tech_table": tech_table,
|
||||
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
|
||||
@@ -134,11 +140,13 @@ def generate_mod(world, output_directory: str):
|
||||
"free_sample_blacklist": {item: 1 for item in free_sample_exclusions},
|
||||
"progressive_technology_table": {tech.name: tech.progressive for tech in
|
||||
progressive_technology_table.values()},
|
||||
"item_id_to_name": {f"{item_id}": item_name for item_name, item_id in all_items.items()},
|
||||
"custom_recipes": world.custom_recipes,
|
||||
"max_science_pack": multiworld.max_science_pack[player].value,
|
||||
"liquids": fluids,
|
||||
"goal": multiworld.goal[player].value,
|
||||
"energy_link": multiworld.energy_link[player].value
|
||||
"energy_link": multiworld.energy_link[player].value,
|
||||
"custom_data_package": 1 if mods else 0
|
||||
}
|
||||
|
||||
for factorio_option in Options.factorio_options:
|
||||
@@ -191,6 +199,8 @@ def generate_mod(world, output_directory: str):
|
||||
f.write(locale_content)
|
||||
info = base_info.copy()
|
||||
info["name"] = mod_name
|
||||
for mod in mods.values():
|
||||
info["dependencies"].append(f"{mod.name} >= {mod.version}")
|
||||
with open(os.path.join(mod_dir, "info.json"), "wt") as f:
|
||||
json.dump(info, f, indent=4)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
from sys import getrecursionlimit
|
||||
from collections import Counter
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any
|
||||
@@ -22,13 +23,14 @@ def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]:
|
||||
import pkgutil
|
||||
return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode())
|
||||
|
||||
|
||||
# TODO: Make use of the lab information. (it has info on the science packs)
|
||||
techs_future = pool.submit(load_json_data, "techs")
|
||||
recipes_future = pool.submit(load_json_data, "recipes")
|
||||
resources_future = pool.submit(load_json_data, "resources")
|
||||
machines_future = pool.submit(load_json_data, "machines")
|
||||
fluids_future = pool.submit(load_json_data, "fluids")
|
||||
items_future = pool.submit(load_json_data, "items")
|
||||
mods_future = pool.submit(load_json_data, "mods")
|
||||
|
||||
tech_table: Dict[str, int] = {}
|
||||
technology_table: Dict[str, Technology] = {}
|
||||
@@ -94,6 +96,8 @@ class CustomTechnology(Technology):
|
||||
|
||||
def __init__(self, origin: Technology, world, allowed_packs: Set[str], player: int):
|
||||
ingredients = origin.ingredients & allowed_packs
|
||||
if origin.ingredients and not ingredients:
|
||||
logging.warning(f"Technology {origin.name} has no vanilla science packs. Custom science packs are not supported.")
|
||||
military_allowed = "military-science-pack" in allowed_packs \
|
||||
and ((ingredients & {"chemical-science-pack", "production-science-pack", "utility-science-pack"})
|
||||
or origin.name == "rocket-silo")
|
||||
@@ -103,7 +107,8 @@ class CustomTechnology(Technology):
|
||||
ingredients.add("military-science-pack")
|
||||
ingredients = list(ingredients)
|
||||
ingredients.sort() # deterministic sample
|
||||
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
||||
if ingredients:
|
||||
ingredients = world.random.sample(ingredients, world.random.randint(1, len(ingredients)))
|
||||
elif origin.name == "rocket-silo" and military_allowed:
|
||||
ingredients.add("military-science-pack")
|
||||
super(CustomTechnology, self).__init__(origin.name, ingredients, origin.factorio_id)
|
||||
@@ -115,13 +120,19 @@ class Recipe(FactorioElement):
|
||||
ingredients: Dict[str, int]
|
||||
products: Dict[str, int]
|
||||
energy: float
|
||||
mining: bool
|
||||
burning: bool
|
||||
unlocked_at_start: bool
|
||||
|
||||
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float):
|
||||
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float, mining: bool = False, unlocked_at_start: bool = False, burning: bool = False):
|
||||
self.name = name
|
||||
self.category = category
|
||||
self.ingredients = ingredients
|
||||
self.products = products
|
||||
self.energy = energy
|
||||
self.mining = mining
|
||||
self.burning = burning
|
||||
self.unlocked_at_start = unlocked_at_start
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.name})"
|
||||
@@ -147,28 +158,56 @@ class Recipe(FactorioElement):
|
||||
@property
|
||||
def rel_cost(self) -> float:
|
||||
ingredients = sum(self.ingredients.values())
|
||||
return min(ingredients / amount for product, amount in self.products.items())
|
||||
if all(amount == 0 for amount in self.products.values()):
|
||||
return float('inf')
|
||||
return min(ingredients / amount for product, amount in self.products.items() if amount > 0)
|
||||
|
||||
@property
|
||||
def base_cost(self) -> Dict[str, int]:
|
||||
ingredients = Counter()
|
||||
for ingredient, cost in self.ingredients.items():
|
||||
if ingredient in all_product_sources:
|
||||
recipe_counters: Dict[str, (Recipe, Counter)] = {}
|
||||
for recipe in all_product_sources[ingredient]:
|
||||
recipe_ingredients = Counter()
|
||||
if recipe.ingredients:
|
||||
ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
|
||||
recipe_ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
|
||||
recipe.base_cost.items()})
|
||||
else:
|
||||
ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
|
||||
recipe_ingredients[ingredient] += recipe.energy * cost / recipe.products[ingredient]
|
||||
recipe_counters[recipe.name] = (recipe, recipe_ingredients)
|
||||
selected_recipe_ingredients = None
|
||||
for recipe_name, (recipe, recipe_ingredients) in recipe_counters.items():
|
||||
if not selected_recipe_ingredients or (
|
||||
sum([rel_cost.get(name, high_cost_item) * value for name, value in recipe_ingredients.items()])
|
||||
< sum([rel_cost.get(name, high_cost_item) * value for name, value in selected_recipe_ingredients.items()])):
|
||||
selected_recipe_ingredients = recipe_ingredients
|
||||
ingredients.update(selected_recipe_ingredients)
|
||||
else:
|
||||
ingredients[ingredient] += cost
|
||||
return ingredients
|
||||
|
||||
recursion_loop = 0
|
||||
max_recursion_loop = 0.85 * getrecursionlimit()
|
||||
|
||||
def detect_recursive_loop(self) -> bool:
|
||||
Recipe.recursion_loop += 1
|
||||
if Recipe.max_recursion_loop < Recipe.recursion_loop:
|
||||
Recipe.recursion_loop = 0
|
||||
return True
|
||||
for ingredient in self.ingredients.keys():
|
||||
if ingredient in all_product_sources:
|
||||
for ingredient_recipe in all_product_sources[ingredient]:
|
||||
if ingredient_recipe.ingredients:
|
||||
if ingredient_recipe.detect_recursive_loop():
|
||||
return True
|
||||
Recipe.recursion_loop -= 1
|
||||
return False
|
||||
|
||||
@property
|
||||
def total_energy(self) -> float:
|
||||
"""Total required energy (crafting time) for single craft"""
|
||||
# TODO: multiply mining energy by 2 since drill has 0.5 speed
|
||||
total_energy = self.energy
|
||||
total_energy = (self.energy / machines[self.crafting_machine].speed)
|
||||
for ingredient, cost in self.ingredients.items():
|
||||
if ingredient in all_product_sources:
|
||||
selected_recipe_energy = float('inf')
|
||||
@@ -182,10 +221,71 @@ class Recipe(FactorioElement):
|
||||
|
||||
|
||||
class Machine(FactorioElement):
|
||||
def __init__(self, name, categories):
|
||||
def __init__(self, name, categories, machine_type, speed, item_sources, input_fluids, output_fluids):
|
||||
self.name: str = name
|
||||
self.categories: set = categories
|
||||
self.machine_type: str = machine_type
|
||||
self.speed: float = speed
|
||||
self.item_sources: set = item_sources
|
||||
self.input_fluids: int = int(input_fluids)
|
||||
self.output_fluids: int = int(output_fluids)
|
||||
|
||||
class Lab(FactorioElement):
|
||||
def __init__(self, name, inputs):
|
||||
self.name: str = name
|
||||
self.inputs: set = inputs
|
||||
|
||||
|
||||
class Mod(FactorioElement):
|
||||
def __init__(self, name, version):
|
||||
self.name: str = name
|
||||
self.version: str = version
|
||||
|
||||
|
||||
class Item(FactorioElement):
|
||||
def __init__(self, name, stack_size, stackable, place_result, burnt_result, fuel_value, fuel_category, rocket_launch_products):
|
||||
self.name: str = name
|
||||
self.stack_size: int = stack_size
|
||||
self.stackable: bool = stackable
|
||||
self.place_result: str = place_result
|
||||
self.burnt_result: str = burnt_result
|
||||
self.fuel_value: int = fuel_value
|
||||
self.fuel_category: str = fuel_category
|
||||
self.rocket_launch_products: Dict[str, int] = rocket_launch_products
|
||||
self.product_sources: Set[Recipe] = set()
|
||||
|
||||
class Fluid(FactorioElement):
|
||||
def __init__(self, name, default_temperature, max_temperature, heat_capacity):
|
||||
self.name: str = name
|
||||
if max_temperature == "inf":
|
||||
max_temperature = 2**64
|
||||
self.default_temperature: int = default_temperature
|
||||
self.max_temperature: int = max_temperature
|
||||
self.heat_capacity = heat_capacity
|
||||
self.product_sources: Set[Recipe] = set()
|
||||
|
||||
|
||||
items: Dict[str, Item] = {}
|
||||
for name, item_data in items_future.result().items():
|
||||
item = Item(name,
|
||||
item_data.get("stack_size"),
|
||||
item_data.get("stackable"),
|
||||
item_data.get("place_result", None),
|
||||
item_data.get("burnt_result", None),
|
||||
item_data.get("fuel_value", 0),
|
||||
item_data.get("fuel_category", None),
|
||||
item_data.get("rocket_launch_products", {}))
|
||||
items[name] = item
|
||||
del items_future
|
||||
|
||||
fluids: Dict[str, Fluid] = {}
|
||||
for name, fluid_data in fluids_future.result().items():
|
||||
fluid = Fluid(name,
|
||||
fluid_data.get("default_temperature", 0),
|
||||
fluid_data.get("max_temperature", 0),
|
||||
fluid_data.get("heat_capacity", 1000))
|
||||
fluids[name] = fluid
|
||||
del fluids_future
|
||||
|
||||
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
|
||||
|
||||
@@ -213,38 +313,149 @@ for resource_name, resource_data in resources_future.result().items():
|
||||
if "required_fluid" in resource_data else {},
|
||||
"products": {data["name"]: data["amount"] for data in resource_data["products"].values()},
|
||||
"energy": resource_data["mining_time"],
|
||||
"category": resource_data["category"]
|
||||
"category": resource_data["category"],
|
||||
"mining": True,
|
||||
"unlocked_at_start": True
|
||||
}
|
||||
del resources_future
|
||||
|
||||
machines: Dict[str, Machine] = {}
|
||||
labs: Dict[str, Lab] = {}
|
||||
rel_cost = {}
|
||||
high_cost_item = 10000
|
||||
|
||||
for name, prototype in machines_future.result().items():
|
||||
machine_prototype = prototype.get("common", {}).get("type", None)
|
||||
machine_item_sources = set(prototype.get("common", {}).get("placeable_by", {}))
|
||||
for machine_type, machine_data in prototype.items():
|
||||
if machine_type == "lab":
|
||||
lab = Lab(name, machine_data.get("inputs", set()))
|
||||
labs[name] = lab
|
||||
if machine_type == "offshore-pump":
|
||||
fluid = machine_data.get("fluid", None)
|
||||
speed = machine_data.get("speed", None)
|
||||
if not fluid or not speed:
|
||||
continue
|
||||
category = f"offshore-pumping-{fluid}-{speed}"
|
||||
raw_recipes[category] = {
|
||||
"ingredients": {},
|
||||
"products": {fluid: (speed*60)},
|
||||
"energy": 1,
|
||||
"category": category,
|
||||
"mining": True,
|
||||
"unlocked_at_start": True
|
||||
}
|
||||
machine = Machine(name, {category}, machine_prototype, 1, machine_item_sources, 0, 1)
|
||||
machines[name] = machine
|
||||
if machine_type == "crafting":
|
||||
categories = machine_data.get("categories", set())
|
||||
if not categories:
|
||||
continue
|
||||
# TODO: Use speed / fluid_box info
|
||||
speed = machine_data.get("speed", 1)
|
||||
input_fluid_box = machine_data.get("input_fluid_box", 0)
|
||||
output_fluid_box = machine_data.get("output_fluid_box", 0)
|
||||
machine = Machine(name, set(categories), machine_prototype, speed, machine_item_sources, input_fluid_box, output_fluid_box)
|
||||
machines[name] = machine
|
||||
if machine_type == "mining":
|
||||
categories = machine_data.get("categories", set())
|
||||
if not categories:
|
||||
continue
|
||||
speed = machine_data.get("speed", 1)
|
||||
input_fluid_box = machine_data.get("input_fluid_box", False) # Can this machine mine resources with required fluids?
|
||||
output_fluid_box = machine_data.get("output_fluid_box", False) # Can this machine mine fluid resources?
|
||||
machine = machines.setdefault(name, Machine(name, set(categories), machine_prototype, speed, machine_item_sources, input_fluid_box, output_fluid_box))
|
||||
machine.categories |= set(categories) # character has both crafting and basic-solid
|
||||
machine.speed = (machine.speed + speed) / 2
|
||||
machines[name] = machine
|
||||
if machine_type == "boiler":
|
||||
input_fluid = machine_data.get("input_fluid")
|
||||
output_fluid = machine_data.get("output_fluid")
|
||||
target_temperature = machine_data.get("target_temperature")
|
||||
energy_usage = machine_data.get("energy_usage")
|
||||
amount = energy_usage / (target_temperature - fluids[input_fluid].default_temperature) / fluids[input_fluid].heat_capacity
|
||||
amount *= 60
|
||||
amount = int(amount)
|
||||
category = f"boiling-{amount}-{input_fluid}-to-{output_fluid}-at-{target_temperature}-degrees-centigrade"
|
||||
raw_recipes[category] = {
|
||||
"ingredients": {input_fluid: amount},
|
||||
"products": {output_fluid: amount},
|
||||
"energy": 1,
|
||||
"category": category,
|
||||
"mining": True,
|
||||
"unlocked_at_start": True
|
||||
}
|
||||
machine = Machine(name, {category}, machine_prototype, 1, machine_item_sources, 1, 1)
|
||||
machines[name] = machine
|
||||
if machine_type == "fuel_burner":
|
||||
categories = set(machine_data.get("categories"))
|
||||
fuel_items = {name: item for name, item in items.items() if item.burnt_result and item.fuel_category in categories}
|
||||
if not fuel_items:
|
||||
continue
|
||||
energy_usage = machine_data.get("energy_usage")
|
||||
for item_name, item in fuel_items.items():
|
||||
recipe_name = f"burning-{item_name}-for-{item.burnt_result}"
|
||||
raw_recipes[recipe_name] = {
|
||||
"ingredients": {item_name: 1},
|
||||
"products": {item.burnt_result: 1},
|
||||
"energy": item.fuel_value,
|
||||
"category": item.fuel_category,
|
||||
"burning": True,
|
||||
"unlocked_at_start": True
|
||||
}
|
||||
machine = machines.get(name, Machine(name, categories, machine_prototype, energy_usage * 60, machine_item_sources, 0, 0))
|
||||
machines[name] = machine
|
||||
|
||||
|
||||
# TODO: set up machine/recipe pairs for burners in order to retrieve the burnt_result from items.
|
||||
# TODO: set up machine/recipe pairs for retrieving rocket_launch_products from items.
|
||||
|
||||
del machines_future
|
||||
|
||||
for recipe_name, recipe_data in raw_recipes.items():
|
||||
# example:
|
||||
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
|
||||
# FIXME: add mining?
|
||||
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"],
|
||||
recipe_data["products"], recipe_data["energy"] if "energy" in recipe_data else 0)
|
||||
recipe = Recipe(recipe_name,
|
||||
recipe_data["category"],
|
||||
recipe_data["ingredients"],
|
||||
recipe_data["products"],
|
||||
recipe_data.get("energy", 0),
|
||||
recipe_data.get("mining", False),
|
||||
recipe_data.get("unlocked_at_start", False),
|
||||
recipe_data.get("burning", False))
|
||||
recipes[recipe_name] = recipe
|
||||
if set(recipe.products).isdisjoint(
|
||||
# prevents loop recipes like uranium centrifuging
|
||||
set(recipe.ingredients)) and ("empty-barrel" not in recipe.products or recipe.name == "empty-barrel") and \
|
||||
not recipe_name.endswith("-reprocessing"):
|
||||
for product_name in recipe.products:
|
||||
if set(recipe.products).isdisjoint(set(recipe.ingredients)):
|
||||
for product_name in [product_name for product_name, amount in recipe.products.items() if amount > 0]:
|
||||
all_product_sources.setdefault(product_name, set()).add(recipe)
|
||||
if recipe.detect_recursive_loop():
|
||||
# prevents loop recipes like uranium centrifuging and fluid unbarreling
|
||||
all_product_sources.setdefault(product_name, set()).remove(recipe)
|
||||
if not all_product_sources[product_name]:
|
||||
del (all_product_sources[product_name])
|
||||
if product_name in items:
|
||||
items[product_name].product_sources.add(recipe)
|
||||
if product_name in fluids:
|
||||
fluids[product_name].product_sources.add(recipe)
|
||||
|
||||
|
||||
machines: Dict[str, Machine] = {}
|
||||
machines["assembling-machine-1"].categories |= machines["assembling-machine-3"].categories # mod enables this
|
||||
machines["assembling-machine-2"].categories |= machines["assembling-machine-3"].categories
|
||||
machines["rocket-silo"].input_fluids = 3
|
||||
# machines["character"].categories.add("basic-crafting")
|
||||
# charter only knows the categories of "crafting" and "basic-solid" by default.
|
||||
|
||||
for name, categories in machines_future.result().items():
|
||||
machine = Machine(name, set(categories))
|
||||
machines[name] = machine
|
||||
|
||||
# add electric mining drill as a crafting machine to resolve basic-solid (mining)
|
||||
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"basic-solid"})
|
||||
machines["pumpjack"] = Machine("pumpjack", {"basic-fluid"})
|
||||
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
|
||||
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
|
||||
mods: Dict[str, Mod] = {}
|
||||
|
||||
for name, version in mods_future.result().items():
|
||||
if name in ["base", "archipelago-extractor"]:
|
||||
continue
|
||||
mod = Mod(name, version)
|
||||
mods[name] = mod
|
||||
|
||||
del mods_future
|
||||
|
||||
del machines_future
|
||||
|
||||
# build requirements graph for all technology ingredients
|
||||
|
||||
@@ -254,7 +465,10 @@ for technology in technology_table.values():
|
||||
|
||||
|
||||
def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
|
||||
current_technologies = recipe.unlocking_technologies
|
||||
if recipe.unlocked_at_start:
|
||||
current_technologies = set()
|
||||
else:
|
||||
current_technologies = recipe.unlocking_technologies
|
||||
for ingredient_name in recipe.ingredients:
|
||||
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done,
|
||||
unlock_func=unlock_just_tech)
|
||||
@@ -262,7 +476,10 @@ def unlock_just_tech(recipe: Recipe, _done) -> Set[Technology]:
|
||||
|
||||
|
||||
def unlock(recipe: Recipe, _done) -> Set[Technology]:
|
||||
current_technologies = recipe.unlocking_technologies
|
||||
if recipe.unlocked_at_start:
|
||||
current_technologies = set()
|
||||
else:
|
||||
current_technologies = recipe.unlocking_technologies
|
||||
for ingredient_name in recipe.ingredients:
|
||||
current_technologies |= recursively_get_unlocking_technologies(ingredient_name, _done, unlock_func=unlock)
|
||||
current_technologies |= required_category_technologies[recipe.category]
|
||||
@@ -289,21 +506,46 @@ def recursively_get_unlocking_technologies(ingredient_name, _done=None, unlock_f
|
||||
return current_technologies
|
||||
|
||||
|
||||
for item_name, item in items.items():
|
||||
if item.place_result and machines.get(item.place_result, None):
|
||||
machines[item.place_result].item_sources |= {item_name}
|
||||
|
||||
|
||||
required_machine_technologies: Dict[str, FrozenSet[Technology]] = {}
|
||||
for ingredient_name in machines:
|
||||
for ingredient_name, machine in machines.items():
|
||||
if ingredient_name == "character":
|
||||
required_machine_technologies[ingredient_name] = frozenset()
|
||||
continue
|
||||
techs = recursively_get_unlocking_technologies(ingredient_name)
|
||||
for item_name in machine.item_sources:
|
||||
techs |= recursively_get_unlocking_technologies(item_name)
|
||||
required_machine_technologies[ingredient_name] = frozenset(techs)
|
||||
|
||||
for ingredient_name in labs:
|
||||
required_machine_technologies[ingredient_name] = frozenset(recursively_get_unlocking_technologies(ingredient_name))
|
||||
|
||||
logical_machines = {}
|
||||
machine_tech_cost = {}
|
||||
|
||||
for category in machines["character"].categories:
|
||||
machine_tech_cost[category] = (10000, "character", machines["character"].speed)
|
||||
|
||||
for machine in machines.values():
|
||||
if machine.name == "character":
|
||||
continue
|
||||
for category in machine.categories:
|
||||
current_cost, current_machine = machine_tech_cost.get(category, (10000, "character"))
|
||||
machine_cost = len(required_machine_technologies[machine.name])
|
||||
if machine_cost < current_cost:
|
||||
machine_tech_cost[category] = machine_cost, machine.name
|
||||
if machine.machine_type == "character" and not machine_cost:
|
||||
machine_cost = 10000
|
||||
if category in machine_tech_cost:
|
||||
current_cost, current_machine, current_speed = machine_tech_cost.get(category)
|
||||
if machine_cost < current_cost or (machine_cost == current_cost and machine.speed > current_speed):
|
||||
machine_tech_cost[category] = machine_cost, machine.name, machine.speed
|
||||
else:
|
||||
machine_tech_cost[category] = machine_cost, machine.name, machine.speed
|
||||
|
||||
machine_per_category: Dict[str: str] = {}
|
||||
for category, (cost, machine_name) in machine_tech_cost.items():
|
||||
for category, (cost, machine_name, speed) in machine_tech_cost.items():
|
||||
machine_per_category[category] = machine_name
|
||||
|
||||
del machine_tech_cost
|
||||
@@ -313,6 +555,10 @@ required_category_technologies: Dict[str, FrozenSet[FrozenSet[Technology]]] = {}
|
||||
for category_name, machine_name in machine_per_category.items():
|
||||
techs = set()
|
||||
techs |= recursively_get_unlocking_technologies(machine_name)
|
||||
if category_name in machines["character"].categories and techs:
|
||||
# Character crafting/mining categories always have no tech assigned.
|
||||
techs = set()
|
||||
machine_per_category[category_name] = "character"
|
||||
required_category_technologies[category_name] = frozenset(techs)
|
||||
|
||||
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
|
||||
@@ -333,7 +579,10 @@ def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe, satellite_
|
||||
return {tech.name for tech in techs}
|
||||
|
||||
|
||||
free_sample_exclusions: Set[str] = all_ingredient_names | {"rocket-part"}
|
||||
free_sample_exclusions: Set[str] = all_ingredient_names.copy() # Rocket-silo crafting recipe results to be added here.
|
||||
for name, recipe in recipes.items():
|
||||
if machines[recipe.crafting_machine].machine_type == "rocket-silo":
|
||||
free_sample_exclusions |= set(recipe.products)
|
||||
|
||||
# progressive technologies
|
||||
# auto-progressive
|
||||
@@ -447,37 +696,49 @@ useless_technologies: Set[str] = {tech_name for tech_name in common_tech_table
|
||||
|
||||
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
|
||||
|
||||
rel_cost = {
|
||||
"wood": 10000,
|
||||
"iron-ore": 1,
|
||||
"copper-ore": 1,
|
||||
"stone": 1,
|
||||
"crude-oil": 0.5,
|
||||
"water": 0.001,
|
||||
"coal": 1,
|
||||
"raw-fish": 1000,
|
||||
"steam": 0.01,
|
||||
"used-up-uranium-fuel-cell": 1000
|
||||
}
|
||||
for name, recipe in {name: recipe for name, recipe in recipes.items() if recipe.mining and not recipe.ingredients}.items():
|
||||
machine = machines[machine_per_category[recipe.category]]
|
||||
cost = recipe.energy / machine.speed
|
||||
for product_name, amount in recipe.products.items():
|
||||
rel_cost[product_name] = cost / amount
|
||||
|
||||
exclusion_list: Set[str] = all_ingredient_names | {"rocket-part", "used-up-uranium-fuel-cell"}
|
||||
fluids: Set[str] = set(fluids_future.result())
|
||||
del fluids_future
|
||||
|
||||
def get_estimated_difficulty(recipe: Recipe):
|
||||
base_ingredients = recipe.base_cost
|
||||
cost = 0
|
||||
|
||||
for ingredient_name, amount in base_ingredients.items():
|
||||
cost += rel_cost.get(ingredient_name, high_cost_item) * amount
|
||||
if recipe.burning:
|
||||
for item in machines[recipe.crafting_machine].item_sources:
|
||||
if items[item].product_sources:
|
||||
cost += min([get_estimated_difficulty(recipe) for recipe in items[item].product_sources])
|
||||
else:
|
||||
cost += high_cost_item
|
||||
|
||||
# print(f"{recipe.name}: {cost} ({({ingredient_name: rel_cost.get(ingredient_name, high_cost_item) * amount for ingredient_name, amount in base_ingredients.items()})})")
|
||||
return cost
|
||||
|
||||
|
||||
for name, recipe in {name: recipe for name, recipe in recipes.items() if recipe.mining and recipe.ingredients}.items():
|
||||
machine = machines[machine_per_category[recipe.category]]
|
||||
cost = (recipe.energy / machine.speed) + get_estimated_difficulty(recipe)
|
||||
for product_name, amount in recipe.products.items():
|
||||
rel_cost[product_name] = cost / amount
|
||||
|
||||
exclusion_list: Set[str] = free_sample_exclusions.copy() # Also exclude the burnt results.
|
||||
|
||||
|
||||
@Utils.cache_argsless
|
||||
def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
def get_estimated_difficulty(recipe: Recipe):
|
||||
base_ingredients = recipe.base_cost
|
||||
cost = 0
|
||||
|
||||
for ingredient_name, amount in base_ingredients.items():
|
||||
cost += rel_cost.get(ingredient_name, 1) * amount
|
||||
return cost
|
||||
|
||||
science_pack_pools: Dict[str, Set[str]] = {}
|
||||
already_taken = exclusion_list.copy()
|
||||
current_difficulty = 5
|
||||
unlocked_recipes = {name: recipe for name, recipe in recipes.items()
|
||||
if recipe.unlocked_at_start
|
||||
and not recipe.recursive_unlocking_technologies
|
||||
and get_estimated_difficulty(recipe) < high_cost_item} # wood in recipe is expensive.
|
||||
average_starting_difficulty = sum([get_estimated_difficulty(recipe) for name, recipe in unlocked_recipes.items()]) / len(unlocked_recipes)
|
||||
current_difficulty = min(average_starting_difficulty, 8)
|
||||
for science_pack in Options.MaxSciencePack.get_ordered_science_packs():
|
||||
current = science_pack_pools[science_pack] = set()
|
||||
for name, recipe in recipes.items():
|
||||
@@ -487,9 +748,7 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
|
||||
if science_pack == "automation-science-pack":
|
||||
# Can't handcraft automation science if fluids end up in its recipe, making the seed impossible.
|
||||
current -= fluids
|
||||
elif science_pack == "logistic-science-pack":
|
||||
current |= {"steam"}
|
||||
current -= set(fluids)
|
||||
|
||||
current -= already_taken
|
||||
already_taken |= current
|
||||
@@ -498,10 +757,9 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
|
||||
return science_pack_pools
|
||||
|
||||
|
||||
item_stack_sizes: Dict[str, int] = items_future.result()
|
||||
non_stacking_items: Set[str] = {item for item, stack in item_stack_sizes.items() if stack == 1}
|
||||
stacking_items: Set[str] = set(item_stack_sizes) - non_stacking_items
|
||||
valid_ingredients: Set[str] = stacking_items | fluids
|
||||
non_stacking_items: Set[str] = {name for name, item in items.items() if not item.stackable}
|
||||
stacking_items: Set[str] = set(items) - non_stacking_items
|
||||
valid_ingredients: Set[str] = stacking_items | set(fluids)
|
||||
|
||||
# cleanup async helpers
|
||||
pool.shutdown()
|
||||
|
||||
@@ -8,7 +8,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
||||
all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, \
|
||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||
fluids, stacking_items, valid_ingredients
|
||||
fluids, stacking_items, valid_ingredients, mods
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal
|
||||
@@ -54,8 +54,8 @@ class Factorio(World):
|
||||
item_name_groups = {
|
||||
"Progressive": set(progressive_tech_table.keys()),
|
||||
}
|
||||
data_version = 5
|
||||
required_client_version = (0, 3, 0)
|
||||
data_version = 0 if mods else 5
|
||||
required_client_version = (0, 3, 5) if mods else (0, 3, 0) # TODO: Update required_client_version to (0, 3, 6) when that version releases.
|
||||
|
||||
def __init__(self, world, player: int):
|
||||
super(Factorio, self).__init__(world, player)
|
||||
@@ -224,7 +224,7 @@ class Factorio(World):
|
||||
liquids_used += 1 if new_ingredient in fluids else 0
|
||||
new_ingredients[new_ingredient] = 1
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
|
||||
original.products, original.energy)
|
||||
original.products, original.energy, original.mining, original.unlocked_at_start)
|
||||
|
||||
def make_balanced_recipe(self, original: Recipe, pool: typing.Set[str], factor: float = 1,
|
||||
allow_liquids: int = 2) -> Recipe:
|
||||
@@ -258,6 +258,8 @@ class Factorio(World):
|
||||
ingredient_energy = 2
|
||||
if not ingredient_raw:
|
||||
ingredient_raw = 1
|
||||
if not ingredient_energy:
|
||||
ingredient_energy = 1/60
|
||||
if remaining_num_ingredients == 1:
|
||||
max_raw = 1.1 * remaining_raw
|
||||
min_raw = 0.9 * remaining_raw
|
||||
@@ -293,13 +295,18 @@ class Factorio(World):
|
||||
continue # can't use this ingredient as we already have maximum liquid in our recipe.
|
||||
|
||||
ingredient_recipe = recipes.get(ingredient, None)
|
||||
if not ingredient_recipe and ingredient.endswith("-barrel"):
|
||||
ingredient_recipe = recipes.get(f"fill-{ingredient}", None)
|
||||
if not ingredient_recipe and ingredient in all_product_sources:
|
||||
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
|
||||
if not ingredient_recipe:
|
||||
logging.warning(f"missing recipe for {ingredient}")
|
||||
continue
|
||||
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
|
||||
ingredient_energy = ingredient_recipe.total_energy
|
||||
if not ingredient_raw:
|
||||
ingredient_raw = 1
|
||||
if not ingredient_energy:
|
||||
ingredient_energy = 1/60
|
||||
|
||||
num_raw = remaining_raw / ingredient_raw / remaining_num_ingredients
|
||||
num_energy = remaining_energy / ingredient_energy / remaining_num_ingredients
|
||||
num = int(min(num_raw, num_energy))
|
||||
@@ -317,7 +324,7 @@ class Factorio(World):
|
||||
logging.warning("could not randomize recipe")
|
||||
|
||||
return Recipe(original.name, self.get_category(original.category, liquids_used), new_ingredients,
|
||||
original.products, original.energy)
|
||||
original.products, original.energy, original.mining, original.unlocked_at_start)
|
||||
|
||||
def set_custom_technologies(self):
|
||||
custom_technologies = {}
|
||||
@@ -330,11 +337,20 @@ class Factorio(World):
|
||||
original_rocket_part = recipes["rocket-part"]
|
||||
science_pack_pools = get_science_pack_pools()
|
||||
valid_pool = sorted(science_pack_pools[self.world.max_science_pack[self.player].get_max_pack()] & valid_ingredients)
|
||||
if len(valid_pool) < 3:
|
||||
# Not enough items in max pool. Extend to entire pool.
|
||||
valid_pool = []
|
||||
for pack in self.world.max_science_pack[self.player].get_ordered_science_packs():
|
||||
valid_pool += sorted(science_pack_pools[pack] & valid_ingredients)
|
||||
if len(valid_pool) < 3:
|
||||
raise Exception("Not enough ingredients available for generation.")
|
||||
self.world.random.shuffle(valid_pool)
|
||||
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
|
||||
{valid_pool[x]: 10 for x in range(3)},
|
||||
original_rocket_part.products,
|
||||
original_rocket_part.energy)}
|
||||
original_rocket_part.energy,
|
||||
original_rocket_part.mining,
|
||||
original_rocket_part.unlocked_at_start)}
|
||||
|
||||
if self.world.recipe_ingredients[self.player]:
|
||||
valid_pool = []
|
||||
@@ -363,7 +379,7 @@ class Factorio(World):
|
||||
bridge = "ap-energy-bridge"
|
||||
new_recipe = self.make_quick_recipe(
|
||||
Recipe(bridge, "crafting", {"replace_1": 1, "replace_2": 1, "replace_3": 1},
|
||||
{bridge: 1}, 10),
|
||||
{bridge: 1}, 10, False, self.world.energy_link[self.player].value),
|
||||
sorted(science_pack_pools[self.world.max_science_pack[self.player].get_ordered_science_packs()[0]]))
|
||||
for ingredient_name in new_recipe.ingredients:
|
||||
new_recipe.ingredients[ingredient_name] = self.world.random.randint(10, 100)
|
||||
|
||||
@@ -1 +1 @@
|
||||
["fluid-unknown","water","crude-oil","steam","heavy-oil","light-oil","petroleum-gas","sulfuric-acid","lubricant"]
|
||||
{"fluid-unknown":{"default_temperature":0,"max_temperature":0,"heat_capacity":1000},"water":{"default_temperature":15,"max_temperature":100,"heat_capacity":200},"crude-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"steam":{"default_temperature":15,"max_temperature":1000,"heat_capacity":200},"heavy-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"light-oil":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"petroleum-gas":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"sulfuric-acid":{"default_temperature":25,"max_temperature":25,"heat_capacity":100},"lubricant":{"default_temperature":25,"max_temperature":25,"heat_capacity":100}}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"stone-furnace":{"smelting":true},"steel-furnace":{"smelting":true},"electric-furnace":{"smelting":true},"assembling-machine-1":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"assembling-machine-2":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"assembling-machine-3":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"oil-refinery":{"oil-processing":true},"chemical-plant":{"chemistry":true},"centrifuge":{"centrifuging":true},"rocket-silo":{"rocket-building":true},"character":{"crafting":true}}
|
||||
{"boiler":{"boiler":{"input_fluid":"water","output_fluid":"steam","target_temperature":165,"energy_usage":30000},"common":{"type":"boiler","placeable_by":{"boiler":true}}},"nuclear-reactor":{"fuel_burner":{"categories":{"nuclear":true},"energy_usage":666666.666666666627861559391021728515625},"common":{"type":"reactor","placeable_by":{"nuclear-reactor":true}}},"heat-exchanger":{"boiler":{"input_fluid":"water","output_fluid":"steam","target_temperature":500,"energy_usage":166666.66666666665696538984775543212890625},"common":{"type":"boiler","placeable_by":{"heat-exchanger":true}}},"burner-mining-drill":{"mining":{"categories":{"basic-solid":true},"speed":0.25,"input_fluid_box":false,"output_fluid_box":false},"common":{"type":"mining-drill","placeable_by":{"burner-mining-drill":true}}},"electric-mining-drill":{"mining":{"categories":{"basic-solid":true},"speed":0.5,"input_fluid_box":true,"output_fluid_box":false},"common":{"type":"mining-drill","placeable_by":{"electric-mining-drill":true}}},"offshore-pump":{"offshore-pump":{"fluid":"water","speed":20},"common":{"type":"offshore-pump","placeable_by":{"offshore-pump":true}}},"pumpjack":{"mining":{"categories":{"basic-fluid":true},"speed":1,"input_fluid_box":false,"output_fluid_box":true},"common":{"type":"mining-drill","placeable_by":{"pumpjack":true}}},"stone-furnace":{"crafting":{"speed":1,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"stone-furnace":true}}},"steel-furnace":{"crafting":{"speed":2,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"steel-furnace":true}}},"electric-furnace":{"crafting":{"speed":2,"categories":{"smelting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"furnace","placeable_by":{"electric-furnace":true}}},"assembling-machine-1":{"crafting":{"speed":0.5,"categories":{"crafting":true,"basic-crafting":true,"advanced-crafting":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-1":true}}},"assembling-machine-2":{"crafting":{"speed":0.75,"categories":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"input_fluid_box":1,"output_fluid_box":1},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-2":true}}},"assembling-machine-3":{"crafting":{"speed":1.25,"categories":{"basic-crafting":true,"crafting":true,"advanced-crafting":true,"crafting-with-fluid":true},"input_fluid_box":1,"output_fluid_box":1},"common":{"type":"assembling-machine","placeable_by":{"assembling-machine-3":true}}},"oil-refinery":{"crafting":{"speed":1,"categories":{"oil-processing":true},"input_fluid_box":2,"output_fluid_box":3},"common":{"type":"assembling-machine","placeable_by":{"oil-refinery":true}}},"chemical-plant":{"crafting":{"speed":1,"categories":{"chemistry":true},"input_fluid_box":2,"output_fluid_box":2},"common":{"type":"assembling-machine","placeable_by":{"chemical-plant":true}}},"centrifuge":{"crafting":{"speed":1,"categories":{"centrifuging":true},"input_fluid_box":0,"output_fluid_box":0},"common":{"type":"assembling-machine","placeable_by":{"centrifuge":true}}},"lab":{"lab":{"inputs":["automation-science-pack","logistic-science-pack","military-science-pack","chemical-science-pack","production-science-pack","utility-science-pack","space-science-pack"]},"common":{"type":"lab","placeable_by":{"lab":true}}},"rocket-silo":{"crafting":{"speed":1,"categories":{"rocket-building":true},"fixed_recipe":"rocket-part","input_fluid_box":0,"output_fluid_box":0},"common":{"type":"rocket-silo","placeable_by":{"rocket-silo":true}}},"character":{"crafting":{"speed":1,"categories":{"crafting":true},"input_fluid_box":0,"output_fluid_box":0},"mining":{"categories":{"basic-solid":true},"speed":0.5,"input_fluid_box":false,"output_fluid_box":false},"common":{"type":"character"}}}
|
||||
@@ -7,7 +7,9 @@
|
||||
"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",
|
||||
"! archipelago-extractor"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ GOAL = {{ goal }}
|
||||
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
|
||||
ENERGY_INCREMENT = {{ energy_link * 1000000 }}
|
||||
ENERGY_LINK_EFFICIENCY = 0.75
|
||||
CUSTOM_DATA_PACKAGE = {{ custom_data_package }}
|
||||
|
||||
if settings.global[ARCHIPELAGO_DEATH_LINK_SETTING].value then
|
||||
DEATH_LINK = 1
|
||||
@@ -515,6 +516,10 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
|
||||
local item_name = chunks[1]
|
||||
local index = chunks[2]
|
||||
local source = chunks[3] or "Archipelago"
|
||||
|
||||
if CUSTOM_DATA_PACKAGE == 1 then
|
||||
item_name = item_id_to_name[item_name] or item_name
|
||||
end
|
||||
if index == -1 then -- for coop sync and restoring from an older savegame
|
||||
tech = force.technologies[item_name]
|
||||
if tech.researched ~= true then
|
||||
@@ -571,7 +576,8 @@ commands.add_command("ap-rcon-info", "Used by the Archipelago client to get info
|
||||
["slot_name"] = SLOT_NAME,
|
||||
["seed_name"] = SEED_NAME,
|
||||
["death_link"] = DEATH_LINK,
|
||||
["energy_link"] = ENERGY_INCREMENT
|
||||
["energy_link"] = ENERGY_INCREMENT,
|
||||
["custom_data_package"] = CUSTOM_DATA_PACKAGE
|
||||
}))
|
||||
end)
|
||||
|
||||
@@ -598,3 +604,4 @@ end)
|
||||
|
||||
-- data
|
||||
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||
item_id_to_name = {{ dict_to_lua(item_id_to_name) }}
|
||||
|
||||
@@ -141,6 +141,9 @@ end
|
||||
{# This got complex, but seems to be required to hit all corner cases #}
|
||||
function adjust_energy(recipe_name, factor)
|
||||
local recipe = data.raw.recipe[recipe_name]
|
||||
if recipe == nil then
|
||||
error("Some mod that is installed has removed recipe \"" .. recipe_name .. "\"")
|
||||
end
|
||||
local energy = recipe.energy_required
|
||||
|
||||
if (recipe.normal ~= nil) then
|
||||
@@ -168,6 +171,9 @@ end
|
||||
|
||||
function set_energy(recipe_name, energy)
|
||||
local recipe = data.raw.recipe[recipe_name]
|
||||
if recipe == nil then
|
||||
error("Some mod that is installed has removed recipe \"" .. recipe_name .. "\"")
|
||||
end
|
||||
|
||||
if (recipe.normal ~= nil) then
|
||||
recipe.normal.energy_required = energy
|
||||
@@ -183,11 +189,26 @@ 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 #}
|
||||
{%- for original_tech_name, item_name, receiving_player, advancement in locations %}
|
||||
original_tech = technologies["{{original_tech_name}}"]
|
||||
if original_tech == nil then
|
||||
error("Some mod that is installed has removed tech \"{{original_tech_name}}\"")
|
||||
end
|
||||
{#- the tech researched by the local player #}
|
||||
new_tree_copy = table.deepcopy(template_tech)
|
||||
new_tree_copy.name = "ap-{{ tech_table[original_tech_name] }}-"{# use AP ID #}
|
||||
@@ -220,13 +241,13 @@ data:extend{new_tree_copy}
|
||||
{% endfor %}
|
||||
{% if recipe_time_scale %}
|
||||
{%- for recipe_name, recipe in recipes.items() %}
|
||||
{%- if recipe.category not in ("basic-solid", "basic-fluid") %}
|
||||
{%- if not recipe.mining and not recipe.burning %}
|
||||
adjust_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_scale) }})
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
{% elif recipe_time_range %}
|
||||
{%- for recipe_name, recipe in recipes.items() %}
|
||||
{%- if recipe.category not in ("basic-solid", "basic-fluid") %}
|
||||
{%- if not recipe.mining and not recipe.burning %}
|
||||
set_energy("{{ recipe_name }}", {{ flop_random(*recipe_time_range) }})
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
1
worlds/factorio/data/mods.json
Normal file
1
worlds/factorio/data/mods.json
Normal file
@@ -0,0 +1 @@
|
||||
{"base":"1.1.69","archipelago-extractor":"1.1.1"}
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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